M cmd/tallyard/main.go => cmd/tallyard/main.go +30 -1
@@ 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)
}
M election/msg.go => election/msg.go +3 -11
@@ 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)
M election/voter.go => election/voter.go +107 -43
@@ 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)
M ui/tui.go => ui/tui.go +56 -34
@@ 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