Commit 7c8c7208 authored by Anders Jensen Løvig's avatar Anders Jensen Løvig
Browse files

Fix election timing and late ballots

parent 786d4d35
Pipeline #21799 failed with stages
in 1 minute and 36 seconds
......@@ -78,6 +78,7 @@ func init() {
RequiredServers: required,
Deadline: time.Now().Add(30 * time.Second),
ResultCallback: resultCallback,
CloseSleep: 5 * time.Second,
}
server.Election = election.NewElection(config)
}
......
......@@ -17,11 +17,11 @@ type UniqueID string
type Reason string
const (
ReasonStarting = "starting election"
ReasonDeadline = "deadline reached"
ReasonCloseTimeout = "timeout while waiting for late ballots"
ReasonStarting = "start"
ReasonDeadline = "deadline"
ReasonCloseTimeout = "close timeout"
ReasonVotes = "all votes received"
ReasonAgreement = "majority agrees"
ReasonAgreement = "majority"
ReasonTally = "received enough tallies"
)
......@@ -42,6 +42,8 @@ func (p Phase) String() string {
switch p {
case PhaseNotStarted:
return "PhaseNotStarted"
case PhaseCloseWait:
return "PhaseCloseWait"
case PhaseCollecting:
return "PhaseCollecting"
case PhaseClosed:
......@@ -188,7 +190,7 @@ func (election *Election) Start() error {
}
func (election *Election) nextPhase(reason Reason) {
log.Println("Election: next phase because", reason)
log.Println("Election: next phase reason:", reason)
switch election.Status.Phase {
case PhaseNotStarted:
log.Println("==================== Collecting ====================")
......@@ -201,20 +203,21 @@ func (election *Election) nextPhase(reason Reason) {
// Next we wait for more than required servers to close.
// Close election and tell other servers
election.Status.CloseTime = time.Now()
now := time.Now()
election.Status.CloseTime = now
election.Status.ClosedServers[election.ServerID] = true
election.server.Broadcast(network.NewMessage(MsgClosing))
// Wait for late ballots if not all votes received.
if reason != ReasonVotes {
log.Println("Election: waiting 5 seconds for late ballots")
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 either 5 seconds or PhaseChannel is set.
// Wait for timeout or all votes received.
select {
case <-time.After(5 * time.Second):
case <-time.After(time.Until(now.Add(e.Config.CloseSleep))):
reason = ReasonCloseTimeout
case <-e.Status.PhaseChannel:
reason = ReasonVotes
......@@ -228,19 +231,21 @@ func (election *Election) nextPhase(reason Reason) {
e.nextPhase(reason)
}(election)
break
} else {
// If we received all votes go directly to next phase
election.Status.Phase = PhaseClosed
}
// If more than required servers have closed, then we can skip the next phase
// Else we have to wait for more servers to close
fallthrough
case PhaseClosed:
// We must wait for required amount of servers to close before we can tally
if len(election.Status.ClosedServers) < election.Participants.RequiredServers {
// Not enough servers to tally.
n := election.Participants.RequiredServers - len(election.Status.ClosedServers)
log.Printf("Election: waiting for %d more servers to close\n", n)
break
}
// Else enough servers to start tallying
fallthrough
case PhaseClosed:
// If the ballot box is empty we can go directly to result
if election.ballotBox.Size() != 0 {
log.Println("===================== Tallying =====================")
......
......@@ -4,10 +4,10 @@ import (
"bsc-shamir/crypto/common"
"bsc-shamir/network"
"crypto/rand"
"fmt"
"io/ioutil"
"log"
"math/big"
"os"
"strconv"
"testing"
"time"
......@@ -210,13 +210,13 @@ func setupElection() *Election {
RequiredServers: 2,
Deadline: time.Now().Add(1 * time.Minute),
ResultCallback: func(result *Result) {},
// CloseSleep: 0 * time.Second, // Disable close delay for testing
CloseSleep: 0 * time.Second, // Disable close delay for testing
}
return NewElection(config)
}
func testCloseCondition(t *testing.T, expectedPhase Phase, fun func(*Election) <-chan bool) *Election {
// log.SetOutput(ioutil.Discard)
func testElection(t *testing.T, expectedPhase Phase, fun func(*Election) <-chan time.Time) *Election {
log.SetOutput(ioutil.Discard)
e := setupElection()
select {
......@@ -232,23 +232,15 @@ func testCloseCondition(t *testing.T, expectedPhase Phase, fun func(*Election) <
}
func TestCloseDeadline(t *testing.T) {
_ = testCloseCondition(t, PhaseClosed, func(e *Election) <-chan bool {
done := make(chan bool, 1)
e.Deadline = time.Now().Add(1 * time.Second)
_ = testElection(t, PhaseClosed, func(e *Election) <-chan time.Time {
e.Deadline = time.Now().Add(1000 * time.Millisecond)
_ = e.Start()
go func() {
time.Sleep(1200 * time.Millisecond)
done <- true
}()
return done
return time.After(1200 * time.Millisecond)
})
}
func TestCloseAgreement(t *testing.T) {
_ = testCloseCondition(t, PhaseTallying, func(e *Election) <-chan bool {
done := make(chan bool, 1)
_ = testElection(t, PhaseTallying, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
......@@ -257,14 +249,12 @@ func TestCloseAgreement(t *testing.T) {
e.handleClosing("2")
e.handleClosing("3")
done <- true
return done
return time.After(100 * time.Millisecond)
})
}
func TestCloseAllVotes(t *testing.T) {
_ = testCloseCondition(t, PhaseClosed, func(e *Election) <-chan bool {
done := make(chan bool, 1)
_ = testElection(t, PhaseClosed, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
for i := 1; i <= e.Participants.Voters; i++ {
......@@ -272,42 +262,34 @@ func TestCloseAllVotes(t *testing.T) {
e.handleBallot(UniqueID(strconv.Itoa(i)), ballots["1"])
}
done <- true
return done
return time.After(100 * time.Millisecond)
})
}
func TestTallyDeadline(t *testing.T) {
_ = testCloseCondition(t, PhaseTallying, func(e *Election) <-chan bool {
done := make(chan bool, 1)
_ = testElection(t, PhaseTallying, func(e *Election) <-chan time.Time {
e.Deadline = time.Now().Add(1 * time.Second)
_ = e.Start()
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
e.handleBallot(UniqueID(strconv.Itoa(1)), ballots["1"])
go func() {
time.Sleep(1200 * time.Millisecond)
done <- true
}()
// By closing another server, we should go directly to tallying when
// deadline is reached
e.handleClosing("2")
return done
return time.After(1200 * time.Millisecond)
})
}
func TestNoVotesResult(t *testing.T) {
_ = testCloseCondition(t, PhaseResult, func(e *Election) <-chan bool {
done := make(chan bool, 1)
_ = testElection(t, PhaseResult, func(e *Election) <-chan time.Time {
done := make(chan time.Time, 1)
e.resultCallback = func(result *Result) {
if result != nil {
t.Error("Got a result wile none was expected")
}
done <- true
done <- time.Now()
}
e.Deadline = time.Now().Add(1000 * time.Millisecond)
_ = e.Start()
......@@ -319,8 +301,7 @@ func TestNoVotesResult(t *testing.T) {
}
func TestTallyAfterClose(t *testing.T) {
_ = testCloseCondition(t, PhaseTallying, func(e *Election) <-chan bool {
done := make(chan bool, 1)
_ = testElection(t, PhaseTallying, func(e *Election) <-chan time.Time {
e.Deadline = time.Now().Add(1000 * time.Millisecond)
_ = e.Start()
......@@ -329,12 +310,7 @@ func TestTallyAfterClose(t *testing.T) {
e.handleClosing("2")
go func() {
time.Sleep(1200 * time.Millisecond)
done <- true
}()
return done
return time.After(1200 * time.Millisecond)
})
}
......@@ -365,8 +341,7 @@ func TestHandleTally(t *testing.T) {
Commits: ballots["4"].Commits,
}
_ = testCloseCondition(t, PhaseResult, func(e *Election) <-chan bool {
done := make(chan bool, 1)
_ = testElection(t, PhaseResult, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.handleBallot("4", ballots["1"])
......@@ -382,13 +357,93 @@ func TestHandleTally(t *testing.T) {
t.Errorf("Expected 1 tally, got %d", e.tallyBox.Size())
}
fmt.Println("Phase:", e.Status.Phase)
e.handleClosing("3")
fmt.Println("Phase:", e.Status.Phase)
e.handleTally("3", tally2)
fmt.Println("Phase:", e.Status.Phase)
done <- true
return done
return time.After(500 * time.Millisecond)
})
}
func TestCloseTimeout(t *testing.T) {
_ = testElection(t, PhaseClosed, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Config.CloseSleep = 1 * time.Second
// This should start a go-routine waiting for late ballots
e.nextPhase(ReasonAgreement)
if e.Status.Phase != PhaseCloseWait {
t.Errorf("Not waiting for late ballots")
}
return time.After(1200 * time.Millisecond)
})
}
func TestCloseTimeoutTally(t *testing.T) {
_ = testElection(t, PhaseTallying, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Config.CloseSleep = 1 * time.Second
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
e.handleBallot(UniqueID(strconv.Itoa(1)), ballots["1"])
e.handleClosing("3")
// This should start a go-routine waiting for late ballots
e.nextPhase(ReasonAgreement)
e.handleClosing("4")
if e.Status.Phase != PhaseCloseWait {
t.Errorf("Not waiting for late ballots")
}
return time.After(1200 * time.Millisecond)
})
}
func TestCloseTimeoutSkipTally(t *testing.T) {
_ = testElection(t, PhaseResult, func(e *Election) <-chan time.Time {
e.Status.Phase = PhaseCollecting
e.Config.CloseSleep = 1 * time.Second
e.handleClosing("3")
// This should start a go-routine waiting for late ballots
e.nextPhase(ReasonAgreement)
e.handleClosing("4")
if e.Status.Phase != PhaseCloseWait {
t.Errorf("Not waiting for late ballots")
}
return time.After(1200 * time.Millisecond)
})
}
func TestCloseTimeoutLateBallot(t *testing.T) {
_ = testElection(t, PhaseClosed, func(e *Election) <-chan time.Time {
log.SetOutput(os.Stdout)
e.Status.Phase = PhaseCollecting
e.Config.CloseSleep = 500 * time.Millisecond
for i := 1; i < e.Participants.Voters; i++ {
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
e.handleBallot(UniqueID(strconv.Itoa(i)), ballots["1"])
}
// This should start a go-routine waiting for late ballots
e.nextPhase(ReasonAgreement)
// Send the last ballot
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
e.handleBallot(UniqueID(strconv.Itoa(e.Participants.Voters)), ballots["1"])
time.Sleep(10 * time.Millisecond)
if e.Status.Phase == PhaseCloseWait {
t.Errorf("Should skip waiting when all ballots received")
}
return time.After(1000 * time.Millisecond)
})
}
......@@ -40,23 +40,38 @@ func (e *Election) handleBallot(voterID UniqueID, ballot *Ballot) {
e.Lock()
defer e.Unlock()
if e.Status.Phase == PhaseCollecting {
if _, ok := e.Status.Turnout[voterID]; ok {
log.Printf("Election: error: voter %s tried to vote again\n", voterID)
return
}
err := e.ballotBox.Put(ballot)
if err != nil {
log.Println("Election: error:", err)
if e.Status.Phase == PhaseNotStarted || e.Status.Phase >= PhaseClosed {
log.Printf("Election error: ballot %s received outside collecting phase\n", ballot.ID)
return
}
// Closing phase have started but a ballot might be delayed.
if e.Status.Phase == PhaseCloseWait {
// Verify the ballot timestamp is before we tried to close
deadline := e.Status.CloseTime.Add(e.Config.CloseSleep)
if ballot.Timestamp.After(deadline) {
log.Printf("Election: error: ballot %s timestamp after close deadline\n", ballot.ID)
return
}
e.Status.Turnout[voterID] = true
}
if _, ok := e.Status.Turnout[voterID]; ok {
log.Printf("Election: error: voter %s tried to vote again\n", voterID)
return
}
err := e.ballotBox.Put(ballot)
if err != nil {
log.Println("Election: error:", err)
return
}
e.Status.Turnout[voterID] = true
if e.ballotBox.Size() >= e.Participants.Voters {
if e.ballotBox.Size() >= e.Participants.Voters {
if e.Status.Phase == PhaseCloseWait {
log.Println("Election: skipping close delay")
e.Status.PhaseChannel <- true
} else {
e.nextPhase(ReasonVotes)
}
} else {
log.Printf("Election: error: recevied ballot %s outside of collecting phase\n", ballot.ID)
}
}
......
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