diff --git a/internal/consensus/state.go b/internal/consensus/state.go index 2046fb4d6..2388f4d77 100644 --- a/internal/consensus/state.go +++ b/internal/consensus/state.go @@ -1265,8 +1265,11 @@ func (cs *State) createProposalBlock() (block *types.Block, blockParts *types.Pa // Enter: `timeoutPropose` after entering Propose. // Enter: proposal block and POL is ready. -// Prevote for LockedBlock if we're locked, or ProposalBlock if valid. -// Otherwise vote nil. +// If we received a valid proposal within this round and we are not locked on a block, +// we will prevote for block. +// Otherwise, if we receive a valid proposal that matches the block we are +// locked on or matches a block that received a POL in a round later than our +// locked round, prevote for the proposal, otherwise vote nil. func (cs *State) enterPrevote(height int64, round int32) { logger := cs.Logger.With("height", height, "round", round) @@ -1296,14 +1299,7 @@ func (cs *State) enterPrevote(height int64, round int32) { func (cs *State) defaultDoPrevote(height int64, round int32) { logger := cs.Logger.With("height", height, "round", round) - // If a block is locked, prevote that. - if cs.LockedBlock != nil { - logger.Debug("prevote step; already locked on a block; prevoting locked block") - cs.signAddVote(tmproto.PrevoteType, cs.LockedBlock.Hash(), cs.LockedBlockParts.Header()) - return - } - - // If ProposalBlock is nil, prevote nil. + // We did not receive a proposal within this round. (and thus executing this from a timeout) if cs.ProposalBlock == nil { logger.Debug("prevote step: ProposalBlock is nil") cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) @@ -1319,11 +1315,67 @@ func (cs *State) defaultDoPrevote(height int64, round int32) { return } - // Prevote cs.ProposalBlock - // NOTE: the proposal signature is validated when it is received, - // and the proposal block parts are validated as they are received (against the merkle hash in the proposal) - logger.Debug("prevote step: ProposalBlock is valid") - cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header()) + /* + 22: upon from proposer(h_p, round_p) while step_p = propose do + 23: if valid(v) && (lockedRound_p = −1 || lockedValue_p = v) then + 24: broadcast + + Here, cs.Proposal.POLRound corresponds to the -1 in the above algorithm rule. + This means that the proposer is producing a new proposal that has not previously + seen a 2/3 majority by the network. + + If we have already locked on a different value that is different from the proposed value, + we prevote nil since we are locked on a different value. Otherwise, if we're not locked on a block + or the proposal matches our locked block, we prevote the proposal. + */ + if cs.Proposal.POLRound == -1 { + if cs.LockedRound == -1 { + logger.Debug("prevote step: ProposalBlock is valid and there is no locked block; prevoting the proposal") + cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header()) + return + } + if cs.ProposalBlock.HashesTo(cs.LockedBlock.Hash()) { + logger.Debug("prevote step: ProposalBlock is valid and matches our locked block; prevoting the proposal") + cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header()) + return + } + } + + /* + 28: upon from proposer(h_p, round_p) AND 2f + 1 while + step_p = propose && (v_r ≥ 0 && v_r < round_p) do + 29: if valid(v) && (lockedRound_p ≤ v_r || lockedValue_p = v) then + 30: broadcast + + This rule is a bit confusing but breaks down as follows: + + If we see a proposal in the current round for value 'v' that lists its valid round as 'v_r' + AND this validator saw a 2/3 majority of the voting power prevote 'v' in round 'v_r', then we will + issue a prevote for 'v' in this round if 'v' is valid and either matches our locked value OR + 'v_r' is a round greater than or equal to our current locked round. + + 'v_r' can be a round greater than to our current locked round if a 2/3 majority of + the network prevoted a value in round 'v_r' but we did not lock on it, possibly because we + missed the proposal in round 'v_r'. + */ + blockID, ok := cs.Votes.Prevotes(cs.Proposal.POLRound).TwoThirdsMajority() + if ok && cs.ProposalBlock.HashesTo(blockID.Hash) && cs.Proposal.POLRound >= 0 && cs.Proposal.POLRound < cs.Round { + if cs.LockedRound <= cs.Proposal.POLRound { + logger.Debug("prevote step: ProposalBlock is valid and received a 2/3" + + "majority in a round later than the locked round; prevoting the proposal") + cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header()) + return + } + if cs.ProposalBlock.HashesTo(cs.LockedBlock.Hash()) { + logger.Debug("prevote step: ProposalBlock is valid and matches our locked block; prevoting the proposal") + cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header()) + return + } + } + + logger.Debug("prevote step: ProposalBlock is valid but was not our locked block or" + + "did not receive a more recent majority; prevoting nil") + cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) } // Enter: any +2/3 prevotes at next round. diff --git a/internal/consensus/state_test.go b/internal/consensus/state_test.go index 95d3ea726..2916a6559 100644 --- a/internal/consensus/state_test.go +++ b/internal/consensus/state_test.go @@ -46,6 +46,7 @@ x * TestStateLock_MissingProposalWhenPOLSeenDoesNotUpdateLock - 4 vals, 1 misses x * TestStateLock_MissingProposalWhenPOLSeenDoesNotUnlock - 4 vals, 1 misses proposal but sees POL. x * TestStateLock_POLSafety1 - 4 vals. We shouldn't change lock based on polka at earlier round x * TestStateLock_POLSafety2 - 4 vals. After unlocking, we shouldn't relock based on polka at earlier round +x_*_TestState_PrevotePOLFromPreviousRound 4 vals, prevote a proposal if a POL was seen for it in a previous round. * TestNetworkLock - once +1/3 precommits, network should be locked * TestNetworkLockPOL - once +1/3 precommits, the block with more recent polka is committed SlashingSuite @@ -459,8 +460,8 @@ func TestStateLock_NoPOL(t *testing.T) { signAddVotes(config, cs1, tmproto.PrevoteType, theBlockHash, thePartSetHeader, vs2) ensurePrevote(t, voteCh, height, round) // prevote - ensurePrecommit(t, voteCh, height, round) // precommit // the proposed block should now be locked and our precommit added + ensurePrecommit(t, voteCh, height, round) validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) // we should now be stuck in limbo forever, waiting for more precommits @@ -492,13 +493,12 @@ func TestStateLock_NoPOL(t *testing.T) { rs := cs1.GetRoundState() if rs.ProposalBlock != nil { - panic("Expected proposal block to be nil") + t.Fatal("Expected proposal block to be nil") } - // wait to finish prevote + // we should have prevoted nil since we did not see a proposal in the round. ensurePrevote(t, voteCh, height, round) - // we should have prevoted our locked block - validatePrevote(t, cs1, round, vss[0], rs.LockedBlock.Hash()) + validatePrevote(t, cs1, round, vss[0], nil) // add a conflicting prevote from the other validator signAddVotes(config, cs1, tmproto.PrevoteType, hash, rs.LockedBlock.MakePartSet(partSize).Header(), vs2) @@ -508,9 +508,9 @@ func TestStateLock_NoPOL(t *testing.T) { // and then prevote wait, which should timeout. then wait for precommit ensureNewTimeout(t, timeoutWaitCh, height, round, cs1.config.Prevote(round).Nanoseconds()) - ensurePrecommit(t, voteCh, height, round) // precommit - // the proposed block should still be locked and our precommit added - // we should precommit nil and be locked on the proposal + // the proposed block should still be locked block. + // we should precommit nil and be locked on the proposal. + ensurePrecommit(t, voteCh, height, round) validatePrecommit(t, cs1, round, 0, vss[0], nil, theBlockHash) // add conflicting precommit from vs2 @@ -586,9 +586,10 @@ func TestStateLock_NoPOL(t *testing.T) { } ensureNewProposal(t, proposalCh, height, round) - ensurePrevote(t, voteCh, height, round) // prevote - // prevote for locked block (not proposal) - validatePrevote(t, cs1, 3, vss[0], cs1.LockedBlock.Hash()) + + // prevote for nil since we did not see a proposal for our locked block in the round. + ensurePrevote(t, voteCh, height, round) + validatePrevote(t, cs1, 3, vss[0], nil) // prevote for proposed block signAddVotes(config, cs1, tmproto.PrevoteType, propBlock.Hash(), propBlock.MakePartSet(partSize).Header(), vs2) @@ -693,11 +694,9 @@ func TestStateLock_POLUpdateLock(t *testing.T) { // ensure that the validator receives the proposal. ensureNewProposal(t, proposalCh, height, round) - // Prevote our locked block. - // TODO: Ensure we prevote for the proposal if it is valid and from a round greater than - // the valid round: https://github.com/tendermint/tendermint/issues/6850. + // Prevote our nil since the proposal does not match our locked block. ensurePrevote(t, voteCh, height, round) - validatePrevote(t, cs1, round, vss[0], theBlockHash) + validatePrevote(t, cs1, round, vss[0], nil) // Add prevotes from the remainder of the validators for the new locked block. signAddVotes(config, cs1, tmproto.PrevoteType, propBlockR1Hash, propBlockR1Parts.Header(), vs2, vs3, vs4) @@ -795,9 +794,7 @@ func TestStateLock_POLRelock(t *testing.T) { // ensure that the validator receives the proposal. ensureNewProposal(t, proposalCh, height, round) - // Prevote our locked block. - // TODO: Ensure we prevote for the proposal if it is valid and from a round greater than - // the valid round: https://github.com/tendermint/tendermint/issues/6850. + // Prevote our locked block since it matches the propsal seen in this round. ensurePrevote(t, voteCh, height, round) validatePrevote(t, cs1, round, vss[0], theBlockHash) @@ -813,6 +810,178 @@ func TestStateLock_POLRelock(t *testing.T) { validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) } +// TestStateLock_PrevoteNilWhenLockedAndMissProposal tests that a validator prevotes nil +// if it is locked on a block and misses the proposal in a round. +func TestStateLock_PrevoteNilWhenLockedAndMissProposal(t *testing.T) { + config := configSetup(t) + + cs1, vss := randState(config, 4) + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + height, round := cs1.Height, cs1.Round + + timeoutWaitCh := subscribe(cs1.eventBus, types.EventQueryTimeoutWait) + proposalCh := subscribe(cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(context.Background()) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(cs1, addr) + lockCh := subscribe(cs1.eventBus, types.EventQueryLock) + newRoundCh := subscribe(cs1.eventBus, types.EventQueryNewRound) + + /* + Round 0: + cs1 creates a proposal for block B. + Send a prevote for B from each of the validators to cs1. + Send a precommit for nil from all of the validators to cs1. + + This ensures that cs1 will lock on B in this round but not precommit it. + */ + t.Log("### Starting Round 0") + + startTestRound(cs1, height, round) + + ensureNewRound(t, newRoundCh, height, round) + ensureNewProposal(t, proposalCh, height, round) + rs := cs1.GetRoundState() + theBlockHash := rs.ProposalBlock.Hash() + theBlockParts := rs.ProposalBlockParts + + ensurePrevote(t, voteCh, height, round) + + signAddVotes(config, cs1, tmproto.PrevoteType, theBlockHash, theBlockParts.Header(), vs2, vs3, vs4) + + // check that the validator generates a Lock event. + ensureLock(t, lockCh, height, round) + + // the proposed block should now be locked and our precommit added. + ensurePrecommit(t, voteCh, height, round) + validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) + + // add precommits from the rest of the validators. + signAddVotes(config, cs1, tmproto.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) + + // timeout to new round. + ensureNewTimeout(t, timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) + + /* + Round 1: + Send a prevote for nil from each of the validators to cs1. + Send a precommit for nil from all of the validtors to cs1. + + Check that cs1 prevotes nil instead of its locked block, but ensure + that it maintains its locked block. + */ + t.Log("### Starting Round 1") + incrementRound(vs2, vs3, vs4) + round++ + + ensureNewRound(t, newRoundCh, height, round) + + // Prevote our nil. + ensurePrevote(t, voteCh, height, round) + validatePrevote(t, cs1, round, vss[0], nil) + + // Add prevotes from the remainder of the validators nil. + signAddVotes(config, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) + ensurePrecommit(t, voteCh, height, round) + // We should now be locked on the same block but with an updated locked round. + validatePrecommit(t, cs1, round, 0, vss[0], nil, theBlockHash) +} + +// TestStateLock_PrevoteNilWhenLockedAndMissProposal tests that a validator prevotes nil +// if it is locked on a block and misses the proposal in a round. +func TestStateLock_PrevoteNilWhenLockedAndDifferentProposal(t *testing.T) { + config := configSetup(t) + /* + All of the assertions in this test occur on the `cs1` validator. + The test sends signed votes from the other validators to cs1 and + cs1's state is then examined to verify that it now matches the expected + state. + */ + + cs1, vss := randState(config, 4) + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + height, round := cs1.Height, cs1.Round + + timeoutWaitCh := subscribe(cs1.eventBus, types.EventQueryTimeoutWait) + proposalCh := subscribe(cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(context.Background()) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(cs1, addr) + lockCh := subscribe(cs1.eventBus, types.EventQueryLock) + newRoundCh := subscribe(cs1.eventBus, types.EventQueryNewRound) + + /* + Round 0: + cs1 creates a proposal for block B. + Send a prevote for B from each of the validators to cs1. + Send a precommit for nil from all of the validators to cs1. + + This ensures that cs1 will lock on B in this round but not precommit it. + */ + t.Log("### Starting Round 0") + startTestRound(cs1, height, round) + + ensureNewRound(t, newRoundCh, height, round) + ensureNewProposal(t, proposalCh, height, round) + rs := cs1.GetRoundState() + theBlockHash := rs.ProposalBlock.Hash() + theBlockParts := rs.ProposalBlockParts + + ensurePrevote(t, voteCh, height, round) + + signAddVotes(config, cs1, tmproto.PrevoteType, theBlockHash, theBlockParts.Header(), vs2, vs3, vs4) + + // check that the validator generates a Lock event. + ensureLock(t, lockCh, height, round) + + // the proposed block should now be locked and our precommit added. + ensurePrecommit(t, voteCh, height, round) + validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) + + // add precommits from the rest of the validators. + signAddVotes(config, cs1, tmproto.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) + + // timeout to new round. + ensureNewTimeout(t, timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) + + /* + Round 1: + Create a proposal for a new block. + Send a prevote for nil from each of the validators to cs1. + Send a precommit for nil from all of the validtors to cs1. + + Check that cs1 prevotes nil instead of its locked block, but ensure + that it maintains its locked block. + */ + t.Log("### Starting Round 1") + incrementRound(vs2, vs3, vs4) + round++ + cs2 := newState(cs1.state, vs2, kvstore.NewApplication()) + propR1, propBlockR1 := decideProposal(t, cs2, vs2, vs2.Height, vs2.Round) + propBlockR1Parts := propBlockR1.MakePartSet(types.BlockPartSizeBytes) + propBlockR1Hash := propBlockR1.Hash() + require.NotEqual(t, propBlockR1Hash, theBlockHash) + if err := cs1.SetProposalAndBlock(propR1, propBlockR1, propBlockR1Parts, "some peer"); err != nil { + t.Fatal(err) + } + + ensureNewRound(t, newRoundCh, height, round) + ensureNewProposal(t, proposalCh, height, round) + + // Prevote our nil. + ensurePrevote(t, voteCh, height, round) + validatePrevote(t, cs1, round, vss[0], nil) + + // Add prevotes from the remainder of the validators for nil. + signAddVotes(config, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) + + // We should now be locked on the same block but prevote nil. + ensurePrecommit(t, voteCh, height, round) + validatePrecommit(t, cs1, round, 0, vss[0], nil, theBlockHash) +} + // TestStateLock_POLDoesNotUnlock tests that a validator maintains its locked block // despite receiving +2/3 nil prevotes and nil precommits from other validators. // Tendermint used to 'unlock' its locked block when greater than 2/3 prevotes @@ -892,8 +1061,10 @@ func TestStateLock_POLDoesNotUnlock(t *testing.T) { t.Log("#### ONTO ROUND 1") round++ incrementRound(vs2, vs3, vs4) - prop, propBlock := decideProposal(t, cs1, vs2, vs2.Height, vs2.Round) + cs2 := newState(cs1.state, vs2, kvstore.NewApplication()) + prop, propBlock := decideProposal(t, cs2, vs2, vs2.Height, vs2.Round) propBlockParts := propBlock.MakePartSet(types.BlockPartSizeBytes) + require.NotEqual(t, propBlock.Hash(), theBlockHash) if err := cs1.SetProposalAndBlock(prop, propBlock, propBlockParts, ""); err != nil { t.Fatal(err) } @@ -902,12 +1073,10 @@ func TestStateLock_POLDoesNotUnlock(t *testing.T) { ensureNewProposal(t, proposalCh, height, round) - // prevote for the locked block. We do not currently prevote for the - // proposal. - // TODO: do not prevote the locked block if it does not match the proposal. - // (https://github.com/tendermint/tendermint/issues/6850) + // Prevote for nil since the proposed block does not match our locked block. ensurePrevote(t, voteCh, height, round) - validatePrevote(t, cs1, round, vss[0], theBlockHash) + validatePrevote(t, cs1, round, vss[0], nil) + // add >2/3 prevotes for nil from all other validators signAddVotes(config, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -928,7 +1097,8 @@ func TestStateLock_POLDoesNotUnlock(t *testing.T) { t.Log("#### ONTO ROUND 2") round++ incrementRound(vs2, vs3, vs4) - prop, propBlock = decideProposal(t, cs1, vs3, vs3.Height, vs3.Round) + cs3 := newState(cs1.state, vs2, kvstore.NewApplication()) + prop, propBlock = decideProposal(t, cs3, vs3, vs3.Height, vs3.Round) propBlockParts = propBlock.MakePartSet(types.BlockPartSizeBytes) if err := cs1.SetProposalAndBlock(prop, propBlock, propBlockParts, ""); err != nil { t.Fatal(err) @@ -938,8 +1108,9 @@ func TestStateLock_POLDoesNotUnlock(t *testing.T) { ensureNewProposal(t, proposalCh, height, round) + // Prevote for nil since the proposal does not match our locked block. ensurePrevote(t, voteCh, height, round) - validatePrevote(t, cs1, round, vss[0], theBlockHash) + validatePrevote(t, cs1, round, vss[0], nil) signAddVotes(config, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -1021,9 +1192,9 @@ func TestStateLock_MissingProposalWhenPOLSeenDoesNotUpdateLock(t *testing.T) { ensureNewRound(t, newRoundCh, height, round) - // go to prevote, node should prevote for locked block (not the new proposal) - this is relocking + // prevote for nil since the proposal was not seen. ensurePrevote(t, voteCh, height, round) - validatePrevote(t, cs1, round, vss[0], firstBlockHash) + validatePrevote(t, cs1, round, vss[0], nil) // now lets add prevotes from everyone else for the new block signAddVotes(config, cs1, tmproto.PrevoteType, secondBlockHash, secondBlockParts.Header(), vs2, vs3, vs4) @@ -1144,8 +1315,6 @@ func TestStateLock_POLSafety1(t *testing.T) { propBlock.Hash(), propBlock.MakePartSet(partSize).Header(), vs2, vs3, vs4) - t.Logf("old prop hash %v", fmt.Sprintf("%X", propBlock.Hash())) - // we do see them precommit nil signAddVotes(config, cs1, tmproto.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -1154,14 +1323,13 @@ func TestStateLock_POLSafety1(t *testing.T) { ensureNewTimeout(t, timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) t.Log("### ONTO ROUND 1") - - prop, propBlock := decideProposal(t, cs1, vs2, vs2.Height, vs2.Round+1) + incrementRound(vs2, vs3, vs4) + round++ // moving to the next round + cs2 := newState(cs1.state, vs2, kvstore.NewApplication()) + prop, propBlock := decideProposal(t, cs2, vs2, vs2.Height, vs2.Round) propBlockHash := propBlock.Hash() propBlockParts := propBlock.MakePartSet(partSize) - incrementRound(vs2, vs3, vs4) - - round++ // moving to the next round ensureNewRound(t, newRoundCh, height, round) //XXX: this isnt guaranteed to get there before the timeoutPropose ... @@ -1178,9 +1346,8 @@ func TestStateLock_POLSafety1(t *testing.T) { rs = cs1.GetRoundState() if rs.LockedBlock != nil { - panic("we should not be locked!") + t.Fatalf("was not expected to be locked on a block") } - t.Logf("new prop hash %v", fmt.Sprintf("%X", propBlockHash)) // go to prevote, prevote for proposal block ensurePrevote(t, voteCh, height, round) @@ -1212,8 +1379,8 @@ func TestStateLock_POLSafety1(t *testing.T) { // finish prevote ensurePrevote(t, voteCh, height, round) - // we should prevote what we're locked on - validatePrevote(t, cs1, round, vss[0], propBlockHash) + // we should prevote for nil + validatePrevote(t, cs1, round, vss[0], nil) newStepCh := subscribe(cs1.eventBus, types.EventQueryNewRoundStep) @@ -1221,8 +1388,6 @@ func TestStateLock_POLSafety1(t *testing.T) { // add prevotes from the earlier round addVotes(cs1, prevotes...) - t.Log("Done adding prevotes!") - ensureNoNewRoundStep(t, newStepCh) } @@ -1325,6 +1490,142 @@ func TestStateLock_POLSafety2(t *testing.T) { } +// TestState_PrevotePOLFromPreviousRound tests that a validator will prevote +// for a block if it is locked on a different block but saw a POL for the block +// it is not locked on in a previous round. +func TestState_PrevotePOLFromPreviousRound(t *testing.T) { + config := configSetup(t) + + cs1, vss := randState(config, 4) + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + height, round := cs1.Height, cs1.Round + + partSize := types.BlockPartSizeBytes + + timeoutWaitCh := subscribe(cs1.eventBus, types.EventQueryTimeoutWait) + proposalCh := subscribe(cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(context.Background()) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(cs1, addr) + lockCh := subscribe(cs1.eventBus, types.EventQueryLock) + newRoundCh := subscribe(cs1.eventBus, types.EventQueryNewRound) + + /* + Round 0: + cs1 creates a proposal for block B. + Send a prevote for B from each of the validators to cs1. + Send a precommit for nil from all of the validators to cs1. + + This ensures that cs1 will lock on B in this round but not precommit it. + */ + t.Log("### Starting Round 0") + + startTestRound(cs1, height, round) + + ensureNewRound(t, newRoundCh, height, round) + ensureNewProposal(t, proposalCh, height, round) + rs := cs1.GetRoundState() + theBlockHash := rs.ProposalBlock.Hash() + theBlockParts := rs.ProposalBlockParts.Header() + + ensurePrevote(t, voteCh, height, round) + + signAddVotes(config, cs1, tmproto.PrevoteType, theBlockHash, theBlockParts, vs2, vs3, vs4) + + // check that the validator generates a Lock event. + ensureLock(t, lockCh, height, round) + + // the proposed block should now be locked and our precommit added. + ensurePrecommit(t, voteCh, height, round) + validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) + + // add precommits from the rest of the validators. + signAddVotes(config, cs1, tmproto.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) + + // timeout to new round. + ensureNewTimeout(t, timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) + + /* + Round 1: + Create a block, D but do not send a proposal for it to cs1. + Send a prevote for D from each of the validators to cs1 so that cs1 sees a POL. + Send a precommit for nil from all of the validtors to cs1. + + cs1 has now seen greater than 2/3 of the voting power prevote D in this round + but cs1 did not see the proposal for D in this round so it will not prevote or precommit it. + */ + t.Log("### Starting Round 1") + incrementRound(vs2, vs3, vs4) + round++ + // Generate a new proposal block. + cs2 := newState(cs1.state, vs2, kvstore.NewApplication()) + cs2.ValidRound = 1 + propR1, propBlockR1 := decideProposal(t, cs2, vs2, vs2.Height, round) + t.Log(propR1.POLRound) + propBlockR1Parts := propBlockR1.MakePartSet(partSize) + propBlockR1Hash := propBlockR1.Hash() + require.NotEqual(t, propBlockR1Hash, theBlockHash) + + ensureNewRound(t, newRoundCh, height, round) + + signAddVotes(config, cs1, tmproto.PrevoteType, propBlockR1Hash, propBlockR1Parts.Header(), vs2, vs3, vs4) + + ensurePrevote(t, voteCh, height, round) + validatePrevote(t, cs1, round, vss[0], nil) + + signAddVotes(config, cs1, tmproto.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) + + ensurePrecommit(t, voteCh, height, round) + + // timeout to new round. + ensureNewTimeout(t, timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) + + /* + Round 2: + Create a new proposal for D, the same block from Round 1. + cs1 already saw greater than 2/3 of the voting power on the network vote for + D in a previous round, so it should prevote D once it receives a proposal for it. + + cs1 does not need to receive prevotes from other validators before the proposal + in this round. It will still prevote the block. + + Send cs1 prevotes for nil and check that it still prevotes its locked block + and not the block that it prevoted. + */ + t.Log("### Starting Round 2") + incrementRound(vs2, vs3, vs4) + round++ + propBlockID := types.BlockID{Hash: propBlockR1Hash, PartSetHeader: propBlockR1Parts.Header()} + propR2 := types.NewProposal(height, round, 1, propBlockID) + p := propR2.ToProto() + if err := vs3.SignProposal(context.Background(), cs1.state.ChainID, p); err != nil { + t.Fatalf("error signing proposal: %s", err) + } + propR2.Signature = p.Signature + + // cs1 receives a proposal for D, the block that received a POL in round 1. + if err := cs1.SetProposalAndBlock(propR2, propBlockR1, propBlockR1Parts, ""); err != nil { + t.Fatal(err) + } + + ensureNewRound(t, newRoundCh, height, round) + + ensureNewProposal(t, proposalCh, height, round) + + // We should now prevote this block, despite being locked on the block from + // round 0. + ensurePrevote(t, voteCh, height, round) + validatePrevote(t, cs1, round, vss[0], propBlockR1Hash) + + signAddVotes(config, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) + + // cs1 did not receive a POL within this round, so it should remain locked + // on the block from round 0. + ensurePrecommit(t, voteCh, height, round) + validatePrecommit(t, cs1, round, 0, vss[0], nil, theBlockHash) +} + // 4 vals. // polka P0 at R0 for B0. We lock B0 on P0 at R0. @@ -1381,8 +1682,9 @@ func TestProposeValidBlock(t *testing.T) { // timeout of propose ensureNewTimeout(t, timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds()) + // We did not see a valid proposal within this round, so prevote nil. ensurePrevote(t, voteCh, height, round) - validatePrevote(t, cs1, round, vss[0], propBlockHash) + validatePrevote(t, cs1, round, vss[0], nil) signAddVotes(config, cs1, tmproto.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -2014,17 +2316,16 @@ func TestStateHalt1(t *testing.T) { round++ // moving to the next round ensureNewRound(t, newRoundCh, height, round) - rs = cs1.GetRoundState() t.Log("### ONTO ROUND 1") /*Round2 - // we timeout and prevote our lock + // we timeout and prevote // a polka happened but we didn't see it! */ - // go to prevote, prevote for locked block + // prevote for nil since we did not receive a proposal in this round. ensurePrevote(t, voteCh, height, round) - validatePrevote(t, cs1, round, vss[0], rs.LockedBlock.Hash()) + validatePrevote(t, cs1, round, vss[0], nil) // now we receive the precommit from the previous round addVotes(cs1, precommit4)