Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: verify the part hash during mempool recovery #1670

Merged
merged 3 commits into from
Mar 12, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions consensus/propagation/commitment.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package propagation

import (
"bytes"
"fmt"

"github.com/tendermint/tendermint/crypto/tmhash"

"github.com/gogo/protobuf/proto"
"github.com/tendermint/tendermint/proto/tendermint/mempool"
"github.com/tendermint/tendermint/proto/tendermint/propagation"
@@ -27,13 +30,19 @@ func (blockProp *Reactor) ProposeBlock(proposal *types.Proposal, block *types.Pa
return
}

partHashes := make([][]byte, block.Total())
for i := 0; i < int(block.Total()); i++ {
partHashes[i] = tmhash.Sum(block.GetPart(i).Bytes)
}

// create the compact block
cb := proptypes.CompactBlock{
Proposal: *proposal,
LastLen: uint32(lastLen),
Signature: cmtrand.Bytes(64), // todo: sign the proposal with a real signature
BpHash: parityBlock.Hash(),
Blobs: txs,
Proposal: *proposal,
LastLen: uint32(lastLen),
Signature: cmtrand.Bytes(64), // todo: sign the proposal with a real signature
BpHash: parityBlock.Hash(),
Blobs: txs,
PartsHashes: partHashes,
}

// 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
return
}
for _, part := range parts {
if !bytes.Equal(tmhash.Sum(part.Bytes), cb.PartsHashes[part.Index]) {
blockProp.Logger.Error(
"recovered part hash is different than compact block",
"part",
part.Index,
"height",
cb.Proposal.Height,
"round",
cb.Proposal.Round,
)
continue
}
added, err := partSet.AddPartWithoutProof(part)
if err != nil {
blockProp.Logger.Error("failed to add locally recovered part", "err", err)
51 changes: 51 additions & 0 deletions consensus/propagation/commitment_test.go
Original file line number Diff line number Diff line change
@@ -129,6 +129,57 @@ func TestRecoverPartsLocally(t *testing.T) {
}
}

// TestInvalidPartHash verifies if the parts hashes verification
// when recovering the parts works.
func TestInvalidPartHash(t *testing.T) {
cleanup, _, sm := state.SetupTestCase(t)
t.Cleanup(func() {
cleanup(t)
})

numberOfTxs := 10
txsMap := make(map[types.TxKey]types.Tx)
txs := make([]types.Tx, numberOfTxs)
for i := 0; i < numberOfTxs; i++ {
tx := types.Tx(cmtrand.Bytes(int(types.BlockPartSizeBytes / 3)))
txKey, err := types.TxKeyFromBytes(tx.Hash())
require.NoError(t, err)
txsMap[txKey] = tx
txs[i] = tx
}

blockStore := store.NewBlockStore(dbm.NewMemDB())
blockPropR := NewReactor("", trace.NoOpTracer(), blockStore, mockMempool{
txs: txsMap,
})

data := types.Data{Txs: txs}

block, partSet := sm.MakeBlock(1, data, types.RandCommit(time.Now()), []types.Evidence{}, cmtrand.Bytes(20))
id := types.BlockID{Hash: block.Hash(), PartSetHeader: partSet.Header()}
prop := types.NewProposal(block.Height, 0, 0, id)
prop.Signature = cmtrand.Bytes(64)

metaData := make([]proptypes.TxMetaData, len(partSet.TxPos))
for i, pos := range partSet.TxPos {
metaData[i] = proptypes.TxMetaData{
Start: uint32(pos.Start),
End: uint32(pos.End),
Hash: block.Txs[i].Hash(),
}
}

// skew the part bytes
partSet.GetPart(2).Bytes[10] = 0x12

blockPropR.ProposeBlock(prop, partSet, metaData)

_, actualParts, _ := blockPropR.GetProposal(prop.Height, prop.Round)

// verify that the part 2 was not added to the part set
assert.Nil(t, actualParts.GetPart(2))
}

var _ Mempool = &mockMempool{}

type mockMempool struct {
2 changes: 1 addition & 1 deletion consensus/propagation/reactor_test.go
Original file line number Diff line number Diff line change
@@ -162,7 +162,7 @@ func TestHandleHavesAndWantsAndRecoveryParts(t *testing.T) {
Data: randomData,
})

time.Sleep(200 * time.Millisecond)
time.Sleep(300 * time.Millisecond)

// check if reactor 3 received the recovery part.
_, parts, found := reactor3.GetProposal(10, 1)
42 changes: 27 additions & 15 deletions consensus/propagation/types/types.go
Original file line number Diff line number Diff line change
@@ -53,7 +53,10 @@ type CompactBlock struct {
Blobs []TxMetaData `json:"blobs,omitempty"`
Signature []byte `json:"signature,omitempty"`
Proposal types.Proposal `json:"proposal,omitempty"`
LastLen uint32 // length of the last part
// length of the last part
LastLen uint32 `json:"last_len,omitempty"`
// the original part set parts hashes.
PartsHashes [][]byte `json:"parts_hashes,omitempty"`
}

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

@@ -81,11 +89,12 @@ func (c *CompactBlock) ToProto() *protoprop.CompactBlock {
blobs[i] = blob.ToProto()
}
return &protoprop.CompactBlock{
BpHash: c.BpHash,
Blobs: blobs,
Signature: c.Signature,
Proposal: c.Proposal.ToProto(),
LastLength: c.LastLen,
BpHash: c.BpHash,
Blobs: blobs,
Signature: c.Signature,
Proposal: c.Proposal.ToProto(),
LastLength: c.LastLen,
PartsHashes: c.PartsHashes,
}
}

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

