diff --git a/internal/consensus/state.go b/internal/consensus/state.go index 00596e36c..8055cbf14 100644 --- a/internal/consensus/state.go +++ b/internal/consensus/state.go @@ -1317,8 +1317,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(ctx context.Context, height int64, round int32) { logger := cs.logger.With("height", height, "round", round) @@ -1348,14 +1351,7 @@ func (cs *State) enterPrevote(ctx context.Context, height int64, round int32) { func (cs *State) defaultDoPrevote(ctx context.Context, 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(ctx, 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(ctx, tmproto.PrevoteType, nil, types.PartSetHeader{}) @@ -1371,11 +1367,67 @@ func (cs *State) defaultDoPrevote(ctx context.Context, 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(ctx, 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(ctx, 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(ctx, 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(ctx, 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(ctx, 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(ctx, 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 e1a072553..a123aa2b8 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 @@ -506,8 +507,8 @@ func TestStateLock_NoPOL(t *testing.T) { signAddVotes(ctx, t, 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(ctx, t, cs1, round, round, vss[0], theBlockHash, theBlockHash) // we should now be stuck in limbo forever, waiting for more precommits @@ -540,10 +541,9 @@ func TestStateLock_NoPOL(t *testing.T) { require.Nil(t, rs.ProposalBlock, "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(ctx, t, cs1, round, vss[0], rs.LockedBlock.Hash()) + validatePrevote(ctx, t, cs1, round, vss[0], nil) // add a conflicting prevote from the other validator bps, err := rs.LockedBlock.MakePartSet(partSize) @@ -555,10 +555,9 @@ func TestStateLock_NoPOL(t *testing.T) { // now we're going to enter prevote again, but with invalid args // 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(ctx, t, cs1, round, 0, vss[0], nil, theBlockHash) // add conflicting precommit from vs2 @@ -619,7 +618,6 @@ func TestStateLock_NoPOL(t *testing.T) { // needed so generated block is different than locked block cs2, _ := randState(ctx, t, config, log.TestingLogger(), 2) - require.NoError(t, err) // before we time out into new round, set next proposal block prop, propBlock := decideProposal(ctx, t, cs2, vs2, vs2.Height, vs2.Round+1) if prop == nil || propBlock == nil { @@ -644,9 +642,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(ctx, 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(ctx, t, cs1, 3, vss[0], nil) // prevote for proposed block bps4, err := propBlock.MakePartSet(partSize) @@ -765,11 +764,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(ctx, t, cs1, round, vss[0], theBlockHash) + validatePrevote(ctx, t, cs1, round, vss[0], nil) // Add prevotes from the remainder of the validators for the new locked block. signAddVotes(ctx, t, config, cs1, tmproto.PrevoteType, propBlockR1Hash, propBlockR1Parts.Header(), vs2, vs3, vs4) @@ -855,7 +852,7 @@ func TestStateLock_POLRelock(t *testing.T) { propBlockID := types.BlockID{Hash: theBlockHash, PartSetHeader: theBlockParts.Header()} propR1 := types.NewProposal(height, round, cs1.ValidRound, propBlockID) p := propR1.ToProto() - if err := vs2.SignProposal(context.Background(), cs1.state.ChainID, p); err != nil { + if err := vs2.SignProposal(ctx, cs1.state.ChainID, p); err != nil { t.Fatalf("error signing proposal: %s", err) } propR1.Signature = p.Signature @@ -868,9 +865,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(ctx, t, cs1, round, vss[0], theBlockHash) @@ -886,6 +881,184 @@ func TestStateLock_POLRelock(t *testing.T) { validatePrecommit(ctx, 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) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + config := configSetup(t) + + cs1, vss := randState(ctx, t, config, log.TestingLogger(), 4) + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + height, round := cs1.Height, cs1.Round + + timeoutWaitCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryTimeoutWait) + proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(context.Background()) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(ctx, t, cs1, addr) + lockCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryLock) + newRoundCh := subscribe(ctx, t, 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(ctx, 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(ctx, t, 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(ctx, t, cs1, round, round, vss[0], theBlockHash, theBlockHash) + + // add precommits from the rest of the validators. + signAddVotes(ctx, t, 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(ctx, t, cs1, round, vss[0], nil) + + // Add prevotes from the remainder of the validators nil. + signAddVotes(ctx, t, 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(ctx, 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) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + logger := log.TestingLogger() + 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(ctx, t, config, logger, 4) + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + height, round := cs1.Height, cs1.Round + + timeoutWaitCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryTimeoutWait) + proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(context.Background()) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(ctx, t, cs1, addr) + lockCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryLock) + newRoundCh := subscribe(ctx, t, 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(ctx, 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(ctx, t, 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(ctx, t, cs1, round, round, vss[0], theBlockHash, theBlockHash) + + // add precommits from the rest of the validators. + signAddVotes(ctx, t, 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(ctx, t, logger, cs1.state, vs2, kvstore.NewApplication()) + propR1, propBlockR1 := decideProposal(ctx, t, cs2, vs2, vs2.Height, vs2.Round) + propBlockR1Parts, err := propBlockR1.MakePartSet(types.BlockPartSizeBytes) + require.NoError(t, err) + propBlockR1Hash := propBlockR1.Hash() + require.NotEqual(t, propBlockR1Hash, theBlockHash) + if err := cs1.SetProposalAndBlock(ctx, 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(ctx, t, cs1, round, vss[0], nil) + + // Add prevotes from the remainder of the validators for nil. + signAddVotes(ctx, t, 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(ctx, 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 @@ -893,6 +1066,7 @@ func TestStateLock_POLRelock(t *testing.T) { // that it has been completely removed. func TestStateLock_POLDoesNotUnlock(t *testing.T) { config := configSetup(t) + logger := log.TestingLogger() ctx, cancel := context.WithCancel(context.Background()) defer cancel() /* @@ -967,9 +1141,11 @@ func TestStateLock_POLDoesNotUnlock(t *testing.T) { t.Log("#### ONTO ROUND 1") round++ incrementRound(vs2, vs3, vs4) - prop, propBlock := decideProposal(ctx, t, cs1, vs2, vs2.Height, vs2.Round) + cs2 := newState(ctx, t, logger, cs1.state, vs2, kvstore.NewApplication()) + prop, propBlock := decideProposal(ctx, t, cs2, vs2, vs2.Height, vs2.Round) propBlockParts, err := propBlock.MakePartSet(types.BlockPartSizeBytes) require.NoError(t, err) + require.NotEqual(t, propBlock.Hash(), theBlockHash) if err := cs1.SetProposalAndBlock(ctx, prop, propBlock, propBlockParts, ""); err != nil { t.Fatal(err) } @@ -978,12 +1154,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(ctx, t, cs1, round, vss[0], theBlockHash) + validatePrevote(ctx, t, cs1, round, vss[0], nil) + // add >2/3 prevotes for nil from all other validators signAddVotes(ctx, t, config, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -1004,7 +1178,8 @@ func TestStateLock_POLDoesNotUnlock(t *testing.T) { t.Log("#### ONTO ROUND 2") round++ incrementRound(vs2, vs3, vs4) - prop, propBlock = decideProposal(ctx, t, cs1, vs3, vs3.Height, vs3.Round) + cs3 := newState(ctx, t, logger, cs1.state, vs2, kvstore.NewApplication()) + prop, propBlock = decideProposal(ctx, t, cs3, vs3, vs3.Height, vs3.Round) propBlockParts, err = propBlock.MakePartSet(types.BlockPartSizeBytes) require.NoError(t, err) if err := cs1.SetProposalAndBlock(ctx, prop, propBlock, propBlockParts, ""); err != nil { @@ -1015,8 +1190,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(ctx, t, cs1, round, vss[0], theBlockHash) + validatePrevote(ctx, t, cs1, round, vss[0], nil) signAddVotes(ctx, t, config, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -1105,9 +1281,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(ctx, t, cs1, round, vss[0], firstBlockHash) + validatePrevote(ctx, t, cs1, round, vss[0], nil) // now lets add prevotes from everyone else for the new block signAddVotes(ctx, t, config, cs1, tmproto.PrevoteType, secondBlockHash, secondBlockParts.Header(), vs2, vs3, vs4) @@ -1197,6 +1373,7 @@ func TestStateLock_DoesNotLockOnOldProposal(t *testing.T) { // then we see the polka from round 1 but shouldn't unlock func TestStateLock_POLSafety1(t *testing.T) { config := configSetup(t) + logger := log.TestingLogger() ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -1234,8 +1411,6 @@ func TestStateLock_POLSafety1(t *testing.T) { propBlock.Hash(), bps.Header(), vs2, vs3, vs4) - t.Logf("old prop hash %v", fmt.Sprintf("%X", propBlock.Hash())) - // we do see them precommit nil signAddVotes(ctx, t, config, cs1, tmproto.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -1244,15 +1419,14 @@ func TestStateLock_POLSafety1(t *testing.T) { ensureNewTimeout(t, timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) t.Log("### ONTO ROUND 1") - - prop, propBlock := decideProposal(ctx, t, cs1, vs2, vs2.Height, vs2.Round+1) + incrementRound(vs2, vs3, vs4) + round++ // moving to the next round + cs2 := newState(ctx, t, logger, cs1.state, vs2, kvstore.NewApplication()) + prop, propBlock := decideProposal(ctx, t, cs2, vs2, vs2.Height, vs2.Round) propBlockHash := propBlock.Hash() propBlockParts, err := propBlock.MakePartSet(partSize) require.NoError(t, err) - 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 ... @@ -1302,8 +1476,8 @@ func TestStateLock_POLSafety1(t *testing.T) { // finish prevote ensurePrevote(t, voteCh, height, round) - // we should prevote what we're locked on - validatePrevote(ctx, t, cs1, round, vss[0], propBlockHash) + // we should prevote for nil + validatePrevote(ctx, t, cs1, round, vss[0], nil) newStepCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryNewRoundStep) @@ -1311,8 +1485,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,10 +1497,11 @@ func TestStateLock_POLSafety1(t *testing.T) { // dont see P0, lock on P1 at R1, dont unlock using P0 at R2 func TestStateLock_POLSafety2(t *testing.T) { config := configSetup(t) + logger := log.TestingLogger() ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cs1, vss := randState(ctx, t, config, log.TestingLogger(), 4) + cs1, vss := randState(ctx, t, config, logger, 4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -1419,6 +1592,146 @@ 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) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + config := configSetup(t) + logger := log.TestingLogger() + + cs1, vss := randState(ctx, t, config, logger, 4) + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + height, round := cs1.Height, cs1.Round + + partSize := types.BlockPartSizeBytes + + timeoutWaitCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryTimeoutWait) + proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(context.Background()) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(ctx, t, cs1, addr) + lockCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryLock) + newRoundCh := subscribe(ctx, t, 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(ctx, 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(ctx, t, 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(ctx, t, cs1, round, round, vss[0], theBlockHash, theBlockHash) + + // add precommits from the rest of the validators. + signAddVotes(ctx, t, 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(ctx, t, logger, cs1.state, vs2, kvstore.NewApplication()) + cs2.ValidRound = 1 + propR1, propBlockR1 := decideProposal(ctx, t, cs2, vs2, vs2.Height, round) + t.Log(propR1.POLRound) + propBlockR1Parts, err := propBlockR1.MakePartSet(partSize) + require.NoError(t, err) + propBlockR1Hash := propBlockR1.Hash() + require.NotEqual(t, propBlockR1Hash, theBlockHash) + + ensureNewRound(t, newRoundCh, height, round) + + signAddVotes(ctx, t, config, cs1, tmproto.PrevoteType, propBlockR1Hash, propBlockR1Parts.Header(), vs2, vs3, vs4) + + ensurePrevote(t, voteCh, height, round) + validatePrevote(ctx, t, cs1, round, vss[0], nil) + + signAddVotes(ctx, t, 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(ctx, 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(ctx, 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(ctx, t, cs1, round, vss[0], propBlockR1Hash) + + signAddVotes(ctx, t, 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(ctx, t, cs1, round, 0, vss[0], nil, theBlockHash) +} + // 4 vals. // polka P0 at R0 for B0. We lock B0 on P0 at R0. @@ -1481,8 +1794,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(ctx, t, cs1, round, vss[0], propBlockHash) + validatePrevote(ctx, t, cs1, round, vss[0], nil) signAddVotes(ctx, t, cfg, cs1, tmproto.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) @@ -2144,17 +2458,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(ctx, t, cs1, round, vss[0], rs.LockedBlock.Hash()) + validatePrevote(ctx, t, cs1, round, vss[0], nil) // now we receive the precommit from the previous round addVotes(cs1, precommit4)