From 5a0610f62806678c5269470853961d01b9a7c838 Mon Sep 17 00:00:00 2001 From: David Florness Date: Sun, 17 Jan 2021 13:56:48 -0500 Subject: [PATCH] Begin work on evals --- cmd/tallyard/main.go | 31 ++++++++- election/msg.go | 14 +--- election/voter.go | 150 ++++++++++++++++++++++++++++++------------- ui/tui.go | 90 ++++++++++++++++---------- 4 files changed, 196 insertions(+), 89 deletions(-) diff --git a/cmd/tallyard/main.go b/cmd/tallyard/main.go index 789acc7..37e2517 100644 --- a/cmd/tallyard/main.go +++ b/cmd/tallyard/main.go @@ -9,14 +9,22 @@ import ( "maunium.net/go/mautrix/event" "tallyard.xyz/election" + "tallyard.xyz/math" "tallyard.xyz/matrix" "tallyard.xyz/ui" ) +func DebugCB(source mautrix.EventSource, evt *event.Event) { + return + 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, @@ -48,21 +56,27 @@ func main() { syncer := client.Syncer.(*mautrix.DefaultSyncer) syncer.OnEvent(client.Store.(*mautrix.InMemoryStore).UpdateState) syncer.OnEventType(election.CreateElectionMessage, func(source mautrix.EventSource, evt *event.Event) { + DebugCB(source, evt) election.OnCreateElectionMessage(source, evt, elections) }) syncer.OnEventType(election.JoinElectionMessage, func(source mautrix.EventSource, evt *event.Event) { + DebugCB(source, evt) election.OnJoinElectionMessage(source, evt, elections) }) syncer.OnEventType(election.StartElectionMessage, func(source mautrix.EventSource, evt *event.Event) { + DebugCB(source, evt) election.OnStartElectionMessage(source, evt, elections) }) syncer.OnEventType(election.EvalMessage, func(source mautrix.EventSource, evt *event.Event) { + DebugCB(source, evt) election.OnEvalMessage(source, evt, elections, localVoter) }) syncer.OnEventType(election.SumMessage, func(source mautrix.EventSource, evt *event.Event) { + DebugCB(source, evt) election.OnSumMessage(source, evt, elections) }) syncer.OnEventType(election.ResultMessage, func(source mautrix.EventSource, evt *event.Event) { + DebugCB(source, evt) election.OnResultMessage(source, evt, elections) }) @@ -78,5 +92,20 @@ func main() { } }() - ui.TUI(client, elections, localVoter) + el, ballot := ui.TUI(client, elections, localVoter) + + localVoter.Poly = math.NewRandomPoly(uint(len(el.Voters)-1), 1024, ballot) + + // TODO we may not have all voters' info + err = localVoter.SendEvals(client, el) + if err != nil { + panic(err) + } + + err = localVoter.SendSum(client, el) + if err != nil { + panic(err) + } + + election.GetSums(client, el) } diff --git a/election/msg.go b/election/msg.go index e79f20a..5148466 100644 --- a/election/msg.go +++ b/election/msg.go @@ -87,13 +87,7 @@ func init() { event.TypeMap[ResultMessage] = reflect.TypeOf(ResultMessageContent{}) } -func DebugCB(source mautrix.EventSource, evt *event.Event) { - return - // 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) -} - func OnCreateElectionMessage(source mautrix.EventSource, evt *event.Event, elections *ElectionsMap) { - DebugCB(source, evt) // TODO: check version content, ok := evt.Content.Parsed.(*CreateElectionContent) if !ok { @@ -105,7 +99,6 @@ func OnCreateElectionMessage(source mautrix.EventSource, evt *event.Event, elect } func OnJoinElectionMessage(source mautrix.EventSource, evt *event.Event, elections *ElectionsMap) { - DebugCB(source, evt) 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) @@ -121,6 +114,7 @@ func OnJoinElectionMessage(source mautrix.EventSource, evt *event.Event, electio 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) return } @@ -135,12 +129,12 @@ func OnJoinElectionMessage(source mautrix.EventSource, evt *event.Event, electio } func OnStartElectionMessage(source mautrix.EventSource, evt *event.Event, elections *ElectionsMap) { - DebugCB(source, evt) 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) return } + // TODO ensure election exists el := elections.Get(content.CreateEventId) el.Lock() defer el.Unlock() @@ -148,11 +142,11 @@ func OnStartElectionMessage(source mautrix.EventSource, evt *event.Event, electi log.Warnf("ignoring %s's election start since they didn't start the election", evt.Sender) return } + // TODO check election voters el.Started = true } func OnEvalMessage(source mautrix.EventSource, evt *event.Event, elections *ElectionsMap, localVoter *LocalVoter) { - DebugCB(source, evt) content, ok := evt.Content.Parsed.(*EvalMessageContent) if !ok { log.Warn("ignoring eval message since we couldn't cast message content to EvalMessageContent") @@ -186,7 +180,6 @@ func OnEvalMessage(source mautrix.EventSource, evt *event.Event, elections *Elec } func OnSumMessage(source mautrix.EventSource, evt *event.Event, elections *ElectionsMap) { - DebugCB(source, evt) content, ok := evt.Content.Parsed.(*SumMessageContent) if !ok { log.Warnf("ignoring %s's sum since we couldn't cast message content to SumMessageContent", evt.Sender) @@ -205,7 +198,6 @@ func OnSumMessage(source mautrix.EventSource, evt *event.Event, elections *Elect } func OnResultMessage(source mautrix.EventSource, evt *event.Event, elections *ElectionsMap) { - DebugCB(source, evt) content, ok := evt.Content.Parsed.(*ResultMessageContent) if !ok { log.Warnf("ignoring %s's result since we couldn't cast message content to ResultMessageContent", evt.Sender) diff --git a/election/voter.go b/election/voter.go index 909707d..9d2a5fe 100644 --- a/election/voter.go +++ b/election/voter.go @@ -4,7 +4,10 @@ import ( "crypto/rand" "encoding/base64" "fmt" + "io" "math/big" + "sync" + "time" log "github.com/sirupsen/logrus" "golang.org/x/crypto/nacl/box" @@ -26,7 +29,7 @@ type LocalVoter struct { *Voter PrivKey *[32]byte ballot []byte - poly *math.Poly + Poly *math.Poly } func NewVoter(userID id.UserID, input *big.Int, pubKey *[32]byte) *Voter { @@ -61,32 +64,102 @@ func CreateElection(client *mautrix.Client, candidates []Candidate, title string return resp.EventID, err } -func StartElection(client *mautrix.Client, election *Election) error { - // TODO check that we created the election - election.RLock() - defer election.RUnlock() - voters := make([]id.UserID, 0, len(election.Voters)) - for userID := range election.Voters { +func (localVoter *LocalVoter) JoinElection(client *mautrix.Client, el *Election) error { + el.RLock() + defer el.RUnlock() + _, err := client.SendMessageEvent(el.RoomID, JoinElectionMessage, JoinElectionContent{ + CreateEventId: el.CreateEventId, + Input: base64.StdEncoding.EncodeToString(localVoter.Input.Bytes()), + NaclPublicKey: base64.StdEncoding.EncodeToString((*localVoter.PubKey)[:]), + }) + return err +} + +func StartElection(client *mautrix.Client, el *Election) 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) } - _, err := client.SendMessageEvent(election.RoomID, StartElectionMessage, StartElectionContent{ - CreateEventId: election.CreateEventId, + _, err := client.SendMessageEvent(el.RoomID, StartElectionMessage, StartElectionContent{ + CreateEventId: el.CreateEventId, Voters: voters, }) return err } -func (localVoter *LocalVoter) JoinElection(client *mautrix.Client, election *Election) error { - election.RLock() - defer election.RUnlock() - _, err := client.SendMessageEvent(election.RoomID, JoinElectionMessage, JoinElectionContent{ - CreateEventId: election.CreateEventId, - Input: base64.StdEncoding.EncodeToString(localVoter.Input.Bytes()), - NaclPublicKey: base64.StdEncoding.EncodeToString((*localVoter.PubKey)[:]), +func (localVoter *LocalVoter) SendEvals(client *mautrix.Client, el *Election) error { + el.RLock() + defer el.RUnlock() + content := EvalMessageContent{ + CreateEventId: el.CreateEventId, + Outputs: make(map[id.UserID]string), + } + for _, voter := range el.Voters { + output := localVoter.Poly.Eval(voter.Input).Bytes() + 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) + } + _, err := client.SendMessageEvent(el.RoomID, EvalMessage, content) + return err +} + +func (locelVoter *LocalVoter) SendSum(client *mautrix.Client, el *Election) error { + sum := big.NewInt(0) + var wg sync.WaitGroup + el.RLock() + for _, voter := range el.Voters { + wg.Add(1) + go func(voter *Voter) { + for voter.Output == nil { + time.Sleep(time.Millisecond * 100) + } + sum.Add(sum, voter.Output) + }(voter) + } + el.RUnlock() + wg.Wait() + _, err := client.SendMessageEvent(el.RoomID, SumMessage, SumMessageContent{ + CreateEventId: el.CreateEventId, + Sum: base64.StdEncoding.EncodeToString(sum.Bytes()), }) return err } +func GetSums(client *mautrix.Client, el *Election) { + var wg sync.WaitGroup + el.RLock() + for _, voter := range el.Voters { + wg.Add(1) + go func(voter *Voter) { + for voter.Sum == nil { + time.Sleep(time.Millisecond * 100) + } + + }(voter) + } + el.RUnlock() + 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 handleCmd(cmd string, rw *bufio.ReadWriter, stream network.Stream, election *Election) { // localVoter := election.localVoter // switch cmd { @@ -261,35 +334,26 @@ func (localVoter *LocalVoter) JoinElection(client *mautrix.Client, election *Ele // select {} // } -// func constructPolyMatrix(election *Election) Matrix { -// mat := make(Matrix, len(election.remoteVoters) + 1) // row for everyone (including ourselves) - -// i := 0 -// for _, voter := range election.RemoteVoters { -// 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(election.RemoteVoters)); j++ { -// row[j].SetInt(new(big.Int).Exp(voter.input, big.NewInt(j), nil)) -// } -// row[j].SetInt(voter.sum) -// i++ -// } +func constructPolyMatrix(el *Election) math.Matrix { + mat := make(math.Matrix, len(el.Voters)) -// // row for ourselves -// mat[i] = make([]big.Rat, len(mat) + 1) -// row := mat[i] -// row[0].SetInt64(1) -// localVoter := election.localVoter -// var j int64 -// for j = 1; j <= int64(len(election.RemoteVoters)); j++ { -// row[j].SetInt(new(big.Int).Exp(localVoter.input, big.NewInt(j), nil)) -// } -// row[j].SetInt(localVoter.sum) + i := 0 + el.RLock() + for _, voter := range el.Voters { + 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)) + } + row[j].SetInt(voter.Sum) + i++ + } + el.RUnlock() -// return mat -// } + return mat +} func printResults(result []byte, candidates []Candidate) { log.Infof("result: %v", result) diff --git a/ui/tui.go b/ui/tui.go index 5780638..1855026 100644 --- a/ui/tui.go +++ b/ui/tui.go @@ -16,7 +16,7 @@ import ( "tallyard.xyz/election" ) -func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *election.LocalVoter) { +func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *election.LocalVoter) (el *election.Election, ballot []byte) { alive := true app := tview.NewApplication() resp, err := client.JoinedRooms() @@ -42,30 +42,28 @@ func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *e } if roomNameEvent, ok := room.State[event.StateRoomName][""]; ok { name := roomNameEvent.Content.AsRoomName().Name - app.QueueUpdateDraw(func() { - list.SetItemText(i, name, string(roomID)) - }) + list.SetItemText(i, name, string(roomID)) } } } go func() { for alive { - time.Sleep(1 * time.Second) // syncing right away can be slow - update() + time.Sleep(1 * time.Second) // 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 - RoomTUI(client, resp.JoinedRooms[i], elections, localVoter) + el, ballot = RoomTUI(client, resp.JoinedRooms[i], elections, localVoter) }) if err := app.SetRoot(list, true).SetFocus(list).Run(); err != nil { panic(err) } - alive = false + return } -func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap, localVoter *election.LocalVoter) { +func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap, localVoter *election.LocalVoter) (el *election.Election, ballot []byte) { alive := true app := tview.NewApplication() list := tview.NewList(). @@ -97,9 +95,7 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect } go func() { for alive { - app.QueueUpdateDraw(func() { - update() - }) + app.QueueUpdateDraw(update) time.Sleep(1 * time.Second) } }() @@ -111,20 +107,20 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect // user wants to join election elections.RLock() defer elections.RUnlock() - el := elections.L[i-1] - if joinElectionConfirmation(el) { - _, electionMember := el.Voters[localVoter.UserID] - if !electionMember { + el = elections.L[i-1] + if joinElectionConfirmation(el, localVoter) { + _, alreadyElectionMember := el.Voters[localVoter.UserID] + if !alreadyElectionMember { localVoter.JoinElection(client, el) } - ElectionTUI(el, localVoter) + ballot = ElectionTUI(client, el, localVoter) } else { - RoomTUI(client, roomID, elections, localVoter) + el, ballot = RoomTUI(client, roomID, elections, localVoter) } return } - // user wants to create election + // user wants to create election (i == 0) title, candidates := CreateElectionTUI() fmt.Println("title", title) fmt.Println("candidates", candidates) @@ -145,17 +141,21 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect if err := app.SetRoot(list, true).SetFocus(list).Run(); err != nil { panic(err) } - alive = false + return } -func joinElectionConfirmation(el *election.Election) (shouldJoin bool) { +func joinElectionConfirmation(el *election.Election, localVoter *election.LocalVoter) (shouldJoin bool) { app := tview.NewApplication() var buttons []string var text string - // TODO: handle when election starts while in modal el.RLock() + if el.Voters[localVoter.UserID] != nil { + return true + } + + // TODO: handle when election starts while in modal if el.Started { buttons = []string{"Ok"} text = "Election has already started, sorry" @@ -220,45 +220,67 @@ func CreateElectionTUI() (title string, candidates []election.Candidate) { return title, candidates } -func ElectionTUI(election *election.Election, localVoter *election.LocalVoter) { +func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *election.LocalVoter) (ballot []byte) { votersTextView := tview.NewTextView() frame := tview.NewFrame(votersTextView) - if election.Creator == localVoter.UserID { + el.RLock() + if el.Creator == localVoter.UserID { frame.AddText("Press enter to start the election", false, tview.AlignCenter, tcell.ColorWhite) } else { frame.AddText("Waiting for election to start...", false, tview.AlignCenter, tcell.ColorWhite) } + el.RUnlock() app := tview.NewApplication() app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - app.Stop() if event.Key() == tcell.KeyEnter { - fmt.Println("ready to rock") + app.QueueUpdateDraw(func() { + frame.Clear() + frame.AddText("Starting election...", false, tview.AlignCenter, tcell.ColorWhite) + }) + err := election.StartElection(client, el) + if err != nil { + panic(err) + } } return event }) update := func() { - election.RLock() - voters := make([]string, 0, len(election.Voters)) - for voterUserID := range election.Voters { + el.RLock() + voters := make([]string, 0, len(el.Voters)) + for voterUserID := range el.Voters { voters = append(voters, voterUserID.String()) } - election.RUnlock() + el.RUnlock() sort.Strings(voters) text := strings.Join(voters, "\n") text = "Joined voters:\n" + text - app.QueueUpdateDraw(func() { - votersTextView.SetText(text) - }) + votersTextView.SetText(text) } go func() { for { - update() + app.QueueUpdateDraw(update) + + // has the election started? + el.RLock() + started := el.Started + el.RUnlock() + if started { + app.Stop() + break + } + time.Sleep(1 * time.Second) } }() if err := app.SetRoot(frame, true).SetFocus(frame).Run(); err != nil { panic(err) } + el.RLock() + candidates := el.Candidates + el.RUnlock() + + ballot = Vote(candidates) + return } // displays a voting UI to the user and returns the encoded ballot -- 2.38.4