Skip to content

Commit

Permalink
feat(validation): sequencer misbehavior detection (dymensionxyz#1167)
Browse files Browse the repository at this point in the history
Co-authored-by: Sergi Rene <[email protected]>
Co-authored-by: Faulty Tolly <@faulttolerance.net>
Co-authored-by: omritoptix <[email protected]>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 7326b64 commit 787fd09
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 51 deletions.
2 changes: 1 addition & 1 deletion block/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ func TestApplyLocalBlock_WithFraudCheck(t *testing.T) {
assert.True(t, manager.State.Height() == 0)

// enough time to sync and produce blocks
ctx, cancel := context.WithTimeout(context.Background(), time.Second*4)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// Capture the error returned by manager.Start.

Expand Down
35 changes: 35 additions & 0 deletions block/slvalidator.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ func (v *SettlementValidator) ValidateDaBlocks(slBatch *settlement.ResultRetriev
return types.NewErrStateUpdateNumBlocksNotMatchingFraud(slBatch.EndHeight, numSLBlocks, numSLBlocks)
}

currentProposer := v.blockManager.State.GetProposer()
if currentProposer == nil {
return fmt.Errorf("proposer is not set")
}

// we compare all DA blocks against the information included in the state info block descriptors
for i, bd := range slBatch.BlockDescriptors {
// height check
Expand All @@ -167,6 +172,35 @@ func (v *SettlementValidator) ValidateDaBlocks(slBatch *settlement.ResultRetriev
if err != nil {
return err
}

// we compare the sequencer address between SL state info and DA block
// if next sequencer is not set, we check if the sequencer hash is equal to the next sequencer hash
// because it did not change. If the next sequencer is set, we check if the next sequencer hash is equal on the
// last block of the batch
isLastBlock := i == len(slBatch.BlockDescriptors)-1
if slBatch.NextSequencer != currentProposer.SettlementAddress && isLastBlock {
err := v.blockManager.UpdateSequencerSetFromSL()
if err != nil {
return fmt.Errorf("update sequencer set from SL: %w", err)
}
nextSequencer, found := v.blockManager.Sequencers.GetByAddress(slBatch.NextSequencer)
if !found {
return fmt.Errorf("next sequencer not found")
}
if !bytes.Equal(nextSequencer.MustHash(), daBlocks[i].Header.NextSequencersHash[:]) {
return types.NewErrInvalidNextSequencersHashFraud(
[32]byte(nextSequencer.MustHash()),
daBlocks[i].Header.NextSequencersHash,
)
}
} else {
if !bytes.Equal(daBlocks[i].Header.SequencerHash[:], daBlocks[i].Header.NextSequencersHash[:]) {
return types.NewErrInvalidNextSequencersHashFraud(
daBlocks[i].Header.SequencerHash,
daBlocks[i].Header.NextSequencersHash,
)
}
}
}
v.logger.Debug("DA blocks validated successfully", "start height", daBlocks[0].Header.Height, "end height", daBlocks[len(daBlocks)-1].Header.Height)
return nil
Expand Down Expand Up @@ -198,6 +232,7 @@ func (v *SettlementValidator) NextValidationHeight() uint64 {
}

// validateDRS compares the DRS version stored for the specific height, obtained from rollapp params.
// DRS checks will work only for non-finalized heights, since it does not store the whole history, but it will never validate finalized heights.
func (v *SettlementValidator) validateDRS(stateIndex uint64, height uint64, version string) error {
drs, err := v.blockManager.Store.LoadDRSVersion(height)
if err != nil {
Expand Down
68 changes: 58 additions & 10 deletions block/slvalidator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package block_test

import (
"crypto/rand"
"encoding/hex"
"github.com/tendermint/tendermint/crypto/ed25519"
tmtypes "github.com/tendermint/tendermint/types"
"reflect"
"testing"
"time"
Expand All @@ -26,7 +29,6 @@ import (
)

func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {

// Init app
app := testutil.GetAppMock(testutil.EndBlock)
app.On("EndBlock", mock.Anything).Return(abci.ResponseEndBlock{
Expand All @@ -49,6 +51,13 @@ func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {
proposerKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)

fakeProposerKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)

nextProposerKey := ed25519.GenPrivKey()
nextSequencerKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)

doubleSigned, err := testutil.GenerateBlocks(1, 10, proposerKey, [32]byte{})
require.NoError(t, err)

Expand All @@ -59,6 +68,7 @@ func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {
doubleSignedBlocks []*types.Block
stateUpdateFraud string
expectedErrType error
last bool
}{
{
name: "Successful validation applied from DA",
Expand Down Expand Up @@ -123,6 +133,14 @@ func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {
doubleSignedBlocks: nil,
expectedErrType: &types.ErrStateUpdateDRSVersionFraud{},
},
{
name: "Failed validation next sequencer",
p2pBlocks: false,
stateUpdateFraud: "nextsequencer",
doubleSignedBlocks: nil,
expectedErrType: &types.ErrInvalidNextSequencersHashFraud{},
last: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand All @@ -132,13 +150,37 @@ func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, manager)

if tc.last {
proposerPubKey := nextProposerKey.PubKey()
pubKeybytes := proposerPubKey.Bytes()
if err != nil {
panic(err)
}

// Set next sequencer
raw, _ := nextSequencerKey.GetPublic().Raw()
pubkey := ed25519.PubKey(raw)
manager.State.SetProposer(types.NewSequencer(pubkey, hex.EncodeToString(pubKeybytes), "", nil))

// set proposer
raw, _ = proposerKey.GetPublic().Raw()
pubkey = ed25519.PubKey(raw)
manager.State.Proposer.Store(types.NewSequencer(pubkey, "", "", nil))
}

// Create DA
manager.DAClient = testutil.GetMockDALC(log.TestingLogger())
manager.Retriever = manager.DAClient.(da.BatchRetriever)

// Generate batch
batch, err := testutil.GenerateBatch(1, 10, proposerKey, [32]byte{})
assert.NoError(t, err)
var batch *types.Batch
if tc.last {
batch, err = testutil.GenerateLastBatch(1, 10, proposerKey, fakeProposerKey, [32]byte{})
assert.NoError(t, err)
} else {
batch, err = testutil.GenerateBatch(1, 10, proposerKey, [32]byte{})
assert.NoError(t, err)
}

// Submit batch to DA
daResultSubmitBatch := manager.DAClient.SubmitBatch(batch)
Expand All @@ -149,7 +191,7 @@ func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {
require.NoError(t, err)

// create the batch in settlement
slBatch := getSLBatch(bds, daResultSubmitBatch.SubmitMetaData, 1, 10)
slBatch := getSLBatch(bds, daResultSubmitBatch.SubmitMetaData, 1, 10, manager.State.GetProposer().SettlementAddress)

// Create the StateUpdateValidator
validator := block.NewSettlementValidator(testutil.NewLogger(t), manager)
Expand All @@ -170,7 +212,9 @@ func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {
}
// otherwise load them from DA
} else {
manager.ApplyBatchFromSL(slBatch.Batch)
if !tc.last {
manager.ApplyBatchFromSL(slBatch.Batch)
}
}

for _, bd := range bds {
Expand Down Expand Up @@ -200,6 +244,9 @@ func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) {
case "height":
// add blockdescriptor with wrong height
slBatch.BlockDescriptors[0].Height = 2
case "nextsequencer":
seq := types.NewSequencerFromValidator(*tmtypes.NewValidator(nextProposerKey.PubKey(), 1))
slBatch.NextSequencer = seq.SettlementAddress
}

// validate the state update
Expand Down Expand Up @@ -332,7 +379,7 @@ func TestStateUpdateValidator_ValidateDAFraud(t *testing.T) {
bds, err := getBlockDescriptors(batch)
require.NoError(t, err)
// Generate batch with block descriptors
slBatch := getSLBatch(bds, daResultSubmitBatch.SubmitMetaData, 1, 10)
slBatch := getSLBatch(bds, daResultSubmitBatch.SubmitMetaData, 1, 10, manager.State.GetProposer().SettlementAddress)

for _, bd := range bds {
manager.Store.SaveDRSVersion(bd.Height, bd.DrsVersion, nil)
Expand Down Expand Up @@ -371,17 +418,18 @@ func getBlockDescriptors(batch *types.Batch) ([]rollapp.BlockDescriptor, error)
return bds, nil
}

func getSLBatch(bds []rollapp.BlockDescriptor, daMetaData *da.DASubmitMetaData, startHeight uint64, endHeight uint64) *settlement.ResultRetrieveBatch {
func getSLBatch(bds []rollapp.BlockDescriptor, daMetaData *da.DASubmitMetaData, startHeight uint64, endHeight uint64, nextSequencer string) *settlement.ResultRetrieveBatch {
// create the batch in settlement
return &settlement.ResultRetrieveBatch{
Batch: &settlement.Batch{
BlockDescriptors: bds,
MetaData: &settlement.BatchMetaData{
DA: daMetaData,
},
StartHeight: startHeight,
EndHeight: endHeight,
NumBlocks: endHeight - startHeight + 1,
StartHeight: startHeight,
EndHeight: endHeight,
NumBlocks: endHeight - startHeight + 1,
NextSequencer: nextSequencer,
},
ResultBase: settlement.ResultBase{
StateIndex: 1,
Expand Down
2 changes: 1 addition & 1 deletion block/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (m *Manager) onNewStateUpdateFinalized(event pubsub.Message) {
m.SettlementValidator.UpdateLastValidatedHeight(eventData.EndHeight)
}

// ValidateLoop listens for syncing events (from new state update or from initial syncing) and validates state updates to the last submitted height.
// SettlementValidateLoop listens for syncing events (from new state update or from initial syncing) and validates state updates to the last submitted height.
func (m *Manager) SettlementValidateLoop(ctx context.Context) error {
for {
select {
Expand Down
3 changes: 3 additions & 0 deletions proto/types/dymensionxyz/dymension/rollapp/state_info.proto
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ message StateInfo {
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"created_at\""
];
// next sequencer is the bech32-encoded address of the next sequencer after the current sequencer
// if empty, it means there is no change in the sequencer
string nextProposer = 11;
}

// StateInfoSummary is a compact representation of StateInfo
Expand Down
1 change: 1 addition & 0 deletions rpc/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ func TestValidatorSetHandling(t *testing.T) {
},
ValidatorUpdates: []abci.ValidatorUpdate{{PubKey: pbValKey, Power: 100}},
})

waitCh := make(chan interface{})

app.On("Commit", mock.Anything).Return(abci.ResponseCommit{}).Times(5)
Expand Down
7 changes: 4 additions & 3 deletions settlement/dymension/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,22 +111,23 @@ func convertToNewBatchEvent(rawEventData ctypes.ResultEvent) (*settlement.EventD
return nil, fmt.Errorf("missing expected attributes in event")
}

numBlocks, err := strconv.ParseInt(rawEventData.Events["state_update.num_blocks"][0], 10, 64)
numBlocks, err := strconv.ParseInt(events["state_update.num_blocks"][0], 10, 64)
if err != nil {
errs = append(errs, err)
}
startHeight, err := strconv.ParseInt(rawEventData.Events["state_update.start_height"][0], 10, 64)
startHeight, err := strconv.ParseInt(events["state_update.start_height"][0], 10, 64)
if err != nil {
errs = append(errs, err)
}
stateIndex, err := strconv.ParseInt(rawEventData.Events["state_update.state_info_index"][0], 10, 64)
stateIndex, err := strconv.ParseInt(events["state_update.state_info_index"][0], 10, 64)
if err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
endHeight := uint64(startHeight + numBlocks - 1)

NewBatchEvent := &settlement.EventDataNewBatch{
StartHeight: uint64(startHeight),
EndHeight: endHeight,
Expand Down
1 change: 1 addition & 0 deletions settlement/dymension/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func convertStateInfoToResultRetrieveBatch(stateInfo *rollapptypes.StateInfo) (*
},
BlockDescriptors: stateInfo.BDs.BD,
NumBlocks: stateInfo.NumBlocks,
NextSequencer: stateInfo.NextProposer,
}

return &settlement.ResultRetrieveBatch{
Expand Down
17 changes: 17 additions & 0 deletions settlement/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,20 @@ import (

// ErrBatchNotAccepted is returned when a batch is not accepted by the settlement layer.
var ErrBatchNotAccepted = fmt.Errorf("batch not accepted: %w", gerrc.ErrUnknown)

type ErrNextSequencerAddressFraud struct {
Expected string
Actual string
}

func NewErrNextSequencerAddressFraud(expected string, actual string) *ErrNextSequencerAddressFraud {
return &ErrNextSequencerAddressFraud{Expected: expected, Actual: actual}
}

func (e ErrNextSequencerAddressFraud) Error() string {
return fmt.Sprintf("next sequencer address fraud: expected %s, got %s", e.Expected, e.Actual)
}

func (e ErrNextSequencerAddressFraud) Wrap(err error) error {
return gerrc.ErrFault
}
2 changes: 2 additions & 0 deletions settlement/settlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type Batch struct {
StartHeight uint64
EndHeight uint64
BlockDescriptors []rollapp.BlockDescriptor
NextSequencer string

// MetaData about the batch in the DA layer
MetaData *BatchMetaData
NumBlocks uint64
Expand Down
57 changes: 57 additions & 0 deletions testutil/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,63 @@ func GenerateBatch(startHeight uint64, endHeight uint64, proposerKey crypto.Priv
return batch, nil
}

// GenerateLastBatch generates a final batch with LastBatch flag set to true and different NextSequencerHash
func GenerateLastBatch(startHeight uint64, endHeight uint64, proposerKey crypto.PrivKey, nextSequencerKey crypto.PrivKey, lastHeaderHash [32]byte) (*types.Batch, error) {
nextSequencerRaw, _ := nextSequencerKey.Raw()
nextSeq := types.NewSequencerFromValidator(*tmtypes.NewValidator(ed25519.PrivKey(nextSequencerRaw).PubKey(), 1))
nextSequencerHash := nextSeq.MustHash()

blocks, err := GenerateLastBlocks(startHeight, endHeight-startHeight+1, proposerKey, lastHeaderHash, [32]byte(nextSequencerHash))
if err != nil {
return nil, err
}

commits, err := GenerateCommits(blocks, proposerKey)
if err != nil {
return nil, err
}

batch := &types.Batch{
Blocks: blocks,
Commits: commits,
LastBatch: true,
}

return batch, nil
}

// GenerateLastBlocks es similar a GenerateBlocks pero incluye el NextSequencerHash
func GenerateLastBlocks(startHeight uint64, num uint64, proposerKey crypto.PrivKey, lastHeaderHash [32]byte, nextSequencerHash [32]byte) ([]*types.Block, error) {
r, _ := proposerKey.Raw()
seq := types.NewSequencerFromValidator(*tmtypes.NewValidator(ed25519.PrivKey(r).PubKey(), 1))
proposerHash := seq.MustHash()
blocks := make([]*types.Block, num)

for i := uint64(0); i < num; i++ {
if i > 0 {
lastHeaderHash = blocks[i-1].Header.Hash()
}
block := generateBlock(i+startHeight, proposerHash, lastHeaderHash)

if i == num-1 {
copy(block.Header.NextSequencersHash[:], nextSequencerHash[:])
}

copy(block.Header.DataHash[:], types.GetDataHash(block))
if i > 0 {
copy(block.Header.LastCommitHash[:], types.GetLastCommitHash(&blocks[i-1].LastCommit, &block.Header))
}

signature, err := generateSignature(proposerKey, &block.Header)
if err != nil {
return nil, err
}
block.LastCommit.Signatures = []types.Signature{signature}
blocks[i] = block
}
return blocks, nil
}

func MustGenerateBatch(startHeight uint64, endHeight uint64, proposerKey crypto.PrivKey) *types.Batch {
blocks, err := GenerateBlocks(startHeight, endHeight-startHeight+1, proposerKey, [32]byte{})
if err != nil {
Expand Down
Loading

0 comments on commit 787fd09

Please sign in to comment.