From 56a20056ec96947a6102917d43f8e1600e1b379c Mon Sep 17 00:00:00 2001 From: William Banfield <4561443+williambanfield@users.noreply.github.com> Date: Wed, 15 Dec 2021 14:46:55 -0500 Subject: [PATCH] internal/consensus: prevote nil if proposal timestamp does not match (#7391) This change updates the proposal logic to use the block's timestamp in the proposal message. It adds an additional piece of validation logic to the prevote step to check that the block's timestamp matches the proposal message's timestamp. --- internal/consensus/byzantine_test.go | 2 +- internal/consensus/common_test.go | 2 +- internal/consensus/pbts_test.go | 2 +- internal/consensus/replay_test.go | 8 +- internal/consensus/state.go | 17 ++++- internal/consensus/state_test.go | 108 +++++++++++++++++++++++++-- types/proposal.go | 4 +- types/proposal_test.go | 9 ++- 8 files changed, 132 insertions(+), 20 deletions(-) diff --git a/internal/consensus/byzantine_test.go b/internal/consensus/byzantine_test.go index 70555d440..e122c8420 100644 --- a/internal/consensus/byzantine_test.go +++ b/internal/consensus/byzantine_test.go @@ -214,7 +214,7 @@ func TestByzantinePrevoteEquivocation(t *testing.T) { // Make proposal propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()} - proposal := types.NewProposal(height, round, lazyNodeState.ValidRound, propBlockID) + proposal := types.NewProposal(height, round, lazyNodeState.ValidRound, propBlockID, block.Header.Time) p := proposal.ToProto() if err := lazyNodeState.privValidator.SignProposal(ctx, lazyNodeState.state.ChainID, p); err == nil { proposal.Signature = p.Signature diff --git a/internal/consensus/common_test.go b/internal/consensus/common_test.go index 550c60b2b..dee25e33c 100644 --- a/internal/consensus/common_test.go +++ b/internal/consensus/common_test.go @@ -248,7 +248,7 @@ func decideProposal( // Make proposal polRound, propBlockID := validRound, types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()} - proposal = types.NewProposal(height, round, polRound, propBlockID) + proposal = types.NewProposal(height, round, polRound, propBlockID, block.Header.Time) p := proposal.ToProto() if err := vs.SignProposal(ctx, chainID, p); err != nil { t.Fatalf("error signing proposal: %s", err) diff --git a/internal/consensus/pbts_test.go b/internal/consensus/pbts_test.go index c050dd6e5..aa29b4a3b 100644 --- a/internal/consensus/pbts_test.go +++ b/internal/consensus/pbts_test.go @@ -204,7 +204,7 @@ func (p *pbtsTestHarness) nextHeight(proposer types.PrivValidator, deliverTime, b.Header.ProposerAddress = k.Address() ps := b.MakePartSet(types.BlockPartSizeBytes) bid := types.BlockID{Hash: b.Hash(), PartSetHeader: ps.Header()} - prop := types.NewProposal(p.currentHeight, 0, -1, bid) + prop := types.NewProposal(p.currentHeight, 0, -1, bid, proposedTime) tp := prop.ToProto() if err := proposer.SignProposal(context.Background(), p.observedState.state.ChainID, tp); err != nil { diff --git a/internal/consensus/replay_test.go b/internal/consensus/replay_test.go index 69124df0d..7893a1f39 100644 --- a/internal/consensus/replay_test.go +++ b/internal/consensus/replay_test.go @@ -384,7 +384,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { propBlockParts := propBlock.MakePartSet(partSize) blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()} - proposal := types.NewProposal(vss[1].Height, round, -1, blockID) + proposal := types.NewProposal(vss[1].Height, round, -1, blockID, propBlock.Header.Time) p := proposal.ToProto() if err := vss[1].SignProposal(ctx, cfg.ChainID(), p); err != nil { t.Fatal("failed to sign bad proposal", err) @@ -416,7 +416,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { propBlockParts = propBlock.MakePartSet(partSize) blockID = types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()} - proposal = types.NewProposal(vss[2].Height, round, -1, blockID) + proposal = types.NewProposal(vss[2].Height, round, -1, blockID, propBlock.Header.Time) p = proposal.ToProto() if err := vss[2].SignProposal(ctx, cfg.ChainID(), p); err != nil { t.Fatal("failed to sign bad proposal", err) @@ -475,7 +475,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { selfIndex := valIndexFn(0) - proposal = types.NewProposal(vss[3].Height, round, -1, blockID) + proposal = types.NewProposal(vss[3].Height, round, -1, blockID, propBlock.Header.Time) p = proposal.ToProto() if err := vss[3].SignProposal(ctx, cfg.ChainID(), p); err != nil { t.Fatal("failed to sign bad proposal", err) @@ -540,7 +540,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { sort.Sort(ValidatorStubsByPower(newVss)) selfIndex = valIndexFn(0) - proposal = types.NewProposal(vss[1].Height, round, -1, blockID) + proposal = types.NewProposal(vss[1].Height, round, -1, blockID, propBlock.Header.Time) p = proposal.ToProto() if err := vss[1].SignProposal(ctx, cfg.ChainID(), p); err != nil { t.Fatal("failed to sign bad proposal", err) diff --git a/internal/consensus/state.go b/internal/consensus/state.go index addcdec05..1409a6f51 100644 --- a/internal/consensus/state.go +++ b/internal/consensus/state.go @@ -1199,7 +1199,7 @@ func (cs *State) defaultDecideProposal(height int64, round int32) { // Make proposal propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()} - proposal := types.NewProposal(height, round, cs.ValidRound, propBlockID) + proposal := types.NewProposal(height, round, cs.ValidRound, propBlockID, block.Header.Time) p := proposal.ToProto() // wait the max amount we would wait for a proposal @@ -1321,6 +1321,12 @@ func (cs *State) defaultDoPrevote(height int64, round int32) { return } + if !cs.Proposal.Timestamp.Equal(cs.ProposalBlock.Header.Time) { + logger.Debug("proposal timestamp not equal, prevoting nil") + cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) + return + } + // Validate proposal block err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock) if err != nil { @@ -1345,6 +1351,7 @@ func (cs *State) defaultDoPrevote(height int64, round int32) { */ if cs.Proposal.POLRound == -1 { if cs.LockedRound == -1 { + // TODO(@wbanfield) add check for timely here as well 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 @@ -1480,9 +1487,15 @@ func (cs *State) enterPrecommit(height int64, round int32) { cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{}) return } - // At this point, +2/3 prevoted for a particular block. + // If the proposal time does not match the block time, precommit nil. + if !cs.Proposal.Timestamp.Equal(cs.ProposalBlock.Header.Time) { + logger.Debug("proposal timestamp not equal, precommitting nil") + cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{}) + return + } + // If we're already locked on that block, precommit it, and update the LockedRound if cs.LockedBlock.HashesTo(blockID.Hash) { logger.Debug("precommit step; +2/3 prevoted locked block; relocking") diff --git a/internal/consensus/state_test.go b/internal/consensus/state_test.go index b3e88748f..a23f6cc1e 100644 --- a/internal/consensus/state_test.go +++ b/internal/consensus/state_test.go @@ -246,7 +246,7 @@ func TestStateBadProposal(t *testing.T) { propBlock.AppHash = stateHash propBlockParts := propBlock.MakePartSet(partSize) blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()} - proposal := types.NewProposal(vs2.Height, round, -1, blockID) + proposal := types.NewProposal(vs2.Height, round, -1, blockID, propBlock.Header.Time) p := proposal.ToProto() if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil { t.Fatal("failed to sign bad proposal", err) @@ -306,7 +306,7 @@ func TestStateOversizedBlock(t *testing.T) { propBlockParts := propBlock.MakePartSet(partSize) blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()} - proposal := types.NewProposal(height, round, -1, blockID) + proposal := types.NewProposal(height, round, -1, blockID, propBlock.Header.Time) p := proposal.ToProto() if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil { t.Fatal("failed to sign bad proposal", err) @@ -856,7 +856,7 @@ func TestStateLock_POLRelock(t *testing.T) { t.Log("### Starting Round 1") incrementRound(vs2, vs3, vs4) round++ - propR1 := types.NewProposal(height, round, cs1.ValidRound, blockID) + propR1 := types.NewProposal(height, round, cs1.ValidRound, blockID, theBlock.Header.Time) p := propR1.ToProto() if err := vs2.SignProposal(ctx, cs1.state.ChainID, p); err != nil { t.Fatalf("error signing proposal: %s", err) @@ -1588,7 +1588,7 @@ func TestStateLock_POLSafety2(t *testing.T) { round++ // moving to the next round // in round 2 we see the polkad block from round 0 - newProp := types.NewProposal(height, round, 0, propBlockID0) + newProp := types.NewProposal(height, round, 0, propBlockID0, propBlock0.Header.Time) p := newProp.ToProto() if err := vs3.SignProposal(ctx, config.ChainID(), p); err != nil { t.Fatal(err) @@ -1730,7 +1730,7 @@ func TestState_PrevotePOLFromPreviousRound(t *testing.T) { t.Log("### Starting Round 2") incrementRound(vs2, vs3, vs4) round++ - propR2 := types.NewProposal(height, round, 1, r1BlockID) + propR2 := types.NewProposal(height, round, 1, r1BlockID, propBlockR1.Header.Time) p := propR2.ToProto() if err := vs3.SignProposal(ctx, cs1.state.ChainID, p); err != nil { t.Fatalf("error signing proposal: %s", err) @@ -2649,6 +2649,104 @@ func TestSignSameVoteTwice(t *testing.T) { require.Equal(t, vote, vote2) } +// TestStateTimestamp_ProposalNotMatch tests that a validator does not prevote a +// proposed block if the timestamp in the block does not matche the timestamp in the +// corresponding proposal message. +func TestStateTimestamp_ProposalNotMatch(t *testing.T) { + config := configSetup(t) + logger := log.TestingLogger() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cs1, vss, err := makeState(ctx, config, logger, 4) + require.NoError(t, err) + height, round := cs1.Height, cs1.Round + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + + proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(ctx) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(ctx, t, cs1, addr) + + propBlock, _ := cs1.createProposalBlock() + round++ + incrementRound(vss[1:]...) + + propBlockParts := propBlock.MakePartSet(types.BlockPartSizeBytes) + blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()} + + // Create a proposal with a timestamp that does not match the timestamp of the block. + proposal := types.NewProposal(vs2.Height, round, -1, blockID, propBlock.Header.Time.Add(time.Millisecond)) + p := proposal.ToProto() + if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + proposal.Signature = p.Signature + require.NoError(t, cs1.SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer")) + + startTestRound(ctx, cs1, height, round) + ensureProposal(t, proposalCh, height, round, blockID) + + signAddVotes(ctx, cs1, tmproto.PrevoteType, config.ChainID(), blockID, vs2, vs3, vs4) + + // ensure that the validator prevotes nil. + ensurePrevote(t, voteCh, height, round) + validatePrevote(ctx, t, cs1, round, vss[0], nil) + + ensurePrecommit(t, voteCh, height, round) + validatePrecommit(ctx, t, cs1, round, -1, vss[0], nil, nil) +} + +// TestStateTimestamp_ProposalMatch tests that a validator prevotes a +// proposed block if the timestamp in the block matches the timestamp in the +// corresponding proposal message. +func TestStateTimestamp_ProposalMatch(t *testing.T) { + config := configSetup(t) + logger := log.TestingLogger() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cs1, vss, err := makeState(ctx, config, logger, 4) + require.NoError(t, err) + height, round := cs1.Height, cs1.Round + vs2, vs3, vs4 := vss[1], vss[2], vss[3] + + proposalCh := subscribe(ctx, t, cs1.eventBus, types.EventQueryCompleteProposal) + pv1, err := cs1.privValidator.GetPubKey(ctx) + require.NoError(t, err) + addr := pv1.Address() + voteCh := subscribeToVoter(ctx, t, cs1, addr) + + propBlock, _ := cs1.createProposalBlock() + round++ + incrementRound(vss[1:]...) + + propBlockParts := propBlock.MakePartSet(types.BlockPartSizeBytes) + blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()} + + // Create a proposal with a timestamp that matches the timestamp of the block. + proposal := types.NewProposal(vs2.Height, round, -1, blockID, propBlock.Header.Time) + p := proposal.ToProto() + if err := vs2.SignProposal(ctx, config.ChainID(), p); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + proposal.Signature = p.Signature + require.NoError(t, cs1.SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer")) + + startTestRound(ctx, cs1, height, round) + ensureProposal(t, proposalCh, height, round, blockID) + + signAddVotes(ctx, cs1, tmproto.PrevoteType, config.ChainID(), blockID, vs2, vs3, vs4) + + // ensure that the validator prevotes the block. + ensurePrevote(t, voteCh, height, round) + validatePrevote(ctx, t, cs1, round, vss[0], propBlock.Hash()) + + ensurePrecommit(t, voteCh, height, round) + validatePrecommit(ctx, t, cs1, round, 1, vss[0], propBlock.Hash(), propBlock.Hash()) +} + // subscribe subscribes test client to the given query and returns a channel with cap = 1. func subscribe( ctx context.Context, diff --git a/types/proposal.go b/types/proposal.go index 26df39a56..288abd6be 100644 --- a/types/proposal.go +++ b/types/proposal.go @@ -34,14 +34,14 @@ type Proposal struct { // NewProposal returns a new Proposal. // If there is no POLRound, polRound should be -1. -func NewProposal(height int64, round int32, polRound int32, blockID BlockID) *Proposal { +func NewProposal(height int64, round int32, polRound int32, blockID BlockID, ts time.Time) *Proposal { return &Proposal{ Type: tmproto.ProposalType, Height: height, Round: round, BlockID: blockID, POLRound: polRound, - Timestamp: tmtime.Now(), + Timestamp: tmtime.Canonical(ts), } } diff --git a/types/proposal_test.go b/types/proposal_test.go index 741396cc9..a7a6083f8 100644 --- a/types/proposal_test.go +++ b/types/proposal_test.go @@ -13,6 +13,7 @@ import ( "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/internal/libs/protoio" tmrand "github.com/tendermint/tendermint/libs/rand" + tmtime "github.com/tendermint/tendermint/libs/time" tmtimemocks "github.com/tendermint/tendermint/libs/time/mocks" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" ) @@ -63,7 +64,7 @@ func TestProposalVerifySignature(t *testing.T) { prop := NewProposal( 4, 2, 2, - BlockID{tmrand.Bytes(tmhash.Size), PartSetHeader{777, tmrand.Bytes(tmhash.Size)}}) + BlockID{tmrand.Bytes(tmhash.Size), PartSetHeader{777, tmrand.Bytes(tmhash.Size)}}, tmtime.Now()) p := prop.ToProto() signBytes := ProposalSignBytes("test_chain_id", p) @@ -154,7 +155,7 @@ func TestProposalValidateBasic(t *testing.T) { t.Run(tc.testName, func(t *testing.T) { prop := NewProposal( 4, 2, 2, - blockID) + blockID, tmtime.Now()) p := prop.ToProto() err := privVal.SignProposal(context.Background(), "test_chain_id", p) prop.Signature = p.Signature @@ -166,9 +167,9 @@ func TestProposalValidateBasic(t *testing.T) { } func TestProposalProtoBuf(t *testing.T) { - proposal := NewProposal(1, 2, 3, makeBlockID([]byte("hash"), 2, []byte("part_set_hash"))) + proposal := NewProposal(1, 2, 3, makeBlockID([]byte("hash"), 2, []byte("part_set_hash")), tmtime.Now()) proposal.Signature = []byte("sig") - proposal2 := NewProposal(1, 2, 3, BlockID{}) + proposal2 := NewProposal(1, 2, 3, BlockID{}, tmtime.Now()) testCases := []struct { msg string