cb := &CompactBlock{
BpHash: c.BpHash,
Blobs: blobs,
Signature: c.Signature,
Proposal: *prop,
BpHash: c.BpHash,
Blobs: blobs,
Signature: c.Signature,
Proposal: *prop,
LastLen: c.LastLength,
PartsHashes: c.PartsHashes,
}
return cb, cb.ValidateBasic()
}
@@ -312,11 +323,12 @@ func MsgFromProto(p *protoprop.Message) (Message, error) {
return nil, err
}
pb = &CompactBlock{
BpHash: msg.BpHash,
Blobs: blobs,
Signature: msg.Signature,
Proposal: *prop,
LastLen: msg.LastLength,
BpHash: msg.BpHash,
Blobs: blobs,
Signature: msg.Signature,
Proposal: *prop,
LastLen: msg.LastLength,
PartsHashes: msg.PartsHashes,
}
case *protoprop.PartMetaData:
pb = &PartMetaData{
19 changes: 15 additions & 4 deletions consensus/propagation/types/types_test.go
Original file line number Diff line number Diff line change
@@ -84,10 +84,11 @@ func TestCompactBlock_RoundTrip(t *testing.T) {
{
"valid block",
&CompactBlock{
BpHash: rand.Bytes(tmhash.Size),
Blobs: []TxMetaData{{Hash: rand.Bytes(tmhash.Size), Start: 0, End: 10}},
Signature: rand.Bytes(types.MaxSignatureSize),
Proposal: *mockProposal,
BpHash: rand.Bytes(tmhash.Size),
Blobs: []TxMetaData{{Hash: rand.Bytes(tmhash.Size), Start: 0, End: 10}},
Signature: rand.Bytes(types.MaxSignatureSize),
Proposal: *mockProposal,
PartsHashes: [][]byte{rand.Bytes(tmhash.Size), rand.Bytes(tmhash.Size)},
},
},
}
@@ -127,6 +128,16 @@ func TestCompactBlock_ValidateBasic(t *testing.T) {
},
errors.New("expected size to be 32 bytes, got 33 bytes"),
},
{
"invalid part set hashes length",
&CompactBlock{
BpHash: rand.Bytes(tmhash.Size),
Blobs: []TxMetaData{{Hash: rand.Bytes(tmhash.Size), Start: 0, End: 10}},
Signature: rand.Bytes(types.MaxSignatureSize),
PartsHashes: [][]byte{{0x1, 0x2}},
},
errors.New("invalid part hash height 0 round 0 index 0: expected size to be 32 bytes, got 2 bytes"),
},
{
"too big of signature",
&CompactBlock{
147 changes: 102 additions & 45 deletions proto/tendermint/propagation/types.pb.go
11 changes: 6 additions & 5 deletions proto/tendermint/propagation/types.proto
Original file line number Diff line number Diff line change
@@ -21,11 +21,12 @@ message TxMetaData {
// clients to reuse already downloaded blobs instead of gossiping them all again
// during the block propagation.
message CompactBlock {
bytes bp_hash = 1;
repeated TxMetaData blobs = 2;
bytes signature = 3;
tendermint.types.Proposal proposal = 4;
uint32 last_length = 5;
bytes bp_hash = 1;
repeated TxMetaData blobs = 2;
bytes signature = 3;
tendermint.types.Proposal proposal = 4;
uint32 last_length = 5;
repeated bytes parts_hashes = 6;
}

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