Skip to content

Commit

Permalink
fix(cons messages): include in header hash for fraud protection (#1288)
Browse files Browse the repository at this point in the history
  • Loading branch information
danwt authored Dec 20, 2024
1 parent e2308e3 commit 55a8bd3
Show file tree
Hide file tree
Showing 17 changed files with 565 additions and 217 deletions.
2 changes: 2 additions & 0 deletions block/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ func (e *Executor) CreateBlock(
},
LastCommit: *lastCommit,
}

block.Header.SetDymHeader(types.MakeDymHeader(block.Data.ConsensusMessages))
copy(block.Header.LastCommitHash[:], types.GetLastCommitHash(lastCommit, &block.Header))
copy(block.Header.DataHash[:], types.GetDataHash(block))
copy(block.Header.SequencerHash[:], state.GetProposerHash())
Expand Down
63 changes: 63 additions & 0 deletions block/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

pb "github.com/dymensionxyz/dymint/types/pb/dymint"
"github.com/gogo/protobuf/proto"
prototypes "github.com/gogo/protobuf/types"
"github.com/golang/groupcache/testpb"
Expand Down Expand Up @@ -86,8 +87,40 @@ func TestCreateBlock(t *testing.T) {
err = mpool.CheckTx(make([]byte, 100), func(r *abci.Response) {}, mempool.TxInfo{})
require.NoError(err)
block = executor.CreateBlock(3, &types.Commit{}, [32]byte{}, [32]byte(state.GetProposerHash()), state, maxBytes)
block.Data.ToProto()
require.NotNil(block)
assert.Len(block.Data.Txs, 2)

// native -> proto -> binary -> proto -> native
var b1 = block
require.NoError(b1.ValidateBasic())
b1p1 := b1.ToProto()
b1p1bz, err := b1p1.Marshal()
require.NoError(err)
var b1p2 pb.Block
err = proto.Unmarshal(b1p1bz, &b1p2)
require.NoError(err)
var b2 types.Block
err = b2.FromProto(&b1p2)
require.NoError(err)
require.NoError(b2.ValidateBasic())

// same
b1bz, err := b1.MarshalBinary()
require.NoError(err)
var b3 types.Block
err = b3.UnmarshalBinary(b1bz)
require.NoError(err)
require.NoError(b3.ValidateBasic())

// only to proto
require.NoError(b1.ValidateBasic())
b1p3 := b1.ToProto()
var b4 types.Block
err = b4.FromProto(b1p3)
require.NoError(err)
require.NoError(b4.ValidateBasic())

}

