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)
}