diff --git a/block/manager.go b/block/manager.go index 61d74a6ab..ed5e09062 100644 --- a/block/manager.go +++ b/block/manager.go @@ -4,9 +4,11 @@ import ( "context" "errors" "fmt" + "os" "sync" "sync/atomic" + "github.com/dymensionxyz/dymint/dofraud" "github.com/dymensionxyz/gerr-cosmos/gerrc" "golang.org/x/sync/errgroup" @@ -126,6 +128,9 @@ type Manager struct { // validates all non-finalized state updates from settlement, checking there is consistency between DA and P2P blocks, and the information in the state update. SettlementValidator *SettlementValidator + + // for testing + fraudSim dofraud.Frauds } // NewManager creates new block Manager. @@ -209,9 +214,28 @@ func NewManager( m.SettlementValidator = NewSettlementValidator(m.logger, m) + err = m.loadFraud(conf.FraudCmdsPath) + if err != nil { + return nil, fmt.Errorf("load frauds: %w", err) + } + return m, nil } +func (m *Manager) loadFraud(path string) error { + frauds, err := dofraud.Load(path) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("load frauds: %w", err) + } + m.logger.Info("Did not load fraud tests - frauds file not found", "path", path) + } else { + m.logger.Info("Loaded frauds.") + } + m.fraudSim = frauds + return nil +} + // Start starts the block manager. func (m *Manager) Start(ctx context.Context) error { m.Ctx, m.Cancel = context.WithCancel(ctx) diff --git a/block/p2p.go b/block/p2p.go index 6dcae3c5e..3887bf52b 100644 --- a/block/p2p.go +++ b/block/p2p.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/dymensionxyz/dymint/dofraud" "github.com/dymensionxyz/dymint/p2p" "github.com/dymensionxyz/dymint/types" "github.com/tendermint/tendermint/libs/pubsub" @@ -66,6 +67,7 @@ func (m *Manager) OnReceivedBlock(event pubsub.Message) { // gossipBlock sends created blocks by the sequencer to full-nodes using P2P gossipSub func (m *Manager) gossipBlock(ctx context.Context, block types.Block, commit types.Commit) error { m.logger.Info("Gossipping block", "height", block.Header.Height) + m.doFraud(dofraud.Gossip, block.Header.Height, &block, &commit) // TODO: technically ought to clone here, but block,commit aren't used afterwards except for managing production rate, fine for MVP gossipedBlock := p2p.BlockData{Block: block, Commit: commit} gossipedBlockBytes, err := gossipedBlock.MarshalBinary() if err != nil { diff --git a/block/produce.go b/block/produce.go index 9a67fe77b..bfc479065 100644 --- a/block/produce.go +++ b/block/produce.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/dymensionxyz/dymint/dofraud" "github.com/dymensionxyz/gerr-cosmos/gerrc" "github.com/dymensionxyz/dymint/node/events" @@ -238,6 +239,7 @@ func (m *Manager) produceBlock(opts ProduceBlockOptions) (*types.Block, *types.C // dequeue consensus messages for the new sequencers while creating a new block block = m.Executor.CreateBlock(newHeight, lastCommit, lastHeaderHash, proposerHashForBlock, m.State, maxBlockDataSize) + // this cannot happen if there are any sequencer set updates // AllowEmpty should be always true in this case if !opts.AllowEmpty && len(block.Data.Txs) == 0 { @@ -248,6 +250,7 @@ func (m *Manager) produceBlock(opts ProduceBlockOptions) (*types.Block, *types.C if err != nil { return nil, nil, fmt.Errorf("create commit: %w: %w", err, ErrNonRecoverable) } + m.doFraud(dofraud.Produce, newHeight, block, commit) m.logger.Info("Block created.", "height", newHeight, "num_tx", len(block.Data.Txs), "size", block.SizeBytes()+commit.SizeBytes()) types.RollappBlockSizeBytesGauge.Set(float64(len(block.Data.Txs))) @@ -348,3 +351,15 @@ func getHeaderHashAndCommit(store store.Store, height uint64) ([32]byte, *types. } return lastBlock.Header.Hash(), lastCommit, nil } + +// if a fraud is specified, apply it (modify block, commit) +func (m *Manager) doFraud(variant dofraud.FraudVariant, h uint64, b *types.Block, c *types.Commit) { + if m.fraudSim.Apply(m.logger, h, variant, b) { + comm, err := m.createCommit(b) + if err != nil { + m.logger.Error("Fraud block, create commit.", "err", err) + } else { + *c = *comm + } + } +} diff --git a/block/submit.go b/block/submit.go index 3ee4e2dc4..8900b7270 100644 --- a/block/submit.go +++ b/block/submit.go @@ -7,6 +7,7 @@ import ( "sync/atomic" "time" + "github.com/dymensionxyz/dymint/dofraud" "github.com/dymensionxyz/gerr-cosmos/gerrc" "github.com/tendermint/tendermint/libs/pubsub" "golang.org/x/sync/errgroup" @@ -240,7 +241,22 @@ func (m *Manager) CreateBatch(maxBatchSize uint64, startHeight uint64, endHeight } func (m *Manager) SubmitBatch(batch *types.Batch) error { - resultSubmitToDA := m.DAClient.SubmitBatch(batch) + daBatch := batch + // a little optimized to avoid expensive clone on every batch + // only clone and apply frauds if a fraud is actually specified + // if fraud is specified, it's only for the DA, not for the SL + for _, b := range batch.Blocks { + if m.fraudSim.Has(b.Header.Height, dofraud.DA) { + var err error + daBatch, err = batch.Clone() + if err != nil { + return fmt.Errorf("deep clone batch: %w", err) + } + m.applyFraudsToBatch(daBatch) + break + } + } + resultSubmitToDA := m.DAClient.SubmitBatch(daBatch) if resultSubmitToDA.Code != da.StatusSuccess { return fmt.Errorf("da client submit batch: %s: %w", resultSubmitToDA.Message, resultSubmitToDA.Error) } @@ -326,3 +342,10 @@ func UpdateBatchSubmissionGauges(skewBytes uint64, skewBlocks uint64, skewTime t types.RollappPendingSubmissionsSkewBlocks.Set(float64(skewBlocks)) types.RollappPendingSubmissionsSkewTimeMinutes.Set(float64(skewTime.Minutes())) } + +// applies frauds to a clone of the batch, if necessary, returns same batch or +func (m *Manager) applyFraudsToBatch(batch *types.Batch) { + for i, block := range batch.Blocks { + m.doFraud(dofraud.DA, block.Header.Height, block, batch.Commits[i]) + } +} diff --git a/config/config.go b/config/config.go index c19c58277..a7eb22b79 100644 --- a/config/config.go +++ b/config/config.go @@ -61,6 +61,8 @@ type BlockManagerConfig struct { BatchSubmitBytes uint64 `mapstructure:"batch_submit_bytes"` // SequencerSetUpdateInterval defines the interval at which to fetch sequencer updates from the settlement layer SequencerSetUpdateInterval time.Duration `mapstructure:"sequencer_update_interval"` + // File path to configure frauds for testing + FraudCmdsPath string `mapstructure:"fraud_cmds_path,omitempty"` } // GetViperConfig reads configuration parameters from Viper instance. diff --git a/dofraud/apply.go b/dofraud/apply.go new file mode 100644 index 000000000..4185a6248 --- /dev/null +++ b/dofraud/apply.go @@ -0,0 +1,114 @@ +package dofraud + +import ( + "fmt" + + "github.com/dymensionxyz/dymint/types" +) + +type ( + FraudVariant = int + FraudType = int +) + +// Variant +const ( + NoneVariant = iota + DA + Gossip + Produce +) + +// Type +const ( + NoneType = iota + HeaderVersionBlock + HeaderVersionApp + HeaderChainID + HeaderHeight + HeaderTime + HeaderLastHeaderHash + HeaderDataHash + HeaderConsensusHash + HeaderAppHash + HeaderLastResultsHash + HeaderProposerAddr + HeaderLastCommitHash + HeaderSequencerHash + HeaderNextSequencerHash + Data + LastCommit +) + +type Cmd struct { + *types.Block + ts []FraudType +} + +// The possibilites are simple, just change +type Frauds struct { + frauds map[string]Cmd +} + +type key struct { + height uint64 + variant FraudVariant +} + +func (k key) String() string { + return fmt.Sprintf("%d:%d", k.height, k.variant) +} + +func (f *Frauds) Has(height uint64, variant FraudVariant) bool { + _, ok := f.frauds[key{height, variant}.String()] + return ok +} + +// apply any loaded frauds, no-op if none +func (f *Frauds) Apply(log types.Logger, height uint64, fraudVariant FraudVariant, b *types.Block) bool { + cmd, ok := f.frauds[key{height, fraudVariant}.String()] + if !ok { + return false + } + + for _, fraud := range cmd.ts { + switch fraud { + case HeaderVersionBlock: + b.Header.Version.Block = cmd.Header.Version.Block + case HeaderVersionApp: + b.Header.Version.App = cmd.Header.Version.App + case HeaderChainID: + b.Header.ChainID = cmd.Header.ChainID + case HeaderHeight: + b.Header.Height = cmd.Header.Height + case HeaderTime: + b.Header.Time = cmd.Header.Time + case HeaderLastHeaderHash: + b.Header.LastHeaderHash = cmd.Header.LastHeaderHash + case HeaderDataHash: + b.Header.DataHash = cmd.Header.DataHash + case HeaderConsensusHash: + b.Header.ConsensusHash = cmd.Header.ConsensusHash + case HeaderAppHash: + b.Header.AppHash = cmd.Header.AppHash + case HeaderLastResultsHash: + b.Header.LastResultsHash = cmd.Header.LastResultsHash + case HeaderProposerAddr: + b.Header.ProposerAddress = cmd.Header.ProposerAddress + case HeaderLastCommitHash: + b.Header.LastCommitHash = cmd.Header.LastCommitHash + case HeaderSequencerHash: + b.Header.SequencerHash = cmd.Header.SequencerHash + case HeaderNextSequencerHash: + b.Header.NextSequencersHash = cmd.Header.NextSequencersHash + case Data: + b.Data = cmd.Data + case LastCommit: + b.LastCommit = cmd.LastCommit + default: + } + } + + log.Info("Applied fraud.", "height", height, "variant", fraudVariant, "types", cmd.ts) + return true +} diff --git a/dofraud/disk.go b/dofraud/disk.go new file mode 100644 index 000000000..bdfa5da0c --- /dev/null +++ b/dofraud/disk.go @@ -0,0 +1,161 @@ +package dofraud + +import ( + "encoding/json" + "io" + "os" + "slices" + "strings" + + "github.com/dymensionxyz/dymint/types" + "github.com/dymensionxyz/gerr-cosmos/gerrc" +) + +type disk struct { + Instances []diskInstance `json:",omitempty"` +} + +type diskInstance struct { + Height uint64 + Variants string + Block diskBlock +} + +type diskBlock struct { + HeaderVersionBlock uint64 `json:",omitempty"` + HeaderVersionApp uint64 `json:",omitempty"` + HeaderChainID string `json:",omitempty"` + HeaderHeight uint64 `json:",omitempty"` + HeaderTime int64 `json:",omitempty"` + HeaderLastHeaderHash string `json:",omitempty"` + HeaderDataHash string `json:",omitempty"` + HeaderConsensusHash string `json:",omitempty"` + HeaderAppHash string `json:",omitempty"` + HeaderLastResultsHash string `json:",omitempty"` + HeaderProposerAddr string `json:",omitempty"` + HeaderLastCommitHash string `json:",omitempty"` + HeaderSequencerHash string `json:",omitempty"` + HeaderNextSequencerHash string `json:",omitempty"` + Data *struct{} `json:",omitempty"` // TODO: + LastCommit *struct{} `json:",omitempty"` // TODO: +} + +func Load(fn string) (Frauds, error) { + file, err := os.Open(fn) //nolint:gosec + if err != nil { + return Frauds{}, err + } + defer file.Close() //nolint:errcheck + + data, err := io.ReadAll(file) + if err != nil { + return Frauds{}, err + } + return loadBz(data) +} + +func loadBz(bz []byte) (Frauds, error) { + var d disk + err := json.Unmarshal(bz, &d) + if err != nil { + return Frauds{}, err + } + + ret := Frauds{} + ret.frauds = make(map[string]Cmd) + for _, ins := range d.Instances { + cmd := Cmd{ + Block: &types.Block{ + Header: types.Header{Version: types.Version{}}, + }, + } + if ins.Block.HeaderVersionBlock != 0 { + cmd.Block.Header.Version.Block = ins.Block.HeaderVersionBlock + cmd.ts = append(cmd.ts, HeaderVersionBlock) + } + if ins.Block.HeaderVersionApp != 0 { + cmd.Block.Header.Version.App = ins.Block.HeaderVersionApp + cmd.ts = append(cmd.ts, HeaderVersionApp) + } + if ins.Block.HeaderChainID != "" { + cmd.Block.Header.ChainID = ins.Block.HeaderChainID + cmd.ts = append(cmd.ts, HeaderChainID) + } + if ins.Block.HeaderHeight != 0 { + cmd.Block.Header.Height = ins.Block.HeaderHeight + cmd.ts = append(cmd.ts, HeaderHeight) + } + if ins.Block.HeaderTime != 0 { + cmd.Block.Header.Time = ins.Block.HeaderTime + cmd.ts = append(cmd.ts, HeaderTime) + } + if ins.Block.HeaderLastHeaderHash != "" { + cmd.Block.Header.LastHeaderHash = parseHash(ins.Block.HeaderLastHeaderHash) + cmd.ts = append(cmd.ts, HeaderLastHeaderHash) + } + if ins.Block.HeaderDataHash != "" { + cmd.Block.Header.DataHash = parseHash(ins.Block.HeaderDataHash) + cmd.ts = append(cmd.ts, HeaderDataHash) + } + if ins.Block.HeaderConsensusHash != "" { + cmd.Block.Header.ConsensusHash = parseHash(ins.Block.HeaderConsensusHash) + cmd.ts = append(cmd.ts, HeaderConsensusHash) + } + if ins.Block.HeaderAppHash != "" { + cmd.Block.Header.AppHash = parseHash(ins.Block.HeaderAppHash) + cmd.ts = append(cmd.ts, HeaderAppHash) + } + if ins.Block.HeaderLastResultsHash != "" { + cmd.Block.Header.LastResultsHash = parseHash(ins.Block.HeaderLastResultsHash) + cmd.ts = append(cmd.ts, HeaderLastResultsHash) + } + if ins.Block.HeaderProposerAddr != "" { + cmd.Block.Header.ProposerAddress = []byte(ins.Block.HeaderProposerAddr) + cmd.ts = append(cmd.ts, HeaderProposerAddr) + } + if ins.Block.HeaderLastCommitHash != "" { + cmd.Block.Header.LastCommitHash = parseHash(ins.Block.HeaderLastCommitHash) + cmd.ts = append(cmd.ts, HeaderLastCommitHash) + } + if ins.Block.HeaderSequencerHash != "" { + cmd.Block.Header.SequencerHash = parseHash(ins.Block.HeaderSequencerHash) + cmd.ts = append(cmd.ts, HeaderSequencerHash) + } + if ins.Block.HeaderNextSequencerHash != "" { + cmd.Block.Header.NextSequencersHash = parseHash(ins.Block.HeaderNextSequencerHash) + cmd.ts = append(cmd.ts, HeaderNextSequencerHash) + } + if ins.Block.Data != nil { + return Frauds{}, gerrc.ErrUnimplemented.Wrap("block data") + } + if ins.Block.LastCommit != nil { + return Frauds{}, gerrc.ErrUnimplemented.Wrap("last commit") + } + vs := parseVariants(ins.Variants) + for _, v := range vs { + ret.frauds[key{ins.Height, v}.String()] = cmd + } + } + return ret, nil +} + +func parseHash(hashStr string) [32]byte { + var hash [32]byte + copy(hash[:], hashStr) + return hash +} + +func parseVariants(s string) []FraudVariant { + ret := []FraudVariant{} + l := strings.Split(s, ",") + if slices.Contains(l, "da") { + ret = append(ret, DA) + } + if slices.Contains(l, "gossip") { + ret = append(ret, Gossip) + } + if slices.Contains(l, "produce") { + ret = append(ret, Produce) + } + return ret +} diff --git a/dofraud/doc.go b/dofraud/doc.go new file mode 100644 index 000000000..fee5306ee --- /dev/null +++ b/dofraud/doc.go @@ -0,0 +1,4 @@ +// Package dofraud is for injecting frauds, for testing. +// User should pass a path to a json file containing frauds to dymint.toml +// Can choose between 3 variants of frauds: DA, Gossip, Produce +package dofraud diff --git a/dofraud/testdata/example.json b/dofraud/testdata/example.json new file mode 100644 index 000000000..b7800bb6c --- /dev/null +++ b/dofraud/testdata/example.json @@ -0,0 +1,35 @@ +{ + "Instances": [ + { + "Height": 10, + "Variants": "da,gossip", + "Block": { + "HeaderProposerAddr": "1092381209381923809182391823098129038" + } + }, + { + "Height": 44, + "Variants": "da,gossip,produce", + "Block": { + "HeaderDataHash": "1290830918230981239812903819823909123", + "HeaderLastResultsHash": "129038120938120938120938120938120938", + "HeaderNextSequencerHash": "1092381209381923809182391823098129038" + } + }, + { + "Height": 105, + "Variants": "gossip", + "Block": { + "HeaderTime": 1734374656000000000, + "HeaderNextSequencerHash": "1092381209381923809182391823098129038" + } + }, + { + "Height": 120, + "Variants": "gossip", + "Block": { + "HeaderVersionApp": 3 + } + } + ] +} \ No newline at end of file diff --git a/dofraud/z_test.go b/dofraud/z_test.go new file mode 100644 index 000000000..540f603d0 --- /dev/null +++ b/dofraud/z_test.go @@ -0,0 +1,51 @@ +package dofraud + +import ( + _ "embed" + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +const fp = "testdata/example.json" + +//go:embed testdata/example.json +var testData []byte + +func TestGenerateJson(t *testing.T) { + t.Skip("Not a real test, just handy for quickly generating an example json.") + + examples := disk{ + Instances: []diskInstance{ + {Height: 10, Variants: "da,gossip", Block: diskBlock{HeaderProposerAddr: "1092381209381923809182391823098129038"}}, + {Height: 44, Variants: "da,gossip,produce", Block: diskBlock{ + HeaderNextSequencerHash: "1092381209381923809182391823098129038", + HeaderDataHash: "1290830918230981239812903819823909123", + HeaderLastResultsHash: "129038120938120938120938120938120938", + }}, + {Height: 105, Variants: "gossip", Block: diskBlock{ + HeaderNextSequencerHash: "1092381209381923809182391823098129038", + HeaderTime: 1734374656000000000, + }}, + {Height: 120, Variants: "gossip", Block: diskBlock{ + HeaderVersionApp: 3, + }}, + }, + } + + data, err := json.MarshalIndent(examples, "", " ") + require.NoError(t, err) + + err = os.WriteFile(fp, data, 0o644) + require.NoError(t, err) +} + +func TestParseJson(t *testing.T) { + fraud, err := loadBz(testData) + require.NoError(t, err) + cmd, ok := fraud.frauds[key{10, DA}.String()] + require.True(t, ok) + require.Contains(t, cmd.ts, HeaderProposerAddr) +} diff --git a/types/batch.go b/types/batch.go index 14d486539..1e9fcb91a 100644 --- a/types/batch.go +++ b/types/batch.go @@ -53,3 +53,10 @@ func (b Batch) SizeBlockAndCommitBytes() int { func (b Batch) SizeBytes() int { return b.ToProto().Size() } + +func (b Batch) Clone() (*Batch, error) { + p := b.ToProto() + ret := &Batch{} + err := ret.FromProto(p) + return ret, err +}