From 6a944b7a2259d2c6714ed40b4e3c8d8826c2bc77 Mon Sep 17 00:00:00 2001 From: David Florness Date: Sun, 14 Feb 2021 19:36:01 -0500 Subject: [PATCH] Send result event and use event store if possible --- cmd/tallyard/main.go | 34 +++++--- election/event.go | 22 ++++- election/msg.go | 36 ++++---- election/result.go | 165 +++++++++++++++++++++++++++++++++++ election/voter.go | 203 ++++++++----------------------------------- 5 files changed, 259 insertions(+), 201 deletions(-) create mode 100644 election/result.go diff --git a/cmd/tallyard/main.go b/cmd/tallyard/main.go index 1e55299..efbb050 100644 --- a/cmd/tallyard/main.go +++ b/cmd/tallyard/main.go @@ -136,32 +136,38 @@ func main() { el.Save() } - // set random poly with ballot - el.LocalVoter.Poly = math.NewRandomPoly(uint(len(*el.FinalJoinIDs)-1), 1024, *el.LocalVoter.Ballot) - el.Save() - - // wait for other voters to finish - el.WaitForJoins(client) + // set random poly with ballot if we haven't already + if el.LocalVoter.Poly == nil { + el.LocalVoter.Poly = math.NewRandomPoly(uint(len(*el.FinalJoinIDs)-1), 1024, *el.LocalVoter.Ballot) + el.Save() + } - // send evals if we need to (if LocalVoter.Evals is set, we've already - // sent the event) + // send evals if needed if el.LocalVoter.EvalsID == nil { - err = el.SendEvals(client) + err = el.SendEvals(client, elections.EventStore) + if err != nil { + panic(err) + } + } + + // send sum if needed + if el.LocalVoter.SumID == nil { + fmt.Println("waiting for evals...") + err = el.SendSum(client, elections.EventStore) if err != nil { panic(err) } } - // send sum if we need to (if LocalVoter.Sum is set, we've already sent - // the event) - if el.LocalVoter.Sum == nil { - err = el.SendSum(client) + // send result if needed + if el.LocalVoter.Result == nil { + err = el.SendResult(client, elections.EventStore) if err != nil { panic(err) } } - el.GetSums(client) + el.PrintResults() } func debugEventHook(_ mautrix.EventSource, evt *event.Event) { diff --git a/election/event.go b/election/event.go index d90ffc6..5f142e0 100644 --- a/election/event.go +++ b/election/event.go @@ -92,6 +92,11 @@ type SumEvent struct { *SumMessageContent } +type ResultEvent struct { + *event.Event + *ResultMessageContent +} + func (store *EventStore) GetCreateEvent(roomID id.RoomID, createID id.EventID) *CreateEvent { evt, err := store.getAndHandleEvent(roomID, createID, CreateElectionMessage) if err != nil { @@ -153,7 +158,7 @@ func (store *EventStore) GetEvalsEvent(roomID id.RoomID, evalsID id.EventID) *Ev } func (store *EventStore) GetSumEvent(roomID id.RoomID, sumID id.EventID) *SumEvent { - evt, err := store.getAndHandleEvent(roomID, sumID, EvalsMessage) + evt, err := store.getAndHandleEvent(roomID, sumID, SumMessage) if err != nil { log.Warnf("an error occurred getting sum event '%s': %s", sumID, err) return nil @@ -167,6 +172,21 @@ func (store *EventStore) GetSumEvent(roomID id.RoomID, sumID id.EventID) *SumEve } } +func (store *EventStore) GetResultEvent(roomID id.RoomID, resultID id.EventID) *ResultEvent { + evt, err := store.getAndHandleEvent(roomID, resultID, ResultMessage) + if err != nil { + log.Warnf("an error occurred getting result event '%s': %s", resultID, err) + return nil + } + if evt == nil { + return nil + } + return &ResultEvent{ + evt, + evt.Content.Parsed.(*ResultMessageContent), + } +} + func (store *EventStore) getAndHandleEvent(roomID id.RoomID, eventID id.EventID, eventType event.Type) (*event.Event, error) { // see if we've handled this event before store.RLock() diff --git a/election/msg.go b/election/msg.go index 8dd0bcb..30b4d5c 100644 --- a/election/msg.go +++ b/election/msg.go @@ -298,13 +298,12 @@ func (elections *ElectionsMap) onStartElectionMessage(evt *event.Event) (success el := elections.GetElection(createEvt.ID) if el == nil { - // should never happen because we retrieved the craete event + // should never happen because we retrieved the create event // above errorf("election %s doesn't exist", createEvt.ID) return } - // election should exist since we were able to getCreateEvent el.Lock() defer el.Save() defer el.Unlock() @@ -398,8 +397,8 @@ func (elections *ElectionsMap) onEvalsMessage(evt *event.Event) (success bool) { // If our ID doesn't exist in the keys of evalsEvtContent.Evals, our // JoinID wasn't included in the startEvtContent.JoinIDs (since we // checked that the two are equivalent above). I'm checking membership - // in evalsEvtContent.Evals instead of startEvtContent.JoinIDs because - // maps are easier than slices for that. + // in Evals instead of startEvt.JoinIDs because maps are easier than + // slices for that. if !exists { debugf("we didn't join the election in time (or the election creator excluded us)") return @@ -478,12 +477,11 @@ func (elections *ElectionsMap) onSumMessage(evt *event.Event) (success bool) { // evals event above. Recursion is wonderful. } - joinEvtContent := joinEvt.Content.Parsed.(*JoinElectionContent) - el := elections.GetElection(joinEvtContent.CreateID) + el := elections.GetElection(joinEvt.CreateID) if el == nil { // should never happen because we retrieved the start/join // events above - errorf("election %s doesn't exist", joinEvtContent.CreateID) + errorf("election %s doesn't exist", joinEvt.CreateID) return } @@ -498,21 +496,19 @@ func (elections *ElectionsMap) onSumMessage(evt *event.Event) (success bool) { return } - if voter.SumID != nil { + if voter.Sum != nil { warnf("voter submitted multiple sum events") return } - voter.SumID = &evt.ID - bytes, err := base64.StdEncoding.DecodeString(content.Sum) if err != nil { warnf("we couldn't decode their sum: %s", err) return } - sum := new(big.Int).SetBytes(bytes) - voter.Sum = sum + voter.SumID = &evt.ID + voter.Sum = new(big.Int).SetBytes(bytes) return true } @@ -573,32 +569,32 @@ func (elections *ElectionsMap) onResultMessage(evt *event.Event) (success bool) el := elections.GetElection(joinEvt.CreateID) if el == nil { // should never happen because we retrieved the join event - errorf("election %s doesn' exist", joinEvt.CreateID) + errorf("election %s doesn't exist", joinEvt.CreateID) return } + el.Lock() + defer el.Save() + defer el.Unlock() + voter := el.Joins[joinEvt.ID] if voter == nil { errorf("voter %s doesn't exist", joinEvt.ID) return } - if voter.ResultID != nil { + if voter.Result != nil { warnf("voter %s submitted multiple results", joinEvt.ID) return } - bytes, err := base64.StdEncoding.DecodeString(content.Result) + result, err := base64.StdEncoding.DecodeString(content.Result) if err != nil { warnf("we couldn't decode the result: %s", err) return } - el.Lock() - defer el.Unlock() - - result := new(big.Int).SetBytes(bytes) - voter.Result = result + voter.Result = &result return true } diff --git a/election/result.go b/election/result.go new file mode 100644 index 0000000..1c0d486 --- /dev/null +++ b/election/result.go @@ -0,0 +1,165 @@ +package election + +import ( + "encoding/base64" + "fmt" + "math/big" + "sort" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/id" + "tallyard.xyz/math" +) + +func (el *Election) SendResult(client *mautrix.Client, eventStore *EventStore) error { + var sumIDs []id.EventID + var wg sync.WaitGroup + for _, voterJoinId := range *el.FinalJoinIDs { + wg.Add(1) + go func(voter *Voter) { + for voter.SumID == nil { + time.Sleep(time.Millisecond * 100) + } + sumIDs = append(sumIDs, *voter.SumID) + wg.Done() + }(el.Joins[voterJoinId]) + } + wg.Wait() + + M := constructPolyMatrix(el) + M.RREF() + constant := M[0][len(M[0])-1] + if !constant.IsInt() { + panic("constant term is not an integer") + } + result := constant.Num().Bytes() + // number of bytes we need to insert at the front since they're zero + diff := (len(el.Candidates) * len(el.Candidates)) - len(result) + result = append(make([]byte, diff), result...) + + resp, err := client.SendMessageEvent(el.RoomID, ResultMessage, ResultMessageContent{ + Version: Version, + JoinID: el.LocalVoter.JoinEvt.ID, + Result: base64.StdEncoding.EncodeToString(result), + SumIDs: sumIDs, + }) + if err != nil { + return err + } + + resultEvt := eventStore.GetResultEvent(el.RoomID, resp.EventID) + if resultEvt == nil { + return fmt.Errorf("couldn't process our own result event, %s", resp.EventID) + } + + return nil +} + +func (el *Election) PrintResults() { + if el.LocalVoter.Result == nil { + log.Error("PrintResults called before SendResult") + return + } + result := *el.LocalVoter.Result + candidates := el.Candidates + + log.Debugf("result: %v", result) + fmt.Println("=== Results ===") + n := len(candidates) + for i, cand := range candidates { + for j, vs := range candidates { + if i != j { + fmt.Printf("%s over %s: %d\n", cand, vs, result[i*n+j]) + } + } + } + + // Schulze method computation + // https://en.wikipedia.org/wiki/Schulze_method#Implementation + + min := func(x, y int) int { + if x < y { + return x + } + return y + } + + max := func(x, y int) int { + if x > y { + return x + } + return y + } + + p := make([][]int, len(candidates)) + for i := range candidates { + p[i] = make([]int, len(candidates)) + for j := range candidates { + if i != j { + if result[i*n+j] > result[j*n+i] { + p[i][j] = int(result[i*n+j]) + } else { + p[i][j] = 0 + } + } + } + } + + for i := range candidates { + for j := range candidates { + if i != j { + for k := range candidates { + if i != k && j != k { + p[j][k] = max(p[j][k], min(p[j][i], p[i][k])) + } + } + } + } + } + + order := []int{} + for i := range candidates { + k := sort.Search(len(order), func(j int) bool { + return p[i][order[j]] > p[order[j]][i] + }) + newOrder := make([]int, len(order)+1) + copy(newOrder[:k], order[:k]) + newOrder[k] = i + copy(newOrder[k+1:], order[k:]) + order = newOrder + } + + fmt.Printf("\n=== Schulze Method Ranking ===\n") + for i:= 0; i < len(order); i++ { + rank := i+1 // don't want 0th place + fmt.Printf("%d) %s\n", rank, candidates[order[i]]) + // candidates that are tied get the same rank + for i+1 < len(order) && p[order[i]][order[i+1]] == p[order[i+1]][order[i]] { + fmt.Printf("%d) %s\n", rank, candidates[order[i+1]]) + i++ + } + } +} + +func constructPolyMatrix(el *Election) math.Matrix { + mat := make(math.Matrix, len(*el.FinalJoinIDs)) + + i := 0 + for _, voterJoinId := range *el.FinalJoinIDs { + 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.FinalJoinIDs)-1); j++ { + row[j].SetInt(new(big.Int).Exp(&voter.Input, big.NewInt(j), nil)) + } + row[j].SetInt(voter.Sum) + i++ + } + + return mat +} diff --git a/election/voter.go b/election/voter.go index 6618e39..946f337 100644 --- a/election/voter.go +++ b/election/voter.go @@ -7,11 +7,9 @@ import ( "fmt" "io" "math/big" - "sort" "sync" "time" - log "github.com/sirupsen/logrus" "golang.org/x/crypto/nacl/box" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" @@ -20,16 +18,16 @@ import ( ) type Voter struct { - Input big.Int `json:"input"` - JoinEvt event.Event `json:"join_evt"` - PubKey [32]byte `json:"pub_key"` - - Eval *big.Int `json:"eval,omitempty"` - EvalsID *id.EventID `json:"evals_id,omitempty"` - Result *big.Int `json:"result,omitempty"` - ResultID *id.EventID `json:"result_id,omitempty"` - Sum *big.Int `json:"sum,omitempty"` - SumID *id.EventID `json:"sum_id,omitempty"` + Input big.Int `json:"input"` + JoinEvt event.Event `json:"join_evt"` + PubKey [32]byte `json:"pub_key"` + + Eval *big.Int `json:"eval,omitempty"` + EvalsID *id.EventID `json:"evals_id,omitempty"` + Result *[]byte `json:"result,omitempty"` + // no ResultID because it's the end of the graph + Sum *big.Int `json:"sum,omitempty"` + SumID *id.EventID `json:"sum_id,omitempty"` } type LocalVoter struct { @@ -47,6 +45,13 @@ func NewVoter(input *big.Int, joinEvt *event.Event, pubKey *[32]byte) *Voter { } } +func NewLocalVoter(voter *Voter, privKey *[32]byte) *LocalVoter { + return &LocalVoter{ + Voter: voter, + PrivKey: *privKey, + } +} + func (elections *ElectionsMap) CreateElection(client *mautrix.Client, candidates []Candidate, title string, roomID id.RoomID) (*Election, error) { resp, err := client.SendMessageEvent(roomID, CreateElectionMessage, CreateElectionContent{ Version: Version, @@ -95,10 +100,7 @@ func (el *Election) JoinElection(client *mautrix.Client, eventStore *EventStore) defer el.Save() defer el.Unlock() - el.LocalVoter = &LocalVoter{ - Voter: el.Joins[joinEvt.ID], - PrivKey: *privKey, - } + el.LocalVoter = NewLocalVoter(el.Joins[joinEvt.ID], privKey) return nil } @@ -145,60 +147,45 @@ func (el *Election) StartElection(client *mautrix.Client, eventStore *EventStore return nil } -func (el *Election) WaitForJoins(client *mautrix.Client) error { - fmt.Println("waiting for others...") +func (el *Election) SendEvals(client *mautrix.Client, eventStore *EventStore) error { el.RLock() if el.StartID == nil { - return errors.New("WaitForJoins called before election started") + return errors.New("SendEvals called before election started") } - finalVoters := *el.FinalJoinIDs - 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.Save() - defer el.Unlock() evals := make(map[id.EventID]string) for _, joinID := range *el.FinalJoinIDs { voter := el.Joins[joinID] eval := el.LocalVoter.Poly.Eval(&voter.Input) var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + el.RUnlock() return err } encrypted := box.Seal(nonce[:], eval.Bytes(), &nonce, &voter.PubKey, &el.LocalVoter.PrivKey) evals[voter.JoinEvt.ID] = base64.StdEncoding.EncodeToString(encrypted) } - _, err := client.SendMessageEvent(el.RoomID, EvalsMessage, EvalsMessageContent{ + + resp, err := client.SendMessageEvent(el.RoomID, EvalsMessage, EvalsMessageContent{ Version: Version, Evals: evals, JoinID: el.LocalVoter.JoinEvt.ID, StartID: *el.StartID, }) + el.RUnlock() if err != nil { - el.LocalVoter.Eval = nil return err } + + evalsEvt := eventStore.GetEvalsEvent(el.RoomID, resp.EventID) + if evalsEvt == nil { + return fmt.Errorf("couldn't process our own evals event, %s", resp.EventID) + } + return nil } -func (el *Election) SendSum(client *mautrix.Client) error { +func (el *Election) SendSum(client *mautrix.Client, eventStore *EventStore) error { sum := big.NewInt(0) var evalsIDs []id.EventID var wg sync.WaitGroup @@ -214,7 +201,8 @@ func (el *Election) SendSum(client *mautrix.Client) error { }(el.Joins[voterJoinId]) } wg.Wait() - _, err := client.SendMessageEvent(el.RoomID, SumMessage, SumMessageContent{ + + resp, err := client.SendMessageEvent(el.RoomID, SumMessage, SumMessageContent{ Version: Version, EvalsIDs: evalsIDs, JoinID: el.LocalVoter.JoinEvt.ID, @@ -223,128 +211,11 @@ func (el *Election) SendSum(client *mautrix.Client) error { if err != nil { return err } - el.Lock() - defer el.Save() - defer el.Unlock() - el.LocalVoter.Sum = sum - return nil -} -func (el *Election) GetSums(client *mautrix.Client) { - var wg sync.WaitGroup - for _, voterJoinId := range *el.FinalJoinIDs { - wg.Add(1) - go func(voter *Voter) { - for voter.Sum == nil { - time.Sleep(time.Millisecond * 100) - } - wg.Done() - }(el.Joins[voterJoinId]) + sumEvt := eventStore.GetSumEvent(el.RoomID, resp.EventID) + if sumEvt == nil { + return fmt.Errorf("couldn't process our own sum event, %s", resp.EventID) } - wg.Wait() - - M := constructPolyMatrix(el) - M.RREF() - constant := M[0][len(M[0])-1] - if !constant.IsInt() { - panic("constant term is not an integer") - } - result := constant.Num().Bytes() - // number of bytes we need to insert at the front since they're zero - diff := (len(el.Candidates) * len(el.Candidates)) - len(result) - result = append(make([]byte, diff), result...) - printResults(result, el.Candidates) -} - -func constructPolyMatrix(el *Election) math.Matrix { - mat := make(math.Matrix, len(*el.FinalJoinIDs)) - i := 0 - for _, voterJoinId := range *el.FinalJoinIDs { - 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.FinalJoinIDs)-1); j++ { - row[j].SetInt(new(big.Int).Exp(&voter.Input, big.NewInt(j), nil)) - } - row[j].SetInt(voter.Sum) - i++ - } - - return mat -} - -func printResults(result []byte, candidates []Candidate) { - log.Debugf("result: %v", result) - fmt.Println("=== Results ===") - n := len(candidates) - for i, cand := range candidates { - for j, vs := range candidates { - if i != j { - fmt.Printf("%s over %s: %d\n", cand, vs, result[i*n+j]) - } - } - } - - // Schulze method computation - // https://en.wikipedia.org/wiki/Schulze_method#Implementation - min := func(x, y int) int { - if x < y { - return x - } - return y - } - max := func(x, y int) int { - if x > y { - return x - } - return y - } - p := make([][]int, len(candidates)) - for i := range candidates { - p[i] = make([]int, len(candidates)) - for j := range candidates { - if i != j { - if result[i*n+j] > result[j*n+i] { - p[i][j] = int(result[i*n+j]) - } else { - p[i][j] = 0 - } - } - } - } - for i := range candidates { - for j := range candidates { - if i != j { - for k := range candidates { - if i != k && j != k { - p[j][k] = max(p[j][k], min(p[j][i], p[i][k])) - } - } - } - } - } - order := []int{} - for i := range candidates { - k := sort.Search(len(order), func(j int) bool { - return p[i][order[j]] > p[order[j]][i] - }) - newOrder := make([]int, len(order)+1) - copy(newOrder[:k], order[:k]) - newOrder[k] = i - copy(newOrder[k+1:], order[k:]) - order = newOrder - } - fmt.Printf("\n=== Schulze Method Ranking ===\n") - for i:= 0; i < len(order); i++ { - rank := i+1 // don't want 0th place - fmt.Printf("%d) %s\n", rank, candidates[order[i]]) - // candidates that are tied get the same rank - for i+1 < len(order) && p[order[i]][order[i+1]] == p[order[i+1]][order[i]] { - fmt.Printf("%d) %s\n", rank, candidates[order[i+1]]) - i++ - } - } + return nil } -- 2.38.4