Commit 50f621eb authored by Anders Jensen Løvig's avatar Anders Jensen Løvig
Browse files

Agreement: send ballot lists

parent ff4cae2c
Pipeline #21811 failed with stages
in 1 minute and 23 seconds
......@@ -2,12 +2,44 @@ package election
import (
"bsc-shamir/crypto/pedersen"
"fmt"
"sync"
"github.com/gonum/stat/combin"
"github.com/google/uuid"
)
type BallotList map[string]uuid.UUID
type Agreement struct {
sync.Mutex
// The minimum servers required for being able to tally
required int
// Map of ballot lists receveid from servers
ballotLists map[UniqueID]BallotList
}
func NewAgreement(required int) *Agreement {
return &Agreement{
Mutex: sync.Mutex{},
required: required,
ballotLists: make(map[UniqueID]BallotList),
}
}
func (a *Agreement) AddList(serverID UniqueID, ballotList map[string]uuid.UUID) error {
a.Lock()
defer a.Unlock()
if _, ok := a.ballotLists[serverID]; ok {
return fmt.Errorf("server %s already sent ballot list", serverID)
}
a.ballotLists[serverID] = ballotList
return nil
}
type Whitelist map[uuid.UUID]pedersen.Proof
type Judge struct {
......
......@@ -9,6 +9,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
......@@ -53,6 +54,23 @@ func (ballot *Ballot) Verify(params *common.Params) bool {
return sigma.NewParams(params).Verify(ballot.Commits[0], ballot.Proofs)
}
func (ballot *Ballot) Hash() string {
// This information
toHash := struct {
ID uuid.UUID
Timestamp time.Time
Commits pedersen.Proof
}{
ballot.ID,
ballot.Timestamp,
ballot.Commits,
}
data, _ := json.Marshal(toHash)
h := sha256.Sum256(data)
return hex.EncodeToString(h[:])
}
// CreateBallots returns n ballots each containing a single share
func CreateBallots(t int, xs []*big.Int, vote *big.Int) map[UniqueID]*Ballot {
params := common.DefaultParams()
......@@ -61,11 +79,12 @@ func CreateBallots(t int, xs []*big.Int, vote *big.Int) map[UniqueID]*Ballot {
proof := sigma.NewParams(params).Prove(vote, binder, commits[0])
timestamp := time.Now()
id := uuid.New()
ballots := make(map[UniqueID]*Ballot)
for _, share := range shares {
ballots[UniqueID(share.X.String())] = &Ballot{
ID: uuid.New(),
ID: id,
Timestamp: timestamp,
Share: share,
Commits: commits,
......@@ -101,6 +120,19 @@ func (box *BallotBox) Put(ballot *Ballot) error {
return nil
}
// ballotList returns a mapping from ballot hash to their uuid.
func (box *BallotBox) ballotList() BallotList {
box.Lock()
defer box.Unlock()
list := make(map[string]uuid.UUID)
for id, ballot := range box.ballots {
list[ballot.Hash()] = id
}
return list
}
// Filter ballots by returning a list of valid ballots
func (box *BallotBox) Filter() []uuid.UUID {
good := []uuid.UUID{}
......
......@@ -37,3 +37,22 @@ func TestBallotOne(t *testing.T) {
}
}
}
func TestBallotHash(t *testing.T) {
ballots := CreateBallots(5, []*big.Int{
big.NewInt(1),
big.NewInt(2),
big.NewInt(3),
big.NewInt(4),
big.NewInt(5),
}, big.NewInt(1))
h := ballots["1"].Hash()
t.Log("Hash", h)
for i, ballot := range ballots {
if ballot.Hash() != h {
t.Errorf("Ballot %s have hash %s", i, h)
}
}
}
......@@ -22,6 +22,8 @@ const (
ReasonCloseTimeout = "close timeout"
ReasonVotes = "all votes received"
ReasonAgreement = "majority"
ReasonTallyTimeout = "tally timeout"
ReasonBallotList = "all ballot lists received"
ReasonTally = "received enough tallies"
)
......@@ -31,9 +33,11 @@ type Phase int
// Election phases
const (
PhaseNotStarted Phase = iota
PhaseCollecting // When voters can send ballots
PhaseCollect // When voters can send ballots
PhaseCloseWait // When server waits after close
PhaseClosed // When waiting for other servers to close
PhasePreTally // When servers send ballot lists
PhaseTally // When servers sent tallies
PhaseTallying // When tallying
PhaseResult // When a result is available
)
......@@ -44,10 +48,12 @@ func (p Phase) String() string {
return "PhaseNotStarted"
case PhaseCloseWait:
return "PhaseCloseWait"
case PhaseCollecting:
case PhaseCollect:
return "PhaseCollecting"
case PhaseClosed:
return "PhaseClosed"
case PhasePreTally:
return "PhasePreTally"
case PhaseTallying:
return "PhaseTallying"
case PhaseResult:
......@@ -97,8 +103,8 @@ type Election struct {
Deadline time.Time
// Struct containing server status about the election.
Status Status
// Judge for deciding which ballots are good
Judge *Judge
// Agreement handles the agreement protocol. This decides which ballots to tally.
Agreement *Agreement
// Called when the final result is available
resultCallback func(*Result)
}
......@@ -153,10 +159,12 @@ func NewElection(config *Config) *Election {
},
resultCallback: config.ResultCallback,
Mutex: sync.Mutex{},
Agreement: NewAgreement(config.RequiredServers),
}
config.Server.OnClientMessage(MsgBallot, e.OnBallot)
config.Server.OnServerMessage(MsgClosing, e.OnClose)
config.Server.OnServerMessage(MsgTally, e.OnTally)
config.Server.OnServerMessage(MsgBallotList, e.OnBallotList)
return e
}
......@@ -183,7 +191,7 @@ func (election *Election) Start() error {
defer election.Unlock()
// Deadline reached: Close election
if election.Status.Phase == PhaseCollecting {
if election.Status.Phase == PhaseCollect {
election.nextPhase(ReasonDeadline)
}
}(election)
......@@ -196,8 +204,8 @@ func (election *Election) nextPhase(reason Reason) {
switch election.Status.Phase {
case PhaseNotStarted:
log.Println("==================== Collecting ====================")
election.Status.Phase = PhaseCollecting
case PhaseCollecting:
election.Status.Phase = PhaseCollect
case PhaseCollect:
log.Println("====================== Closed ======================")
// The closed phase is twofold. If the reason for closing is another
// than all votes received (ReasonVotes), then we must wait for late
......@@ -214,24 +222,7 @@ func (election *Election) nextPhase(reason Reason) {
if reason != ReasonVotes && election.Config.CloseSleep != 0 {
log.Printf("Election: waiting %s for late ballots\n", election.Config.CloseSleep)
election.Status.Phase = PhaseCloseWait
go func(e *Election) {
var reason Reason
// Wait for timeout or all votes received.
select {
case <-time.After(time.Until(now.Add(e.Config.CloseSleep))):
reason = ReasonCloseTimeout
case <-e.Status.PhaseChannel:
reason = ReasonVotes
}
// Then next phase
e.Lock()
defer e.Unlock()
// Actually close the election
e.Status.Phase = PhaseClosed
e.nextPhase(reason)
}(election)
go delay(election, now, ReasonCloseTimeout, ReasonVotes, PhaseClosed)
break
} else {
// If we received all votes go directly to next phase
......@@ -247,23 +238,38 @@ func (election *Election) nextPhase(reason Reason) {
break
}
// Else enough servers to start tallying
// If the ballot box is empty we can go directly to result
if election.ballotBox.Size() != 0 {
log.Println("===================== Tallying =====================")
log.Println("===================== Tallying =====================")
// Tallying is two phased. First we wait for servers to sent ballot lists.
// Then we find the largest intersection of ballot lists and tally this intersection.
election.Status.Phase = PhasePreTally
ballotList := election.ballotBox.ballotList()
_ = election.Agreement.AddList(election.ServerID, ballotList)
election.server.Broadcast(network.NewMessage(MsgBallotList, ballotList))
if election.Config.CloseSleep != 0 {
log.Printf("Election: waiting %s for ballots lists\n", election.Config.CloseSleep)
go delay(election, time.Now(), ReasonTallyTimeout, ReasonBallotList, PhaseTally)
break
} else {
// Skip PhasePreTally
election.Status.Phase = PhaseTallying
election.Judge = NewJudge(election.Participants.RequiredServers, election.ballotBox)
}
fallthrough
case PhaseTally:
election.Status.Phase = PhaseTallying
tally := election.ballotBox.Tally(election.Judge.GoodIds())
// Only tally if we have ballots
if election.ballotBox.Size() != 0 {
tally := election.ballotBox.Tally(nil)
election.Status.TalliedServers[election.ServerID] = true
err := election.tallyBox.Put(tally)
if err != nil {
panic(err) // This should never happen
panic(err) // Should not happen
}
election.server.Broadcast(network.NewMessage(MsgTally, tally))
break
}
fallthrough
case PhaseTallying:
log.Println("====================== Result ======================")
......@@ -271,6 +277,8 @@ func (election *Election) nextPhase(reason Reason) {
result := election.createResults()
election.resultCallback(result)
}
log.Println("Election: new phase:", election.Status.Phase)
}
func (election *Election) createResults() *Result {
......@@ -283,3 +291,21 @@ func (election *Election) createResults() *Result {
Tally: int(election.tallyBox.Combine().Int64()),
}
}
func delay(e *Election, now time.Time, r1, r2 Reason, p Phase) {
var reason Reason
// Wait for timeout or all votes received.
select {
case <-time.After(time.Until(now.Add(e.Config.CloseSleep))):
reason = r1
case <-e.Status.PhaseChannel:
reason = r2
}
// Then next phase
e.Lock()
defer e.Unlock()
// Actually close the election
e.Status.Phase = p
e.nextPhase(reason)
}
......@@ -247,7 +247,7 @@ func TestCloseDeadline(t *testing.T) {
func TestCloseAgreement(t *testing.T) {
_ = testElection(t, PhaseTallying, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
e.handleBallot(UniqueID(strconv.Itoa(1)), ballots["1"])
......@@ -261,7 +261,7 @@ func TestCloseAgreement(t *testing.T) {
func TestCloseAllVotes(t *testing.T) {
_ = testElection(t, PhaseClosed, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
for i := 1; i <= e.Participants.Voters; i++ {
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
......@@ -322,7 +322,7 @@ func TestTallyAfterClose(t *testing.T) {
func TestOnlyVoteOnce(t *testing.T) {
e := setupElection()
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
......@@ -348,7 +348,7 @@ func TestHandleTally(t *testing.T) {
}
_ = testElection(t, PhaseResult, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
e.handleBallot("4", ballots["1"])
e.handleTally("2", tally1)
......@@ -372,7 +372,7 @@ func TestHandleTally(t *testing.T) {
func TestCloseTimeout(t *testing.T) {
_ = testElection(t, PhaseClosed, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
e.Config.CloseSleep = 1 * time.Second
// This should start a go-routine waiting for late ballots
......@@ -388,7 +388,7 @@ func TestCloseTimeout(t *testing.T) {
func TestCloseTimeoutTally(t *testing.T) {
_ = testElection(t, PhaseTallying, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
e.Config.CloseSleep = 1 * time.Second
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
......@@ -409,7 +409,7 @@ func TestCloseTimeoutTally(t *testing.T) {
func TestCloseTimeoutSkipTally(t *testing.T) {
_ = testElection(t, PhaseResult, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
e.Config.CloseSleep = 1 * time.Second
e.handleClosing("3")
......@@ -429,7 +429,7 @@ func TestCloseTimeoutSkipTally(t *testing.T) {
func TestCloseTimeoutLateBallot(t *testing.T) {
_ = testElection(t, PhaseClosed, func(e *Election) <-chan time.Time {
log.SetOutput(os.Stdout)
e.Status.Phase = PhaseCollecting
e.Status.Phase = PhaseCollect
e.Config.CloseSleep = 500 * time.Millisecond
for i := 1; i < e.Participants.Voters; i++ {
......
......@@ -16,6 +16,8 @@ const (
MsgClosing = "election_closing"
// Server -> Server: Sent when server have tallied
MsgTally = "election_tally"
// Server -> Server: Sent when servers agree on ballots to tally
MsgBallotList = "ballot_list"
)
func (e *Election) OnBallot(conn *network.Conn, data network.MessageData) {
......@@ -142,3 +144,25 @@ func (election *Election) handleTally(serverID UniqueID, tally *Tally) {
election.nextPhase(ReasonTally)
}
}
func (e *Election) OnBallotList(conn *network.Conn, data network.MessageData) {
var ballotList BallotList
err := data.Unmarshal(&ballotList)
if err != nil {
log.Println("Election: error:", err)
return
}
e.Lock()
defer e.Unlock()
err = e.Agreement.AddList(UniqueID(conn.SerialNumber()), ballotList)
if err != nil {
log.Println("Election: error:", err)
}
if len(e.Agreement.ballotLists) >= e.Participants.Servers &&
e.Status.Phase == PhasePreTally {
e.Status.PhaseChannel <- true
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment