M cmd/tallyard/main.go => cmd/tallyard/main.go +41 -33
@@ 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)
}
M election/election.go => election/election.go +13 -11
@@ 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),
}
}
M election/map.go => election/map.go +48 -15
@@ 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
})
M election/msg.go => election/msg.go +104 -60
@@ 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
}
M election/version.go => election/version.go +1 -1
@@ 1,3 1,3 @@
package election
-const Version string = "0.0.0"
+const Version string = "0.3.0"
M election/voter.go => election/voter.go +156 -75
@@ 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 {
M math/poly.go => math/poly.go +4 -4
@@ 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)
M matrix/data.go => matrix/data.go +6 -6
@@ 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"
M ui/tui.go => ui/tui.go +38 -40
@@ 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