~edwargix/tallyard

afd718e65efcb1f9ed211611082c478b225d1da8 — David Florness 4 years ago a6d4f6b
Store elections in data and use join_event_id for some messages
M cmd/tallyard/main.go => cmd/tallyard/main.go +41 -33
@@ 4,10 4,10 @@ import (
	"fmt"
	"os"

	"github.com/kyoh86/xdg"
	log "github.com/sirupsen/logrus"
	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"

	"tallyard.xyz/election"
	"tallyard.xyz/math"


@@ 20,22 20,25 @@ func DebugCB(source mautrix.EventSource, evt *event.Event) {
	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,
				election.StartElectionMessage,
				election.EvalMessage,
				election.SumMessage,
				election.ResultMessage,
func electionFilter(localUserID id.UserID) *mautrix.Filter {
	return &mautrix.Filter{
		Room: mautrix.RoomFilter{
			Timeline: mautrix.FilterPart{
				// TODO properly handle too many events (newest
				// events are likely to be in first batch)
				Limit: 100000,
				NotSenders: []id.UserID{localUserID},
				Types: []event.Type{
					election.CreateElectionMessage,
					election.JoinElectionMessage,
					election.StartElectionMessage,
					election.EvalMessage,
					election.SumMessage,
					election.ResultMessage,
				},
			},
		},
	},
	}
}

func main() {


@@ 50,13 53,10 @@ func main() {
		panic(err)
	}

	if data.LocalVoter == nil {
		data.LocalVoter = election.NewLocalVoter(client.UserID)
		data.Save()
	if data.Elections == nil {
		data.Elections = election.NewElectionsMap()
	}

	elections := election.NewElectionsMap()

	syncer := client.Syncer.(*mautrix.DefaultSyncer)
	syncer.OnEvent(client.Store.(*mautrix.InMemoryStore).UpdateState)
	syncer.OnEventType(election.CreateElectionMessage, func(source mautrix.EventSource, evt *event.Event) {


@@ 65,7 65,8 @@ func main() {
			log.Debug("redacted")
			return
		}
		election.OnCreateElectionMessage(evt, elections)
		election.OnCreateElectionMessage(evt, data.Elections)
		data.Save()
	})
	syncer.OnEventType(election.JoinElectionMessage, func(source mautrix.EventSource, evt *event.Event) {
		DebugCB(source, evt)


@@ 73,7 74,8 @@ func main() {
			log.Debug("redacted")
			return
		}
		election.OnJoinElectionMessage(client, evt, elections)
		election.OnJoinElectionMessage(client, evt, data.Elections)
		data.Save()
	})
	syncer.OnEventType(election.StartElectionMessage, func(source mautrix.EventSource, evt *event.Event) {
		DebugCB(source, evt)


@@ 81,7 83,8 @@ func main() {
			log.Debug("redacted")
			return
		}
		election.OnStartElectionMessage(client, evt, elections)
		election.OnStartElectionMessage(client, evt, data.Elections)
		data.Save()
	})
	syncer.OnEventType(election.EvalMessage, func(source mautrix.EventSource, evt *event.Event) {
		DebugCB(source, evt)


@@ 89,7 92,8 @@ func main() {
			log.Debug("redacted")
			return
		}
		election.OnEvalMessage(client, evt, elections, data.LocalVoter)
		election.OnEvalMessage(client, evt, data.Elections)
		data.Save()
	})
	syncer.OnEventType(election.SumMessage, func(source mautrix.EventSource, evt *event.Event) {
		DebugCB(source, evt)


@@ 97,19 101,21 @@ func main() {
			log.Debug("redacted")
			return
		}
		election.OnSumMessage(client, evt, elections)
		election.OnSumMessage(client, evt, data.Elections)
		data.Save()
	})
	syncer.OnEventType(election.ResultMessage, func(source mautrix.EventSource, evt *event.Event) {
		DebugCB(source, evt)
		if evt.Unsigned.RedactedBecause != nil {
			log.Debug("redacted")
			return
		}
		DebugCB(source, evt)
		election.OnResultMessage(client, evt, elections)
		election.OnResultMessage(client, evt, data.Elections)
		data.Save()
	})

	go func() {
		res, err := client.CreateFilter(electionFilter)
		res, err := client.CreateFilter(electionFilter(client.UserID))
		if err != nil {
			panic(err)
		}


@@ 120,20 126,22 @@ func main() {
		}
	}()

	el, ballot := ui.TUI(client, elections, data.LocalVoter)
	el := ui.TUI(client, data.Elections)

	data.LocalVoter.Poly = math.NewRandomPoly(uint(len(el.Voters)-1), 1024, ballot)
	el.Lock()
	el.LocalVoter.Poly = math.NewRandomPoly(uint(len(*el.FinalVoters)-1), 1024, *el.LocalVoter.Ballot)
	el.Unlock()

	// TODO we may not have all voters' info
	err = data.LocalVoter.SendEvals(client, el)
	err = el.SendEvals(client)
	if err != nil {
		panic(err)
	}

	err = data.LocalVoter.SendSum(client, el)
	err = el.SendSum(client)
	if err != nil {
		panic(err)
	}

	election.GetSums(client, el)
	el.GetSums(client)
}

M election/election.go => election/election.go +13 -11
@@ 3,33 3,35 @@ package election
import (
	"sync"

	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)

type Election struct {
	sync.RWMutex

	Candidates        []Candidate
	CreateEventId     id.EventID
	CreationTimestamp int64
	Creator           id.UserID
	RoomID            id.RoomID
	Started           bool
	Title             string
	Voters            map[id.UserID]*Voter
	Candidates        []Candidate          `json:"candidates"`
	CreateEventId     id.EventID           `json:"create_event_id"`
	CreationTimestamp int64                `json:"creation_timestamp"`
	Creator           id.UserID            `json:"creator"`
	FinalVoters       *[]id.EventID        `json:"final_voters,omitempty"`
	Joins            map[id.EventID]*Voter `json:"joins"`
	LocalVoter        *LocalVoter          `json:"local_voter,omitempty"`
	RoomID            id.RoomID            `json:"room_id"`
	StartEvt          *event.Event         `json:"start_evt,omitempty"`
	Title             string               `json:"title"`
}

func NewElection(candidates []Candidate, createEventId id.EventID,
	creationTimestamp int64, creator id.UserID, roomID id.RoomID,
	started bool, title string) *Election {
	title string) *Election {
	return &Election{
		Candidates:        candidates,
		CreateEventId:     createEventId,
		CreationTimestamp: creationTimestamp,
		Creator:           creator,
		Joins:             make(map[id.EventID]*Voter),
		RoomID:            roomID,
		Started:           started,
		Title:             title,
		Voters:            make(map[id.UserID]*Voter),
	}
}

M election/map.go => election/map.go +48 -15
@@ 1,6 1,7 @@
package election

import (
	"encoding/json"
	"sort"
	"sync"



@@ 9,49 10,81 @@ import (

type ElectionsMap struct{
	sync.RWMutex
	M map[id.EventID]*Election
	L []*Election // sorted list of elections by CreationTimestamp (newest to oldest)
	M     map[id.EventID]*Election
	L     []*Election // sorted list of elections by CreationTimestamp (newest to oldest)
	Joins map[id.EventID]*Voter
}

func NewElectionsMap() *ElectionsMap {
	return &ElectionsMap{
		M: make(map[id.EventID]*Election),
		L: make([]*Election, 0),
		M:     make(map[id.EventID]*Election),
		L:     make([]*Election, 0),
		Joins: make(map[id.EventID]*Voter),
	}
}

func (em *ElectionsMap) Get(eventID id.EventID) *Election {
func (em *ElectionsMap) MarshalJSON() ([]byte, error) {
	return json.Marshal(em.M)
}

func (em *ElectionsMap) UnmarshalJSON(b []byte) error {
	if err := json.Unmarshal(b, &em.M); err != nil {
		return err
	}
	em.L = make([]*Election, 0)
	for createEventId, el := range em.M {
		em.insort(createEventId, el)
	}
	em.Joins = make(map[id.EventID]*Voter)
	for _, el := range em.M {
		for joinEventId, voter := range el.Joins {
			em.Joins[joinEventId] = voter
		}
	}
	return nil
}

func (em *ElectionsMap) Get(createEventID id.EventID) *Election {
	em.RLock()
	defer em.RUnlock()
	return em.M[eventID]
	return em.M[createEventID]
}

func (em *ElectionsMap) GetOk(eventID id.EventID) (*Election, bool) {
func (em *ElectionsMap) GetOk(createEventID id.EventID) (*Election, bool) {
	em.RLock()
	defer em.RUnlock()
	el, ok := em.M[eventID]
	el, ok := em.M[createEventID]
	return el, ok
}

func (em *ElectionsMap) Set(eventID id.EventID, el *Election) {
func (em *ElectionsMap) GetI(i int) (*Election) {
	em.RLock()
	defer em.RUnlock()
	return em.L[i]
}

func (em *ElectionsMap) Set(createEventID id.EventID, el *Election) {
	em.Lock()
	defer em.Unlock()
	em.set(eventID, el)
	em.set(createEventID, el)
}

func (em *ElectionsMap) SetIfNotExists(eventID id.EventID, el *Election) {
func (em *ElectionsMap) SetIfNotExists(createEventID id.EventID, el *Election) {
	em.Lock()
	defer em.Unlock()
	_, exists := em.M[eventID]
	_, exists := em.M[createEventID]
	if exists {
		return
	}
	em.set(eventID, el)
	em.set(createEventID, el)
}

func (em *ElectionsMap) set(eventID id.EventID, el *Election) {
	em.M[eventID] = el
func (em *ElectionsMap) set(createEventID id.EventID, el *Election) {
	em.M[createEventID] = el
	em.insort(createEventID, el)
}

func (em *ElectionsMap) insort(createEventID id.EventID, el *Election) {
	i := sort.Search(len(em.L), func(i int) bool {
		return em.L[i].CreationTimestamp < el.CreationTimestamp
	})

M election/msg.go => election/msg.go +104 -60
@@ 59,23 59,23 @@ type JoinElectionContent struct {
}

type StartElectionContent struct {
	CreateEventId id.EventID  `json:"create_event_id"`
	Voters        []id.UserID `json:"voters"`
	CreateEventId id.EventID   `json:"create_event_id"`
	VoterJoinIds  []id.EventID `json:"voter_join_ids"`
}

type EvalMessageContent struct {
	CreateEventId id.EventID           `json:"create_event_id"`
	Outputs       map[id.UserID]string `json:"outputs"`
	JoinEventId id.EventID            `json:"join_event_id"`
	Outputs     map[id.EventID]string `json:"outputs"`
}

type SumMessageContent struct {
	CreateEventId id.EventID `json:"create_event_id"`
	Sum           string     `json:"sum"`
	JoinEventId id.EventID `json:"join_event_id"`
	Sum         string     `json:"sum"`
}

type ResultMessageContent struct {
	CreateEventId id.EventID `json:"create_event_id"`
	Result        string     `json:"result"`
	JoinEventId id.EventID `json:"join_event_id"`
	Result      string     `json:"result"`
}

func init() {


@@ 95,7 95,7 @@ func OnCreateElectionMessage(evt *event.Event, elections *ElectionsMap) {
		return
	}
	elections.SetIfNotExists(evt.ID, NewElection(content.Candidates, evt.ID,
		evt.Timestamp, evt.Sender, evt.RoomID, false, content.Title))
		evt.Timestamp, evt.Sender, evt.RoomID, content.Title))
}

func getElection(client *mautrix.Client, roomID id.RoomID, createEventId id.EventID, elections *ElectionsMap) *Election {


@@ 103,118 103,152 @@ func getElection(client *mautrix.Client, roomID id.RoomID, createEventId id.Even
	if exists {
		return el
	}
	// TODO: this endpoint will be deprecated
	createEvent, err := client.GetEvent(roomID, createEventId)
	if err != nil {
		log.Warnf("couldn't retrieve election create event: %s", err)
		return nil
	}
	if createEvent.Unsigned.RedactedBecause != nil {
		log.Warnf("election redacted")
		log.Debug("election redacted")
		return nil
	}
	OnCreateElectionMessage(createEvent, elections)
	el, exists = elections.GetOk(createEventId)
	if !exists {
		log.Warnf("couldn't create election")
		log.Warn("couldn't create election")
		return nil
	}
	return el
}

func getVoter(client *mautrix.Client, roomID id.RoomID, joinEventId id.EventID, elections *ElectionsMap) *Voter {
	voter, exists := elections.Joins[joinEventId]
	if exists {
		return voter
	}
	// TODO: this endpoint will be deprecated
	joinEvent, err := client.GetEvent(roomID, joinEventId)
	if err != nil {
		log.Warnf("couldn't retrieve join event: %s", err)
		return nil
	}
	if joinEvent.Unsigned.RedactedBecause != nil {
		log.Debug("join redacted")
		return nil
	}
	OnJoinElectionMessage(client, joinEvent, elections)
	voter, exists = elections.Joins[joinEventId]
	if !exists {
		log.Warn("couldn't find voter")
		return nil
	}
	return voter
}

func OnJoinElectionMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) {
	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)
		return
	}
	bytes, err := base64.StdEncoding.DecodeString(content.Input)
	if err != nil {
		log.Warnf("ignoring %s's join since we couldn't decode their input", evt.Sender)
		log.Warnf("ignoring %s's join msg since we couldn't cast message content to JoinElectionContent", evt.Sender)
		return
	}
	input := new(big.Int).SetBytes(bytes)
	el := getElection(client, evt.RoomID, content.CreateEventId, elections)
	if el == nil {
		log.Warnf("ignoring %s's join since the election doesn't exist", evt.Sender)
		log.Warnf("ignoring %s's join msg since the election doesn't exist", evt.Sender)
		return
	}
	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)
	_, voterExists := el.Joins[evt.ID]
	if voterExists {
		log.Debugf("ignoring %s's join msg since we already have their info", evt.Sender)
		return
	}
	var pubKey [32]byte
	bytes, err := base64.StdEncoding.DecodeString(content.Input)
	if err != nil {
		log.Warnf("ignoring %s's join msg since we couldn't decode their input", evt.Sender)
		return
	}
	input := new(big.Int).SetBytes(bytes)
	bytes, err = base64.StdEncoding.DecodeString(content.NaclPublicKey)
	if err != nil {
		log.Warnf("ignoring %s's join since we couldn't decode their public key", evt.Sender)
		log.Warnf("ignoring %s's join msg since we couldn't decode their public key: %s", evt.Sender, err)
		return
	}
	var pubKey [32]byte
	copy(pubKey[:], bytes)
	el.Voters[evt.Sender] = NewVoter(evt.Sender, input, &pubKey)
	voter := NewVoter(evt.Sender, input, &pubKey, evt)
	el.Joins[evt.ID] = voter
	elections.Joins[evt.ID] = voter
}

func OnStartElectionMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) {
	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)
		log.Warnf("ignoring %s's start msg since we couldn't cast message content to StartElectionContent", evt.Sender)
		return
	}
	el := getElection(client, evt.RoomID, content.CreateEventId, elections)
	if el == nil {
		log.Warnf("ignoring %s's election start since the election doesn't exist", evt.Sender)
		log.Warnf("ignoring %s's start msg since the election doesn't exist", evt.Sender)
		return
	}
	el.Lock()
	defer el.Unlock()
	if evt.Sender != el.Creator {
		log.Warnf("ignoring %s's election start since they didn't start the election", evt.Sender)
		log.Warnf("ignoring %s's start msg since they didn't start the election", evt.Sender)
		return
	}
	// TODO we should probably just bail when there are multiple start messages
	if el.StartEvt != nil && el.StartEvt.Timestamp < evt.Timestamp {
		log.Warnf("ignoring %s's start msg since the election's already been started", evt.Sender)
		return
	}
	el.StartEvt = evt
	el.FinalVoters = &content.VoterJoinIds
	// TODO check election voters
	el.Started = true
}

func OnEvalMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap, localVoter *LocalVoter) {
func OnEvalMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) {
	content, ok := evt.Content.Parsed.(*EvalMessageContent)
	if !ok {
		log.Warn("ignoring %s's eval message since we couldn't cast message content to EvalMessageContent", evt.Sender)
		log.Warn("ignoring %s's eval msg since we couldn't cast message content to EvalMessageContent", evt.Sender)
		return
	}
	encodedEncryptedOutput, ok := content.Outputs[localVoter.UserID]
	if !ok {
		log.Errorf("our user ID was not included in an eval message! The election will be unable to finish; blame %s", evt.Sender)
	voter := getVoter(client, evt.RoomID, content.JoinEventId, elections)
	if voter == nil {
		log.Warnf("ignoring %s's eval msg since voter doesn't exist", evt.Sender)
		return
	}
	encryptedOutput, err := base64.StdEncoding.DecodeString(encodedEncryptedOutput)
	if err != nil {
		log.Errorf("couldn't decode %s's encrypted output: %s", evt.Sender, err)
		return
	}
	el := getElection(client, evt.RoomID, content.CreateEventId, elections)
	// if Content were faulty, voter would have been nil
	createEventId := voter.JoinEvt.Content.Parsed.(*JoinElectionContent).CreateEventId
	el := getElection(client, evt.RoomID, createEventId, elections)
	if el == nil {
		log.Warnf("ignoring %s's eval message since the election doesn't exist", evt.Sender)
		log.Warnf("ignoring %s's eval msg since the election doesn't exist", evt.Sender)
		return
	}
	el.Lock()
	defer el.Unlock()
	voter, exists := el.Voters[evt.Sender]
	if !exists {
		log.Warnf("ignoring %s's eval message since they are not a voter", evt.Sender)
	encodedEncryptedOutput, ok := content.Outputs[el.LocalVoter.JoinEvt.ID]
	if !ok {
		log.Errorf("our user ID was not included in an eval message! The election will be unable to finish; blame %s", evt.Sender)
		return
	}
	encryptedOutput, err := base64.StdEncoding.DecodeString(encodedEncryptedOutput)
	if err != nil {
		log.Errorf("couldn't decode %s's encrypted output: %s", evt.Sender, err)
		return
	}

	var decryptNonce [24]byte
	copy(decryptNonce[:], encryptedOutput[:24])
	decryptedOutput, ok := box.Open(nil, encryptedOutput[24:], &decryptNonce, voter.PubKey, localVoter.PrivKey)

	decryptedOutput, ok := box.Open(nil, encryptedOutput[24:], &decryptNonce, &voter.PubKey, &el.LocalVoter.PrivKey)
	if !ok {
		log.Errorf("decryption error")
		return
	}

	voter.Output = new(big.Int).SetBytes(decryptedOutput)
	voter.Eval = new(big.Int).SetBytes(decryptedOutput)
}

func OnSumMessage(client *mautrix.Client, evt *event.Event, elections *ElectionsMap) {


@@ 223,21 257,26 @@ func OnSumMessage(client *mautrix.Client, evt *event.Event, elections *Elections
		log.Warnf("ignoring %s's sum since we couldn't cast message content to SumMessageContent", evt.Sender)
		return
	}
	bytes, err := base64.StdEncoding.DecodeString(content.Sum)
	if err != nil {
		log.Warnf("ignoring %s's sum since we couldn't decode their sum", evt.Sender)
	voter := getVoter(client, evt.RoomID, content.JoinEventId, elections)
	if voter == nil {
		log.Warnf("ignoring %s's sum since voter doesn't exist", evt.Sender)
		return
	}
	sum := new(big.Int).SetBytes(bytes)
	el := getElection(client, evt.RoomID, content.CreateEventId, elections)
	// if Content were faulty, voter would have been nil
	createEventId := voter.JoinEvt.Content.Parsed.(*JoinElectionContent).CreateEventId
	el := getElection(client, evt.RoomID, createEventId, elections)
	if el == nil {
		log.Warnf("ignoring %s's sum since the election does not exist", evt.Sender)
		return
	}
	el.Lock()
	defer el.Unlock()
	// TODO: ensure voter exists
	voter := el.Voters[evt.Sender]
	bytes, err := base64.StdEncoding.DecodeString(content.Sum)
	if err != nil {
		log.Warnf("ignoring %s's sum since we couldn't decode their sum: %s", evt.Sender, err)
		return
	}
	sum := new(big.Int).SetBytes(bytes)
	voter.Sum = sum
}



@@ 247,19 286,24 @@ func OnResultMessage(client *mautrix.Client, evt *event.Event, elections *Electi
		log.Warnf("ignoring %s's result since we couldn't cast message content to ResultMessageContent", evt.Sender)
		return
	}
	result, ok := new(big.Int).SetString(content.Result, 64)
	if !ok {
		log.Warnf("ignoring %s's result since we couldn't base64 decode the result", evt.Sender)
	voter := getVoter(client, evt.RoomID, content.JoinEventId, elections)
	if voter == nil {
		log.Warnf("ignoring %s's sum since voter doesn't exist", evt.Sender)
		return
	}
	el := getElection(client, evt.RoomID, content.CreateEventId, elections)
	createEventId := voter.JoinEvt.Content.Parsed.(*JoinElectionContent).CreateEventId
	el := getElection(client, evt.RoomID, createEventId, elections)
	if el == nil {
		log.Warnf("ignoring %s's result since the election does not exist", evt.Sender)
		return
	}
	el.Lock()
	defer el.Unlock()
	// TODO check that voter exists
	voter := el.Voters[evt.Sender]
	bytes, err := base64.StdEncoding.DecodeString(content.Result)
	if err != nil {
		log.Warnf("ignoring %s's result since we couldn't decode the result: %s", evt.Sender, err)
		return
	}
	result := new(big.Int).SetBytes(bytes)
	voter.Result = result
}

M election/version.go => election/version.go +1 -1
@@ 1,3 1,3 @@
package election

const Version string = "0.0.0"
const Version string = "0.3.0"

M election/voter.go => election/voter.go +156 -75
@@ 3,6 3,7 @@ package election
import (
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"math/big"


@@ 12,140 13,221 @@ import (
	log "github.com/sirupsen/logrus"
	"golang.org/x/crypto/nacl/box"
	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
	"tallyard.xyz/math"
)

type Voter struct {
	Input  *big.Int
	Output *big.Int
	PubKey *[32]byte
	Result *big.Int
	Sum    *big.Int
	UserID id.UserID
	Eval    *big.Int    `json:"eval,omitempty"`
	Input   big.Int     `json:"input"`
	JoinEvt event.Event `json:"join_evt"`
	PubKey  [32]byte    `json:"pub_key"`
	Result  *big.Int    `json:"result,omitempty"`
	Sum     *big.Int    `json:"sum,omitempty"`
	UserID  id.UserID   `json:"user_id"`
}

type LocalVoter struct {
	*Voter
	PrivKey *[32]byte
	ballot  []byte
	Poly    *math.Poly
	Ballot  *[]byte    `json:"ballot,omitempty"`
	Poly    *math.Poly `json:"poly,omitempty"`
	PrivKey [32]byte   `json:"priv_key"`
}

func NewVoter(userID id.UserID, input *big.Int, pubKey *[32]byte) *Voter {
func NewVoter(userID id.UserID, input *big.Int, pubKey *[32]byte, joinEvt *event.Event) *Voter {
	return &Voter{
		UserID: userID,
		Input:  input,
		PubKey: pubKey,
		Input:   *new(big.Int).Set(input),
		JoinEvt: *joinEvt,
		PubKey:  *pubKey,
		UserID:  userID,
	}
}

func NewLocalVoter(userID id.UserID) *LocalVoter {
	pubKey, privKey, err := box.GenerateKey(rand.Reader)
func CreateElection(client *mautrix.Client, candidates []Candidate, title string, roomID id.RoomID, elections *ElectionsMap) (*Election, error) {
	resp, err := client.SendMessageEvent(roomID, CreateElectionMessage, CreateElectionContent{
		Candidates: candidates,
		Title:      title,
		Version:    Version,
	})
	if err != nil {
		panic(err)
		return nil, err
	}
	input, err := math.RandomBigInt(1024, false)

	// TODO: this will be deprecated
	createEvt, err := client.GetEvent(roomID, resp.EventID)
	if err != nil {
		panic(err)
		return nil, err
	}
	return &LocalVoter{
		Voter:   NewVoter(userID, input, pubKey),
		PrivKey: privKey,

	OnCreateElectionMessage(createEvt, elections)
	el, exists := elections.GetOk(resp.EventID)
	if !exists {
		return nil, errors.New("couldn't create election")
	}
	return el, nil
}

func CreateElection(client *mautrix.Client, candidates []Candidate, title string, roomID id.RoomID) (id.EventID, error) {
	resp, err := client.SendMessageEvent(roomID, CreateElectionMessage, CreateElectionContent{
		Candidates: candidates,
		Title:      title,
		Version:    Version,
	})
	return resp.EventID, err
}
func (el *Election) JoinElection(client *mautrix.Client) error {
	pubKey, privKey, err := box.GenerateKey(rand.Reader)
	if err != nil {
		return err
	}
	input, err := math.RandomBigInt(1024, false)
	if err != nil {
		return err
	}

func (localVoter *LocalVoter) JoinElection(client *mautrix.Client, el *Election) error {
	el.RLock()
	defer el.RUnlock()
	_, err := client.SendMessageEvent(el.RoomID, JoinElectionMessage, JoinElectionContent{
	el.Lock()
	defer el.Unlock()

	resp, err := client.SendMessageEvent(el.RoomID, JoinElectionMessage, JoinElectionContent{
		CreateEventId: el.CreateEventId,
		Input:         base64.StdEncoding.EncodeToString(localVoter.Input.Bytes()),
		NaclPublicKey: base64.StdEncoding.EncodeToString((*localVoter.PubKey)[:]),
		Input:         base64.StdEncoding.EncodeToString(input.Bytes()),
		NaclPublicKey: base64.StdEncoding.EncodeToString((*pubKey)[:]),
	})
	return err
	if err != nil {
		return err
	}

	// TODO: this will be deprecated
	joinEvt, err := client.GetEvent(el.RoomID, resp.EventID)
	if err != nil {
		return err
	}

	el.LocalVoter = &LocalVoter{
		Voter:   NewVoter(client.UserID, input, pubKey, joinEvt),
		PrivKey: *privKey,
	}
	el.Joins[resp.EventID] = el.LocalVoter.Voter
	return nil
}

func StartElection(client *mautrix.Client, el *Election) error {
func (el *Election) StartElection(client *mautrix.Client) 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)
	el.Lock()
	defer el.Unlock()
	userIdMap := make(map[id.UserID]*Voter)
	// one vote per userID
	for _, voter := range el.Joins {
		prevVoter, exists := userIdMap[voter.UserID]
		if exists {
			// use latest join
			if voter.JoinEvt.Timestamp > prevVoter.JoinEvt.Timestamp {
				userIdMap[voter.UserID] = voter
			}
		} else {
			userIdMap[voter.UserID] = voter
		}
	}
	voters := make([]id.EventID, 0, len(userIdMap))
	for _, voter := range userIdMap {
		voters = append(voters, voter.JoinEvt.ID)
	}
	_, err := client.SendMessageEvent(el.RoomID, StartElectionMessage, StartElectionContent{
	resp, err := client.SendMessageEvent(el.RoomID, StartElectionMessage, StartElectionContent{
		CreateEventId: el.CreateEventId,
		Voters:        voters,
		VoterJoinIds:  voters,
	})
	if err != nil {
		return err
	}
	startEvt, err := client.GetEvent(el.RoomID, resp.EventID)
	if err != nil {
		return err
	}
	el.StartEvt = startEvt
	return err
}

func (localVoter *LocalVoter) SendEvals(client *mautrix.Client, el *Election) error {
func (el *Election) WaitForVoters(client *mautrix.Client) error {
	el.RLock()
	defer el.RUnlock()
	if el.StartEvt == nil {
		return errors.New("WaitForVoters called before election started")
	}
	finalVoters := *el.FinalVoters
	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.Unlock()
	content := EvalMessageContent{
		CreateEventId: el.CreateEventId,
		Outputs:       make(map[id.UserID]string),
		JoinEventId: el.LocalVoter.JoinEvt.ID,
		Outputs:     make(map[id.EventID]string),
	}
	for _, voter := range el.Voters {
		output := localVoter.Poly.Eval(voter.Input).Bytes()
	for _, voterJoinId := range *el.FinalVoters {
		voter := el.Joins[voterJoinId]
		output := el.LocalVoter.Poly.Eval(&voter.Input)
		if voter.JoinEvt.ID == el.LocalVoter.JoinEvt.ID {
			el.LocalVoter.Eval = output
		}
		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)
		encrypted := box.Seal(nonce[:], output.Bytes(), &nonce, &voter.PubKey, &el.LocalVoter.PrivKey)
		content.Outputs[voter.JoinEvt.ID] = base64.StdEncoding.EncodeToString(encrypted)
	}
	_, err := client.SendMessageEvent(el.RoomID, EvalMessage, content)
	return err
	if err != nil {
		el.LocalVoter.Eval = nil
		return err
	}
	return nil
}

func (locelVoter *LocalVoter) SendSum(client *mautrix.Client, el *Election) error {
func (el *Election) SendSum(client *mautrix.Client) error {
	sum := big.NewInt(0)
	var wg sync.WaitGroup
	el.RLock()
	for _, voter := range el.Voters {
	for _, voterJoinId := range *el.FinalVoters {
		wg.Add(1)
		go func(voter *Voter) {
			for voter.Output == nil {
			for voter.Eval == nil {
				time.Sleep(time.Millisecond * 100)
			}
			sum.Add(sum, voter.Output)
			sum.Add(sum, voter.Eval)
			wg.Done()
		}(voter)
		}(el.Joins[voterJoinId])
	}
	el.RUnlock()
	wg.Wait()
	_, err := client.SendMessageEvent(el.RoomID, SumMessage, SumMessageContent{
		CreateEventId: el.CreateEventId,
		Sum:           base64.StdEncoding.EncodeToString(sum.Bytes()),
		JoinEventId: el.LocalVoter.JoinEvt.ID,
		Sum:         base64.StdEncoding.EncodeToString(sum.Bytes()),
	})
	return err
	if err != nil {
		return err
	}
	el.LocalVoter.Sum = sum
	return nil
}

func GetSums(client *mautrix.Client, el *Election) {
func (el *Election) GetSums(client *mautrix.Client) {
	var wg sync.WaitGroup
	el.RLock()
	for _, voter := range el.Voters {
	for _, voterJoinId := range *el.FinalVoters {
		wg.Add(1)
		go func(voter *Voter) {
			for voter.Sum == nil {
				time.Sleep(time.Millisecond * 100)
			}
			wg.Done()
		}(voter)
		}(el.Joins[voterJoinId])
	}
	el.RUnlock()
	wg.Wait()

	M := constructPolyMatrix(el)


@@ 336,28 418,27 @@ func GetSums(client *mautrix.Client, el *Election) {
// }

func constructPolyMatrix(el *Election) math.Matrix {
	mat := make(math.Matrix, len(el.Voters))
	mat := make(math.Matrix, len(el.Joins))

	i := 0
	el.RLock()
	for _, voter := range el.Voters {
	for _, voterJoinId := range *el.FinalVoters {
		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.Voters)-1); j++ {
			row[j].SetInt(new(big.Int).Exp(voter.Input, big.NewInt(j), nil))
		for j = 1; j <= int64(len(*el.FinalVoters)-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
}

func printResults(result []byte, candidates []Candidate) {
	log.Infof("result: %v", result)
	log.Debugf("result: %v", result)
	fmt.Println("=== Results ===")
	n := len(candidates)
	for i, cand := range candidates {

M math/poly.go => math/poly.go +4 -4
@@ 7,8 7,8 @@ import (
)

type Poly struct {
	constant *big.Int
	coefs    []*big.Int
	Constant *big.Int   `json:"constant"`
	Coefs    []*big.Int `json:"coefs"`
}

func RandomBigInt(numBytes uint, allowAllZeros bool) (*big.Int, error) {


@@ 56,9 56,9 @@ func NewRandomPoly(degree uint, entropy uint, ballot []byte) *Poly {
}

func (p *Poly) Eval(input *big.Int) *big.Int {
	res := new(big.Int).Set(p.constant)
	res := new(big.Int).Set(p.Constant)

	for i, coef := range p.coefs {
	for i, coef := range p.Coefs {
		degree := big.NewInt(int64(i + 1))
		term := new(big.Int).Exp(input, degree, nil)
		term.Mul(term, coef)

M matrix/data.go => matrix/data.go +6 -6
@@ 14,12 14,12 @@ import (
)

type Data struct {
	Homeserver  string               `json:"homeserver"`
	Username    string               `json:"username"`
	AccessToken string               `json:"access_token"`
	DeviceID    id.DeviceID          `json:"device_id"`
	UserID      id.UserID            `json:"user_id"`
	LocalVoter  *election.LocalVoter `json:"local_voter,omitempty"`
	AccessToken string                 `json:"access_token"`
	DeviceID    id.DeviceID            `json:"device_id"`
	Elections   *election.ElectionsMap `json:"elections,omitempty"`
	Homeserver  string                 `json:"homeserver"`
	UserID      id.UserID              `json:"user_id"`
	Username    string                 `json:"username"`
}

var dataFname = xdg.DataHome() + "/tallyard/data.json"

M ui/tui.go => ui/tui.go +38 -40
@@ 8,6 8,8 @@ import (
	"time"
	"unicode"

	log "github.com/sirupsen/logrus"

	"github.com/gdamore/tcell"
	"github.com/rivo/tview"
	"maunium.net/go/mautrix"


@@ 16,7 18,7 @@ import (
	"tallyard.xyz/election"
)

func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *election.LocalVoter) (el *election.Election, ballot []byte) {
func TUI(client *mautrix.Client, elections *election.ElectionsMap) (el *election.Election) {
	alive := true
	app := tview.NewApplication()
	resp, err := client.JoinedRooms()


@@ 48,14 50,14 @@ func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *e
	}
	go func() {
		for alive {
			time.Sleep(1 * time.Second) // syncing right away can be slow, so this is before the next line
			time.Sleep(100 * time.Millisecond) // 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
		el, ballot = RoomTUI(client, resp.JoinedRooms[i], elections, localVoter)
		app.Stop()
		el = RoomTUI(client, resp.JoinedRooms[i], elections)
	})
	if err := app.SetRoot(list, true).SetFocus(list).Run(); err != nil {
		panic(err)


@@ 63,7 65,7 @@ func TUI(client *mautrix.Client, elections *election.ElectionsMap, localVoter *e
	return
}

func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap, localVoter *election.LocalVoter) (el *election.Election, ballot []byte) {
func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap) (el *election.Election) {
	alive := true
	app := tview.NewApplication()
	list := tview.NewList().


@@ 100,45 102,44 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect
		}
	}()
	list.SetSelectedFunc(func(i int, _ string, _ string, _ rune) {
		app.Stop()
		alive = false
		app.Stop()

		if i > 0 {
			// user wants to join election
			elections.RLock()
			defer elections.RUnlock()
			el = elections.L[i-1]
			if joinElectionConfirmation(el, localVoter) {
				_, alreadyElectionMember := el.Voters[localVoter.UserID]
				if !alreadyElectionMember {
					localVoter.JoinElection(client, el)

			el = elections.GetI(i-1)
			// don't need to lock because this goroutine controls LocalVoter
			if el.LocalVoter != nil {
				if el.LocalVoter.Ballot != nil {
					ElectionTUI(client, el)
				}
				ballot = ElectionTUI(client, el, localVoter)
			} else if joinElectionConfirmation(el) {
				err := el.JoinElection(client)
				if err != nil {
					panic(err)
				}
				ElectionTUI(client, el)
			} else {
				el, ballot = RoomTUI(client, roomID, elections, localVoter)
				el = RoomTUI(client, roomID, elections)
			}
			return
		}

		// user wants to create election (i == 0)
		title, candidates := CreateElectionTUI()
		fmt.Println("title", title)
		fmt.Println("candidates", candidates)
		eventID, err := election.CreateElection(client, candidates, title, roomID)
		log.Debugf("title: %s", title)
		log.Debugf("candidates: %s", candidates)
		var err error
		el, err = election.CreateElection(client, candidates, title, roomID, elections)
		if err != nil {
			panic(err)
		}
		var ok bool
		el, ok = elections.GetOk(eventID)
		for !ok {
			time.Sleep(10 * time.Millisecond)
			el, ok = elections.GetOk(eventID)
		}
		err = localVoter.JoinElection(client, el)
		err = el.JoinElection(client)
		if err != nil {
			panic(err)
		}
		ballot = ElectionTUI(client, el, localVoter)
		ElectionTUI(client, el)
	})
	if err := app.SetRoot(list, true).SetFocus(list).Run(); err != nil {
		panic(err)


@@ 146,19 147,15 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect
	return
}

func joinElectionConfirmation(el *election.Election, localVoter *election.LocalVoter) (shouldJoin bool) {
func joinElectionConfirmation(el *election.Election) (shouldJoin bool) {
	app := tview.NewApplication()

	var buttons []string
	var text string

	el.RLock()
	if el.Voters[localVoter.UserID] != nil {
		return true
	}

	// TODO: handle when election starts while in modal
	if el.Started {
	if el.StartEvt != nil {
		buttons = []string{"Ok"}
		text = "Election has already started, sorry"
	} else {


@@ 222,12 219,12 @@ func CreateElectionTUI() (title string, candidates []election.Candidate) {
	return title, candidates
}

func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *election.LocalVoter) (ballot []byte) {
func ElectionTUI(client *mautrix.Client, el *election.Election) {
	votersTextView := tview.NewTextView()
	frame := tview.NewFrame(votersTextView)
	app := tview.NewApplication()
	el.RLock()
	if el.Creator == localVoter.UserID {
	if el.Creator == el.LocalVoter.UserID {
		frame.AddText("Press enter to start the election", false, tview.AlignCenter, tcell.ColorWhite)
		app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
			if event.Key() == tcell.KeyEnter {


@@ 235,7 232,7 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec
					frame.Clear()
					frame.AddText("Starting election...", false, tview.AlignCenter, tcell.ColorWhite)
				})
				err := election.StartElection(client, el)
				err := el.StartElection(client)
				if err != nil {
					panic(err)
				}


@@ 248,8 245,9 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec
	el.RUnlock()
	update := func() {
		el.RLock()
		voters := make([]string, 0, len(el.Voters))
		for voterUserID := range el.Voters {
		// TODO: handle duplicate joins from one UserID
		voters := make([]string, 0, len(el.Joins))
		for voterUserID := range el.Joins {
			voters = append(voters, voterUserID.String())
		}
		el.RUnlock()


@@ 264,7 262,7 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec

			// has the election started?
			el.RLock()
			started := el.Started
			started := el.StartEvt != nil
			el.RUnlock()
			if started {
				app.Stop()


@@ 281,8 279,8 @@ func ElectionTUI(client *mautrix.Client, el *election.Election, localVoter *elec
	candidates := el.Candidates
	el.RUnlock()

	ballot = Vote(candidates)
	return
	ballot := Vote(candidates)
	el.LocalVoter.Ballot = &ballot
}

// displays a voting UI to the user and returns the encoded ballot