From 52994aa2a9cc0640d542344433f06e776da74b08 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Thu, 1 Oct 2020 16:11:54 +0200 Subject: [PATCH] consensus: check block parts don't exceed maximum block bytes (#5436) --- consensus/common_test.go | 2 +- consensus/state.go | 7 +++++ consensus/state_test.go | 59 ++++++++++++++++++++++++++++++++++++++++ types/part_set.go | 13 +++++++++ types/part_set_test.go | 13 +++++---- 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/consensus/common_test.go b/consensus/common_test.go index 8049c1109..5a87ce402 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -603,7 +603,7 @@ func ensureProposal(proposalCh <-chan tmpubsub.Message, height int64, round int3 panic(fmt.Sprintf("expected round %v, got %v", round, proposalEvent.Round)) } if !proposalEvent.BlockID.Equals(propID) { - panic("Proposed block does not match expected block") + panic(fmt.Sprintf("Proposed block does not match expected block (%v != %v)", proposalEvent.BlockID, propID)) } } } diff --git a/consensus/state.go b/consensus/state.go index fada3b222..d1140e66a 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1779,16 +1779,23 @@ func (cs *State) addProposalBlockPart(msg *BlockPartMessage, peerID p2p.ID) (add if err != nil { return added, err } + if cs.ProposalBlockParts.ByteSize() > cs.state.ConsensusParams.Block.MaxBytes { + return added, fmt.Errorf("total size of proposal block parts exceeds maximum block bytes (%d > %d)", + cs.ProposalBlockParts.ByteSize(), cs.state.ConsensusParams.Block.MaxBytes, + ) + } if added && cs.ProposalBlockParts.IsComplete() { bz, err := ioutil.ReadAll(cs.ProposalBlockParts.GetReader()) if err != nil { return added, err } + var pbb = new(tmproto.Block) err = proto.Unmarshal(bz, pbb) if err != nil { return added, err } + block, err := types.BlockFromProto(pbb) if err != nil { return added, err diff --git a/consensus/state_test.go b/consensus/state_test.go index 5b0c581c2..85358ae50 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -29,6 +29,7 @@ x * TestProposerSelection2 - round robin ordering, round 2++ x * TestEnterProposeNoValidator - timeout into prevote round x * TestEnterPropose - finish propose without timing out (we have the proposal) x * TestBadProposal - 2 vals, bad proposal (bad block state hash), should prevote and precommit nil +x * TestOversizedBlock - block with too many txs should be rejected FullRoundSuite x * TestFullRound1 - 1 val, full successful round x * TestFullRoundNil - 1 val, full round of nil @@ -238,6 +239,64 @@ func TestStateBadProposal(t *testing.T) { signAddVotes(cs1, tmproto.PrecommitType, propBlock.Hash(), propBlock.MakePartSet(partSize).Header(), vs2) } +func TestStateOversizedBlock(t *testing.T) { + cs1, vss := randState(2) + cs1.state.ConsensusParams.Block.MaxBytes = 2000 + height, round := cs1.Height, cs1.Round + vs2 := vss[1] + + partSize := types.BlockPartSizeBytes + + timeoutProposeCh := subscribe(cs1.eventBus, types.EventQueryTimeoutPropose) + voteCh := subscribe(cs1.eventBus, types.EventQueryVote) + + propBlock, _ := cs1.createProposalBlock() + propBlock.Data.Txs = []types.Tx{tmrand.Bytes(2001)} + propBlock.Header.DataHash = propBlock.Data.Hash() + + // make the second validator the proposer by incrementing round + round++ + incrementRound(vss[1:]...) + + propBlockParts := propBlock.MakePartSet(partSize) + blockID := types.BlockID{Hash: propBlock.Hash(), PartSetHeader: propBlockParts.Header()} + proposal := types.NewProposal(height, round, -1, blockID) + p := proposal.ToProto() + if err := vs2.SignProposal(config.ChainID(), p); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + proposal.Signature = p.Signature + + totalBytes := 0 + for i := 0; i < int(propBlockParts.Total()); i++ { + part := propBlockParts.GetPart(i) + totalBytes += len(part.Bytes) + } + + if err := cs1.SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil { + t.Fatal(err) + } + + // start the machine + startTestRound(cs1, height, round) + + t.Log("Block Sizes", "Limit", cs1.state.ConsensusParams.Block.MaxBytes, "Current", totalBytes) + + // c1 should log an error with the block part message as it exceeds the consensus params. The + // block is not added to cs.ProposalBlock so the node timeouts. + ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds()) + + // and then should send nil prevote and precommit regardless of whether other validators prevote and + // precommit on it + ensurePrevote(voteCh, height, round) + validatePrevote(t, cs1, round, vss[0], nil) + signAddVotes(cs1, tmproto.PrevoteType, propBlock.Hash(), propBlock.MakePartSet(partSize).Header(), vs2) + ensurePrevote(voteCh, height, round) + ensurePrecommit(voteCh, height, round) + validatePrecommit(t, cs1, round, -1, vss[0], nil, nil) + signAddVotes(cs1, tmproto.PrecommitType, propBlock.Hash(), propBlock.MakePartSet(partSize).Header(), vs2) +} + //---------------------------------------------------------------------------------------------------- // FullRoundSuite diff --git a/types/part_set.go b/types/part_set.go index ca93b2b1a..b16fc583c 100644 --- a/types/part_set.go +++ b/types/part_set.go @@ -155,6 +155,9 @@ type PartSet struct { parts []*Part partsBitArray *bits.BitArray count uint32 + // a count of the total size (in bytes). Used to ensure that the + // part set doesn't exceed the maximum block bytes + byteSize int64 } // Returns an immutable, full PartSet from the data bytes. @@ -186,6 +189,7 @@ func NewPartSetFromData(data []byte, partSize uint32) *PartSet { parts: parts, partsBitArray: partsBitArray, count: total, + byteSize: int64(len(data)), } } @@ -197,6 +201,7 @@ func NewPartSetFromHeader(header PartSetHeader) *PartSet { parts: make([]*Part, header.Total), partsBitArray: bits.NewBitArray(int(header.Total)), count: 0, + byteSize: 0, } } @@ -244,6 +249,13 @@ func (ps *PartSet) Count() uint32 { return ps.count } +func (ps *PartSet) ByteSize() int64 { + if ps == nil { + return 0 + } + return ps.byteSize +} + func (ps *PartSet) Total() uint32 { if ps == nil { return 0 @@ -277,6 +289,7 @@ func (ps *PartSet) AddPart(part *Part) (bool, error) { ps.parts[part.Index] = part ps.partsBitArray.SetIndex(int(part.Index), true) ps.count++ + ps.byteSize += int64(len(part.Bytes)) return true, nil } diff --git a/types/part_set_test.go b/types/part_set_test.go index b7253da10..e7347e2f1 100644 --- a/types/part_set_test.go +++ b/types/part_set_test.go @@ -17,15 +17,17 @@ const ( func TestBasicPartSet(t *testing.T) { // Construct random data of size partSize * 100 - data := tmrand.Bytes(testPartSize * 100) + nParts := 100 + data := tmrand.Bytes(testPartSize * nParts) partSet := NewPartSetFromData(data, testPartSize) assert.NotEmpty(t, partSet.Hash()) - assert.EqualValues(t, 100, partSet.Total()) - assert.Equal(t, 100, partSet.BitArray().Size()) + assert.EqualValues(t, nParts, partSet.Total()) + assert.Equal(t, nParts, partSet.BitArray().Size()) assert.True(t, partSet.HashesTo(partSet.Hash())) assert.True(t, partSet.IsComplete()) - assert.EqualValues(t, 100, partSet.Count()) + assert.EqualValues(t, nParts, partSet.Count()) + assert.EqualValues(t, testPartSize*nParts, partSet.ByteSize()) // Test adding parts to a new partSet. partSet2 := NewPartSetFromHeader(partSet.Header()) @@ -49,7 +51,8 @@ func TestBasicPartSet(t *testing.T) { assert.Nil(t, err) assert.Equal(t, partSet.Hash(), partSet2.Hash()) - assert.EqualValues(t, 100, partSet2.Total()) + assert.EqualValues(t, nParts, partSet2.Total()) + assert.EqualValues(t, nParts*testPartSize, partSet.ByteSize()) assert.True(t, partSet2.IsComplete()) // Reconstruct data, assert that they are equal.