M cmd/tallyard/main.go => cmd/tallyard/main.go +20 -14
@@ 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) {
M election/event.go => election/event.go +21 -1
@@ 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()
M election/msg.go => election/msg.go +16 -20
@@ 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
}
A election/result.go => election/result.go +165 -0
@@ 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
+}
M election/voter.go => election/voter.go +37 -166
@@ 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
}