~edwargix/tallyard

5a0610f62806678c5269470853961d01b9a7c838 — David Florness 4 years ago f3db4d6
Begin work on evals
4 files changed, 196 insertions(+), 89 deletions(-)

M cmd/tallyard/main.go
M election/msg.go
M election/voter.go
M ui/tui.go
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