// propose, prevote, and precommit a block func TestFullRound1(t *testing.T) { cs, vss := randConsensusState(1) height, round := cs.Height, cs.Round voteCh := subscribeToEvent(cs.evsw, "tester", types.EventStringVote(), 1) propCh := subscribeToEvent(cs.evsw, "tester", types.EventStringCompleteProposal(), 1) newRoundCh := subscribeToEvent(cs.evsw, "tester", types.EventStringNewRound(), 1) startTestRound(cs, height, round) <-newRoundCh // grab proposal re := <-propCh propBlockHash := re.(types.EventDataRoundState).RoundState.(*RoundState).ProposalBlock.Hash() <-voteCh // wait for prevote validatePrevote(t, cs, round, vss[0], propBlockHash) <-voteCh // wait for precommit // we're going to roll right into new height <-newRoundCh validateLastPrecommit(t, cs, vss[0], propBlockHash) }
// 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) { cs1, vss := randConsensusState(2) cs2 := vss[1] height, round := cs1.Height, cs1.Round voteCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringVote(), 1) newBlockCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringNewBlock(), 1) // start round and wait for propose and prevote startTestRound(cs1, height, round) <-voteCh // prevote // we should be stuck in limbo waiting for more prevotes propBlockHash, propPartsHeader := cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header() // prevote arrives from cs2: signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, propBlockHash, propPartsHeader) <-voteCh <-voteCh //precommit // the proposed block should now be locked and our precommit added validatePrecommit(t, cs1, 0, 0, vss[0], propBlockHash, propBlockHash) // we should be stuck in limbo waiting for more precommits // precommit arrives from cs2: signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, propBlockHash, propPartsHeader) <-voteCh // wait to finish commit, propose in next height <-newBlockCh }
// Listens for new steps and votes, // broadcasting the result to peers func (conR *ConsensusReactor) registerEventCallbacks() { conR.evsw.AddListenerForEvent("conR", types.EventStringNewRoundStep(), func(data events.EventData) { rs := data.(*types.EventDataRoundState).RoundState.(*RoundState) conR.broadcastNewRoundStep(rs) }) conR.evsw.AddListenerForEvent("conR", types.EventStringVote(), func(data events.EventData) { edv := data.(*types.EventDataVote) conR.broadcastHasVoteMessage(edv.Vote, edv.Index) }) }
func TestBadProposal(t *testing.T) { cs1, vss := randConsensusState(2) height, round := cs1.Height, cs1.Round cs2 := vss[1] proposalCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringCompleteProposal(), 1) voteCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringVote(), 1) propBlock, _ := cs1.createProposalBlock() //changeProposer(t, cs1, cs2) // make the second validator the proposer by incrementing round round = round + 1 incrementRound(vss[1:]...) // make the block bad by tampering with statehash stateHash := propBlock.AppHash if len(stateHash) == 0 { stateHash = make([]byte, 32) } stateHash[0] = byte((stateHash[0] + 1) % 255) propBlock.AppHash = stateHash propBlockParts := propBlock.MakePartSet() proposal := types.NewProposal(cs2.Height, round, propBlockParts.Header(), -1) if err := cs2.SignProposal(chainID, proposal); err != nil { t.Fatal("failed to sign bad proposal", err) } // set the proposal block cs1.SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer") // start the machine startTestRound(cs1, height, round) // wait for proposal <-proposalCh // wait for prevote <-voteCh validatePrevote(t, cs1, round, vss[0], nil) // add bad prevote from cs2 and wait for it signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, propBlock.Hash(), propBlock.MakePartSet().Header()) <-voteCh // wait for precommit <-voteCh validatePrecommit(t, cs1, round, 0, vss[0], nil, nil) signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, propBlock.Hash(), propBlock.MakePartSet().Header()) }
// nil is proposed, so prevote and precommit nil func TestFullRoundNil(t *testing.T) { cs, vss := randConsensusState(1) height, round := cs.Height, cs.Round voteCh := subscribeToEvent(cs.evsw, "tester", types.EventStringVote(), 1) cs.enterPrevote(height, round) cs.startRoutines(4) <-voteCh // prevote <-voteCh // precommit // should prevote and precommit nil validatePrevoteAndPrecommit(t, cs, round, 0, vss[0], nil, nil) }
func subscribeToVoter(cs *ConsensusState, addr []byte) chan interface{} { voteCh0 := subscribeToEvent(cs.evsw, "tester", types.EventStringVote(), 1) voteCh := make(chan interface{}) go func() { for { v := <-voteCh0 vote := v.(types.EventDataVote) // we only fire for our own votes if bytes.Equal(addr, vote.Address) { voteCh <- v } } }() return voteCh }
// 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka func TestLockPOLRelock(t *testing.T) { cs1, vss := randConsensusState(4) cs2, cs3, cs4 := vss[1], vss[2], vss[3] timeoutProposeCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringTimeoutPropose(), 1) timeoutWaitCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringTimeoutWait(), 1) proposalCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringCompleteProposal(), 1) voteCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringVote(), 1) newRoundCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringNewRound(), 1) newBlockCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringNewBlock(), 1) log.Debug("cs2 last round", "lr", cs2.PrivValidator.LastRound) // 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 startTestRound(cs1, cs1.Height, 0) <-newRoundCh re := <-proposalCh rs := re.(types.EventDataRoundState).RoundState.(*RoundState) theBlockHash := rs.ProposalBlock.Hash() <-voteCh // prevote signAddVoteToFromMany(types.VoteTypePrevote, cs1, cs1.ProposalBlock.Hash(), cs1.ProposalBlockParts.Header(), cs2, cs3, cs4) _, _, _ = <-voteCh, <-voteCh, <-voteCh // prevotes <-voteCh // our precommit // the proposed block should now be locked and our precommit added validatePrecommit(t, cs1, 0, 0, vss[0], theBlockHash, theBlockHash) // 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()) _, _, _ = <-voteCh, <-voteCh, <-voteCh // precommits // before we timeout to the new round set the new proposal prop, propBlock := decideProposal(cs1, cs2, cs2.Height, cs2.Round+1) propBlockParts := propBlock.MakePartSet() propBlockHash := propBlock.Hash() incrementRound(cs2, cs3, cs4) // timeout to new round <-timeoutWaitCh //XXX: this isnt gauranteed to get there before the timeoutPropose ... cs1.SetProposalAndBlock(prop, propBlock, propBlockParts, "some peer") <-newRoundCh log.Notice("### ONTO ROUND 1") /* Round2 (cs2, C) // B C C C // C C C _) cs1 changes lock! */ // now we're on a new round and not the proposer // but we should receive the proposal select { case <-proposalCh: case <-timeoutProposeCh: <-proposalCh } // go to prevote, prevote for locked block (not proposal), move on <-voteCh validatePrevote(t, cs1, 0, vss[0], theBlockHash) // now lets add prevotes from everyone else for the new block signAddVoteToFromMany(types.VoteTypePrevote, cs1, propBlockHash, propBlockParts.Header(), cs2, cs3, cs4) _, _, _ = <-voteCh, <-voteCh, <-voteCh // prevotes // now either we go to PrevoteWait or Precommit select { case <-timeoutWaitCh: // we're in PrevoteWait, go to Precommit <-voteCh case <-voteCh: // we went straight to Precommit } // we should have unlocked and locked on the new block validatePrecommit(t, cs1, 1, 1, vss[0], propBlockHash, propBlockHash) signAddVoteToFromMany(types.VoteTypePrecommit, cs1, propBlockHash, propBlockParts.Header(), cs2, cs3) _, _ = <-voteCh, <-voteCh be := <-newBlockCh b := be.(types.EventDataNewBlock) re = <-newRoundCh rs = re.(types.EventDataRoundState).RoundState.(*RoundState) if rs.Height != 2 { t.Fatal("Expected height to increment") } if !bytes.Equal(b.Block.Hash(), propBlockHash) { t.Fatal("Expected new block to be proposal block") } }
// two validators, 4 rounds. // two vals take turns proposing. val1 locks on first one, precommits nil on everything else func TestLockNoPOL(t *testing.T) { cs1, vss := randConsensusState(2) cs2 := vss[1] height := cs1.Height timeoutProposeCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringTimeoutPropose(), 1) timeoutWaitCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringTimeoutWait(), 1) voteCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringVote(), 1) proposalCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringCompleteProposal(), 1) newRoundCh := subscribeToEvent(cs1.evsw, "tester", types.EventStringNewRound(), 1) /* Round1 (cs1, B) // B B // B B2 */ // start round and wait for prevote cs1.enterNewRound(height, 0) cs1.startRoutines(0) re := <-proposalCh rs := re.(types.EventDataRoundState).RoundState.(*RoundState) theBlockHash := rs.ProposalBlock.Hash() <-voteCh // prevote // 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()) <-voteCh // prevote <-voteCh // precommit // the proposed block should now be locked and our precommit added validatePrecommit(t, cs1, 0, 0, vss[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 := make([]byte, len(theBlockHash)) copy(hash, theBlockHash) hash[0] = byte((hash[0] + 1) % 255) signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, hash, rs.ProposalBlock.MakePartSet().Header()) <-voteCh // precommit // (note we're entering precommit for a second time this round) // but with invalid args. then we enterPrecommitWait, and the timeout to new round <-timeoutWaitCh /// <-newRoundCh log.Notice("#### ONTO ROUND 1") /* Round2 (cs1, B) // B B2 */ incrementRound(cs2) // now we're on a new round and not the proposer, so wait for timeout re = <-timeoutProposeCh rs = re.(types.EventDataRoundState).RoundState.(*RoundState) if rs.ProposalBlock != nil { t.Fatal("Expected proposal block to be nil") } // wait to finish prevote <-voteCh // we should have prevoted our locked block validatePrevote(t, cs1, 1, vss[0], rs.LockedBlock.Hash()) // add a conflicting prevote from the other validator signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, hash, rs.ProposalBlock.MakePartSet().Header()) <-voteCh // now we're going to enter prevote again, but with invalid args // and then prevote wait, which should timeout. then wait for precommit <-timeoutWaitCh <-voteCh // precommit // 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, vss[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, rs.ProposalBlock.MakePartSet().Header()) <-voteCh // (note we're entering precommit for a second time this round, but with invalid args // then we enterPrecommitWait and timeout into NewRound <-timeoutWaitCh <-newRoundCh log.Notice("#### ONTO ROUND 2") /* Round3 (cs2, _) // B, B2 */ incrementRound(cs2) re = <-proposalCh rs = re.(types.EventDataRoundState).RoundState.(*RoundState) // now we're on a new round and are the proposer if !bytes.Equal(rs.ProposalBlock.Hash(), rs.LockedBlock.Hash()) { t.Fatalf("Expected proposal block to be locked block. Got %v, Expected %v", rs.ProposalBlock, rs.LockedBlock) } <-voteCh // prevote validatePrevote(t, cs1, 2, vss[0], rs.LockedBlock.Hash()) signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, hash, rs.ProposalBlock.MakePartSet().Header()) <-voteCh <-timeoutWaitCh // prevote wait <-voteCh // precommit validatePrecommit(t, cs1, 2, 0, vss[0], nil, theBlockHash) // precommit nil but be locked on proposal signAddVoteToFrom(types.VoteTypePrecommit, cs1, cs2, hash, rs.ProposalBlock.MakePartSet().Header()) // NOTE: conflicting precommits at same height <-voteCh <-timeoutWaitCh // before we time out into new round, set next proposal block prop, propBlock := decideProposal(cs1, cs2, cs2.Height, cs2.Round+1) if prop == nil || propBlock == nil { t.Fatal("Failed to create proposal block with cs2") } incrementRound(cs2) <-newRoundCh log.Notice("#### ONTO ROUND 3") /* Round4 (cs2, C) // B C // B C */ // now we're on a new round and not the proposer // so set the proposal block cs1.SetProposalAndBlock(prop, propBlock, propBlock.MakePartSet(), "") <-proposalCh <-voteCh // prevote // prevote for locked block (not proposal) validatePrevote(t, cs1, 0, vss[0], cs1.LockedBlock.Hash()) signAddVoteToFrom(types.VoteTypePrevote, cs1, cs2, propBlock.Hash(), propBlock.MakePartSet().Header()) <-voteCh <-timeoutWaitCh <-voteCh validatePrecommit(t, cs1, 2, 0, vss[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 <-voteCh }
func (cs *ConsensusState) addVote(valIndex int, vote *types.Vote, peerKey string) (added bool, address []byte, err error) { log.Debug("addVote", "voteHeight", vote.Height, "voteType", vote.Type, "csHeight", cs.Height) defer func() { if added { cs.evsw.FireEvent(types.EventStringVote(), &types.EventDataVote{valIndex, address, vote}) } }() // A precommit for the previous height? if vote.Height+1 == cs.Height { if !(cs.Step == RoundStepNewHeight && vote.Type == types.VoteTypePrecommit) { // TODO: give the reason .. // fmt.Errorf("TryAddVote: Wrong height, not a LastCommit straggler commit.") return added, nil, ErrVoteHeightMismatch } added, address, err = cs.LastCommit.AddByIndex(valIndex, vote) if added { log.Info(Fmt("Added to lastPrecommits: %v", cs.LastCommit.StringShort())) } return } // A prevote/precommit for this height? if vote.Height == cs.Height { height := cs.Height added, address, err = cs.Votes.AddByIndex(valIndex, vote, peerKey) if added { switch vote.Type { case types.VoteTypePrevote: prevotes := cs.Votes.Prevotes(vote.Round) log.Info(Fmt("Added to prevotes: %v", prevotes.StringShort())) // First, unlock if prevotes is a valid POL. // >> lockRound < POLRound <= unlockOrChangeLockRound (see spec) // NOTE: If (lockRound < POLRound) but !(POLRound <= unlockOrChangeLockRound), // we'll still EnterNewRound(H,vote.R) and EnterPrecommit(H,vote.R) to process it // there. if (cs.LockedBlock != nil) && (cs.LockedRound < vote.Round) && (vote.Round <= cs.Round) { hash, _, ok := prevotes.TwoThirdsMajority() if ok && !cs.LockedBlock.HashesTo(hash) { log.Notice("Unlocking because of POL.", "lockedRound", cs.LockedRound, "POLRound", vote.Round) cs.LockedRound = 0 cs.LockedBlock = nil cs.LockedBlockParts = nil cs.evsw.FireEvent(types.EventStringUnlock(), cs.RoundStateEvent()) } } if cs.Round <= vote.Round && prevotes.HasTwoThirdsAny() { // Round-skip over to PrevoteWait or goto Precommit. go func() { cs.EnterNewRound(height, vote.Round, false) if prevotes.HasTwoThirdsMajority() { cs.EnterPrecommit(height, vote.Round, false) } else { cs.EnterPrevote(height, vote.Round, false) cs.EnterPrevoteWait(height, vote.Round) } }() } else if cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round { // If the proposal is now complete, enter prevote of cs.Round. if cs.isProposalComplete() { go cs.EnterPrevote(height, cs.Round, false) } } case types.VoteTypePrecommit: precommits := cs.Votes.Precommits(vote.Round) log.Info(Fmt("Added to precommit: %v", precommits.StringShort())) hash, _, ok := precommits.TwoThirdsMajority() if ok { go func() { if len(hash) == 0 { cs.EnterNewRound(height, vote.Round+1, false) } else { cs.EnterNewRound(height, vote.Round, false) cs.EnterPrecommit(height, vote.Round, false) cs.EnterCommit(height, vote.Round) } }() } else if cs.Round <= vote.Round && precommits.HasTwoThirdsAny() { go func() { cs.EnterNewRound(height, vote.Round, false) cs.EnterPrecommit(height, vote.Round, false) cs.EnterPrecommitWait(height, vote.Round) }() } default: PanicSanity(Fmt("Unexpected vote type %X", vote.Type)) // Should not happen. } } // Either duplicate, or error upon cs.Votes.AddByIndex() return } else { err = ErrVoteHeightMismatch } // Height mismatch, bad peer? log.Info("Vote ignored and not added", "voteHeight", vote.Height, "csHeight", cs.Height) return }
// 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") } }
// 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, 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, 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") } }