Skip to content

Commit 187e638

Browse files
authoredMar 12, 2025··
feat: verify the part hash during mempool recovery (#1670)
## Description Closes #1667 #### PR checklist - [ ] Tests written/updated - [ ] Changelog entry added in `.changelog` (we use [unclog](https://github.com/informalsystems/unclog) to manage our changelog) - [ ] Updated relevant documentation (`docs/` or `spec/`) and code comments

File tree

7 files changed

+228
-75
lines changed

7 files changed

+228
-75
lines changed
 

‎consensus/propagation/commitment.go

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package propagation
22

33
import (
4+
"bytes"
45
"fmt"
56

7+
"github.com/tendermint/tendermint/crypto/tmhash"
8+
69
"github.com/gogo/protobuf/proto"
710
"github.com/tendermint/tendermint/proto/tendermint/mempool"
811
"github.com/tendermint/tendermint/proto/tendermint/propagation"
@@ -27,13 +30,19 @@ func (blockProp *Reactor) ProposeBlock(proposal *types.Proposal, block *types.Pa
2730
return
2831
}
2932

33+
partHashes := make([][]byte, block.Total())
34+
for i := 0; i < int(block.Total()); i++ {
35+
partHashes[i] = tmhash.Sum(block.GetPart(i).Bytes)
36+
}
37+
3038
// create the compact block
3139
cb := proptypes.CompactBlock{
32-
Proposal: *proposal,
33-
LastLen: uint32(lastLen),
34-
Signature: cmtrand.Bytes(64), // todo: sign the proposal with a real signature
35-
BpHash: parityBlock.Hash(),
36-
Blobs: txs,
40+
Proposal: *proposal,
41+
LastLen: uint32(lastLen),
42+
Signature: cmtrand.Bytes(64), // todo: sign the proposal with a real signature
43+
BpHash: parityBlock.Hash(),
44+
Blobs: txs,
45+
PartsHashes: partHashes,
3746
}
3847

3948
// save the compact block locally and broadcast it to the connected peers
@@ -98,6 +107,18 @@ func (blockProp *Reactor) handleCompactBlock(cb *proptypes.CompactBlock, peer p2
98107
return
99108
}
100109
for _, part := range parts {
110+
if !bytes.Equal(tmhash.Sum(part.Bytes), cb.PartsHashes[part.Index]) {
111+
blockProp.Logger.Error(
112+
"recovered part hash is different than compact block",
113+
"part",
114+
part.Index,
115+
"height",
116+
cb.Proposal.Height,
117+
"round",
118+
cb.Proposal.Round,
119+
)
120+
continue
121+
}
101122
added, err := partSet.AddPartWithoutProof(part)
102123
if err != nil {
103124
blockProp.Logger.Error("failed to add locally recovered part", "err", err)

‎consensus/propagation/commitment_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,57 @@ func TestRecoverPartsLocally(t *testing.T) {
129129
}
130130
}
131131

132+
// TestInvalidPartHash verifies if the parts hashes verification
133+
// when recovering the parts works.
134+
func TestInvalidPartHash(t *testing.T) {
135+
cleanup, _, sm := state.SetupTestCase(t)
136+
t.Cleanup(func() {
137+
cleanup(t)
138+
})
139+
140+
numberOfTxs := 10
141+
txsMap := make(map[types.TxKey]types.Tx)
142+
txs := make([]types.Tx, numberOfTxs)
143+
for i := 0; i < numberOfTxs; i++ {
144+
tx := types.Tx(cmtrand.Bytes(int(types.BlockPartSizeBytes / 3)))
145+
txKey, err := types.TxKeyFromBytes(tx.Hash())
146+
require.NoError(t, err)
147+
txsMap[txKey] = tx
148+
txs[i] = tx
149+
}
150+
151+
blockStore := store.NewBlockStore(dbm.NewMemDB())
152+
blockPropR := NewReactor("", trace.NoOpTracer(), blockStore, mockMempool{
153+
txs: txsMap,
154+
})
155+
156+
data := types.Data{Txs: txs}
157+
158+
block, partSet := sm.MakeBlock(1, data, types.RandCommit(time.Now()), []types.Evidence{}, cmtrand.Bytes(20))
159+
id := types.BlockID{Hash: block.Hash(), PartSetHeader: partSet.Header()}
160+
prop := types.NewProposal(block.Height, 0, 0, id)
161+
prop.Signature = cmtrand.Bytes(64)
162+
163+
metaData := make([]proptypes.TxMetaData, len(partSet.TxPos))
164+
for i, pos := range partSet.TxPos {
165+
metaData[i] = proptypes.TxMetaData{
166+
Start: uint32(pos.Start),
167+
End: uint32(pos.End),
168+
Hash: block.Txs[i].Hash(),
169+
}
170+
}
171+
172+
// skew the part bytes
173+
partSet.GetPart(2).Bytes[10] = 0x12
174+
175+
blockPropR.ProposeBlock(prop, partSet, metaData)
176+
177+
_, actualParts, _ := blockPropR.GetProposal(prop.Height, prop.Round)
178+
179+
// verify that the part 2 was not added to the part set
180+
assert.Nil(t, actualParts.GetPart(2))
181+
}
182+
132183
var _ Mempool = &mockMempool{}
133184

134185
type mockMempool struct {

‎consensus/propagation/reactor_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func TestHandleHavesAndWantsAndRecoveryParts(t *testing.T) {
162162
Data: randomData,
163163
})
164164

165-
time.Sleep(200 * time.Millisecond)
165+
time.Sleep(300 * time.Millisecond)
166166

167167
// check if reactor 3 received the recovery part.
168168
_, parts, found := reactor3.GetProposal(10, 1)

‎consensus/propagation/types/types.go

+27-15
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ type CompactBlock struct {
5353
Blobs []TxMetaData `json:"blobs,omitempty"`
5454
Signature []byte `json:"signature,omitempty"`
5555
Proposal types.Proposal `json:"proposal,omitempty"`
56-
LastLen uint32 // length of the last part
56+
// length of the last part
57+
LastLen uint32 `json:"last_len,omitempty"`
58+
// the original part set parts hashes.
59+
PartsHashes [][]byte `json:"parts_hashes,omitempty"`
5760
}
5861

5962
// ValidateBasic checks if the CompactBlock is valid. It fails if the height is
@@ -71,6 +74,11 @@ func (c *CompactBlock) ValidateBasic() error {
7174
if len(c.Signature) > types.MaxSignatureSize {
7275
return errors.New("CompactBlock: Signature is too big")
7376
}
77+
for index, partHash := range c.PartsHashes {
78+
if err := types.ValidateHash(partHash); err != nil {
79+
return fmt.Errorf("invalid part hash height %d round %d index %d: %w", c.Proposal.Height, c.Proposal.Round, index, err)
80+
}
81+
}
7482
return nil
7583
}
7684

@@ -81,11 +89,12 @@ func (c *CompactBlock) ToProto() *protoprop.CompactBlock {
8189
blobs[i] = blob.ToProto()
8290
}
8391
return &protoprop.CompactBlock{
84-
BpHash: c.BpHash,
85-
Blobs: blobs,
86-
Signature: c.Signature,
87-
Proposal: c.Proposal.ToProto(),
88-
LastLength: c.LastLen,
92+
BpHash: c.BpHash,
93+
Blobs: blobs,
94+
Signature: c.Signature,
95+
Proposal: c.Proposal.ToProto(),
96+
LastLength: c.LastLen,
97+
PartsHashes: c.PartsHashes,
8998
}
9099
}
91100

@@ -102,10 +111,12 @@ func CompactBlockFromProto(c *protoprop.CompactBlock) (*CompactBlock, error) {
102111
}
103112

104113
cb := &CompactBlock{
105-
BpHash: c.BpHash,
106-
Blobs: blobs,
107-
Signature: c.Signature,
108-
Proposal: *prop,
114+
BpHash: c.BpHash,
115+
Blobs: blobs,
116+
Signature: c.Signature,
117+
Proposal: *prop,
118+
LastLen: c.LastLength,
119+
PartsHashes: c.PartsHashes,
109120
}
110121
return cb, cb.ValidateBasic()
111122
}
@@ -312,11 +323,12 @@ func MsgFromProto(p *protoprop.Message) (Message, error) {
312323
return nil, err
313324
}
314325
pb = &CompactBlock{
315-
BpHash: msg.BpHash,
316-
Blobs: blobs,
317-
Signature: msg.Signature,
318-
Proposal: *prop,
319-
LastLen: msg.LastLength,
326+
BpHash: msg.BpHash,
327+
Blobs: blobs,
328+
Signature: msg.Signature,
329+
Proposal: *prop,
330+
LastLen: msg.LastLength,
331+
PartsHashes: msg.PartsHashes,
320332
}
321333
case *protoprop.PartMetaData:
322334
pb = &PartMetaData{

‎consensus/propagation/types/types_test.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,11 @@ func TestCompactBlock_RoundTrip(t *testing.T) {
8484
{
8585
"valid block",
8686
&CompactBlock{
87-
BpHash: rand.Bytes(tmhash.Size),
88-
Blobs: []TxMetaData{{Hash: rand.Bytes(tmhash.Size), Start: 0, End: 10}},
89-
Signature: rand.Bytes(types.MaxSignatureSize),
90-
Proposal: *mockProposal,
87+
BpHash: rand.Bytes(tmhash.Size),
88+
Blobs: []TxMetaData{{Hash: rand.Bytes(tmhash.Size), Start: 0, End: 10}},
89+
Signature: rand.Bytes(types.MaxSignatureSize),
90+
Proposal: *mockProposal,
91+
PartsHashes: [][]byte{rand.Bytes(tmhash.Size), rand.Bytes(tmhash.Size)},
9192
},
9293
},
9394
}
@@ -127,6 +128,16 @@ func TestCompactBlock_ValidateBasic(t *testing.T) {
127128
},
128129
errors.New("expected size to be 32 bytes, got 33 bytes"),
129130
},
131+
{
132+
"invalid part set hashes length",
133+
&CompactBlock{
134+
BpHash: rand.Bytes(tmhash.Size),
135+
Blobs: []TxMetaData{{Hash: rand.Bytes(tmhash.Size), Start: 0, End: 10}},
136+
Signature: rand.Bytes(types.MaxSignatureSize),
137+
PartsHashes: [][]byte{{0x1, 0x2}},
138+
},
139+
errors.New("invalid part hash height 0 round 0 index 0: expected size to be 32 bytes, got 2 bytes"),
140+
},
130141
{
131142
"too big of signature",
132143
&CompactBlock{

‎proto/tendermint/propagation/types.pb.go

+102-45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎proto/tendermint/propagation/types.proto

+6-5
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ message TxMetaData {
2121
// clients to reuse already downloaded blobs instead of gossiping them all again
2222
// during the block propagation.
2323
message CompactBlock {
24-
bytes bp_hash = 1;
25-
repeated TxMetaData blobs = 2;
26-
bytes signature = 3;
27-
tendermint.types.Proposal proposal = 4;
28-
uint32 last_length = 5;
24+
bytes bp_hash = 1;
25+
repeated TxMetaData blobs = 2;
26+
bytes signature = 3;
27+
tendermint.types.Proposal proposal = 4;
28+
uint32 last_length = 5;
29+
repeated bytes parts_hashes = 6;
2930
}
3031

3132
// PartMetaData proves the inclusion of a part to the block.

0 commit comments

Comments
 (0)
Please sign in to comment.