~edwargix/tallyard

917b59b72034f25e6a10576decddd6539cf9382c — David Florness 5 years ago d2b1b9d
Refactor election info and save election info
3 files changed, 176 insertions(+), 59 deletions(-)

M config.go
M config_test.go
M main.go
M config.go => config.go +109 -27
@@ 2,6 2,7 @@ package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path"


@@ 11,9 12,25 @@ import (
	"github.com/mr-tron/base58/base58"
)

// Used to decode an election in the JSON elections file, which will be a list
// of these.  Maps the election key to the election metadata
type RawElections map[string]struct{
// where cached election info is stored
var (
	tallyardConfigPath = path.Join(xdg.ConfigHome(), "tallyard")
	electionInfoFilePath = path.Join(tallyardConfigPath, "elections.json")
)

// maps an election's key to its cached info
type ElectionInfos map[string]*ElectionInfo

// an entry of an election's cached info
type ElectionInfo struct{
	PrivKey crypto.PrivKey
	PubKey  crypto.PubKey
}

// used for JSON {en,de}coding
type RawElectionInfos map[string]RawElectionInfo

type RawElectionInfo struct{
	// Our private key in base58. TODO: encrypt
	PrivKey string
	// Our public key in base58. Kept in addition to the private key to


@@ 21,50 38,115 @@ type RawElections map[string]struct{
	PubKey  string
}

type ElectionConfig struct{
	PrivKey crypto.PrivKey
	PubKey  crypto.PubKey
// Saves an election's info into a file that can later be easily retrieved, even
// on separate invocations of tallyard.  Returns an error if there was one.
func saveElectionInfo(electionKey string, privKey crypto.PrivKey, pubKey crypto.PubKey) error {
	err := os.MkdirAll(tallyardConfigPath, 0700)
	if err != nil {
		return err
	}
	infos, err := loadPastElectionInfos()
	if err != nil {
		return fmt.Errorf("unable to load election info while saving election info: %s", err)
	}
	file, err := os.OpenFile(electionInfoFilePath,
		os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		return fmt.Errorf("unable to open election info file: %s", err)
	}
	defer file.Close()
	infos[electionKey] = &ElectionInfo{
		PrivKey: privKey,
		PubKey:  pubKey,
	}
	raw, err := marshalElectionInfos(infos)
	if err != nil {
		return fmt.Errorf("unable to marshal election info for caching: %s", err)
	}
	err = encodeElectionsInfos(file, raw)
	if err != nil {
		return fmt.Errorf("unable to encode election info for caching: %s", err)
	}
	return nil
}

type ElectionConfigs map[string]*ElectionConfig
// Checks whether we've joined this election before and attempts to retrieve its
// info we cached for it.  Useful if a connection was suddently lost and
// tallyard had to restart.  Returns the election's info (nil if no such info
// exists) and an error
func getElectionInfo(electionKey string) (*ElectionInfo, error) {
	infos, err := loadPastElectionInfos()
	if err != nil {
		return nil, fmt.Errorf("unable to get election info: %s", err)
	}
	if info, hasElectionKey := infos[electionKey]; hasElectionKey {
		return info, nil
	}
	return nil, nil
}

// sees if we've joined this election before and attempts to retrieve the data
// we stored for it
func getElectionConfigMaybe(electionKey string) *ElectionConfig {
	root := path.Join(xdg.ConfigHome(), "tallyard")
	file, err := os.OpenFile(path.Join(root, "elections.json"), os.O_RDONLY|os.O_CREATE, 0600)
func loadPastElectionInfos() (ElectionInfos, error) {
	err := os.MkdirAll(tallyardConfigPath, 0700)
	if err != nil {
		return nil, err
	}
	file, err := os.OpenFile(electionInfoFilePath,
		os.O_RDONLY|os.O_CREATE, 0600)
	if err != nil {
		panic(err)
		return nil, fmt.Errorf("unable to open election info file: %s", err)
	}
	defer file.Close()

	rawElections, err := decodeElections(file)
	raw, err := decodeElectionInfos(file)
	if err != nil {
		panic(err)
		return nil, fmt.Errorf("unable to decode election info for caching: %s", err)
	}

	electionConfigs, err := unmarshalRawElections(rawElections)
	infos, err := unmarshalRawElectionInfos(raw)
	if err != nil {
		panic(err)
		return nil, fmt.Errorf("unable to encode election info for caching: %s", err)
	}
	return infos, nil
}

	if cfg, hasElectionKey := electionConfigs[electionKey]; hasElectionKey {
		return cfg
	} else {
		return nil
	}
func encodeElectionsInfos(writer io.Writer, rawElections RawElectionInfos) error {
	encoder := json.NewEncoder(writer)
	return encoder.Encode(rawElections)
}

func decodeElections(reader io.Reader) (rawElections RawElections, err error) {
func decodeElectionInfos(reader io.Reader) (rawElections RawElectionInfos, err error) {
	decoder := json.NewDecoder(reader)
	err = decoder.Decode(&rawElections)
	if err == io.EOF {
		return make(RawElectionInfos), nil
	}
	if err != nil {
		return nil, err
	}
	return rawElections, nil
}

func unmarshalRawElections(rawElections RawElections) (electionConfigs ElectionConfigs, err error) {
func marshalElectionInfos(electionConfigs ElectionInfos) (rawElections RawElectionInfos, err error) {
	rawElections = make(RawElectionInfos)
	for electionKey, electionConfig := range electionConfigs {
		privKeyBytes, err := crypto.MarshalPrivateKey(electionConfig.PrivKey)
		if err != nil {
			return nil, err
		}
		privKeyStr := base58.Encode(privKeyBytes)
		pubKeyBytes, err := crypto.MarshalPublicKey(electionConfig.PubKey)
		if err != nil {
			return nil, err
		}
		pubKeyStr := base58.Encode(pubKeyBytes)
		rawElections[electionKey] = RawElectionInfo{
			PrivKey: privKeyStr,
			PubKey:  pubKeyStr,
		}
	}
	return rawElections, nil
}

func unmarshalRawElectionInfos(rawElections RawElectionInfos) (electionConfigs ElectionInfos, err error) {
	electionConfigs = make(ElectionInfos)
	for electionKey, info := range rawElections {
		privKeyBytes, err := base58.Decode(info.PrivKey)
		if err != nil {


@@ 82,7 164,7 @@ func unmarshalRawElections(rawElections RawElections) (electionConfigs ElectionC
		if err != nil {
			return nil, err
		}
		electionConfigs[electionKey] = &ElectionConfig{
		electionConfigs[electionKey] = &ElectionInfo{
			PrivKey: privKey,
			PubKey:  pubKey,
		}

M config_test.go => config_test.go +19 -20
@@ 7,60 7,59 @@ import (
)

const (
	ek0 = "DpgZ3EG8cyNAxrbGejTTUuJB6KkvKVGK9WEpQWDh6RLd0QmVitsctABm8xbadtkqU46WMLCjuAadUR2RAkjaPf7xz7X"
	ek1 = "Dmfo6renA9YKuTFdGL4iSP5fcHbxHG4Qc9XTHkBzdHPa0QmVjm2QACdwMYguMtMooDoLsiagyVfe7qwxpuzUmX1stTM"
	electionKey0 = "DpgZ3EG8cyNAxrbGejTTUuJB6KkvKVGK9WEpQWDh6RLd0QmVitsctABm8xbadtkqU46WMLCjuAadUR2RAkjaPf7xz7X"
	electionKey1 = "Dmfo6renA9YKuTFdGL4iSP5fcHbxHG4Qc9XTHkBzdHPa0QmVjm2QACdwMYguMtMooDoLsiagyVfe7qwxpuzUmX1stTM"
)

var rawElectionsRef = RawElections{
	ek0:
var rawElectionInfosRef = RawElectionInfos{
	electionKey0:
	{
		PrivKey:  "7r1r8iBFRYi5hLWbNS5vx53fAvnyFFGrgnDEwu8DSGRVdik5TVuPhCBpqummjaMMXgogcoz6h88rEWvM1af5G4VV4t5cNeFF6hXDyM14gxAMBkm2cEgpcf7d3AAH4auiHhZcB5XWUZeGKUgjz2WGtLPChHhAZzCA8GnDZHhrLSL2qfSt1ZYhyyvTLbEvfNZ7VhwXK64Y4UxjaRAXsMpBCret9aAck9pJxJnE5gm91JxTQybcTKULN9BxvJV99kLUdZ66PJ6ff5TZB4oNzShEQcdyP2G8tUJkCujokXWpZQh7ZDE4dtuoP6uvQhYSkPtWTgYv888BRYDoNDDqWc1nqZGqftr5EHogUTC4H7D3UWngVpwd1Mjbi8vS2DicKDXwJqP2bSyeVGLG3yLkWyjfxfBwNwnHGcBmhiSrL6bHfUrpJqy95UPC3ZSJeLNaYEToQAJmCEah4D7BZchF13rXjLGMsXxTUTHt9mMmEa8jdxLbqUkafVsN1VcHeZBVScvnFCS1e4xe1Ja54xg8H7b5HddB8JcSHaW2fJkvTrr3avT24NivAw4nvRg5UasMR2WMzgYP1rkmyV5vyPoJAnytYX8hZp2zX6gPbZBqUEDAQcvAegibjcDhRZMxMLCrR73UReEqXjjFhKYh7AsDz6uNDrwE6oVRJZ9GLBAetQ9HothETXr2PUaFkh82zCCCYbNL16aFWgiKYBAd1iaKL7idA4GrzTXuYR5HHJxcGSXNc9mUrGEEY2tLkNLPk3VHrqEDpzKArA72PGy1mKXVghoaJeZAe5am49ZPuPG4VQnkxHXB8NkhPkc26cbD6iYxq1cC7XDEMrQ8ggvecjhFxVnnFsDE2Ub7cZyVompvckPMX7R7VuERq7Ax1Vk4qNgbP5hHpn5F5DGpRNRdtpsEpzseSvBKQ6nDknSXFYSya99b4T7nwVkYCj7KuD21Xfmj4mRYQxy4aebKggUvPWAmMNSTWdrPK9QF7Y2cwagpDYQx8KEcVgGU5CrCeKscAbc84PbP6H2HMaMB5ivdMmuEbQfJuyMpX7sXwXCvHUg4p8R4UMf7nkF5c76H4oRH2CaJnnQ25DwYtMtfWmidTFFJECtaLU154z47L2TzNtn3G16UtUNvyLBA5b5HBKhKcjzwVHmRBreWz3urCF1obBT49mERb9mrsyicYjfoKqXS1mVVVCkvs1vAEm73RepXBHD37aQ4DVk9ThFB2RMiDxCwaW7EYUkNLpRaH1FHPJg2o9x1EkPsqcTg5TL31e3EctivtyLCVPLsaUsFYFmzyXCsniXeJPdPGxvt32fbeP1GUXv8N2xLAoVg815cuEnGvkjvztLK8Mv2fxtsPGQaEH6D9FEM2x1xRAFj6cspkbC5RpEbTcuEaaXBpBgVZSxT7TkYWUyerSYKLhocacwmGSYgduxvTB5w1nTfbk3sobEEjCrZwZjiQzdwkrP1iG4x1YDSB4f97n7gnbkEs5LDvuDwgS6KBeWnWTw1V2P5U8VkTCFhj6UAiv1FP9Tdi664MgDVNXQoEGQryy98zShcyg3BZ2Jr8yCdri3gVgMLG7b6Q91AXehz4RRs2X6SrZtCLKN2uBmhGwntvL2FvXDkGanXuRfUEMP8g2VdtWuW8",
		PubKey:   "7ySHQR7YZJApfZRKds9hzzFypZo8s6WQyZqXCYVHRjug84TSqhdKgdVBhS3cZ6NYMfygyfPMvAgjnn2mp3c5dSME49opHiD3ioUaArqGqddyWf3gonWVfnjkTnMcdpGXvDt74uAYf2NrwsphN1MuBBX5NAoHtE5vTRMAiZL94KSg7qFNZM1PckSJfo4xJUD1JgmEMqBvavosjFoenhZv7FjLkXFJhHFGfF7KaK4Q62bUubYtgiR1bkYk1RtctkjUvnBW8YnSx1QFGrmA5qAz8ZUCRmeqgktvpYDR2bj3XFDUGmjqNG3N4QMikg2a6vquCuuT6kv5WefzUYncvijQLHx85z74F8wMZV5huUxQM3RS2aZgc89WvFjeP5Pkmv1bUqvdZ5n1Yi3U65b9KBCR9ycL",
	},
	ek1:
	electionKey1:
	{
		PrivKey:  "B9bLmHeKLQU1hX23meSn2kJP4wwPAGeaXRpYnkZyKfR98RYs51wo7syA9N7oSbcf49hyrp9vaFpo2GA9yYkiWsPh3smeDDbSZsuzNoAZXr4tjPFytaaddvbsbsme5YbdMejRh1qPncRR6vmhBwb9r4xZBugb2PZPNbqBKKkVotF3YzVzJ3s6kjKyTirGW8xowFbma8hwWntfaNVuZF143BeHQMjyAB5QYRtLtx85RS4Zkxm4wv71Y5ZAcf3VMgmjGXTVXivWX4pkDHBSYfXYr6CoqLy4iNNWstnehBVPAJFM9YvYTnoT3DYXyXXgBSnfPLXFgX9yxkKh8YtXVNKewLqa3b9uKHzCh1FbxTEThfTK1Q4YPFLEek8eR2DUHinGuPrEgCmH4H8ZKXmfLkmGTgHWDkzDr4u2e74jE7Fz9XWmwHq39ZzUsE5SFVWVm6domENEgQMPHU83X7RFXPwgfCCiNdNYkozL9hvBgPujDtiiyBPExbUqY5dnCceTuD6ysJmhwL4SHWpdHKZwwWETWeDTzyA7snZjbebcmE1visFydZ2UQaQdHKnNU5gV2NkiQupyyCEvs9rrHW6xf6g9myMrzdRQVRBBuKswCjq91CNA2uAtkyNLzEhD9GSLuGFRgmtzEwGxek2p12ysUjJyfgBDY28CoMJVJfeK8dyCHQnyvtmA32uxQ8m83QDRxTGY52ubsLaKm9n1L4MiJKW73BC8JoZxdQmuW8278JUAWdTK7KDk2yCDfFfUwGLq6Jz1aTqV5KM23Ysjto6ENeYSHiNrwJ4yBCP2cq4bs48dfdCqWodzBN2HaUpiFiPfQDG5r52zow1aULEaLeUrUbCChKTi6WqWyEBKxEpVMiL7dRQqzHyq3JAFyu51CgmbfFyQtamV8ULE4aVMotrCm2QcrvWxLxkjvSb4cCcnJwyQ9QyT8jFpfVnDWSdLSpbRAxJzr4c4pxWMDjhhPfQYWLArMTv7N6woQwMg8L6AJVbm96qQQ8v99VHVRzib4gVP36DYmTT8WEks2LtFXBwypU8x17iREvwex2ZXHQgvYRAkgw1uYg8rkGMQ5ZTrekTbbsBnUxmcV7Y5Cb18PUVeTRvDtFUB8K2VsZZeTnfWsfvKNqURs4nLyn4VPicgtUbDwLvJnpijDfodv6ydYfbdpDEtkGrKXKwNTwWZPFAts61Wnsouu7dLCN2gKWfhh8x4NdcnZP3fKE15GfLViFMeanAZvww2DVyW78cXpeztanCmkbUqLSvoGkRJsZ49pFEtVeGv1at6SFNKYzyPCWYnegfvs2f54rUKCchWDoNZwYw7fja32d87qEUzWouorLXc4ATkykaKzHthoiwEMjweQKGgGgUXbNbP56x1jZYgLPSCVHHgGaLNKgqW6tSXEKThEEg42kzDNiymr7soWwGrZPsNyE5vpfoxd7ebeKy2LJ4H9AwqAbUFhuFAFwzcTRLsiWGJKg9ysi8fpcGBcg7NbrrkHwsR1jvvpV7f855pRTPMXCUQpYiGWVPtJ1UZAsmX5RqVmgfdyJHYok8aL2XML9GeHTHZZNg7iD2ST8DSdKNhaYs7oxyTMvWsWnqPezUq5kaQxRa8zTkFqUJYUAgiZrKDFrgsXC4EgboyZM3CK",
		PubKey:   "7ySHQR7YZJApfZRKds9hzzFypZo8s6WQyZqXCYVHRjug84TSqhdKDnnZpz69WtL8kvTgtrjEF3GHmsfjgMSt2t7vYgU26qHZfCXPUhA9u93vkCXSG6ZLBWw8K2HxMWPR69LoWzCsfXnLnw9mbr64uHmEeP64PSidtyshApUS73ikt7KGMdk3twJLr7SZ8uaqw6DCvBd9po5tVtPZ9gS1NPG3vuavLfm2P9YT2LMzCf68bJmqzgiVpUxRemvpwppVP675C5m6ctDELBTvqwQ9dc8baG4ztZPehEeUhULG5JpgtMKayCYRNhQVKdENyWXfFYzpMJ27iMH3LT2dixjgXRkqh8PKFt1srJA4we6vCLx1yjDjJV7MELo9P9nkXbojjQLXnqa41bYYGMN4ChK1C78U",
	},
}

func TestRawElectionDecoding(t *testing.T) {
func TestRawElectionInfosDecoding(t *testing.T) {
	jsonStream := fmt.Sprintf(`
	{"%s": {"PrivKey": "%s",
		"PubKey":  "%s"},
	 "%s": {"PrivKey": "%s",
		"PubKey":  "%s"}}`,
		ek0,
		rawElectionsRef[ek0].PrivKey,
		rawElectionsRef[ek0].PubKey,
		ek1,
		rawElectionsRef[ek1].PrivKey,
		rawElectionsRef[ek1].PubKey,
		electionKey0,
		rawElectionInfosRef[electionKey0].PrivKey,
		rawElectionInfosRef[electionKey0].PubKey,
		electionKey1,
		rawElectionInfosRef[electionKey1].PrivKey,
		rawElectionInfosRef[electionKey1].PubKey,
	)
	rawElections, err := decodeElections(strings.NewReader(jsonStream))
	rawElections, err := decodeElectionInfos(strings.NewReader(jsonStream))
	if err != nil {
		panic(err)
	}

	if len(rawElections) != 2 {
		t.FailNow()
	}
	if _, exists := rawElections[ek0]; !exists {
	if _, exists := rawElections[electionKey0]; !exists {
		t.FailNow()
	}
	if _, exists := rawElections[ek1]; !exists {
	if _, exists := rawElections[electionKey1]; !exists {
		t.FailNow()
	}
	if rawElections[ek0].PrivKey != rawElectionsRef[ek0].PrivKey {
	if rawElections[electionKey0].PrivKey != rawElectionInfosRef[electionKey0].PrivKey {
		t.FailNow()
	}
	if rawElections[ek1].PrivKey != rawElectionsRef[ek1].PrivKey {
	if rawElections[electionKey1].PrivKey != rawElectionInfosRef[electionKey1].PrivKey {
		t.FailNow()
	}
	if rawElections[ek0].PubKey != rawElectionsRef[ek0].PubKey {
	if rawElections[electionKey0].PubKey != rawElectionInfosRef[electionKey0].PubKey {
		t.FailNow()
	}
	if rawElections[ek1].PubKey != rawElectionsRef[ek1].PubKey {
	if rawElections[electionKey1].PubKey != rawElectionInfosRef[electionKey1].PubKey {
		t.FailNow()
	}
}

M main.go => main.go +48 -12
@@ 24,7 24,7 @@ const protocolID = protocol.ID("/tallyard/0.0.0")

var logger = log.Logger("tallyard")

func bootstrap(election *Election, me *Me, merkleRoot []byte, hostOpts ...libp2p.Option) {
func bootstrap(election *Election, me *Me, merkleRoot []byte, electionKey string, hostOpts ...libp2p.Option) {
	me.ctx = context.Background()
	election.remoteVoters = make(map[peer.ID]*Voter)



@@ 67,7 67,17 @@ func bootstrap(election *Election, me *Me, merkleRoot []byte, hostOpts ...libp2p

	if election.masterID == "" { // we are the master
		fmt.Println("share this with peers:")
		fmt.Printf("%s0%s\n", base58.Encode(merkleRoot), me.ID())
		electionKey = fmt.Sprintf("%s0%s", base58.Encode(merkleRoot), me.ID())
		fmt.Printf("%s\n", electionKey)

		err := saveElectionInfo(
			electionKey,
			me.Peerstore().PrivKey(me.ID()),
			me.Peerstore().PubKey(me.ID()),
		)
		if err != nil {
			panic(err)
		}

		logger.Info("waiting for incoming streams and finding voters...")



@@ 83,7 93,7 @@ func bootstrap(election *Election, me *Me, merkleRoot []byte, hostOpts ...libp2p

		fmt.Println("press ENTER to solidify group of voters and start voting")
		stdReader := bufio.NewReader(os.Stdin)
		_, err := stdReader.ReadString('\n')
		_, err = stdReader.ReadString('\n')
		if err != nil {
			panic(err)
		}


@@ 107,6 117,15 @@ func bootstrap(election *Election, me *Me, merkleRoot []byte, hostOpts ...libp2p
		}
		election.RUnlock()
	} else { // we are a slave
		err := saveElectionInfo(
			electionKey,
			me.Peerstore().PrivKey(me.ID()),
			me.Peerstore().PubKey(me.ID()),
		)
		if err != nil {
			panic(err)
		}

		logger.Info("attempting to open stream with master peer...")
		stream, err := me.NewStream(me.ctx, election.masterID, protocolID)
		rw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream))


@@ 163,9 182,11 @@ func bootstrap(election *Election, me *Me, merkleRoot []byte, hostOpts ...libp2p
func main() {
	log.SetAllLoggers(log.LevelWarn)

	debug := flag.Bool("v", false, "enable verbose logging for debugging")
	verbose := flag.Bool("v", false, "enable verbose logging for debugging")
	master := flag.Bool("m", false, "indicate that this node is the " +
		"master and the candidates are given via positional arguments")
	flag.Parse()
	if *debug {
	if *verbose {
		log.SetLogLevel("tallyard", "info")
	} else {
		log.SetLogLevel("dht", "critical")


@@ 181,8 202,19 @@ func main() {
		merkleRoot  []byte
	)

	// check if election key was given via CLI args
	if electionKey = flag.Arg(0); electionKey != "" {
	if *master {
		// we are the master and the candidates are the positional
		// arguments
		election = new(Election)
		for _, candidate := range flag.Args() {
			election.candidates = append(
				election.candidates,
				Candidate(candidate),
			)
		}
	} else if electionKey = flag.Arg(0); electionKey != "" {
		// we are a slave slave the and election key was given via CLI
		// args
		election = new(Election)
		merkleRoot, election.masterID = splitElectionKey(electionKey)
	} else {


@@ 196,18 228,22 @@ func main() {

	if electionKey != "" { // we are a slave
		// try to recover election info if we've joined before
		if electionConfig := getElectionConfigMaybe(electionKey); electionConfig != nil {
		electionInfo, err := getElectionInfo(electionKey)
		if err != nil {
			panic(err)
		}
		if electionInfo != nil {
			// ensure public key from config is same as one derived
			// from private key
			if !electionConfig.PrivKey.GetPublic().Equals(electionConfig.PubKey) {
			if !electionInfo.PrivKey.GetPublic().Equals(electionInfo.PubKey) {
				// TODO: handle properly
				panic("public keys do not match")
				panic("loaded public key does not match derived one from private key")
			}
			identityOpt = libp2p.Identity(electionConfig.PrivKey)
			identityOpt = libp2p.Identity(electionInfo.PrivKey)
		}
	}

	bootstrap(election, me, merkleRoot, identityOpt)
	bootstrap(election, me, merkleRoot, electionKey, identityOpt)

	startVoting(election, me)
}