Commit 84f69800 authored by Anders Jensen Løvig's avatar Anders Jensen Løvig
Browse files

Election: test close conditions

parent 0c751942
Pipeline #21330 failed with stages
in 14 seconds
......@@ -21,11 +21,13 @@ const (
MsgTally = "election_tally"
)
type Reason string
const (
reasonDeadline = "Deadline reached"
reasonVotes = "All votes received"
reasonAgreement = "Majority decided to end election"
reasonTally = "Recevied enough tallies"
ReasonDeadline = "Deadline reached"
ReasonVotes = "All votes received"
ReasonAgreement = "Majority decided to end election"
ReasonTally = "Recevied enough tallies"
)
// Phase of an election
......@@ -33,12 +35,29 @@ type Phase int
// Election phases
const (
PhaseCollecting Phase = iota // When voters can send ballots
PhaseWaiting // When waiting for other servers to close
PhaseTallying // When tallying
PhaseResult // When a result is available
PhaseNotStarted Phase = iota
PhaseCollecting // When voters can send ballots
PhaseClosed // When waiting for other servers to close
PhaseTallying // When tallying
PhaseResult // When a result is available
)
func (p Phase) String() string {
switch p {
case PhaseNotStarted:
return "PhaseNotStarted"
case PhaseCollecting:
return "PhaseCollecting"
case PhaseClosed:
return "PhaseClosed"
case PhaseTallying:
return "PhaseTallying"
case PhaseResult:
return "PhaseResult"
}
return "Unknown"
}
// Participants contains information about how many participants
/// are participating in this election.
type Participants struct {
......@@ -75,7 +94,7 @@ type Election struct {
// Struct containing server status about the election.
Status Status
// Called when the election is closed locally
closeCallback func()
closeCallback func(Reason)
// Called when a enough servers (Participants.RequiredServers) have closed
tallyCallback func()
// Called when the final result is available
......@@ -89,7 +108,7 @@ type Config struct {
Servers int `json:"servers"`
RequiredServers int `json:"required"`
Deadline time.Time `json:"deadline"`
CloseCallback func() `json:"-"`
CloseCallback func(Reason) `json:"-"`
TallyCallback func() `json:"-"`
ResultCallback func(*Result) `json:"-"`
}
......@@ -120,7 +139,7 @@ func NewElection(config *Config) *Election {
Result: nil,
Deadline: config.Deadline,
Status: Status{
Closed: true, // Election starts closed
Phase: PhaseNotStarted,
ClosedServers: make(map[UniqueID]bool),
},
closeCallback: config.CloseCallback,
......@@ -149,7 +168,10 @@ func (election *Election) Start() error {
election.Lock()
defer election.Unlock()
election.nextPhase(reasonDeadline)
// Deadline reached: Close election
if election.Status.Phase == PhaseCollecting {
election.nextPhase(ReasonDeadline)
}
}(election)
log.Println("Election started")
......@@ -160,41 +182,64 @@ func (election *Election) Start() error {
return nil
}
func (election *Election) nextPhase(reason string) {
func (election *Election) nextPhase(reason Reason) {
switch election.Status.Phase {
case PhaseCollecting:
log.Println("Election closing, reason:", reason)
election.closeElection(reason)
election.Status.Phase = PhaseWaiting
if !election.shouldTally() {
log.Println("Waiting for enough servers to close election")
} else {
election.nextPhase(reason) // Recursion :D
// If more than required servers have closed, then we can skip the next phase
// Else we have to wait for more servers to close
if len(election.Status.ClosedServers) < election.Participants.RequiredServers {
// Not enough servers to tally.
election.Status.Phase = PhaseClosed
n := len(election.Status.ClosedServers) - election.Participants.RequiredServers
log.Printf("Need an additional %d servers to tally election\n", n)
break
}
case PhaseWaiting:
// Else enough servers to start tallying
fallthrough
case PhaseClosed:
// If the ballot box is empty, there is no need to
if election.ballotBox.Count() == 0 {
// No votes
election.Status.Phase = PhaseResult
go election.resultCallback(nil)
} else {
election.Status.Phase = PhaseTallying
go election.tallyCallback()
}
case PhaseTallying:
election.Result = election.createResults()
election.Status.Phase = PhaseResult
go election.resultCallback(election.Result)
election.nextPhaseTally()
}
// switch election.Status.Phase {
// case PhaseCollecting:
// election.closeElection(reason)
// election.Status.Phase = PhaseClosed
// if !election.shouldTally() {
// log.Println("Waiting for enough servers to close election")
// } else {
// election.nextPhase(reason) // Recursion :D
// }
// case PhaseClosed:
// if election.ballotBox.Count() == 0 {
// // No votes
// election.Status.Phase = PhaseResult
// go election.resultCallback(nil)
// } else {
// election.Status.Phase = PhaseTallying
// go election.tallyCallback()
// }
// case PhaseTallying:
// election.Result = election.createResults()
// election.Status.Phase = PhaseResult
// go election.resultCallback(election.Result)
// }
}
func (election *Election) closeElection(reason string) {
if !election.Status.Closed {
log.Println("Election closing: " + reason)
func (election *Election) nextPhaseTally() {
// TODO
}
election.Status.Closed = true
election.Status.ClosedServers[election.ServerID] = true
func (election *Election) closeElection(reason Reason) {
election.Status.Closed = true
election.Status.ClosedServers[election.ServerID] = true
go election.closeCallback()
}
go election.closeCallback(reason)
}
// HandleBallot puts ballot in the ballot box if ballot is valid.
......@@ -215,7 +260,7 @@ func (election *Election) HandleBallot(voterID UniqueID, ballot *Ballot) {
defer election.Unlock()
if election.ballotBox.Count() >= election.Participants.Voters {
election.nextPhase(reasonVotes)
election.nextPhase(ReasonVotes)
}
}
......@@ -230,8 +275,8 @@ func (election *Election) HandleClosing(id UniqueID) {
}
election.Status.ClosedServers[id] = true
if election.shouldTally() && election.Status.Phase == PhaseWaiting {
election.nextPhase(reasonAgreement)
if election.shouldTally() && election.Status.Phase < PhaseTallying {
election.nextPhase(ReasonAgreement)
}
}
......@@ -259,7 +304,7 @@ func (election *Election) HandleTally(serverID UniqueID, tally *Tally) {
defer election.Unlock()
if election.tallyBox.Count() >= election.Participants.RequiredServers {
election.nextPhase(reasonTally)
election.nextPhase(ReasonTally)
}
}
......
......@@ -3,8 +3,12 @@ package election
import (
"bsc-shamir/crypto/common"
"crypto/rand"
"io/ioutil"
"log"
"math/big"
"strconv"
"testing"
"time"
)
/////////////////////
......@@ -108,3 +112,72 @@ func BenchmarkServerBallot(b *testing.B) {
b.Run("100 Votes", func(b *testing.B) { benchmarkServer(100, 5, 3, b) })
}
// Actual tests
func setupElection() *Election {
config := &Config{
ServerID: "1",
Voters: 5,
Servers: 3,
RequiredServers: 2,
Deadline: time.Now().Add(1 * time.Minute),
CloseCallback: func(reason Reason) {},
TallyCallback: func() {},
ResultCallback: func(result *Result) {},
}
return NewElection(config)
}
func testCloseCondition(t *testing.T, expectedReason Reason, expectedPhase Phase, fun func(*Election)) {
log.SetOutput(ioutil.Discard)
close := make(chan bool, 1)
timeout := make(chan bool, 1)
e := setupElection()
e.closeCallback = func(reason Reason) {
if reason != expectedReason {
t.Errorf("Unexpected close reason\n Expected: %s\n Actual: %s\n", expectedReason, reason)
}
close <- true
}
go func() {
time.Sleep(2 * time.Second)
timeout <- true
}()
e.Deadline = time.Now().Add(1 * time.Second)
_ = e.Start()
fun(e)
select {
case <-close:
case <-timeout:
t.Error("Timeout")
}
if e.Status.Phase != expectedPhase {
t.Errorf("Unexpected phase\n Expected: %s\n Actual: %s\n", expectedPhase, e.Status.Phase)
}
}
func TestCloseDeadline(t *testing.T) {
testCloseCondition(t, ReasonDeadline, PhaseClosed, func(e *Election) {})
}
func TestCloseAgreement(t *testing.T) {
testCloseCondition(t, ReasonAgreement, PhaseTallying, func(e *Election) {
e.HandleClosing("2")
e.HandleClosing("3")
})
}
func TestCloseAllVotes(t *testing.T) {
testCloseCondition(t, ReasonVotes, PhaseClosed, func(e *Election) {
for i := 1; i <= e.Participants.Voters; i++ {
ballots := CreateBallots(3, createXS(1), big.NewInt(1))
e.HandleBallot(UniqueID(strconv.Itoa(i)), ballots["1"])
}
})
}
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