From afd718e65efcb1f9ed211611082c478b225d1da8 Mon Sep 17 00:00:00 2001 From: David Florness Date: Mon, 18 Jan 2021 20:06:45 -0500 Subject: [PATCH] Store elections in data and use join_event_id for some messages --- cmd/tallyard/main.go | 74 +++++++------- election/election.go | 24 ++--- election/map.go | 63 +++++++++--- election/msg.go | 164 +++++++++++++++++++----------- election/version.go | 2 +- election/voter.go | 231 +++++++++++++++++++++++++++++-------------- math/poly.go | 8 +- matrix/data.go | 12 +-- ui/tui.go | 78 +++++++-------- 9 files changed, 411 insertions(+), 245 deletions(-) diff --git a/cmd/tallyard/main.go b/cmd/tallyard/main.go index 565553d..a935e2c 100644 --- a/cmd/tallyard/main.go +++ b/cmd/tallyard/main.go @@ -4,10 +4,10 @@ import ( "fmt" "os" - "github.com/kyoh86/xdg" log "github.com/sirupsen/logrus" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "tallyard.xyz/election" "tallyard.xyz/math" @@ -20,22 +20,25 @@ func DebugCB(source mautrix.EventSource, evt *event.Event) { fmt.Printf("%[5]d: <%[1]s> %[4]s (%[2]s/%[3]s)\n", evt.Sender, evt.Type.String(), evt.ID, evt.Content.AsMessage().Body, source) } -var gobStorePath string = xdg.DataHome() + "/tallyard/gob.dat" -var electionFilter *mautrix.Filter = &mautrix.Filter{ - Room: mautrix.RoomFilter{ - Timeline: mautrix.FilterPart{ - // TODO properly handle too many events (newest events may be first batch) - Limit: 100000, - Types: []event.Type{ - election.CreateElectionMessage, - election.JoinElectionMessage, - election.StartElectionMessage, - election.EvalMessage, - election.SumMessage, - election.ResultMessage, +func electionFilter(localUserID id.UserID) *mautrix.Filter { + return &mautrix.Filter{ + Room: mautrix.RoomFilter{ + Timeline: mautrix.FilterPart{ + // TODO properly handle too many events (newest + // events are likely to be in first batch) + Limit: 100000, + NotSenders: []id.UserID{localUserID}, + Types: []event.Type{ + election.CreateElectionMessage, + election.JoinElectionMessage, + election.StartElectionMessage, + election.EvalMessage, + election.SumMessage, + election.ResultMessage, + }, }, }, - }, + } } func main() { @@ -50,13 +53,10 @@ func main() { panic(err) } - if data.LocalVoter == nil { - data.LocalVoter = election.NewLocalVoter(client.UserID) - data.Save() + if data.Elections == nil { + data.Elections = election.NewElectionsMap() } - elections := election.NewElectionsMap() - syncer := client.Syncer.(*mautrix.DefaultSyncer) syncer.OnEvent(client.Store.(*mautrix.InMemoryStore).UpdateState) syncer.OnEventType(election.CreateElectionMessage, func(source mautrix.EventSource, evt *event.Event) { @@ -65,7 +65,8 @@ func main() { log.Debug("redacted") return } - election.OnCreateElectionMessage(evt, elections) + election.OnCreateElectionMessage(evt, data.Elections) + data.Save() }) syncer.OnEventType(election.JoinElectionMessage, func(source mautrix.EventSource, evt *event.Event) { DebugCB(source, evt) @@ -73,7 +74,8 @@ func main() { log.Debug("redacted") return } - election.OnJoinElectionMessage(client, evt, elections) + election.OnJoinElectionMessage(client, evt, data.Elections) + data.Save() }) syncer.OnEventType(election.StartElectionMessage, func(source mautrix.EventSource, evt *event.Event) { DebugCB(source, evt) @@ -81,7 +83,8 @@ func main() { log.Debug("redacted") return } - election.OnStartElectionMessage(client, evt, elections) + election.OnStartElectionMessage(client, evt, data.Elections) + data.Save() }) syncer.OnEventType(election.EvalMessage, func(source mautrix.EventSource, evt *event.Event) { DebugCB(source, evt) @@ -89,7 +92,8 @@ func main() { log.Debug("redacted") return } - election.OnEvalMessage(client, evt, elections, data.LocalVoter) + election.OnEvalMessage(client, evt, data.Elections) + data.Save() }) syncer.OnEventType(election.SumMessage, func(source mautrix.EventSource, evt *event.Event) { DebugCB(source, evt) @@ -97,19 +101,21 @@ func main() { log.Debug("redacted") return } - election.OnSumMessage(client, evt, elections) + election.OnSumMessage(client, evt, data.Elections) + data.Save() }) syncer.OnEventType(election.ResultMessage, func(source mautrix.EventSource, evt *event.Event) { + DebugCB(source, evt) if evt.Unsigned.RedactedBecause != nil { log.Debug("redacted") return } - DebugCB(source, evt) - election.OnResultMessage(client, evt, elections) + election.OnResultMessage(client, evt, data.Elections) + data.Save() }) go func() { - res, err := client.CreateFilter(electionFilter) + res, err := client.CreateFilter(electionFilter(client.UserID)) if err != nil { panic(err) } @@ -120,20 +126,22 @@ func main() { } }() - el, ballot := ui.TUI(client, elections, data.LocalVoter) + el := ui.TUI(client, data.Elections) - data.LocalVoter.Poly = math.NewRandomPoly(uint(len(el.Voters)-1), 1024, ballot) + el.Lock() + el.LocalVoter.Poly = math.NewRandomPoly(uint(len(*el.FinalVoters)-1), 1024, *el.LocalVoter.Ballot) + el.Unlock() // TODO we may not have all voters' info - err = data.LocalVoter.SendEvals(client, el) + err = el.SendEvals(client) if err != nil { panic(err) } - err = data.LocalVoter.SendSum(client, el) + err = el.SendSum(client) if err != nil { panic(err) } - election.GetSums(client, el) + el.GetSums(client) } diff --git a/election/election.go b/election/election.go index b4b1c42..00af9cc 100644 --- a/election/election.go +++ b/election/election.go @@ -3,33 +3,35 @@ package election import ( "sync" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) type Election struct { sync.RWMutex - Candidates []Candidate - CreateEventId id.EventID - CreationTimestamp int64 - Creator id.UserID - RoomID id.RoomID - Started bool - Title string - Voters map[id.UserID]*Voter + Candidates []Candidate `json:"candidates"` + CreateEventId id.EventID `json:"create_event_id"` + CreationTimestamp int64 `json:"creation_timestamp"` + Creator id.UserID `json:"creator"` + FinalVoters *[]id.EventID `json:"final_voters,omitempty"` + Joins map[id.EventID]*Voter `json:"joins"` + LocalVoter *LocalVoter `json:"local_voter,omitempty"` + RoomID id.RoomID `json:"room_id"` + StartEvt *event.Event `json:"start_evt,omitempty"` + Title string `json:"title"` } func NewElection(candidates []Candidate, createEventId id.EventID, creationTimestamp int64, creator id.UserID, roomID id.RoomID, - started bool, title string) *Election { + title string) *Election { return &Election{ Candidates: candidates, CreateEventId: createEventId, CreationTimestamp: creationTimestamp, Creator: creator, + Joins: make(map[id.EventID]*Voter), RoomID: roomID, - Started: started, Title: title, - Voters: make(map[id.UserID]*Voter), } } diff --git a/election/map.go b/election/map.go index 1eff491..9ca94c9 100644 --- a/election/map.go +++ b/election/map.go @@ -1,6 +1,7 @@ package election import ( + "encoding/json" "sort" "sync" @@ -9,49 +10,81 @@ import ( type ElectionsMap struct{ sync.RWMutex - M map[id.EventID]*Election - L []*Election // sorted list of elections by CreationTimestamp (newest to oldest) + M map[id.EventID]*Election + L []*Election // sorted list of elections by CreationTimestamp (newest to oldest) + Joins map[id.EventID]*Voter } func NewElectionsMap() *ElectionsMap { return &ElectionsMap{ - M: make(map[id.EventID]*Election), - L: make([]*Election, 0), + M: make(map[id.EventID]*Election), + L: make([]*Election, 0), + Joins: make(map[id.EventID]*Voter), } } -func (em *ElectionsMap) Get(eventID id.EventID) *Election { +func (em *ElectionsMap) MarshalJSON() ([]byte, error) { + return json.Marshal(em.M) +} + +func (em *ElectionsMap) UnmarshalJSON(b []byte) error { + if err := json.Unmarshal(b, &em.M); err != nil { + return err + } + em.L = make([]*Election, 0) + for createEventId, el := range em.M { + em.insort(createEventId, el) + } + em.Joins = make(map[id.EventID]*Voter) + for _, el := range em.M { + for joinEventId, voter := range el.Joins { + em.Joins[joinEventId] = voter + } + } + return nil +} + +func (em *ElectionsMap) Get(createEventID id.EventID) *Election { em.RLock() defer em.RUnlock() - return em.M[eventID] + return em.M[createEventID] } -func (em *ElectionsMap) GetOk(eventID id.EventID) (*Election, bool) { +func (em *ElectionsMap) GetOk(createEventID id.EventID) (*Election, bool) { em.RLock() defer em.RUnlock() - el, ok := em.M[eventID] + el, ok := em.M[createEventID] return el, ok } -func (em *ElectionsMap) Set(eventID id.EventID, el *Election) { +func (em *ElectionsMap) GetI(i int) (*Election) { + em.RLock() + defer em.RUnlock() + return em.L[i] +} + +func (em *ElectionsMap) Set(createEventID id.EventID, el *Election) { em.Lock() defer em.Unlock() - em.set(eventID, el) + em.set(createEventID, el) } -func (em *ElectionsMap) SetIfNotExists(eventID id.EventID, el *Election) { +func (em *ElectionsMap) SetIfNotExists(createEventID id.EventID, el *Election) { em.Lock() defer em.Unlock() - _, exists := em.M[eventID] + _, exists := em.M[createEventID] if exists { return } - em.set(eventID, el) + em.set(createEventID, el) } -func (em *ElectionsMap) set(eventID id.EventID, el *Election) { - em.M[eventID] = el +func (em *ElectionsMap) set(createEventID id.EventID, el *Election) { + em.M[createEventID] = el + em.insort(createEventID, el) +} +func (em *ElectionsMap) insort(createEventID id.EventID, el *Election) { i := sort.Search(len(em.L), func(i int) bool { return em.L[i].CreationTimestamp < el.CreationTimestamp }) diff --git a/election/msg.go b/election/msg.go index b9bd08e..46ed119 100644 --- a/election/msg.go +++ b/election/msg.go @@ -59,23 +59,23 @@ type JoinElectionContent struct { } type StartElectionContent struct { - CreateEventId id.EventID `json:"create_event_id"` - Voters []id.UserID `json:"voters"` + CreateEventId id.EventID `json:"create_event_id"` + VoterJoinIds []id.EventID `json:"voter_join_ids"` } type EvalMessageContent struct { - CreateEventId id.EventID `json:"create_event_id"` - Outputs map[id.UserID]string `json:"outputs"` + JoinEventId id.EventID `json:"join_event_id"` + Outputs map[id.EventID]string `json:"outputs"` } type SumMessageContent struct { - CreateEventId id.EventID `json:"create_event_id"` - Sum string `json:"sum"` + JoinEventId id.EventID `json:"join_event_id"` + Sum string `json:"sum"` } type ResultMessageContent struct { - CreateEventId id.EventID `json:"create_event_id"` - Result string `json:"result"` + JoinEventId id.EventID `json:"join_event_id"` + Result string `json:"result"` } func init() { @@ -95,7 +95,7 @@ func OnCreateElectionMessage(evt *event.Event, elections *ElectionsMap) { return } elections.SetIfNotExists(evt.ID, NewElection(content.Candidates, evt.ID, - evt.Timestamp, evt.Sender, evt.RoomID, false, content.Title)) + evt.Timestamp, evt.Sender, evt.RoomID, content.Title)) } func getElection(client *mautrix.Client, roomID id.RoomID, createEventId id.EventID, elections *ElectionsMap) *Election { @@ -103,118 +103,152 @@ func getElection(client *mautrix.Client, roomID id.RoomID, createEventId id.Even if exists { return el } + // TODO: this endpoint will be deprecated createEvent, err := client.GetEvent(roomID, createEventId) if err != nil { log.Warnf("couldn't retrieve election create event: %s", err) return nil } if createEvent.Unsigned.RedactedBecause != nil { - log.Warnf("election redacted") + log.Debug("election redacted") return nil } OnCreateElectionMessage(createEvent, elections) el, exists = elections.GetOk(createEventId) if !exists { - log.Warnf("couldn't create election") + log.Warn("couldn't create election") return nil } return el } +func getVoter(client *mautrix.Client, roomID id.RoomID, joinEventId id.EventID, elections *ElectionsMap) *Voter { + voter, exists := elections.Joins[joinEventId] + if exists { + return voter + } + // TODO: this endpoint will be deprecated + joinEvent, err := client.GetEvent(roomID, joinEventId) + if err != nil { + log.Warnf("couldn't retrieve join event: %s", err) + return nil + } + if joinEvent.Unsigned.RedactedBecause != nil { + log.Debug("join redacted") + return nil + } + OnJoinElectionMessage(client, joinEvent, elections) + voter, exists = elections.Joins[joinEventId] + if !exists { + log.Warn("couldn't find voter") + return nil + } + return voter +} + func OnJoinElectionMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) { content, ok := evt.Content.Parsed.(*JoinElectionContent) if !ok { - log.Warnf("ignoring %s's join since we couldn't cast message content to JoinElectionContent", evt.Sender) - return - } - bytes, err := base64.StdEncoding.DecodeString(content.Input) - if err != nil { - log.Warnf("ignoring %s's join since we couldn't decode their input", evt.Sender) + log.Warnf("ignoring %s's join msg since we couldn't cast message content to JoinElectionContent", evt.Sender) return } - input := new(big.Int).SetBytes(bytes) el := getElection(client, evt.RoomID, content.CreateEventId, elections) if el == nil { - log.Warnf("ignoring %s's join since the election doesn't exist", evt.Sender) + log.Warnf("ignoring %s's join msg since the election doesn't exist", evt.Sender) return } el.Lock() defer el.Unlock() - if el.Started { - // TODO this may not be true - log.Warnf("ignoring %s's join since the election has already started", evt.Sender) + _, voterExists := el.Joins[evt.ID] + if voterExists { + log.Debugf("ignoring %s's join msg since we already have their info", evt.Sender) return } - var pubKey [32]byte + bytes, err := base64.StdEncoding.DecodeString(content.Input) + if err != nil { + log.Warnf("ignoring %s's join msg since we couldn't decode their input", evt.Sender) + return + } + input := new(big.Int).SetBytes(bytes) bytes, err = base64.StdEncoding.DecodeString(content.NaclPublicKey) if err != nil { - log.Warnf("ignoring %s's join since we couldn't decode their public key", evt.Sender) + log.Warnf("ignoring %s's join msg since we couldn't decode their public key: %s", evt.Sender, err) return } + var pubKey [32]byte copy(pubKey[:], bytes) - el.Voters[evt.Sender] = NewVoter(evt.Sender, input, &pubKey) + voter := NewVoter(evt.Sender, input, &pubKey, evt) + el.Joins[evt.ID] = voter + elections.Joins[evt.ID] = voter } func OnStartElectionMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) { content, ok := evt.Content.Parsed.(*StartElectionContent) if !ok { - log.Warnf("ignoring %s's election start since we couldn't cast message content to StartElectionContent", evt.Sender) + log.Warnf("ignoring %s's start msg since we couldn't cast message content to StartElectionContent", evt.Sender) return } el := getElection(client, evt.RoomID, content.CreateEventId, elections) if el == nil { - log.Warnf("ignoring %s's election start since the election doesn't exist", evt.Sender) + log.Warnf("ignoring %s's start msg since the election doesn't exist", evt.Sender) return } el.Lock() defer el.Unlock() if evt.Sender != el.Creator { - log.Warnf("ignoring %s's election start since they didn't start the election", evt.Sender) + log.Warnf("ignoring %s's start msg since they didn't start the election", evt.Sender) return } + // TODO we should probably just bail when there are multiple start messages + if el.StartEvt != nil && el.StartEvt.Timestamp < evt.Timestamp { + log.Warnf("ignoring %s's start msg since the election's already been started", evt.Sender) + return + } + el.StartEvt = evt + el.FinalVoters = &content.VoterJoinIds // TODO check election voters - el.Started = true } -func OnEvalMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap, localVoter *LocalVoter) { +func OnEvalMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) { content, ok := evt.Content.Parsed.(*EvalMessageContent) if !ok { - log.Warn("ignoring %s's eval message since we couldn't cast message content to EvalMessageContent", evt.Sender) + log.Warn("ignoring %s's eval msg since we couldn't cast message content to EvalMessageContent", evt.Sender) return } - encodedEncryptedOutput, ok := content.Outputs[localVoter.UserID] - if !ok { - log.Errorf("our user ID was not included in an eval message! The election will be unable to finish; blame %s", evt.Sender) + voter := getVoter(client, evt.RoomID, content.JoinEventId, elections) + if voter == nil { + log.Warnf("ignoring %s's eval msg since voter doesn't exist", evt.Sender) return } - encryptedOutput, err := base64.StdEncoding.DecodeString(encodedEncryptedOutput) - if err != nil { - log.Errorf("couldn't decode %s's encrypted output: %s", evt.Sender, err) - return - } - el := getElection(client, evt.RoomID, content.CreateEventId, elections) + // if Content were faulty, voter would have been nil + createEventId := voter.JoinEvt.Content.Parsed.(*JoinElectionContent).CreateEventId + el := getElection(client, evt.RoomID, createEventId, elections) if el == nil { - log.Warnf("ignoring %s's eval message since the election doesn't exist", evt.Sender) + log.Warnf("ignoring %s's eval msg since the election doesn't exist", evt.Sender) return } el.Lock() defer el.Unlock() - voter, exists := el.Voters[evt.Sender] - if !exists { - log.Warnf("ignoring %s's eval message since they are not a voter", evt.Sender) + encodedEncryptedOutput, ok := content.Outputs[el.LocalVoter.JoinEvt.ID] + if !ok { + log.Errorf("our user ID was not included in an eval message! The election will be unable to finish; blame %s", evt.Sender) + return + } + encryptedOutput, err := base64.StdEncoding.DecodeString(encodedEncryptedOutput) + if err != nil { + log.Errorf("couldn't decode %s's encrypted output: %s", evt.Sender, err) return } var decryptNonce [24]byte copy(decryptNonce[:], encryptedOutput[:24]) - decryptedOutput, ok := box.Open(nil, encryptedOutput[24:], &decryptNonce, voter.PubKey, localVoter.PrivKey) - + decryptedOutput, ok := box.Open(nil, encryptedOutput[24:], &decryptNonce, &voter.PubKey, &el.LocalVoter.PrivKey) if !ok { log.Errorf("decryption error") return } - voter.Output = new(big.Int).SetBytes(decryptedOutput) + voter.Eval = new(big.Int).SetBytes(decryptedOutput) } func OnSumMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) { @@ -223,21 +257,26 @@ func OnSumMessage(client *mautrix.Client, evt *event.Event, elections *Elections log.Warnf("ignoring %s's sum since we couldn't cast message content to SumMessageContent", evt.Sender) return } - bytes, err := base64.StdEncoding.DecodeString(content.Sum) - if err != nil { - log.Warnf("ignoring %s's sum since we couldn't decode their sum", evt.Sender) + voter := getVoter(client, evt.RoomID, content.JoinEventId, elections) + if voter == nil { + log.Warnf("ignoring %s's sum since voter doesn't exist", evt.Sender) return } - sum := new(big.Int).SetBytes(bytes) - el := getElection(client, evt.RoomID, content.CreateEventId, elections) + // if Content were faulty, voter would have been nil + createEventId := voter.JoinEvt.Content.Parsed.(*JoinElectionContent).CreateEventId + el := getElection(client, evt.RoomID, createEventId, elections) if el == nil { log.Warnf("ignoring %s's sum since the election does not exist", evt.Sender) return } el.Lock() defer el.Unlock() - // TODO: ensure voter exists - voter := el.Voters[evt.Sender] + bytes, err := base64.StdEncoding.DecodeString(content.Sum) + if err != nil { + log.Warnf("ignoring %s's sum since we couldn't decode their sum: %s", evt.Sender, err) + return + } + sum := new(big.Int).SetBytes(bytes) voter.Sum = sum } @@ -247,19 +286,24 @@ func OnResultMessage(client *mautrix.Client, evt *event.Event, elections *Electi log.Warnf("ignoring %s's result since we couldn't cast message content to ResultMessageContent", evt.Sender) return } - result, ok := new(big.Int).SetString(content.Result, 64) - if !ok { - log.Warnf("ignoring %s's result since we couldn't base64 decode the result", evt.Sender) + voter := getVoter(client, evt.RoomID, content.JoinEventId, elections) + if voter == nil { + log.Warnf("ignoring %s's sum since voter doesn't exist", evt.Sender) return } - el := getElection(client, evt.RoomID, content.CreateEventId, elections) + createEventId := voter.JoinEvt.Content.Parsed.(*JoinElectionContent).CreateEventId + el := getElection(client, evt.RoomID, createEventId, elections) if el == nil { log.Warnf("ignoring %s's result since the election does not exist", evt.Sender) return } el.Lock() defer el.Unlock() - // TODO check that voter exists - voter := el.Voters[evt.Sender] + bytes, err := base64.StdEncoding.DecodeString(content.Result) + if err != nil { + log.Warnf("ignoring %s's result since we couldn't decode the result: %s", evt.Sender, err) + return + } + result := new(big.Int).SetBytes(bytes) voter.Result = result } diff --git a/election/version.go b/election/version.go index 49df425..a13309a 100644 --- a/election/version.go +++ b/election/version.go @@ -1,3 +1,3 @@ package election -const Version string = "0.0.0" +const Version string = "0.3.0" diff --git a/election/voter.go b/election/voter.go index 8806a54..3a848d8 100644 --- a/election/voter.go +++ b/election/voter.go @@ -3,6 +3,7 @@ package election import ( "crypto/rand" "encoding/base64" + "errors" "fmt" "io" "math/big" @@ -12,140 +13,221 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/crypto/nacl/box" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "tallyard.xyz/math" ) type Voter struct { - Input *big.Int - Output *big.Int - PubKey *[32]byte - Result *big.Int - Sum *big.Int - UserID id.UserID + Eval *big.Int `json:"eval,omitempty"` + Input big.Int `json:"input"` + JoinEvt event.Event `json:"join_evt"` + PubKey [32]byte `json:"pub_key"` + Result *big.Int `json:"result,omitempty"` + Sum *big.Int `json:"sum,omitempty"` + UserID id.UserID `json:"user_id"` } type LocalVoter struct { *Voter - PrivKey *[32]byte - ballot []byte - Poly *math.Poly + Ballot *[]byte `json:"ballot,omitempty"` + Poly *math.Poly `json:"poly,omitempty"` + PrivKey [32]byte `json:"priv_key"` } -func NewVoter(userID id.UserID, input *big.Int, pubKey *[32]byte) *Voter { +func NewVoter(userID id.UserID, input *big.Int, pubKey *[32]byte, joinEvt *event.Event) *Voter { return &Voter{ - UserID: userID, - Input: input, - PubKey: pubKey, + Input: *new(big.Int).Set(input), + JoinEvt: *joinEvt, + PubKey: *pubKey, + UserID: userID, } } -func NewLocalVoter(userID id.UserID) *LocalVoter { - pubKey, privKey, err := box.GenerateKey(rand.Reader) +func CreateElection(client *mautrix.Client, candidates []Candidate, title string, roomID id.RoomID, elections *ElectionsMap) (*Election, error) { + resp, err := client.SendMessageEvent(roomID, CreateElectionMessage, CreateElectionContent{ + Candidates: candidates, + Title: title, + Version: Version, + }) if err != nil { - panic(err) + return nil, err } - input, err := math.RandomBigInt(1024, false) + + // TODO: this will be deprecated + createEvt, err := client.GetEvent(roomID, resp.EventID) if err != nil { - panic(err) + return nil, err } - return &LocalVoter{ - Voter: NewVoter(userID, input, pubKey), - PrivKey: privKey, + + OnCreateElectionMessage(createEvt, elections) + el, exists := elections.GetOk(resp.EventID) + if !exists { + return nil, errors.New("couldn't create election") } + return el, nil } -func CreateElection(client *mautrix.Client, candidates []Candidate, title string, roomID id.RoomID) (id.EventID, error) { - resp, err := client.SendMessageEvent(roomID, CreateElectionMessage, CreateElectionContent{ - Candidates: candidates, - Title: title, - Version: Version, - }) - return resp.EventID, err -} +func (el *Election) JoinElection(client *mautrix.Client) error { + pubKey, privKey, err := box.GenerateKey(rand.Reader) + if err != nil { + return err + } + input, err := math.RandomBigInt(1024, false) + if err != nil { + return err + } -func (localVoter *LocalVoter) JoinElection(client *mautrix.Client, el *Election) error { - el.RLock() - defer el.RUnlock() - _, err := client.SendMessageEvent(el.RoomID, JoinElectionMessage, JoinElectionContent{ + el.Lock() + defer el.Unlock() + + resp, err := client.SendMessageEvent(el.RoomID, JoinElectionMessage, JoinElectionContent{ CreateEventId: el.CreateEventId, - Input: base64.StdEncoding.EncodeToString(localVoter.Input.Bytes()), - NaclPublicKey: base64.StdEncoding.EncodeToString((*localVoter.PubKey)[:]), + Input: base64.StdEncoding.EncodeToString(input.Bytes()), + NaclPublicKey: base64.StdEncoding.EncodeToString((*pubKey)[:]), }) - return err + if err != nil { + return err + } + + // TODO: this will be deprecated + joinEvt, err := client.GetEvent(el.RoomID, resp.EventID) + if err != nil { + return err + } + + el.LocalVoter = &LocalVoter{ + Voter: NewVoter(client.UserID, input, pubKey, joinEvt), + PrivKey: *privKey, + } + el.Joins[resp.EventID] = el.LocalVoter.Voter + return nil } -func StartElection(client *mautrix.Client, el *Election) error { +func (el *Election) StartElection(client *mautrix.Client) error { // TODO err from this function if we didn't create the election - el.RLock() - defer el.RUnlock() - voters := make([]id.UserID, 0, len(el.Voters)) - for userID := range el.Voters { - voters = append(voters, userID) + el.Lock() + defer el.Unlock() + userIdMap := make(map[id.UserID]*Voter) + // one vote per userID + for _, voter := range el.Joins { + prevVoter, exists := userIdMap[voter.UserID] + if exists { + // use latest join + if voter.JoinEvt.Timestamp > prevVoter.JoinEvt.Timestamp { + userIdMap[voter.UserID] = voter + } + } else { + userIdMap[voter.UserID] = voter + } + } + voters := make([]id.EventID, 0, len(userIdMap)) + for _, voter := range userIdMap { + voters = append(voters, voter.JoinEvt.ID) } - _, err := client.SendMessageEvent(el.RoomID, StartElectionMessage, StartElectionContent{ + resp, err := client.SendMessageEvent(el.RoomID, StartElectionMessage, StartElectionContent{ CreateEventId: el.CreateEventId, - Voters: voters, + VoterJoinIds: voters, }) + if err != nil { + return err + } + startEvt, err := client.GetEvent(el.RoomID, resp.EventID) + if err != nil { + return err + } + el.StartEvt = startEvt return err } -func (localVoter *LocalVoter) SendEvals(client *mautrix.Client, el *Election) error { +func (el *Election) WaitForVoters(client *mautrix.Client) error { el.RLock() - defer el.RUnlock() + if el.StartEvt == nil { + return errors.New("WaitForVoters called before election started") + } + finalVoters := *el.FinalVoters + el.RUnlock() + var wg sync.WaitGroup + for _, voterJoinId := range finalVoters { + wg.Add(1) + go func(voterJoinId id.EventID) { + _, exists := el.Joins[voterJoinId] + for !exists { + time.Sleep(time.Millisecond * 100) + _, exists = el.Joins[voterJoinId] + } + wg.Done() + }(voterJoinId) + } + wg.Wait() + return nil +} + +func (el *Election) SendEvals(client *mautrix.Client) error { + // this assumes we have all needed voter data + el.Lock() + defer el.Unlock() content := EvalMessageContent{ - CreateEventId: el.CreateEventId, - Outputs: make(map[id.UserID]string), + JoinEventId: el.LocalVoter.JoinEvt.ID, + Outputs: make(map[id.EventID]string), } - for _, voter := range el.Voters { - output := localVoter.Poly.Eval(voter.Input).Bytes() + for _, voterJoinId := range *el.FinalVoters { + voter := el.Joins[voterJoinId] + output := el.LocalVoter.Poly.Eval(&voter.Input) + if voter.JoinEvt.ID == el.LocalVoter.JoinEvt.ID { + el.LocalVoter.Eval = output + } var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { return err } - encrypted := box.Seal(nonce[:], output, &nonce, voter.PubKey, localVoter.PrivKey) - content.Outputs[voter.UserID] = base64.StdEncoding.EncodeToString(encrypted) + encrypted := box.Seal(nonce[:], output.Bytes(), &nonce, &voter.PubKey, &el.LocalVoter.PrivKey) + content.Outputs[voter.JoinEvt.ID] = base64.StdEncoding.EncodeToString(encrypted) } _, err := client.SendMessageEvent(el.RoomID, EvalMessage, content) - return err + if err != nil { + el.LocalVoter.Eval = nil + return err + } + return nil } -func (locelVoter *LocalVoter) SendSum(client *mautrix.Client, el *Election) error { +func (el *Election) SendSum(client *mautrix.Client) error { sum := big.NewInt(0) var wg sync.WaitGroup - el.RLock() - for _, voter := range el.Voters { + for _, voterJoinId := range *el.FinalVoters { wg.Add(1) go func(voter *Voter) { - for voter.Output == nil { + for voter.Eval == nil { time.Sleep(time.Millisecond * 100) } - sum.Add(sum, voter.Output) + sum.Add(sum, voter.Eval) wg.Done() - }(voter) + }(el.Joins[voterJoinId]) } - el.RUnlock() wg.Wait() _, err := client.SendMessageEvent(el.RoomID, SumMessage, SumMessageContent{ - CreateEventId: el.CreateEventId, - Sum: base64.StdEncoding.EncodeToString(sum.Bytes()), + JoinEventId: el.LocalVoter.JoinEvt.ID, + Sum: base64.StdEncoding.EncodeToString(sum.Bytes()), }) - return err + if err != nil { + return err + } + el.LocalVoter.Sum = sum + return nil } -func GetSums(client *mautrix.Client, el *Election) { +func (el *Election) GetSums(client *mautrix.Client) { var wg sync.WaitGroup - el.RLock() - for _, voter := range el.Voters { + for _, voterJoinId := range *el.FinalVoters { wg.Add(1) go func(voter *Voter) { for voter.Sum == nil { time.Sleep(time.Millisecond * 100) } wg.Done() - }(voter) + }(el.Joins[voterJoinId]) } - el.RUnlock() wg.Wait() M := constructPolyMatrix(el) @@ -336,28 +418,27 @@ func GetSums(client *mautrix.Client, el *Election) { // } func constructPolyMatrix(el *Election) math.Matrix { - mat := make(math.Matrix, len(el.Voters)) + mat := make(math.Matrix, len(el.Joins)) i := 0 - el.RLock() - for _, voter := range el.Voters { + for _, voterJoinId := range *el.FinalVoters { + voter := el.Joins[voterJoinId] mat[i] = make([]big.Rat, len(mat) + 1) // includes column for sum row := mat[i] row[0].SetInt64(1) var j int64 - for j = 1; j <= int64(len(el.Voters)-1); j++ { - row[j].SetInt(new(big.Int).Exp(voter.Input, big.NewInt(j), nil)) + for j = 1; j <= int64(len(*el.FinalVoters)-1); j++ { + row[j].SetInt(new(big.Int).Exp(&voter.Input, big.NewInt(j), nil)) } row[j].SetInt(voter.Sum) i++ } - el.RUnlock() return mat } func printResults(result []byte, candidates []Candidate) { - log.Infof("result: %v", result) + log.Debugf("result: %v", result) fmt.Println("=== Results ===") n := len(candidates) for i, cand := range candidates { diff --git a/math/poly.go b/math/poly.go index 22e995f..6187ad6 100644 --- a/math/poly.go +++ b/math/poly.go @@ -7,8 +7,8 @@ import ( ) type Poly struct { - constant *big.Int - coefs []*big.Int + Constant *big.Int `json:"constant"` + Coefs []*big.Int `json:"coefs"` } func RandomBigInt(numBytes uint, allowAllZeros bool) (*big.Int, error) { @@ -56,9 +56,9 @@ func NewRandomPoly(degree uint, entropy uint, ballot []byte) *Poly { } func (p *Poly) Eval(input *big.Int) *big.Int { - res := new(big.Int).Set(p.constant) + res := new(big.Int).Set(p.Constant) - for i, coef := range p.coefs { + for i, coef := range p.Coefs { degree := big.NewInt(int64(i + 1)) term := new(big.Int).Exp(input, degree, nil) term.Mul(term, coef) diff --git a/matrix/data.go b/matrix/data.go index 6fac0ed..8ae4979 100644 --- a/matrix/data.go +++ b/matrix/data.go @@ -14,12 +14,12 @@ import ( ) type Data struct { - Homeserver string `json:"homeserver"` - Username string `json:"username"` - AccessToken string `json:"access_token"` - DeviceID id.DeviceID `json:"device_id"` - UserID id.UserID `json:"user_id"` - LocalVoter *election.LocalVoter `json:"local_voter,omitempty"` + AccessToken string `json:"access_token"` + DeviceID id.DeviceID `json:"device_id"` + Elections *election.ElectionsMap `json:"elections,omitempty"` + Homeserver string `json:"homeserver"` + UserID id.UserID `json:"user_id"` + Username string `json:"username"` } var dataFname = xdg.DataHome() + "/tallyard/data.json" diff --git a/ui/tui.go b/ui/tui.go index 3ad20b1..3574169 100644 --- a/ui/tui.go +++ b/ui/tui.go @@ -8,6 +8,8 @@ import ( "time" "unicode" + log "github.com/sirupsen/logrus" + "github.com/gdamore/tcell" "github.com/rivo/tview" "maunium.net/go/mautrix" @@ -16,7 +18,7 @@ import ( "tallyard.xyz/election" ) -func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *election.LocalVoter) (el *election.Election, ballot []byte) { +func TUI(client *mautrix.Client, elections *election.ElectionsMap) (el *election.Election) { alive := true app := tview.NewApplication() resp, err := client.JoinedRooms() @@ -48,14 +50,14 @@ func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *e } go func() { for alive { - time.Sleep(1 * time.Second) // syncing right away can be slow, so this is before the next line + time.Sleep(100 * time.Millisecond) // syncing right away can be slow, so this is before the next line app.QueueUpdateDraw(update) } }() list.SetSelectedFunc(func(i int, _ string, _ string, _ rune) { - app.Stop() alive = false - el, ballot = RoomTUI(client, resp.JoinedRooms[i], elections, localVoter) + app.Stop() + el = RoomTUI(client, resp.JoinedRooms[i], elections) }) if err := app.SetRoot(list, true).SetFocus(list).Run(); err != nil { panic(err) @@ -63,7 +65,7 @@ func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *e return } -func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap, localVoter *election.LocalVoter) (el *election.Election, ballot []byte) { +func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap) (el *election.Election) { alive := true app := tview.NewApplication() list := tview.NewList(). @@ -100,45 +102,44 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect } }() list.SetSelectedFunc(func(i int, _ string, _ string, _ rune) { - app.Stop() alive = false + app.Stop() if i > 0 { // user wants to join election - elections.RLock() - defer elections.RUnlock() - el = elections.L[i-1] - if joinElectionConfirmation(el, localVoter) { - _, alreadyElectionMember := el.Voters[localVoter.UserID] - if !alreadyElectionMember { - localVoter.JoinElection(client, el) + + el = elections.GetI(i-1) + // don't need to lock because this goroutine controls LocalVoter + if el.LocalVoter != nil { + if el.LocalVoter.Ballot != nil { + ElectionTUI(client, el) } - ballot = ElectionTUI(client, el, localVoter) + } else if joinElectionConfirmation(el) { + err := el.JoinElection(client) + if err != nil { + panic(err) + } + ElectionTUI(client, el) } else { - el, ballot = RoomTUI(client, roomID, elections, localVoter) + el = RoomTUI(client, roomID, elections) } return } // user wants to create election (i == 0) title, candidates := CreateElectionTUI() - fmt.Println("title", title) - fmt.Println("candidates", candidates) - eventID, err := election.CreateElection(client, candidates, title, roomID) + log.Debugf("title: %s", title) + log.Debugf("candidates: %s", candidates) + var err error + el, err = election.CreateElection(client, candidates, title, roomID, elections) if err != nil { panic(err) } - var ok bool - el, ok = elections.GetOk(eventID) - for !ok { - time.Sleep(10 * time.Millisecond) - el, ok = elections.GetOk(eventID) - } - err = localVoter.JoinElection(client, el) + err = el.JoinElection(client) if err != nil { panic(err) } - ballot = ElectionTUI(client, el, localVoter) + ElectionTUI(client, el) }) if err := app.SetRoot(list, true).SetFocus(list).Run(); err != nil { panic(err) @@ -146,19 +147,15 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect return } -func joinElectionConfirmation(el *election.Election, localVoter *election.LocalVoter) (shouldJoin bool) { +func joinElectionConfirmation(el *election.Election) (shouldJoin bool) { app := tview.NewApplication() var buttons []string var text string el.RLock() - if el.Voters[localVoter.UserID] != nil { - return true - } - // TODO: handle when election starts while in modal - if el.Started { + if el.StartEvt != nil { buttons = []string{"Ok"} text = "Election has already started, sorry" } else { @@ -222,12 +219,12 @@ func CreateElectionTUI() (title string, candidates []election.Candidate) { return title, candidates } -func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *election.LocalVoter) (ballot []byte) { +func ElectionTUI(client *mautrix.Client, el *election.Election) { votersTextView := tview.NewTextView() frame := tview.NewFrame(votersTextView) app := tview.NewApplication() el.RLock() - if el.Creator == localVoter.UserID { + if el.Creator == el.LocalVoter.UserID { frame.AddText("Press enter to start the election", false, tview.AlignCenter, tcell.ColorWhite) app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEnter { @@ -235,7 +232,7 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec frame.Clear() frame.AddText("Starting election...", false, tview.AlignCenter, tcell.ColorWhite) }) - err := election.StartElection(client, el) + err := el.StartElection(client) if err != nil { panic(err) } @@ -248,8 +245,9 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec el.RUnlock() update := func() { el.RLock() - voters := make([]string, 0, len(el.Voters)) - for voterUserID := range el.Voters { + // TODO: handle duplicate joins from one UserID + voters := make([]string, 0, len(el.Joins)) + for voterUserID := range el.Joins { voters = append(voters, voterUserID.String()) } el.RUnlock() @@ -264,7 +262,7 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec // has the election started? el.RLock() - started := el.Started + started := el.StartEvt != nil el.RUnlock() if started { app.Stop() @@ -281,8 +279,8 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec candidates := el.Candidates el.RUnlock() - ballot = Vote(candidates) - return + ballot := Vote(candidates) + el.LocalVoter.Ballot = &ballot } // displays a voting UI to the user and returns the encoded ballot -- 2.38.4