diff --git a/consensus/state.go b/consensus/state.go index aa488b52..41a88e99 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1316,10 +1316,8 @@ func (cs *ConsensusState) saveBlock(block *types.Block, blockParts *types.PartSe // Fire off event if cs.evsw != nil && cs.evc != nil { - go func(block *types.Block) { - cs.evsw.FireEvent(types.EventStringNewBlock(), types.EventDataNewBlock{block}) - cs.evc.Flush() - }(block) + cs.evsw.FireEvent(types.EventStringNewBlock(), types.EventDataNewBlock{block}) + go cs.evc.Flush() } } diff --git a/consensus/state_test.go b/consensus/state_test.go new file mode 100644 index 00000000..b168041e --- /dev/null +++ b/consensus/state_test.go @@ -0,0 +1,1177 @@ +package consensus + +import ( + "bytes" + "fmt" + "testing" + "time" + + _ "github.com/tendermint/tendermint/config/tendermint_test" + "github.com/tendermint/tendermint/events" + "github.com/tendermint/tendermint/types" +) + +/* + +ProposeSuite +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 +FullRoundSuite +x * TestFullRound1 - 1 val, full successful round +x * TestFullRoundNil - 1 val, full round of nil +x * TestFullRound2 - 2 vals, both required for fuill round +LockSuite +x * TestLockNoPOL - 2 vals, 4 rounds. one val locked, precommits nil every round except first. +x * TestLockPOLRelock - 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka +x * TestLockPOLUnlock - 4 vals, one precommits, other 3 polka nil at next round, so we unlock and precomit nil +x * TestLockPOLSafety1 - 4 vals. We shouldn't change lock based on polka at earlier round +x * TestLockPOLSafety2 - 4 vals. After unlocking, we shouldn't relock based on polka at earlier round + * TestNetworkLock - once +1/3 precommits, network should be locked + * TestNetworkLockPOL - once +1/3 precommits, the block with more recent polka is committed +SlashingSuite +x * TestSlashingPrevotes - a validator prevoting twice in a round gets slashed +x * TestSlashingPrecommits - a validator precomitting twice in a round gets slashed +CatchupSuite + * TestCatchup - if we might be behind and we've seen any 2/3 prevotes, round skip to new round, precommit, or prevote +HaltSuite +x * TestHalt1 - if we see +2/3 precommits after timing out into new round, we should still commit + +*/ + +//---------------------------------------------------------------------------------------------------- +// ProposeSuite + +func init() { + fmt.Println("") + timeoutPropose = 1000 * time.Millisecond +} + +// a non-validator should timeout into the prevote round +func TestEnterProposeNoPrivValidator(t *testing.T) { + css, _ := simpleConsensusState(1) + cs := css[0] + 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) + + // starts a go routine for EnterPropose + cs.EnterNewRound(cs.Height, 0, false) + + // go to prevote + <-cs.NewStepCh() + + // if we're not a validator, EnterPropose should timeout + select { + case rs := <-cs.NewStepCh(): + log.Info(rs.String()) + t.Fatal("Expected EnterPropose to timeout") + case <-timeoutChan: + rs := cs.GetRoundState() + if rs.Proposal != nil { + t.Error("Expected to make no proposal, since no privValidator") + } + break + } +} + +// a validator should not timeout of the prevote round (TODO: unless the block is really big!) +func TestEnterPropose(t *testing.T) { + css, _ := simpleConsensusState(1) + cs := css[0] + + timeoutChan := make(chan struct{}) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { + timeoutChan <- struct{}{} + }) + cs.SetFireable(evsw) + + // starts a go routine for EnterPropose + cs.EnterNewRound(cs.Height, 0, false) + + // go to prevote + <-cs.NewStepCh() + + // if we are a validator, we expect it not to timeout + select { + case <-cs.NewStepCh(): + rs := cs.GetRoundState() + + // Check that Proposal, ProposalBlock, ProposalBlockParts are set. + if rs.Proposal == nil { + t.Error("rs.Proposal should be set") + } + if rs.ProposalBlock == nil { + t.Error("rs.ProposalBlock should be set") + } + if rs.ProposalBlockParts.Total() == 0 { + t.Error("rs.ProposalBlockParts should be set") + } + break + case <-timeoutChan: + t.Fatal("Expected EnterPropose not to timeout") + } +} + +func TestBadProposal(t *testing.T) { + css, privVals := simpleConsensusState(2) + cs1, cs2 := css[0], css[1] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + timeoutChan := make(chan struct{}) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { + timeoutChan <- struct{}{} + }) + evsw.AddListenerForEvent("tester", types.EventStringTimeoutWait(), func(data types.EventData) { + timeoutChan <- struct{}{} + }) + cs1.SetFireable(evsw) + + // make the second validator the proposer + propBlock := changeProposer(t, cs1, cs2) + + // make the block bad by tampering with statehash + stateHash := propBlock.StateHash + stateHash[0] = byte((stateHash[0] + 1) % 255) + propBlock.StateHash = stateHash + propBlockParts := propBlock.MakePartSet() + proposal := types.NewProposal(cs2.Height, cs2.Round, propBlockParts.Header(), cs2.Votes.POLRound()) + if err := cs2.privValidator.SignProposal(cs2.state.ChainID, proposal); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + + // start round + cs1.EnterNewRound(cs1.Height, 0, false) + + // now we're on a new round and not the proposer + <-cs1.NewStepCh() + // so set the proposal block (and fix voting power) + cs1.mtx.Lock() + cs1.Proposal, cs1.ProposalBlock, cs1.ProposalBlockParts = proposal, propBlock, propBlockParts + fixVotingPower(t, cs1, privVals[1].Address) + cs1.mtx.Unlock() + // and wait for timeout + <-timeoutChan + + // go to prevote, prevote for nil (proposal is bad) + <-cs1.NewStepCh() + validatePrevote(t, cs1, 0, privVals[0], nil) + + // add bad prevote from cs2. we should precommit nil + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, propBlock.Hash(), propBlock.MakePartSet().Header()) + _, _, _ = <-cs1.NewStepCh(), <-timeoutChan, <-cs1.NewStepCh() + validatePrecommit(t, cs1, 0, 0, privVals[0], nil, nil) + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, propBlock.Hash(), propBlock.MakePartSet().Header()) +} + +//---------------------------------------------------------------------------------------------------- +// FulLRoundSuite + +// propose, prevote, and precommit a block +func TestFullRound1(t *testing.T) { + css, privVals := simpleConsensusState(1) + cs := css[0] + + // starts a go routine for EnterPropose + cs.EnterNewRound(cs.Height, 0, false) + // wait to finish propose and prevote + _, _ = <-cs.NewStepCh(), <-cs.NewStepCh() + + // we should now be in precommit + // verify our prevote is there + cs.mtx.Lock() + propBlockHash := cs.ProposalBlock.Hash() + cs.mtx.Unlock() + + // the proposed block should be prevoted, precommitted, and locked + validatePrevoteAndPrecommit(t, cs, 0, 0, privVals[0], propBlockHash, propBlockHash, nil) +} + +// nil is proposed, so prevote and precommit nil +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) + + // 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() + + // should prevote and precommit nil + validatePrevoteAndPrecommit(t, cs, 0, 0, privVals[0], nil, nil, nil) +} + +// run through propose, prevote, precommit commit with two validators +// where the first validator has to wait for votes from the second +func TestFullRound2(t *testing.T) { + css, privVals := simpleConsensusState(2) + cs1, cs2 := css[0], css[1] + cs1.newStepCh = make(chan *RoundState) // so it blocks + cs2.newStepCh = make(chan *RoundState) // so it blocks + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh() + + // we should now be stuck in limbo forever, waiting for more prevotes + ensureNoNewStep(t, cs1) + + propBlockHash, propPartsHeader := cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header() + + // prevote arrives from cs2: + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, propBlockHash, propPartsHeader) + + // wait to finish precommit + <-cs1.NewStepCh() + + // the proposed block should now be locked and our precommit added + validatePrecommit(t, cs1, 0, 0, privVals[0], propBlockHash, propBlockHash) + + // we should now be stuck in limbo forever, waiting for more precommits + ensureNoNewStep(t, cs1) + + // precommit arrives from cs2: + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, propBlockHash, propPartsHeader) + + // wait to finish commit, propose in next height + _, rs := <-cs1.NewStepCh(), <-cs1.NewStepCh() + if rs.Height != 2 { + t.Fatal("Expected height to increment") + } +} + +//------------------------------------------------------------------------------------------ +// LockSuite + +// two validators, 4 rounds. +// val1 proposes the first 2 rounds, and is locked in the first. +// val2 proposes the next two. val1 should precommit nil on all (except first where he locks) +func TestLockNoPOL(t *testing.T) { + css, privVals := simpleConsensusState(2) + cs1, cs2 := css[0], css[1] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + timeoutChan := make(chan struct{}) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { + timeoutChan <- struct{}{} + }) + evsw.AddListenerForEvent("tester", types.EventStringTimeoutWait(), func(data types.EventData) { + timeoutChan <- struct{}{} + }) + cs1.SetFireable(evsw) + + /* + Round1 (cs1, B) // B B // B B2 + */ + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh() + + // we should now be stuck in limbo forever, waiting for more prevotes + // prevote arrives from cs2: + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + + cs1.mtx.Lock() // XXX: sigh + theBlockHash := cs1.ProposalBlock.Hash() + cs1.mtx.Unlock() + + // wait to finish precommit + <-cs1.NewStepCh() + + // the proposed block should now be locked and our precommit added + validatePrecommit(t, cs1, 0, 0, privVals[0], theBlockHash, theBlockHash) + + // we should now be stuck in limbo forever, waiting for more precommits + // lets add one for a different block + // NOTE: in practice we should never get to a point where there are precommits for different blocks at the same round + hash := cs1.ProposalBlock.Hash() + hash[0] = byte((hash[0] + 1) % 255) + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, hash, cs1.ProposalBlockParts.Header()) + + // (note we're entering precommit for a second time this round) + // but with invalid args. then we EnterPrecommitWait, and the timeout to new round + _, _ = <-cs1.NewStepCh(), <-timeoutChan + + log.Info("#### ONTO ROUND 2") + /* + Round2 (cs1, B) // B B2 + */ + + 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) + } + + // wait to finish prevote + <-cs1.NewStepCh() + + // we should have prevoted our locked block + validatePrevote(t, cs1, 1, privVals[0], cs1.LockedBlock.Hash()) + + // add a conflicting prevote from the other validator + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, hash, cs1.ProposalBlockParts.Header()) + + // now we're going to enter prevote again, but with invalid args + // and then prevote wait, which should timeout. then wait for precommit + _, _, _ = <-cs1.NewStepCh(), <-timeoutChan, <-cs1.NewStepCh() + + // the proposed block should still be locked and our precommit added + // we should precommit nil and be locked on the proposal + validatePrecommit(t, cs1, 1, 0, privVals[0], nil, theBlockHash) + + // add conflicting precommit from cs2 + // NOTE: in practice we should never get to a point where there are precommits for different blocks at the same round + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, hash, cs1.ProposalBlockParts.Header()) + + // (note we're entering precommit for a second time this round, but with invalid args + // then we EnterPrecommitWait and timeout into NewRound + _, _ = <-cs1.NewStepCh(), <-timeoutChan + + log.Info("#### ONTO ROUND 3") + /* + Round3 (cs2, _) // B, B2 + */ + + 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") + } + + // go to prevote, prevote for locked block + <-cs1.NewStepCh() + validatePrevote(t, cs1, 0, privVals[0], cs1.LockedBlock.Hash()) + + // TODO: quick fastforward to new round, set proposer + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, hash, cs1.ProposalBlockParts.Header()) + _, _, _ = <-cs1.NewStepCh(), <-timeoutChan, <-cs1.NewStepCh() + validatePrecommit(t, cs1, 2, 0, privVals[0], nil, theBlockHash) // precommit nil but be locked on proposal + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, hash, cs1.ProposalBlockParts.Header()) // NOTE: conflicting precommits at same height + + <-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") + } + + cs2.decideProposal(cs2.Height, cs2.Round+1) + prop, propBlock := cs2.Proposal, cs2.ProposalBlock + if prop == nil || propBlock == nil { + t.Fatal("Failed to create proposal block with cs2") + } + + incrementRound(cs2) + + <-timeoutChan + + log.Info("#### ONTO ROUND 4") + /* + Round4 (cs2, C) // B C // B C + */ + + // now we're on a new round and not the proposer + <-cs1.NewStepCh() + // so set the proposal block + cs1.mtx.Lock() + cs1.Proposal, cs1.ProposalBlock = prop, propBlock + cs1.mtx.Unlock() + // and wait for timeout + <-timeoutChan + // go to prevote, prevote for locked block (not proposal) + <-cs1.NewStepCh() + validatePrevote(t, cs1, 0, privVals[0], cs1.LockedBlock.Hash()) + + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, propBlock.Hash(), propBlock.MakePartSet().Header()) + _, _, _ = <-cs1.NewStepCh(), <-timeoutChan, <-cs1.NewStepCh() + validatePrecommit(t, cs1, 2, 0, privVals[0], nil, theBlockHash) // precommit nil but locked on proposal + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, propBlock.Hash(), propBlock.MakePartSet().Header()) // NOTE: conflicting precommits at same height +} + +// 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka +func TestLockPOLRelock(t *testing.T) { + css, privVals := simpleConsensusState(4) + cs1, cs2, cs3, cs4 := css[0], css[1], css[2], css[3] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + timeoutChan := make(chan *types.EventDataRoundState) + voteChan := make(chan *types.EventDataVote) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringTimeoutWait(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringVote(), func(data types.EventData) { + vote := data.(*types.EventDataVote) + // we only fire for our own votes + if bytes.Equal(cs1.privValidator.Address, vote.Address) { + voteChan <- vote + } + }) + cs1.SetFireable(evsw) + + // everything done from perspective of cs1 + + /* + Round1 (cs1, B) // B B B B// B nil B nil + + eg. cs2 and cs4 didn't see the 2/3 prevotes + */ + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _, _ = <-cs1.NewStepCh(), <-voteChan, <-cs1.NewStepCh() + + theBlockHash := cs1.ProposalBlock.Hash() + + // wait to finish precommit after prevotes done + // we do this in a go routine with another channel since otherwise + // we may get deadlock with EnterPrecommit waiting to send on newStepCh and the final + // signAddVoteToFrom waiting for the cs.mtx.Lock + donePrecommit := make(chan struct{}) + go func() { + <-voteChan + <-cs1.NewStepCh() + donePrecommit <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrevote, cs1, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header(), cs2, cs3, cs4) + <-donePrecommit + + // the proposed block should now be locked and our precommit added + validatePrecommit(t, cs1, 0, 0, privVals[0], theBlockHash, theBlockHash) + + donePrecommitWait := make(chan struct{}) + go func() { + // (note we're entering precommit for a second time this round) + // but with invalid args. then we EnterPrecommitWait, twice (?) + <-cs1.NewStepCh() + donePrecommitWait <- struct{}{} + }() + // add precommits from the rest + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, types.PartSetHeader{}, cs2, cs4) + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs3, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + <-donePrecommitWait + + // 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") + } + + cs2.decideProposal(cs2.Height, cs2.Round+1) + prop, propBlock := cs2.Proposal, cs2.ProposalBlock + if prop == nil || propBlock == nil { + t.Fatal("Failed to create proposal block with cs2") + } + + incrementRound(cs2, cs3, cs4) + + // timeout to new round + te := <-timeoutChan + if te.Step != RoundStepPrecommitWait.String() { + t.Fatalf("expected to timeout of precommit into new round. got %v", te.Step) + } + + log.Info("### ONTO ROUND 2") + + /* + Round2 (cs2, C) // B C C C // C C C _) + + cs1 changes lock! + */ + + // now we're on a new round and not the proposer + <-cs1.NewStepCh() + cs1.mtx.Lock() + // so set the proposal block + propBlockHash, propBlockParts := propBlock.Hash(), propBlock.MakePartSet() + cs1.Proposal, cs1.ProposalBlock, cs1.ProposalBlockParts = prop, propBlock, propBlockParts + cs1.mtx.Unlock() + // and wait for timeout + te = <-timeoutChan + if te.Step != RoundStepPropose.String() { + t.Fatalf("expected to timeout of propose. got %v", te.Step) + } + // go to prevote, prevote for locked block (not proposal), move on + _, _ = <-voteChan, <-cs1.NewStepCh() + validatePrevote(t, cs1, 0, privVals[0], theBlockHash) + + donePrecommit = make(chan struct{}) + go func() { + // we need this go routine because if we go into PrevoteWait it has to pull on newStepCh + // before the final vote will get added (because it holds the mutex). + select { + case <-cs1.NewStepCh(): // we're in PrevoteWait, go to Precommit + <-voteChan + case <-voteChan: // we went straight to Precommit + } + donePrecommit <- struct{}{} + }() + // now lets add prevotes from everyone else for the new block + signAddVoteToFromMany(types.VoteTypePrevote, cs1, propBlockHash, propBlockParts.Header(), cs2, cs3, cs4) + <-donePrecommit + + // we should have unlocked and locked on the new block + validatePrecommit(t, cs1, 1, 1, privVals[0], propBlockHash, propBlockHash) + + donePrecommitWait = make(chan struct{}) + go func() { + // (note we're entering precommit for a second time this round) + // but with invalid args. then we EnterPrecommitWait, + <-cs1.NewStepCh() + donePrecommitWait <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, propBlockHash, propBlockParts.Header(), cs2, cs3) + <-donePrecommitWait + + <-cs1.NewStepCh() + rs := <-cs1.NewStepCh() + if rs.Height != 2 { + t.Fatal("Expected height to increment") + } + + if hash, _, ok := rs.LastCommit.TwoThirdsMajority(); !ok || !bytes.Equal(hash, propBlockHash) { + t.Fatal("Expected block to get committed") + } +} + +// 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka +func TestLockPOLUnlock(t *testing.T) { + css, privVals := simpleConsensusState(4) + cs1, cs2, cs3, cs4 := css[0], css[1], css[2], css[3] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + timeoutChan := make(chan *types.EventDataRoundState) + voteChan := make(chan *types.EventDataVote) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringTimeoutWait(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringVote(), func(data types.EventData) { + vote := data.(*types.EventDataVote) + // we only fire for our own votes + if bytes.Equal(cs1.privValidator.Address, vote.Address) { + voteChan <- vote + } + }) + cs1.SetFireable(evsw) + + // everything done from perspective of cs1 + + /* + Round1 (cs1, B) // B B B B // B nil B nil + + eg. didn't see the 2/3 prevotes + */ + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _, _ = <-cs1.NewStepCh(), <-voteChan, <-cs1.NewStepCh() + + theBlockHash := cs1.ProposalBlock.Hash() + + donePrecommit := make(chan struct{}) + go func() { + <-voteChan + <-cs1.NewStepCh() + donePrecommit <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrevote, cs1, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header(), cs2, cs3, cs4) + <-donePrecommit + + // the proposed block should now be locked and our precommit added + validatePrecommit(t, cs1, 0, 0, privVals[0], theBlockHash, theBlockHash) + + donePrecommitWait := make(chan struct{}) + go func() { + <-cs1.NewStepCh() + donePrecommitWait <- struct{}{} + }() + // add precommits from the rest + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, types.PartSetHeader{}, cs2, cs4) + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs3, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + <-donePrecommitWait + + // 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") + } + + cs2.decideProposal(cs2.Height, cs2.Round+1) + prop, propBlock := cs2.Proposal, cs2.ProposalBlock + if prop == nil || propBlock == nil { + t.Fatal("Failed to create proposal block with cs2") + } + + incrementRound(cs2, cs3, cs4) + + // timeout to new round + <-timeoutChan + + log.Info("#### ONTO ROUND 2") + /* + Round2 (cs2, C) // B nil nil nil // nil nil nil _ + + cs1 unlocks! + */ + + // now we're on a new round and not the proposer, + <-cs1.NewStepCh() + cs1.mtx.Lock() + // so set the proposal block + cs1.Proposal, cs1.ProposalBlock, cs1.ProposalBlockParts = prop, propBlock, propBlock.MakePartSet() + lockedBlockHash := cs1.LockedBlock.Hash() + cs1.mtx.Unlock() + // and wait for timeout + <-timeoutChan + + // go to prevote, prevote for locked block (not proposal) + _, _ = <-voteChan, <-cs1.NewStepCh() + validatePrevote(t, cs1, 0, privVals[0], lockedBlockHash) + + donePrecommit = make(chan struct{}) + go func() { + select { + case <-cs1.NewStepCh(): // we're in PrevoteWait, go to Precommit + <-voteChan + case <-voteChan: // we went straight to Precommit + } + donePrecommit <- struct{}{} + }() + // now lets add prevotes from everyone else for the new block + signAddVoteToFromMany(types.VoteTypePrevote, cs1, nil, types.PartSetHeader{}, cs2, cs3, cs4) + <-donePrecommit + + // we should have unlocked + // NOTE: we don't lock on nil, so LockedRound is still 0 + validatePrecommit(t, cs1, 1, 0, privVals[0], nil, nil) + + donePrecommitWait = make(chan struct{}) + go func() { + // the votes will bring us to new round right away + // we should timeout of it + _, _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh(), <-timeoutChan + donePrecommitWait <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, types.PartSetHeader{}, cs2, cs3) + <-donePrecommitWait +} + +// 4 vals +// a polka at round 1 but we miss it +// then a polka at round 2 that we lock on +// then we see the polka from round 1 but shouldn't unlock +func TestLockPOLSafety1(t *testing.T) { + css, privVals := simpleConsensusState(4) + cs1, cs2, cs3, cs4 := css[0], css[1], css[2], css[3] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + timeoutChan := make(chan *types.EventDataRoundState) + voteChan := make(chan *types.EventDataVote) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringTimeoutWait(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringVote(), func(data types.EventData) { + vote := data.(*types.EventDataVote) + // we only fire for our own votes + if bytes.Equal(cs1.privValidator.Address, vote.Address) { + voteChan <- vote + } + }) + cs1.SetFireable(evsw) + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _, _ = <-cs1.NewStepCh(), <-voteChan, <-cs1.NewStepCh() + + propBlock := cs1.ProposalBlock + + validatePrevote(t, cs1, 0, privVals[0], cs1.ProposalBlock.Hash()) + + // the others sign a polka but we don't see it + prevotes := signVoteMany(types.VoteTypePrevote, propBlock.Hash(), propBlock.MakePartSet().Header(), cs2, cs3, cs4) + + // 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") + } + + log.Warn("old prop", "hash", fmt.Sprintf("%X", propBlock.Hash())) + + // we do see them precommit nil + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, types.PartSetHeader{}, cs2, cs3, cs4) + + cs2.decideProposal(cs2.Height, cs2.Round+1) + prop, propBlock := cs2.Proposal, cs2.ProposalBlock + if prop == nil || propBlock == nil { + t.Fatal("Failed to create proposal block with cs2") + } + + incrementRound(cs2, cs3, cs4) + + log.Info("### ONTO ROUND 2") + /*Round2 + // we timeout and prevote our lock + // a polka happened but we didn't see it! + */ + + // now we're on a new round and not the proposer, + <-cs1.NewStepCh() + // so set proposal + cs1.mtx.Lock() + propBlockHash, propBlockParts := propBlock.Hash(), propBlock.MakePartSet() + cs1.Proposal, cs1.ProposalBlock, cs1.ProposalBlockParts = prop, propBlock, propBlockParts + cs1.mtx.Unlock() + // and wait for timeout + <-timeoutChan + if cs1.LockedBlock != nil { + t.Fatal("we should not be locked!") + } + log.Warn("new prop", "hash", fmt.Sprintf("%X", propBlockHash)) + // go to prevote, prevote for proposal block + _, _ = <-voteChan, <-cs1.NewStepCh() + validatePrevote(t, cs1, 1, privVals[0], propBlockHash) + + // now we see the others prevote for it, so we should lock on it + donePrecommit := make(chan struct{}) + go func() { + select { + case <-cs1.NewStepCh(): // we're in PrevoteWait, go to Precommit + <-voteChan + case <-voteChan: // we went straight to Precommit + } + <-cs1.NewStepCh() + donePrecommit <- struct{}{} + }() + // now lets add prevotes from everyone else for nil + signAddVoteToFromMany(types.VoteTypePrevote, cs1, propBlockHash, propBlockParts.Header(), cs2, cs3, cs4) + <-donePrecommit + + // we should have precommitted + validatePrecommit(t, cs1, 1, 1, privVals[0], propBlockHash, propBlockHash) + + // now we see precommits for nil + donePrecommitWait := make(chan struct{}) + go func() { + // the votes will bring us to new round + // we should timeut of it and go to prevote + <-cs1.NewStepCh() + <-timeoutChan + donePrecommitWait <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, types.PartSetHeader{}, cs2, cs3) + <-donePrecommitWait + + incrementRound(cs2, cs3, cs4) + + log.Info("### ONTO ROUND 3") + /*Round3 + we see the polka from round 1 but we shouldn't unlock! + */ + + // timeout of propose + _, _ = <-cs1.NewStepCh(), <-timeoutChan + + // finish prevote + _, _ = <-voteChan, <-cs1.NewStepCh() + + // we should prevote what we're locked on + validatePrevote(t, cs1, 2, privVals[0], propBlockHash) + + // add prevotes from the earlier round + addVoteToFromMany(cs1, prevotes, cs2, cs3, cs4) + + log.Warn("Done adding prevotes!") + + ensureNoNewStep(t, cs1) +} + +// 4 vals. +// polka P1 at R1, P2 at R2, and P3 at R3, +// we lock on P1 at R1, don't see P2, and unlock using P3 at R3 +// then we should make sure we don't lock using P2 +func TestLockPOLSafety2(t *testing.T) { + css, privVals := simpleConsensusState(4) + cs1, cs2, cs3, cs4 := css[0], css[1], css[2], css[3] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + timeoutChan := make(chan *types.EventDataRoundState) + voteChan := make(chan *types.EventDataVote) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutPropose(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringTimeoutWait(), func(data types.EventData) { + timeoutChan <- data.(*types.EventDataRoundState) + }) + evsw.AddListenerForEvent("tester", types.EventStringVote(), func(data types.EventData) { + vote := data.(*types.EventDataVote) + // we only fire for our own votes + if bytes.Equal(cs1.privValidator.Address, vote.Address) { + voteChan <- vote + } + }) + cs1.SetFireable(evsw) + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _, _ = <-cs1.NewStepCh(), <-voteChan, <-cs1.NewStepCh() + + theBlockHash := cs1.ProposalBlock.Hash() + + donePrecommit := make(chan struct{}) + go func() { + <-voteChan + <-cs1.NewStepCh() + donePrecommit <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrevote, cs1, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header(), cs2, cs3, cs4) + <-donePrecommit + + // the proposed block should now be locked and our precommit added + validatePrecommit(t, cs1, 0, 0, privVals[0], theBlockHash, theBlockHash) + + donePrecommitWait := make(chan struct{}) + go func() { + <-cs1.NewStepCh() + donePrecommitWait <- struct{}{} + }() + // add precommits from the rest + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, types.PartSetHeader{}, cs2, cs4) + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs3, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + <-donePrecommitWait + + // 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") + } + + cs2.decideProposal(cs2.Height, cs2.Round+1) + prop, propBlock := cs2.Proposal, cs2.ProposalBlock + if prop == nil || propBlock == nil { + t.Fatal("Failed to create proposal block with cs2") + } + + incrementRound(cs2, cs3, cs4) + + // timeout to new round + <-timeoutChan + + log.Info("### ONTO Round 2") + /*Round2 + // we timeout and prevote our lock + // a polka happened but we didn't see it! + */ + + // now we're on a new round and not the proposer, so wait for timeout + _, _ = <-cs1.NewStepCh(), <-timeoutChan + // go to prevote, prevote for locked block + _, _ = <-voteChan, <-cs1.NewStepCh() + validatePrevote(t, cs1, 0, privVals[0], cs1.LockedBlock.Hash()) + + // the others sign a polka but we don't see it + prevotes := signVoteMany(types.VoteTypePrevote, propBlock.Hash(), propBlock.MakePartSet().Header(), cs2, cs3, cs4) + + // once we see prevotes for the next round we'll skip ahead + + incrementRound(cs2, cs3, cs4) + + log.Info("### ONTO Round 3") + /*Round3 + a polka for nil causes us to unlock + */ + + // these prevotes will send us straight to precommit at the higher round + donePrecommit = make(chan struct{}) + go func() { + select { + case <-cs1.NewStepCh(): // we're in PrevoteWait, go to Precommit + <-voteChan + case <-voteChan: // we went straight to Precommit + } + <-cs1.NewStepCh() + donePrecommit <- struct{}{} + }() + // now lets add prevotes from everyone else for nil + signAddVoteToFromMany(types.VoteTypePrevote, cs1, nil, types.PartSetHeader{}, cs2, cs3, cs4) + <-donePrecommit + + // we should have unlocked + // NOTE: we don't lock on nil, so LockedRound is still 0 + validatePrecommit(t, cs1, 2, 0, privVals[0], nil, nil) + + donePrecommitWait = make(chan struct{}) + go func() { + // the votes will bring us to new round right away + // we should timeut of it and go to prevote + <-cs1.NewStepCh() + // set the proposal block to be that which got a polka in R2 + cs1.mtx.Lock() + cs1.Proposal, cs1.ProposalBlock, cs1.ProposalBlockParts = prop, propBlock, propBlock.MakePartSet() + cs1.mtx.Unlock() + // timeout into prevote, finish prevote + _, _, _ = <-timeoutChan, <-voteChan, <-cs1.NewStepCh() + donePrecommitWait <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrecommit, cs1, nil, types.PartSetHeader{}, cs2, cs3) + <-donePrecommitWait + + log.Info("### ONTO ROUND 4") + /*Round4 + we see the polka from R2 + make sure we don't lock because of it! + */ + // new round and not proposer + // (we already timed out and stepped into prevote) + + log.Warn("adding prevotes from round 2") + + addVoteToFromMany(cs1, prevotes, cs2, cs3, cs4) + + log.Warn("Done adding prevotes!") + + // we should prevote it now + validatePrevote(t, cs1, 3, privVals[0], cs1.ProposalBlock.Hash()) + + // but we shouldn't precommit it + precommits := cs1.Votes.Precommits(3) + vote := precommits.GetByIndex(0) + if vote != nil { + t.Fatal("validator precommitted at round 4 based on an old polka") + } +} + +//------------------------------------------------------------------------------------------ +// SlashingSuite + +func TestSlashingPrevotes(t *testing.T) { + css, _ := simpleConsensusState(2) + cs1, cs2 := css[0], css[1] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh() + + // we should now be stuck in limbo forever, waiting for more prevotes + // add one for a different block should cause us to go into prevote wait + hash := cs1.ProposalBlock.Hash() + hash[0] = byte(hash[0]+1) % 255 + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, hash, cs1.ProposalBlockParts.Header()) + + // pass prevote wait + <-cs1.NewStepCh() + + // NOTE: we have to send the vote for different block first so we don't just go into precommit round right + // away and ignore more prevotes (and thus fail to slash!) + + // add the conflicting vote + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + + // conflicting vote should cause us to broadcast dupeout tx on mempool + txs := cs1.mempoolReactor.Mempool.GetProposalTxs() + if len(txs) != 1 { + t.Fatal("expected to find a transaction in the mempool after double signing") + } + dupeoutTx, ok := txs[0].(*types.DupeoutTx) + if !ok { + t.Fatal("expected to find DupeoutTx in mempool after double signing") + } + + if !bytes.Equal(dupeoutTx.Address, cs2.privValidator.Address) { + t.Fatalf("expected DupeoutTx for %X, got %X", cs2.privValidator.Address, dupeoutTx.Address) + } + + // TODO: validate the sig +} + +func TestSlashingPrecommits(t *testing.T) { + css, _ := simpleConsensusState(2) + cs1, cs2 := css[0], css[1] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh() + + // add prevote from cs2 + signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + + // wait to finish precommit + <-cs1.NewStepCh() + + // we should now be stuck in limbo forever, waiting for more prevotes + // add one for a different block should cause us to go into prevote wait + hash := cs1.ProposalBlock.Hash() + hash[0] = byte(hash[0]+1) % 255 + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, hash, cs1.ProposalBlockParts.Header()) + + // pass prevote wait + <-cs1.NewStepCh() + + // NOTE: we have to send the vote for different block first so we don't just go into precommit round right + // away and ignore more prevotes (and thus fail to slash!) + + // add precommit from cs2 + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + + // conflicting vote should cause us to broadcast dupeout tx on mempool + txs := cs1.mempoolReactor.Mempool.GetProposalTxs() + if len(txs) != 1 { + t.Fatal("expected to find a transaction in the mempool after double signing") + } + dupeoutTx, ok := txs[0].(*types.DupeoutTx) + if !ok { + t.Fatal("expected to find DupeoutTx in mempool after double signing") + } + + if !bytes.Equal(dupeoutTx.Address, cs2.privValidator.Address) { + t.Fatalf("expected DupeoutTx for %X, got %X", cs2.privValidator.Address, dupeoutTx.Address) + } + + // TODO: validate the sig + +} + +//------------------------------------------------------------------------------------------ +// CatchupSuite + +//------------------------------------------------------------------------------------------ +// HaltSuite + +// 4 vals. +// we receive a final precommit after going into next round, but others might have gone to commit already! +func TestHalt1(t *testing.T) { + css, privVals := simpleConsensusState(4) + cs1, cs2, cs3, cs4 := css[0], css[1], css[2], css[3] + cs1.newStepCh = make(chan *RoundState) // so it blocks + + timeoutChan := make(chan struct{}) + evsw := events.NewEventSwitch() + evsw.OnStart() + evsw.AddListenerForEvent("tester", types.EventStringTimeoutWait(), func(data types.EventData) { + timeoutChan <- struct{}{} + }) + cs1.SetFireable(evsw) + + // start round and wait for propose and prevote + cs1.EnterNewRound(cs1.Height, 0, false) + _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh() + + theBlockHash := cs1.ProposalBlock.Hash() + + donePrecommit := make(chan struct{}) + go func() { + <-cs1.NewStepCh() + donePrecommit <- struct{}{} + }() + signAddVoteToFromMany(types.VoteTypePrevote, cs1, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header(), cs3, cs4) + <-donePrecommit + + // the proposed block should now be locked and our precommit added + validatePrecommit(t, cs1, 0, 0, privVals[0], theBlockHash, theBlockHash) + + donePrecommitWait := make(chan struct{}) + go func() { + <-cs1.NewStepCh() + donePrecommitWait <- struct{}{} + }() + // add precommits from the rest + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, nil, types.PartSetHeader{}) // didnt receive proposal + signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs3, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + // we receive this later, but cs3 might receive it earlier and with ours will go to commit! + precommit4 := signVote(cs4, types.VoteTypePrecommit, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header()) + <-donePrecommitWait + + incrementRound(cs2, cs3, cs4) + + // timeout to new round + <-timeoutChan + + log.Info("### ONTO ROUND 2") + /*Round2 + // we timeout and prevote our lock + // a polka happened but we didn't see it! + */ + + // go to prevote, prevote for locked block + _, _ = <-cs1.NewStepCh(), <-cs1.NewStepCh() + validatePrevote(t, cs1, 0, privVals[0], cs1.LockedBlock.Hash()) + + // now we receive the precommit from the previous round + addVoteToFrom(cs1, cs4, precommit4) + + // receiving that precommit should take us straight to commit + ensureNewStep(t, cs1) + log.Warn("done enter commit!") + + // update to state + ensureNewStep(t, cs1) + + if cs1.Height != 2 { + t.Fatal("expected height to increment") + } +} diff --git a/consensus/test.go b/consensus/test.go index be0066e5..2483581b 100644 --- a/consensus/test.go +++ b/consensus/test.go @@ -8,6 +8,7 @@ import ( bc "github.com/tendermint/tendermint/blockchain" dbm "github.com/tendermint/tendermint/db" + "github.com/tendermint/tendermint/events" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" @@ -206,6 +207,9 @@ func simpleConsensusState(nValidators int) ([]*ConsensusState, []*types.PrivVali cs := NewConsensusState(state, blockStore, mempoolReactor) cs.SetPrivValidator(privVals[i]) + evsw := events.NewEventSwitch() + cs.SetFireable(evsw) + // read off the NewHeightStep <-cs.NewStepCh()