func TestCreateBlockWithConsensusMessages(t *testing.T) {
Expand Down Expand Up @@ -160,6 +193,36 @@ func TestCreateBlockWithConsensusMessages(t *testing.T) {

assert.True(proto.Equal(anyMsg1, block.Data.ConsensusMessages[0]))
assert.True(proto.Equal(anyMsg2, block.Data.ConsensusMessages[1]))

// native -> proto -> binary -> proto -> native
var b1 = block
require.NoError(b1.ValidateBasic())
b1p1 := b1.ToProto()
b1p1bz, err := b1p1.Marshal()
require.NoError(err)
var b1p2 pb.Block
err = proto.Unmarshal(b1p1bz, &b1p2)
require.NoError(err)
var b2 types.Block
err = b2.FromProto(&b1p2)
require.NoError(err)
require.NoError(b2.ValidateBasic())

// same
b1bz, err := b1.MarshalBinary()
require.NoError(err)
var b3 types.Block
err = b3.UnmarshalBinary(b1bz)
require.NoError(err)
require.NoError(b3.ValidateBasic())

// only to proto
require.NoError(b1.ValidateBasic())
b1p3 := b1.ToProto()
var b4 types.Block
err = b4.FromProto(b1p3)
require.NoError(err)
require.NoError(b4.ValidateBasic())
}

func TestApplyBlock(t *testing.T) {
Expand Down
70 changes: 70 additions & 0 deletions block/signature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package block_test

import (
"context"
"testing"

proto "github.com/gogo/protobuf/types"

"github.com/dymensionxyz/dymint/block"
"github.com/dymensionxyz/dymint/p2p"
"github.com/dymensionxyz/dymint/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/proxy"
)

// produce a block, mutate it, confirm the sig check fails
func TestProduceNewBlockMutation(t *testing.T) {
// Init app
app := testutil.GetAppMock(testutil.Commit, testutil.EndBlock)
commitHash := [32]byte{1}
app.On("Commit", mock.Anything).Return(abci.ResponseCommit{Data: commitHash[:]})
app.On("EndBlock", mock.Anything).Return(abci.ResponseEndBlock{
RollappParamUpdates: &abci.RollappParams{
Da: "mock",
DrsVersion: 0,
},
ConsensusParamUpdates: &abci.ConsensusParams{
Block: &abci.BlockParams{
MaxGas: 40000000,
MaxBytes: 500000,
},
},
})
// Create proxy app
clientCreator := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(clientCreator)
err := proxyApp.Start()
require.NoError(t, err)
// Init manager
manager, err := testutil.GetManager(testutil.GetManagerConfig(), nil, 1, 1, 0, proxyApp, nil)
require.NoError(t, err)
// Produce block
b, c, err := manager.ProduceApplyGossipBlock(context.Background(), block.ProduceBlockOptions{AllowEmpty: true})
require.NoError(t, err)
// Validate state is updated with the commit hash
assert.Equal(t, uint64(1), manager.State.Height())
assert.Equal(t, commitHash, manager.State.AppHash)

err = b.ValidateBasic()
require.NoError(t, err)

// TODO: better way to write test is to clone the block and commit and check a table of mutations
bd := p2p.BlockData{
Block: *b,
Commit: *c,
}
err = bd.Validate(manager.State.GetProposerPubKey())
require.NoError(t, err)

b.Data.ConsensusMessages = []*proto.Any{{}}
bd = p2p.BlockData{
Block: *b,
Commit: *c,
}
err = bd.Validate(manager.State.GetProposerPubKey())
require.Error(t, err)
}
3 changes: 2 additions & 1 deletion da/celestia/celestia_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ func compareBatches(t *testing.T, b1, b2 *types.Batch) {
func getRandomBlock(height uint64, nTxs int) *types.Block {
block := &types.Block{
Header: types.Header{
Height: height,
Height: height,
ConsensusMessagesHash: types.ConsMessagesHash(nil),
},
Data: types.Data{
Txs: make(types.Txs, nTxs),
Expand Down
3 changes: 2 additions & 1 deletion da/da_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ func doTestRetrieve(t *testing.T, dalc da.DataAvailabilityLayerClient) {
func getRandomBlock(height uint64, nTxs int) *types.Block {
block := &types.Block{
Header: types.Header{
Height: height,
Height: height,
ConsensusMessagesHash: types.ConsMessagesHash(nil),
},
Data: types.Data{
Txs: make(types.Txs, nTxs),
Expand Down
4 changes: 4 additions & 0 deletions proto/types/dymint/dymint.proto
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ message Header {

// Chain ID the block belongs to
string chain_id = 13;

// The following fields are added on top of the normal TM header, for dymension purposes
// Note: LOSSY when converted to tendermint (squashed into a single hash)
bytes consensus_messages_hash = 15;
}

message Commit {
Expand Down
9 changes: 4 additions & 5 deletions rpc/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,6 @@ func TestValidatedHeight(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {

node.BlockManager.SettlementValidator.UpdateLastValidatedHeight(test.validatedHeight)
node.BlockManager.LastSettlementHeight.Store(test.submittedHeight)

Expand All @@ -389,7 +388,6 @@ func TestValidatedHeight(t *testing.T) {

err = node.Stop()
require.NoError(err)

}

func TestGetCommit(t *testing.T) {
Expand Down Expand Up @@ -929,9 +927,10 @@ func TestValidatorSetHandling(t *testing.T) {
func getRandomBlock(height uint64, nTxs int) *types.Block {
block := &types.Block{
Header: types.Header{
Height: height,
Version: types.Version{Block: testutil.BlockVersion},
ProposerAddress: getRandomBytes(20),
Height: height,
Version: types.Version{Block: testutil.BlockVersion},
ProposerAddress: getRandomBytes(20),
ConsensusMessagesHash: types.ConsMessagesHash(nil),
},
Data: types.Data{
Txs: make(types.Txs, nTxs),
Expand Down
28 changes: 15 additions & 13 deletions testutil/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,19 @@ func generateBlock(height uint64, proposerHash []byte, lastHeaderHash [32]byte)
Block: BlockVersion,
App: AppVersion,
},
Height: height,
Time: 4567,
LastHeaderHash: lastHeaderHash,
LastCommitHash: h[0],
DataHash: h[1],
ConsensusHash: h[2],
AppHash: [32]byte{},
LastResultsHash: GetEmptyLastResultsHash(),
ProposerAddress: []byte{4, 3, 2, 1},
SequencerHash: [32]byte(proposerHash),
NextSequencersHash: [32]byte(proposerHash),
ChainID: "test-chain",
Height: height,
Time: 4567,
LastHeaderHash: lastHeaderHash,
LastCommitHash: h[0],
DataHash: h[1],
ConsensusHash: h[2],
AppHash: [32]byte{},
LastResultsHash: GetEmptyLastResultsHash(),
ProposerAddress: []byte{4, 3, 2, 1},
SequencerHash: [32]byte(proposerHash),
NextSequencersHash: [32]byte(proposerHash),
ChainID: "test-chain",
ConsensusMessagesHash: types.ConsMessagesHash(nil),
},
Data: types.Data{
Txs: nil,
Expand Down Expand Up @@ -388,7 +389,8 @@ func GetEmptyLastResultsHash() [32]byte {
func GetRandomBlock(height uint64, nTxs int) *types.Block {
block := &types.Block{
Header: types.Header{
Height: height,
Height: height,
ConsensusMessagesHash: types.ConsMessagesHash(nil),
},
Data: types.Data{
Txs: make(types.Txs, nTxs),
Expand Down
6 changes: 6 additions & 0 deletions types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ type Header struct {

// The Chain ID
ChainID string

// The following fields are added on top of the normal TM header, for dymension purposes
// Note: LOSSY when converted to tendermint (squashed into a single hash)
ConsensusMessagesHash [32]byte // must be the hash of the (merkle root) of the consensus messages
}

func (h Header) GetTimestamp() time.Time {
Expand Down Expand Up @@ -124,6 +128,8 @@ func GetLastCommitHash(lastCommit *Commit, header *Header) []byte {
}

// GetDataHash returns the hash of the block data to be set in the block header.
// Doesn't include consensus messages because we want to avoid touching
// fundamental primitive and allow tx inclusion proofs.
func GetDataHash(block *Block) []byte {
abciData := tmtypes.Data{
Txs: ToABCIBlockDataTxs(&block.Data),
Expand Down
5 changes: 4 additions & 1 deletion types/conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ package types
import (
"github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/proto/tendermint/version"

tmtypes "github.com/tendermint/tendermint/types"
)

// ToABCIHeaderPB converts Dymint header to Header format defined in ABCI.
// Caller should fill all the fields that are not available in Dymint header (like ChainID).
// WARNING: THIS IS A LOSSY CONVERSION
func ToABCIHeaderPB(header *Header) types.Header {
tmheader := ToABCIHeader(header)
return *tmheader.ToProto()
}

// ToABCIHeader converts Dymint header to Header format defined in ABCI.
// Caller should fill all the fields that are not available in Dymint header (like ChainID).
// WARNING: THIS IS A LOSSY CONVERSION
func ToABCIHeader(header *Header) tmtypes.Header {
return tmtypes.Header{
Version: version.Consensus{
Expand All @@ -37,7 +40,7 @@ func ToABCIHeader(header *Header) tmtypes.Header {
ConsensusHash: header.ConsensusHash[:],
AppHash: header.AppHash[:],
LastResultsHash: header.LastResultsHash[:],
EvidenceHash: new(tmtypes.EvidenceData).Hash(),
EvidenceHash: header.DymHash(), // Overloaded, we don't need the evidence field because we don't use comet.
ProposerAddress: header.ProposerAddress,
ChainID: header.ChainID,
}
Expand Down
56 changes: 56 additions & 0 deletions types/dym_header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package types

import (
"fmt"

proto "github.com/gogo/protobuf/types"
"github.com/tendermint/tendermint/crypto/merkle"
cmtbytes "github.com/tendermint/tendermint/libs/bytes"
_ "github.com/tendermint/tendermint/types"
)

// a convenience struct to make computing easier
// persisted and over the wire types use a flat representation
type DymHeader struct {
ConsensusMessagesHash [32]byte
}

func MakeDymHeader(consMessages []*proto.Any) DymHeader {
return DymHeader{
ConsensusMessagesHash: ConsMessagesHash(consMessages),
}
}

func (h DymHeader) Hash() cmtbytes.HexBytes {
// 32 bytes long
return merkle.HashFromByteSlices([][]byte{
h.ConsensusMessagesHash[:],
// can be extended with other things if we need to later
})
}

func (h *Header) SetDymHeader(dh DymHeader) {
copy(h.ConsensusMessagesHash[:], dh.ConsensusMessagesHash[:])
}

func (h *Header) DymHash() cmtbytes.HexBytes {
ret := DymHeader{}
copy(ret.ConsensusMessagesHash[:], h.ConsensusMessagesHash[:])
return ret.Hash()
}

func ConsMessagesHash(msgs []*proto.Any) [32]byte {
bzz := make([][]byte, len(msgs))
for i, msg := range msgs {
var err error
bzz[i], err = msg.Marshal()
if err != nil {
// Not obvious how to recover here. Shouldn't happen.
panic(fmt.Errorf("marshal consensus message: %w", err))
}
}
merkleRoot := merkle.HashFromByteSlices(bzz)
ret := [32]byte{}
copy(ret[:], merkleRoot) // merkleRoot is already 32 bytes
return ret
}
Loading

0 comments on commit 55a8bd3

Please sign in to comment.