~edwargix/tallyard

6a944b7a2259d2c6714ed40b4e3c8d8826c2bc77 — David Florness 4 years ago a313d73
Send result event and use event store if possible
5 files changed, 259 insertions(+), 201 deletions(-)

M cmd/tallyard/main.go
M election/event.go
M election/msg.go
A election/result.go
M election/voter.go
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
}