Skip to content

Commit d7cdcaf

Browse files
committed
consensus: reject oversized proposals
Validate proposal part counts and add tests.
1 parent dd0e3a3 commit d7cdcaf

6 files changed

Lines changed: 147 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ encouraged to upgrade as soon as possible.
1515
Cap `SynchronyParams.MessageDelay` to 24hrs.
1616
Cap `SynchronyParams.Precision` to 30 sec.
1717
([\#4815](https://github.com/cometbft/cometbft/issues/4815))
18+
- `[consensus]` Reject oversized proposals
19+
([\#5324](https://github.com/cometbft/cometbft/pull/5324))
1820
- `[crypto/bls12381]` Fix JSON marshal of private key
1921
([\#4772](https://github.com/cometbft/cometbft/pull/4772))
2022
- `[crypto/bls12381]` Modify `Sign`, `Verify` to use `dstMinPk`
@@ -1210,4 +1212,3 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/cosmos).
12101212
## Previous changes
12111213

12121214
For changes released before the creation of CometBFT, please refer to the Tendermint Core [CHANGELOG.md](https://github.com/tendermint/tendermint/blob/a9feb1c023e172b542c972605311af83b777855b/CHANGELOG.md).
1213-

internal/consensus/byzantine_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,3 +593,87 @@ func (br *ByzantineReactor) Receive(e p2p.Envelope) {
593593
}
594594

595595
func (*ByzantineReactor) InitPeer(peer p2p.Peer) p2p.Peer { return peer }
596+
597+
// Large/oversized proposals should be rejected.
598+
func TestRejectOversizedProposals(t *testing.T) {
599+
ctx, cancel := context.WithCancel(context.Background())
600+
defer cancel()
601+
602+
n := 2
603+
css, cleanup := randConsensusNet(t, n, "consensus_reactor_test", newMockTickerFunc(false), newKVStore)
604+
defer cleanup()
605+
606+
switches := make([]*p2p.Switch, n)
607+
p2pLogger := consensusLogger().With("module", "p2p")
608+
for i := 0; i < n; i++ {
609+
switches[i] = p2p.MakeSwitch(
610+
config.P2P,
611+
i,
612+
func(_ int, sw *p2p.Switch) *p2p.Switch {
613+
return sw
614+
})
615+
switches[i].SetLogger(p2pLogger.With("validator", i))
616+
}
617+
618+
reactors := make([]p2p.Reactor, n)
619+
for i := 0; i < n; i++ {
620+
conR := NewReactor(css[i], false)
621+
defer func() { require.NoError(t, conR.Stop()) }()
622+
623+
conR.SetLogger(consensusLogger().With("validator", i))
624+
reactors[i] = conR
625+
}
626+
627+
p2p.MakeConnectedSwitches(config.P2P, n, func(i int, _ *p2p.Switch) *p2p.Switch {
628+
switches[i].AddReactor("CONSENSUS", reactors[i])
629+
return switches[i]
630+
}, p2p.Connect2Switches)
631+
632+
peers := switches[0].Peers().Copy()
633+
require.NotEmpty(t, peers)
634+
targetPeer := peers[0]
635+
636+
height := int64(1)
637+
round := int32(0)
638+
cs := css[0]
639+
640+
block, err := cs.createProposalBlock(ctx)
641+
require.NoError(t, err)
642+
643+
blockParts, err := block.MakePartSet(types.BlockPartSizeBytes)
644+
require.NoError(t, err)
645+
646+
// create oversized proposal
647+
propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()}
648+
propBlockID.PartSetHeader.Total = 4294967295
649+
650+
proposal := types.NewProposal(height, round, -1, propBlockID, block.Header.Time)
651+
p := proposal.ToProto()
652+
if err := cs.privValidator.SignProposal(cs.state.ChainID, p); err != nil {
653+
t.Error(err)
654+
}
655+
proposal.Signature = p.Signature
656+
657+
success := targetPeer.Send(p2p.Envelope{
658+
ChannelID: DataChannel,
659+
Message: &cmtcons.Proposal{Proposal: *proposal.ToProto()},
660+
})
661+
require.True(t, success)
662+
663+
select {
664+
case e := <-css[1].peerMsgQueue:
665+
// if we receive a message here, the peer incorrectly accepted the
666+
// oversized proposal
667+
if _, receivedProposal := e.Msg.(*ProposalMessage); receivedProposal {
668+
assert.Fail(t, "peer incorrectly accepted oversized proposal")
669+
return
670+
}
671+
// invalid state, we received some other unexpected message type, fail
672+
// the test
673+
assert.Fail(t, "received unexpected message type on peer msg queue, expected *ProposalMessage")
674+
case <-ctx.Done():
675+
case <-time.After(500 * time.Millisecond):
676+
// timeout after 500ms if nothing has happened and assume peer rejected
677+
// the proposal
678+
}
679+
}

internal/consensus/reactor.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,15 @@ func (conR *Reactor) Receive(e p2p.Envelope) {
332332
}
333333
switch msg := msg.(type) {
334334
case *ProposalMessage:
335+
conR.conS.mtx.RLock()
336+
maxBytes := conR.conS.state.ConsensusParams.Block.MaxBytes
337+
conR.conS.mtx.RUnlock()
338+
if err := msg.Proposal.ValidateBlockSize(maxBytes); err != nil {
339+
conR.Logger.Error("Rejecting oversized proposal", "peer", e.Src, "height", msg.Proposal.Height)
340+
conR.Switch.StopPeerForError(e.Src, ErrProposalTooManyParts)
341+
return
342+
}
343+
335344
ps.SetHasProposal(msg.Proposal)
336345
conR.conS.peerMsgQueue <- msgInfo{msg, e.Src.ID(), cmttime.Now()}
337346
case *ProposalPOLMessage:
@@ -1793,6 +1802,12 @@ func (m *ProposalMessage) ValidateBasic() error {
17931802
return m.Proposal.ValidateBasic()
17941803
}
17951804

1805+
// ValidateBlockSize validates the proposal's block size against a maximum. If
1806+
// -1 is passed, types.MaxBlockSizeBytes will be used as the maximum.
1807+
func (m *ProposalMessage) ValidateBlockSize(maxBlockSizeBytes int64) error {
1808+
return m.Proposal.ValidateBlockSize(maxBlockSizeBytes)
1809+
}
1810+
17961811
// String returns a string representation.
17971812
func (m *ProposalMessage) String() string {
17981813
return fmt.Sprintf("[Proposal %v]", m.Proposal)

types/proposal.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ func (p *Proposal) ValidateBasic() error {
8282
return nil
8383
}
8484

85+
// ValidateBlockSize ensures a proposal block is not larger than a maximum size,
86+
// based on the total number of parts reported in the PartSetHeader. If -1 is
87+
// passed as the maxBlockSizeBytes, MaxBlockSizeBytes will be used.
88+
func (p *Proposal) ValidateBlockSize(maxBlockSizeBytes int64) error {
89+
if maxBlockSizeBytes == -1 {
90+
maxBlockSizeBytes = int64(MaxBlockSizeBytes)
91+
}
92+
totalParts := int64(p.BlockID.PartSetHeader.Total)
93+
maxParts := (maxBlockSizeBytes-1)/int64(BlockPartSizeBytes) + 1
94+
if totalParts > maxParts {
95+
return fmt.Errorf("proposal has too many parts %d (max: %d)", totalParts, maxParts)
96+
}
97+
return nil
98+
}
99+
85100
// IsTimely validates that the proposal timestamp is 'timely' according to the
86101
// proposer-based timestamp algorithm. To evaluate if a proposal is timely, its
87102
// timestamp is compared to the local time of the validator when it receives

types/proposal_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,33 @@ func TestProposalProtoBuf(t *testing.T) {
221221
}
222222
}
223223

224+
func TestProposalValidateBlockSize(t *testing.T) {
225+
testCases := []struct {
226+
testName string
227+
maxBlockSize int64
228+
proposal *Proposal
229+
expectPass bool
230+
}{
231+
{"10 chunk max, 5 chunk proposal, success", int64(10 * BlockPartSizeBytes), NewProposal(0, 0, 0, BlockID{PartSetHeader: PartSetHeader{Total: 5}}, cmttime.Now()), true},
232+
{"10 chunk max, 20 chunk proposal, fail", int64(10 * BlockPartSizeBytes), NewProposal(0, 0, 0, BlockID{PartSetHeader: PartSetHeader{Total: 20}}, cmttime.Now()), false},
233+
{"10 chunk max, max uint32 chunk proposal, fail", int64(10 * BlockPartSizeBytes), NewProposal(0, 0, 0, BlockID{PartSetHeader: PartSetHeader{Total: math.MaxUint32}}, cmttime.Now()), false},
234+
{"-1 chunk max, max uint32 chunk proposal, fail", -1, NewProposal(0, 0, 0, BlockID{PartSetHeader: PartSetHeader{Total: math.MaxUint32}}, cmttime.Now()), false},
235+
{"0 chunk max, max uint32 chunk proposal, fail", -1, NewProposal(0, 0, 0, BlockID{PartSetHeader: PartSetHeader{Total: math.MaxUint32}}, cmttime.Now()), false},
236+
{"total parts equals chunk max, success", -1, NewProposal(0, 0, 0, BlockID{PartSetHeader: PartSetHeader{Total: 1600}}, cmttime.Now()), true},
237+
}
238+
for _, tc := range testCases {
239+
tc := tc
240+
t.Run(tc.testName, func(t *testing.T) {
241+
err := tc.proposal.ValidateBlockSize(tc.maxBlockSize)
242+
if tc.expectPass {
243+
require.NoError(t, err)
244+
} else {
245+
require.Error(t, err)
246+
}
247+
})
248+
}
249+
}
250+
224251
func TestProposalIsTimely(t *testing.T) {
225252
timestamp, err := time.Parse(time.RFC3339, "2019-03-13T23:00:00Z")
226253
require.NoError(t, err)

types/validator_set_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -477,13 +477,14 @@ func TestValidatorSetFromProtoReturnsErrorOnOverflow(t *testing.T) {
477477
pubKey := ed25519.GenPrivKey().PubKey()
478478
pkProto, err := cryptoenc.PubKeyToProto(pubKey)
479479
require.NoError(t, err)
480+
pkProtoPtr := &pkProto
480481

481482
protoVals := &cmtproto.ValidatorSet{
482483
Validators: []*cmtproto.Validator{
483-
{Address: pubKey.Address(), PubKey: pkProto, VotingPower: math.MaxInt64, ProposerPriority: 0},
484-
{Address: pubKey.Address(), PubKey: pkProto, VotingPower: math.MaxInt64, ProposerPriority: 0},
484+
{Address: pubKey.Address(), PubKey: pkProtoPtr, VotingPower: math.MaxInt64, ProposerPriority: 0},
485+
{Address: pubKey.Address(), PubKey: pkProtoPtr, VotingPower: math.MaxInt64, ProposerPriority: 0},
485486
},
486-
Proposer: &cmtproto.Validator{Address: pubKey.Address(), PubKey: pkProto, VotingPower: math.MaxInt64, ProposerPriority: 0},
487+
Proposer: &cmtproto.Validator{Address: pubKey.Address(), PubKey: pkProtoPtr, VotingPower: math.MaxInt64, ProposerPriority: 0},
487488
}
488489

489490
_, err = ValidatorSetFromProto(protoVals)

0 commit comments

Comments
 (0)