From 209bcf905e1c8a50673ca2b73346cfdc232fa836 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 28 Oct 2015 19:49:35 +0200 Subject: [PATCH] proposer selection tests. closes #53 --- consensus/state.go | 19 ++++-- consensus/state_test.go | 114 ++++++++++++++++++++++++++---------- consensus/test.go | 32 +++++++++- types/validator_set.go | 7 ++- types/validator_set_test.go | 101 +++++++++++++++++++++++++------- 5 files changed, 217 insertions(+), 56 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 7490d39c0..6a46a4e5e 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -166,13 +166,16 @@ type ConsensusState struct { evsw events.Fireable evc *events.EventCache // set in stageBlock and passed into state + + decideProposalFunc func(cs *ConsensusState, height int, round int) } func NewConsensusState(state *sm.State, blockStore *bc.BlockStore, mempoolReactor *mempl.MempoolReactor) *ConsensusState { cs := &ConsensusState{ - blockStore: blockStore, - mempoolReactor: mempoolReactor, - newStepCh: make(chan *RoundState, 10), + blockStore: blockStore, + mempoolReactor: mempoolReactor, + newStepCh: make(chan *RoundState, 10), + decideProposalFunc: decideProposal, } cs.updateToState(state) // Don't call scheduleRound0 yet. @@ -182,6 +185,10 @@ func NewConsensusState(state *sm.State, blockStore *bc.BlockStore, mempoolReacto return cs } +func (cs *ConsensusState) SetDecideProposalFunc(f func(cs *ConsensusState, height int, round int)) { + cs.decideProposalFunc = f +} + // Reconstruct LastCommit from SeenValidation, which we saved along with the block, // (which happens even before saving the state) func (cs *ConsensusState) reconstructLastCommit(state *sm.State) { @@ -418,8 +425,12 @@ func (cs *ConsensusState) EnterPropose(height int, round int) { } } +func (cs *ConsensusState) decideProposal(height, round int) { + cs.decideProposalFunc(cs, height, round) +} + // Decides on the next proposal and sets them onto cs.Proposal* -func (cs *ConsensusState) decideProposal(height int, round int) { +func decideProposal(cs *ConsensusState, height, round int) { var block *types.Block var blockParts *types.PartSet diff --git a/consensus/state_test.go b/consensus/state_test.go index 335ad0291..5adc940e7 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -14,6 +14,7 @@ import ( /* ProposeSuite +x * TestProposerSelection - round robin ordering x * TestEnterProposeNoValidator - timeout into prevote round x * TestEnterPropose - finish propose without timing out (we have the proposal) x * TestBadProposal - 2 vals, bad proposal (bad block state hash), should prevote and precommit nil @@ -44,7 +45,77 @@ x * TestHalt1 - if we see +2/3 precommits after timing out into new round, we sh func init() { fmt.Println("") - timeoutPropose = 1000 * time.Millisecond + timeoutPropose = 500 * time.Millisecond +} + +func TestProposerSelection(t *testing.T) { + css, _ := simpleConsensusState(3) // test needs more work for more than 3 validators + cs1 := css[0] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + cs1.SetDecideProposalFunc(nilProposal) + + cs1.EnterNewRound(cs1.Height, 0, false) + + // everyone just votes nil. we get a new proposer each round + for i := 0; i < len(css); i++ { + if i == len(css)-1 { + // reset cs1's decideProposal function for later + cs1.SetDecideProposalFunc(decideProposal) + } + prop := cs1.Validators.Proposer() + if !bytes.Equal(prop.Address, css[i].privValidator.Address) { + t.Fatalf("expected proposer to be validator %d. Got %X", i, prop.Address) + } + nilRound(t, 0, cs1, css[1:]...) + incrementRound(css[1:]...) + + } + + // now we should be back at first validator. + // lets commit a block and ensure proposer for the next height is correct + height := cs1.Height + prop := cs1.Validators.Proposer() + if !bytes.Equal(prop.Address, cs1.privValidator.Address) { + t.Fatalf("expected proposer to be validator %d. Got %X", 0, prop.Address) + } + signAddVoteToFromMany(types.VoteTypePrevote, cs1, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header(), css[1:]...) + <-cs1.NewStepCh() // prevotes + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header(), css[1:]...) + <-cs1.NewStepCh() // + <-cs1.NewStepCh() // go to next round + if cs1.Height != height+1 { + t.Fatal("Expected height to increment. Got", cs1.Height) + } + + prop = cs1.Validators.Proposer() + if !bytes.Equal(prop.Address, css[1].privValidator.Address) { + t.Fatalf("expected proposer to be validator %d. Got %X", 1, prop.Address) + } + + // Now let's do it all again, but starting from round 2 instead of 0 + + css, _ = simpleConsensusState(3) // test needs more work for more than 3 validators + cs1 = css[0] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + cs1.SetDecideProposalFunc(nilProposal) + + // this time we jump in at round 2 + incrementRound(css[1:]...) + incrementRound(css[1:]...) + cs1.EnterNewRound(cs1.Height, 2, false) + + // everyone just votes nil. we get a new proposer each round + for i := 0; i < len(css); i++ { + prop := cs1.Validators.Proposer() + if !bytes.Equal(prop.Address, css[(i+2)%len(css)].privValidator.Address) { + t.Fatalf("expected proposer to be validator %d. Got %X", (i+2)%len(css), prop.Address) + } + nilRound(t, 2, cs1, css[1:]...) + incrementRound(css[1:]...) + } + } // a non-validator should timeout into the prevote round @@ -175,7 +246,7 @@ func TestBadProposal(t *testing.T) { } //---------------------------------------------------------------------------------------------------- -// FulLRoundSuite +// FullRoundSuite // propose, prevote, and precommit a block func TestFullRound1(t *testing.T) { @@ -202,23 +273,14 @@ func TestFullRoundNil(t *testing.T) { css, privVals := simpleConsensusState(1) cs := css[0] cs.newStepCh = make(chan *RoundState) // so it blocks - cs.SetPrivValidator(nil) - timeoutChan := make(chan struct{}) - evsw := events.NewEventSwitch() - evsw.OnStart() - evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { - timeoutChan <- struct{}{} - }) - cs.SetFireable(evsw) + cs.SetDecideProposalFunc(nilProposal) // starts a go routine for EnterPropose cs.EnterNewRound(cs.Height, 0, false) // wait to finish propose (we should time out) <-cs.NewStepCh() - cs.SetPrivValidator(privVals[0]) // this might be a race condition (uses the mutex that EnterPropose has just released and EnterPrevote is about to grab) - <-timeoutChan // wait to finish prevote <-cs.NewStepCh() @@ -328,12 +390,10 @@ func TestLockNoPOL(t *testing.T) { incrementRound(cs2) - // go to prevote - <-cs1.NewStepCh() - - // now we're on a new round and the proposer - if cs1.ProposalBlock != cs1.LockedBlock { - t.Fatalf("Expected proposal block to be locked block. Got %v, Expected %v", cs1.ProposalBlock, cs1.LockedBlock) + // now we're on a new round and not the proposer, so wait for timeout + _, _ = <-cs1.NewStepCh(), <-timeoutChan + if cs1.ProposalBlock != nil { + t.Fatal("Expected proposal block to be nil") } // wait to finish prevote @@ -368,10 +428,11 @@ func TestLockNoPOL(t *testing.T) { incrementRound(cs2) - // now we're on a new round and not the proposer, so wait for timeout - _, _ = <-cs1.NewStepCh(), <-timeoutChan - if cs1.ProposalBlock != nil { - t.Fatal("Expected proposal block to be nil") + <-cs1.newStepCh + + // now we're on a new round and are the proposer + if cs1.ProposalBlock != cs1.LockedBlock { + t.Fatalf("Expected proposal block to be locked block. Got %v, Expected %v", cs1.ProposalBlock, cs1.LockedBlock) } // go to prevote, prevote for locked block @@ -386,14 +447,7 @@ func TestLockNoPOL(t *testing.T) { <-cs1.NewStepCh() - // before we time out into new round, set next proposer - // and next proposal block - _, v1 := cs1.Validators.GetByAddress(privVals[0].Address) - v1.VotingPower = 1 - if updated := cs1.Validators.Update(v1); !updated { - t.Fatal("failed to update validator") - } - + // before we time out into new round, set next proposal block cs2.decideProposal(cs2.Height, cs2.Round+1) prop, propBlock := cs2.Proposal, cs2.ProposalBlock if prop == nil || propBlock == nil { diff --git a/consensus/test.go b/consensus/test.go index 4a5e9bf04..6f210aa81 100644 --- a/consensus/test.go +++ b/consensus/test.go @@ -18,6 +18,36 @@ import ( //------------------------------------------------------------------------------- // utils +func nilProposal(cs *ConsensusState, height, round int) { + // Make proposal + proposal := types.NewProposal(height, round, types.PartSetHeader{}, -1) + err := cs.privValidator.SignProposal(cs.state.ChainID, proposal) + if err == nil { + log.Notice("Signed and set proposal", "height", height, "round", round, "proposal", proposal) + // Set fields + cs.Proposal = proposal + cs.ProposalBlock = nil + cs.ProposalBlockParts = nil + } else { + log.Warn("EnterPropose: Error signing proposal", "height", height, "round", round, "error", err) + } +} + +func nilRound(t *testing.T, startRound int, cs1 *ConsensusState, css ...*ConsensusState) { + round := cs1.Round + if round == startRound { + _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh() + } + signAddVoteToFromMany(types.VoteTypePrevote, cs1, nil, cs1.ProposalBlockParts.Header(), css...) + <-cs1.NewStepCh() // prevotes + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, cs1.ProposalBlockParts.Header(), css...) + <-cs1.NewStepCh() // + <-cs1.NewStepCh() // go to next round + if cs1.Round != round+1 { + t.Fatal("Expected round to increment. Got", cs1.Round) + } +} + func changeProposer(t *testing.T, perspectiveOf, newProposer *ConsensusState) *types.Block { _, v1 := perspectiveOf.Validators.GetByAddress(perspectiveOf.privValidator.Address) v1.Accum, v1.VotingPower = 0, 0 @@ -62,7 +92,7 @@ func addVoteToFrom(to, from *ConsensusState, vote *types.Vote) { if _, ok := err.(*types.ErrVoteConflictingSignature); ok { // let it fly } else if !added { - panic("Failed to add vote") + panic(fmt.Sprintln("Failed to add vote. Err:", err)) } else if err != nil { panic(fmt.Sprintln("Failed to add vote:", err)) } diff --git a/types/validator_set.go b/types/validator_set.go index 763a6ce2d..b00178197 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -35,9 +35,14 @@ func NewValidatorSet(vals []*Validator) *ValidatorSet { validators[i] = val.Copy() } sort.Sort(ValidatorsByAddress(validators)) - return &ValidatorSet{ + vs := &ValidatorSet{ Validators: validators, } + + if vals != nil { + vs.IncrementAccum(1) + } + return vs } // TODO: mind the overflow when times and votingPower shares too large. diff --git a/types/validator_set_test.go b/types/validator_set_test.go index c20f010c4..a94a4ebbf 100644 --- a/types/validator_set_test.go +++ b/types/validator_set_test.go @@ -49,37 +49,98 @@ func TestCopy(t *testing.T) { func TestProposerSelection(t *testing.T) { vset := NewValidatorSet([]*Validator{ - &Validator{ - Address: []byte("foo"), - PubKey: randPubKey(), - VotingPower: 1000, - Accum: 0, - }, - &Validator{ - Address: []byte("bar"), - PubKey: randPubKey(), - VotingPower: 300, - Accum: 0, - }, - &Validator{ - Address: []byte("baz"), - PubKey: randPubKey(), - VotingPower: 330, - Accum: 0, - }, + newValidator([]byte("foo"), 1000), + newValidator([]byte("bar"), 300), + newValidator([]byte("baz"), 330), }) proposers := []string{} - for i := 0; i < 100; i++ { + for i := 0; i < 99; i++ { val := vset.Proposer() proposers = append(proposers, string(val.Address)) vset.IncrementAccum(1) } - expected := `bar foo baz foo bar foo foo baz foo bar foo foo baz foo foo bar foo baz foo foo bar foo foo baz foo bar foo foo baz foo bar foo foo baz foo foo bar foo baz foo foo bar foo baz foo foo bar foo baz foo foo bar foo baz foo foo foo baz bar foo foo foo baz foo bar foo foo baz foo bar foo foo baz foo bar foo foo baz foo bar foo foo baz foo foo bar foo baz foo foo bar foo baz foo foo bar foo baz foo foo` + expected := `foo baz foo bar foo foo baz foo bar foo foo baz foo foo bar foo baz foo foo bar foo foo baz foo bar foo foo baz foo bar foo foo baz foo foo bar foo baz foo foo bar foo baz foo foo bar foo baz foo foo bar foo baz foo foo foo baz bar foo foo foo baz foo bar foo foo baz foo bar foo foo baz foo bar foo foo baz foo bar foo foo baz foo foo bar foo baz foo foo bar foo baz foo foo bar foo baz foo foo` if expected != strings.Join(proposers, " ") { t.Errorf("Expected sequence of proposers was\n%v\nbut got \n%v", expected, strings.Join(proposers, " ")) } } +func newValidator(address []byte, power int64) *Validator { + return &Validator{Address: address, VotingPower: power} +} + +func TestProposerSelection2(t *testing.T) { + addr1 := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + addr2 := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + addr3 := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} + + // when all voting power is same, we go in order of addresses + val1, val2, val3 := newValidator(addr1, 100), newValidator(addr2, 100), newValidator(addr3, 100) + valList := []*Validator{val1, val2, val3} + vals := NewValidatorSet(valList) + for i := 0; i < len(valList)*5; i++ { + ii := i % len(valList) + prop := vals.Proposer() + if !bytes.Equal(prop.Address, valList[ii].Address) { + t.Fatalf("Expected %X. Got %X", valList[ii].Address, prop.Address) + } + vals.IncrementAccum(1) + } + + // One validator has more than the others, but not enough to propose twice in a row + *val3 = *newValidator(addr3, 400) + vals = NewValidatorSet(valList) + prop := vals.Proposer() + if !bytes.Equal(prop.Address, addr3) { + t.Fatalf("Expected address with highest voting power to be first proposer. Got %X", prop.Address) + } + vals.IncrementAccum(1) + prop = vals.Proposer() + if !bytes.Equal(prop.Address, addr1) { + t.Fatalf("Expected smallest address to be validator. Got %X", prop.Address) + } + + // One validator has more than the others, and enough to be proposer twice in a row + *val3 = *newValidator(addr3, 401) + vals = NewValidatorSet(valList) + prop = vals.Proposer() + if !bytes.Equal(prop.Address, addr3) { + t.Fatalf("Expected address with highest voting power to be first proposer. Got %X", prop.Address) + } + vals.IncrementAccum(1) + prop = vals.Proposer() + if !bytes.Equal(prop.Address, addr3) { + t.Fatalf("Expected address with highest voting power to be second proposer. Got %X", prop.Address) + } + vals.IncrementAccum(1) + prop = vals.Proposer() + if !bytes.Equal(prop.Address, addr1) { + t.Fatalf("Expected smallest address to be validator. Got %X", prop.Address) + } + + // each validator should be the proposer a proportional number of times + val1, val2, val3 = newValidator(addr1, 4), newValidator(addr2, 5), newValidator(addr3, 3) + valList = []*Validator{val1, val2, val3} + propCount := make([]int, 3) + vals = NewValidatorSet(valList) + for i := 0; i < 120; i++ { + prop := vals.Proposer() + ii := prop.Address[19] + propCount[ii] += 1 + vals.IncrementAccum(1) + } + + if propCount[0] != 40 { + t.Fatalf("Expected prop count for validator with 4/12 of voting power to be 40/120. Got %d/120", propCount[0]) + } + if propCount[1] != 50 { + t.Fatalf("Expected prop count for validator with 5/12 of voting power to be 50/120. Got %d/120", propCount[1]) + } + if propCount[2] != 30 { + t.Fatalf("Expected prop count for validator with 3/12 of voting power to be 30/120. Got %d/120", propCount[2]) + } +} + func BenchmarkValidatorSetCopy(b *testing.B) { b.StopTimer() vset := NewValidatorSet([]*Validator{})