From 2800a75cf04fa15137dddc1e7925041a7b8a52ed Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 13:27:25 +0100 Subject: [PATCH 1/3] feat: expose execution client params to ev-node --- apps/evm/cmd/run.go | 11 +- apps/testapp/kv/http_server_test.go | 6 +- apps/testapp/kv/kvexecutor.go | 36 ++-- apps/testapp/kv/kvexecutor_test.go | 63 ++---- block/components_test.go | 4 +- block/internal/common/replay.go | 2 +- block/internal/common/replay_test.go | 8 +- block/internal/executing/executor.go | 4 +- .../internal/executing/executor_lazy_test.go | 10 +- .../internal/executing/executor_logic_test.go | 22 +- .../executing/executor_restart_test.go | 12 +- block/internal/syncing/syncer.go | 4 +- .../internal/syncing/syncer_benchmark_test.go | 2 +- .../syncing/syncer_forced_inclusion_test.go | 16 +- block/internal/syncing/syncer_test.go | 26 +-- core/execution/dummy.go | 29 ++- core/execution/dummy_test.go | 26 +-- core/execution/execution.go | 54 ++++- execution/evm/execution.go | 142 +++++++++---- execution/grpc/client.go | 14 +- execution/grpc/client_test.go | 32 ++- execution/grpc/go.mod | 2 + execution/grpc/server.go | 6 +- execution/grpc/server_test.go | 39 ++-- node/execution_test.go | 25 +-- pkg/sequencers/single/sequencer.go | 76 ++++++- pkg/sequencers/single/sequencer_test.go | 193 ++++++++++++++++++ pkg/telemetry/executor_tracing.go | 61 +++++- pkg/telemetry/executor_tracing_test.go | 22 +- test/mocks/execution.go | 48 ++--- test/mocks/height_aware_executor.go | 8 +- 31 files changed, 676 insertions(+), 327 deletions(-) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index eef0fa379c..26a6752113 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -89,7 +89,7 @@ var RunCmd = &cobra.Command{ } // Create sequencer based on configuration - sequencer, err := createSequencer(logger, datastore, nodeConfig, genesis, daClient) + sequencer, err := createSequencer(logger, datastore, nodeConfig, genesis, daClient, executor) if err != nil { return err } @@ -160,6 +160,7 @@ func createSequencer( nodeConfig config.Config, genesis genesis.Genesis, daClient block.FullDAClient, + executor execution.Executor, ) (coresequencer.Sequencer, error) { if nodeConfig.Node.BasedSequencer { // Based sequencer mode - fetch transactions only from DA @@ -193,6 +194,14 @@ func createSequencer( return nil, fmt.Errorf("failed to create single sequencer: %w", err) } + // Configure DA transaction filter if executor supports it + if filter, ok := executor.(execution.DATransactionFilter); ok { + if infoProvider, ok := executor.(execution.ExecutionInfoProvider); ok { + sequencer.SetDATransactionFilter(filter, infoProvider) + logger.Info().Msg("DA transaction filter configured for gas-based filtering") + } + } + logger.Info(). Str("forced_inclusion_namespace", nodeConfig.DA.GetForcedInclusionNamespace()). Msg("single sequencer initialized") diff --git a/apps/testapp/kv/http_server_test.go b/apps/testapp/kv/http_server_test.go index 7845204fbf..fc476733e0 100644 --- a/apps/testapp/kv/http_server_test.go +++ b/apps/testapp/kv/http_server_test.go @@ -141,7 +141,7 @@ func TestHandleKV_Get(t *testing.T) { // Create and execute the transaction directly tx := []byte(fmt.Sprintf("%s=%s", tt.key, tt.value)) ctx := context.Background() - _, _, err := exec.ExecuteTxs(ctx, [][]byte{tx}, 1, time.Now(), []byte("")) + _, err := exec.ExecuteTxs(ctx, [][]byte{tx}, 1, time.Now(), []byte("")) if err != nil { t.Fatalf("Failed to execute setup transaction: %v", err) } @@ -287,13 +287,13 @@ func TestHTTPIntegration_GetKVWithMultipleHeights(t *testing.T) { // Execute transactions at different heights for the same key txsHeight1 := [][]byte{[]byte("testkey=original_value")} - _, _, err = exec.ExecuteTxs(ctx, txsHeight1, 1, time.Now(), []byte("")) + _, err = exec.ExecuteTxs(ctx, txsHeight1, 1, time.Now(), []byte("")) if err != nil { t.Fatalf("ExecuteTxs failed for height 1: %v", err) } txsHeight2 := [][]byte{[]byte("testkey=updated_value")} - _, _, err = exec.ExecuteTxs(ctx, txsHeight2, 2, time.Now(), []byte("")) + _, err = exec.ExecuteTxs(ctx, txsHeight2, 2, time.Now(), []byte("")) if err != nil { t.Fatalf("ExecuteTxs failed for height 2: %v", err) } diff --git a/apps/testapp/kv/kvexecutor.go b/apps/testapp/kv/kvexecutor.go index 49d6c21e5e..7ddf4cf588 100644 --- a/apps/testapp/kv/kvexecutor.go +++ b/apps/testapp/kv/kvexecutor.go @@ -141,52 +141,52 @@ func (k *KVExecutor) computeStateRoot(ctx context.Context) ([]byte, error) { // InitChain initializes the chain state with genesis parameters. // It checks the database to see if genesis was already performed. // If not, it computes the state root from the current DB state and persists genesis info. -func (k *KVExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { +func (k *KVExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { select { case <-ctx.Done(): - return nil, 0, ctx.Err() + return nil, ctx.Err() default: } initialized, err := k.db.Has(ctx, genesisInitializedKey) if err != nil { - return nil, 0, fmt.Errorf("failed to check genesis initialization status: %w", err) + return nil, fmt.Errorf("failed to check genesis initialization status: %w", err) } if initialized { genesisRoot, err := k.db.Get(ctx, genesisStateRootKey) if err != nil { - return nil, 0, fmt.Errorf("genesis initialized but failed to retrieve state root: %w", err) + return nil, fmt.Errorf("genesis initialized but failed to retrieve state root: %w", err) } - return genesisRoot, 1024, nil // Assuming 1024 is a constant gas value + return genesisRoot, nil } // Genesis not initialized. Compute state root from the current DB state. // Note: The DB might not be empty if restarting, this reflects the state *at genesis time*. stateRoot, err := k.computeStateRoot(ctx) if err != nil { - return nil, 0, fmt.Errorf("failed to compute initial state root for genesis: %w", err) + return nil, fmt.Errorf("failed to compute initial state root for genesis: %w", err) } // Persist genesis state root and initialized flag batch, err := k.db.Batch(ctx) if err != nil { - return nil, 0, fmt.Errorf("failed to create batch for genesis persistence: %w", err) + return nil, fmt.Errorf("failed to create batch for genesis persistence: %w", err) } err = batch.Put(ctx, genesisStateRootKey, stateRoot) if err != nil { - return nil, 0, fmt.Errorf("failed to put genesis state root in batch: %w", err) + return nil, fmt.Errorf("failed to put genesis state root in batch: %w", err) } err = batch.Put(ctx, genesisInitializedKey, []byte("true")) // Store a marker value if err != nil { - return nil, 0, fmt.Errorf("failed to put genesis initialized flag in batch: %w", err) + return nil, fmt.Errorf("failed to put genesis initialized flag in batch: %w", err) } err = batch.Commit(ctx) if err != nil { - return nil, 0, fmt.Errorf("failed to commit genesis persistence batch: %w", err) + return nil, fmt.Errorf("failed to commit genesis persistence batch: %w", err) } - return stateRoot, 1024, nil // Assuming 1024 is a constant gas value + return stateRoot, nil } // GetTxs retrieves available transactions from the mempool channel. @@ -222,16 +222,16 @@ func (k *KVExecutor) GetTxs(ctx context.Context) ([][]byte, error) { // ExecuteTxs processes each transaction assumed to be in the format "key=value". // It updates the database accordingly using a batch and removes the executed transactions from the mempool. // Invalid transactions are filtered out and logged, but execution continues. -func (k *KVExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) { +func (k *KVExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) { select { case <-ctx.Done(): - return nil, 0, ctx.Err() + return nil, ctx.Err() default: } batch, err := k.db.Batch(ctx) if err != nil { - return nil, 0, fmt.Errorf("failed to create database batch: %w", err) + return nil, fmt.Errorf("failed to create database batch: %w", err) } validTxCount := 0 @@ -274,7 +274,7 @@ func (k *KVExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight u err = batch.Put(ctx, dsKey, []byte(value)) if err != nil { // This error is unlikely for Put unless the context is cancelled. - return nil, 0, fmt.Errorf("failed to stage put operation in batch for key '%s': %w", key, err) + return nil, fmt.Errorf("failed to stage put operation in batch for key '%s': %w", key, err) } validTxCount++ } @@ -287,7 +287,7 @@ func (k *KVExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight u // Commit the batch to apply all changes atomically err = batch.Commit(ctx) if err != nil { - return nil, 0, fmt.Errorf("failed to commit transaction batch: %w", err) + return nil, fmt.Errorf("failed to commit transaction batch: %w", err) } // Compute the new state root *after* successful commit @@ -295,10 +295,10 @@ func (k *KVExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight u if err != nil { // This is problematic, state was changed but root calculation failed. // May need more robust error handling or recovery logic. - return nil, 0, fmt.Errorf("failed to compute state root after executing transactions: %w", err) + return nil, fmt.Errorf("failed to compute state root after executing transactions: %w", err) } - return stateRoot, 1024, nil + return stateRoot, nil } // SetFinal marks a block as finalized at the specified height. diff --git a/apps/testapp/kv/kvexecutor_test.go b/apps/testapp/kv/kvexecutor_test.go index c10eff65a5..97280aee10 100644 --- a/apps/testapp/kv/kvexecutor_test.go +++ b/apps/testapp/kv/kvexecutor_test.go @@ -20,25 +20,19 @@ func TestInitChain_Idempotency(t *testing.T) { chainID := "test-chain" // First call initializes genesis state - stateRoot1, maxBytes1, err := exec.InitChain(ctx, genesisTime, initialHeight, chainID) + stateRoot1, err := exec.InitChain(ctx, genesisTime, initialHeight, chainID) if err != nil { t.Fatalf("InitChain failed on first call: %v", err) } - if maxBytes1 != 1024 { - t.Errorf("Expected maxBytes 1024, got %d", maxBytes1) - } // Second call should return the same genesis state root - stateRoot2, maxBytes2, err := exec.InitChain(ctx, genesisTime, initialHeight, chainID) + stateRoot2, err := exec.InitChain(ctx, genesisTime, initialHeight, chainID) if err != nil { t.Fatalf("InitChain failed on second call: %v", err) } if !bytes.Equal(stateRoot1, stateRoot2) { t.Errorf("Genesis state roots do not match: %s vs %s", stateRoot1, stateRoot2) } - if maxBytes2 != 1024 { - t.Errorf("Expected maxBytes 1024, got %d", maxBytes2) - } } func TestGetTxs(t *testing.T) { @@ -58,43 +52,34 @@ func TestGetTxs(t *testing.T) { // though for buffered channels it should be immediate unless full. time.Sleep(10 * time.Millisecond) - // First call to GetTxs should retrieve the injected transactions txs, err := exec.GetTxs(ctx) if err != nil { - t.Fatalf("GetTxs returned error on first call: %v", err) + t.Fatalf("GetTxs returned error: %v", err) } if len(txs) != 2 { - t.Errorf("Expected 2 transactions on first call, got %d", len(txs)) + t.Errorf("Expected 2 transactions, got %d", len(txs)) } - - // Verify the content (order might not be guaranteed depending on channel internals, but likely FIFO here) - foundTx1 := false - foundTx2 := false - for _, tx := range txs { - if reflect.DeepEqual(tx, tx1) { - foundTx1 = true - } - if reflect.DeepEqual(tx, tx2) { - foundTx2 = true - } + if !reflect.DeepEqual(txs[0], tx1) { + t.Errorf("Expected first tx 'a=1', got %s", string(txs[0])) } - if !foundTx1 || !foundTx2 { - t.Errorf("Did not retrieve expected transactions. Got: %v", txs) + if !reflect.DeepEqual(txs[1], tx2) { + t.Errorf("Expected second tx 'b=2', got %s", string(txs[1])) } - // Second call to GetTxs should return no transactions as the channel was drained - txsAfterDrain, err := exec.GetTxs(ctx) + // GetTxs should drain the channel, so a second call should return empty or nil + txsAgain, err := exec.GetTxs(ctx) if err != nil { - t.Fatalf("GetTxs returned error on second call: %v", err) + t.Fatalf("GetTxs (second call) returned error: %v", err) } - if len(txsAfterDrain) != 0 { - t.Errorf("Expected 0 transactions after drain, got %d", len(txsAfterDrain)) + if len(txsAgain) != 0 { + t.Errorf("Expected 0 transactions on second call (drained), got %d", len(txsAgain)) } - // Test injecting again after draining + // Inject another transaction and verify it's available tx3 := []byte("c=3") exec.InjectTx(tx3) time.Sleep(10 * time.Millisecond) + txsAfterReinject, err := exec.GetTxs(ctx) if err != nil { t.Fatalf("GetTxs returned error after re-inject: %v", err) @@ -120,13 +105,10 @@ func TestExecuteTxs_Valid(t *testing.T) { []byte("key2=value2"), } - stateRoot, maxBytes, err := exec.ExecuteTxs(ctx, txs, 1, time.Now(), []byte("")) + stateRoot, err := exec.ExecuteTxs(ctx, txs, 1, time.Now(), []byte("")) if err != nil { t.Fatalf("ExecuteTxs failed: %v", err) } - if maxBytes != 1024 { - t.Errorf("Expected maxBytes 1024, got %d", maxBytes) - } // Check that stateRoot contains the updated key-value pairs rootStr := string(stateRoot) @@ -152,13 +134,10 @@ func TestExecuteTxs_Invalid(t *testing.T) { []byte(""), } - stateRoot, maxBytes, err := exec.ExecuteTxs(ctx, txs, 1, time.Now(), []byte("")) + stateRoot, err := exec.ExecuteTxs(ctx, txs, 1, time.Now(), []byte("")) if err != nil { t.Fatalf("ExecuteTxs should handle gibberish gracefully, got error: %v", err) } - if maxBytes != 1024 { - t.Errorf("Expected maxBytes 1024, got %d", maxBytes) - } // State root should still be computed (empty block is valid) if stateRoot == nil { @@ -173,7 +152,7 @@ func TestExecuteTxs_Invalid(t *testing.T) { []byte(""), } - stateRoot2, _, err := exec.ExecuteTxs(ctx, mixedTxs, 2, time.Now(), stateRoot) + stateRoot2, err := exec.ExecuteTxs(ctx, mixedTxs, 2, time.Now(), stateRoot) if err != nil { t.Fatalf("ExecuteTxs should filter invalid transactions and process valid ones, got error: %v", err) } @@ -213,7 +192,7 @@ func TestReservedKeysExcludedFromAppHash(t *testing.T) { ctx := context.Background() // Initialize chain to set up genesis state (this writes genesis reserved keys) - _, _, err = exec.InitChain(ctx, time.Now(), 1, "test-chain") + _, err = exec.InitChain(ctx, time.Now(), 1, "test-chain") if err != nil { t.Fatalf("Failed to initialize chain: %v", err) } @@ -223,7 +202,7 @@ func TestReservedKeysExcludedFromAppHash(t *testing.T) { []byte("user/key1=value1"), []byte("user/key2=value2"), } - _, _, err = exec.ExecuteTxs(ctx, txs, 1, time.Now(), []byte("")) + _, err = exec.ExecuteTxs(ctx, txs, 1, time.Now(), []byte("")) if err != nil { t.Fatalf("Failed to execute transactions: %v", err) } @@ -279,7 +258,7 @@ func TestReservedKeysExcludedFromAppHash(t *testing.T) { moreTxs := [][]byte{ []byte("user/key3=value3"), } - _, _, err = exec.ExecuteTxs(ctx, moreTxs, 2, time.Now(), stateRootAfterReservedKeyWrite) + _, err = exec.ExecuteTxs(ctx, moreTxs, 2, time.Now(), stateRootAfterReservedKeyWrite) if err != nil { t.Fatalf("Failed to execute more transactions: %v", err) } diff --git a/block/components_test.go b/block/components_test.go index d2ecfa5cf1..f1e56ec2ef 100644 --- a/block/components_test.go +++ b/block/components_test.go @@ -201,7 +201,7 @@ func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { // Mock InitChain to succeed initially mockExec.On("InitChain", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return([]byte("state-root"), uint64(1024), nil).Once() + Return([]byte("state-root"), nil).Once() // Mock SetDAHeight to be called during initialization mockSeq.On("SetDAHeight", uint64(0)).Return().Once() @@ -220,7 +220,7 @@ func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { // Mock ExecuteTxs to fail with a critical error criticalError := errors.New("execution client RPC connection failed") mockExec.On("ExecuteTxs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(nil, uint64(0), criticalError).Maybe() + Return(nil, criticalError).Maybe() // Create aggregator node components, err := NewAggregatorComponents( diff --git a/block/internal/common/replay.go b/block/internal/common/replay.go index b96567ef70..73677b7737 100644 --- a/block/internal/common/replay.go +++ b/block/internal/common/replay.go @@ -153,7 +153,7 @@ func (s *Replayer) replayBlock(ctx context.Context, height uint64) error { Int("tx_count", len(rawTxs)). Msg("executing transactions on execution layer") - newAppHash, _, err := s.exec.ExecuteTxs(ctx, rawTxs, height, header.Time(), prevState.AppHash) + newAppHash, err := s.exec.ExecuteTxs(ctx, rawTxs, height, header.Time(), prevState.AppHash) if err != nil { return fmt.Errorf("failed to execute transactions: %w", err) } diff --git a/block/internal/common/replay_test.go b/block/internal/common/replay_test.go index 188e819d0c..c9350da6c4 100644 --- a/block/internal/common/replay_test.go +++ b/block/internal/common/replay_test.go @@ -83,7 +83,7 @@ func TestReplayer_SyncToHeight_ExecutorBehind(t *testing.T) { // Expect ExecuteTxs to be called for height 100 mockExec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, []byte("app-hash-99")). - Return([]byte("app-hash-100"), uint64(1000), nil) + Return([]byte("app-hash-100"), nil) // Execute sync err := syncer.SyncToHeight(ctx, targetHeight) @@ -272,7 +272,7 @@ func TestReplayer_SyncToHeight_MultipleBlocks(t *testing.T) { // ExecuteTxs for current block mockExec.On("ExecuteTxs", mock.Anything, mock.Anything, height, mock.Anything, mock.Anything). - Return([]byte("app-hash-"+string(rune('0'+height))), uint64(1000), nil).Once() + Return([]byte("app-hash-"+string(rune('0'+height))), nil).Once() } // Execute sync @@ -321,7 +321,7 @@ func TestReplayer_ReplayBlock_FirstBlock(t *testing.T) { // For first block, ExecuteTxs should be called with genesis app hash mockExec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(1), mock.Anything, []byte("app-hash-1")). - Return([]byte("app-hash-1"), uint64(1000), nil) + Return([]byte("app-hash-1"), nil) // Call replayBlock directly (this is a private method, so we test it through SyncToHeight) mockExec.On("GetLatestHeight", mock.Anything).Return(uint64(0), nil) @@ -398,7 +398,7 @@ func TestReplayer_AppHashMismatch(t *testing.T) { // ExecuteTxs returns a different app hash than expected mockExec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, []byte("app-hash-99")). - Return([]byte("different-app-hash"), uint64(1000), nil) + Return([]byte("different-app-hash"), nil) // Should fail with mismatch error err := syncer.SyncToHeight(ctx, targetHeight) diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index 621b507a30..ac7aa12eb1 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -198,7 +198,7 @@ func (e *Executor) initializeState() error { // Initialize new chain e.logger.Info().Msg("initializing new blockchain state") - stateRoot, _, err := e.exec.InitChain(e.ctx, e.genesis.StartTime, + stateRoot, err := e.exec.InitChain(e.ctx, e.genesis.StartTime, e.genesis.InitialHeight, e.genesis.ChainID) if err != nil { e.sendCriticalError(fmt.Errorf("failed to initialize chain: %w", err)) @@ -642,7 +642,7 @@ func (e *Executor) signHeader(header types.Header) (types.Signature, error) { // NOTE: the function retries the execution client call regardless of the error. Some execution clients errors are irrecoverable, and will eventually halt the node, as expected. func (e *Executor) executeTxsWithRetry(ctx context.Context, rawTxs [][]byte, header types.Header, currentState types.State) ([]byte, error) { for attempt := 1; attempt <= common.MaxRetriesBeforeHalt; attempt++ { - newAppHash, _, err := e.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), currentState.AppHash) + newAppHash, err := e.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), currentState.AppHash) if err != nil { if attempt == common.MaxRetriesBeforeHalt { return nil, fmt.Errorf("failed to execute transactions: %w", err) diff --git a/block/internal/executing/executor_lazy_test.go b/block/internal/executing/executor_lazy_test.go index fdc29bb860..a9acd4505f 100644 --- a/block/internal/executing/executor_lazy_test.go +++ b/block/internal/executing/executor_lazy_test.go @@ -72,7 +72,7 @@ func TestLazyMode_ProduceBlockLogic(t *testing.T) { // Initialize state initStateRoot := []byte("init_root") mockExec.EXPECT().InitChain(mock.Anything, mock.AnythingOfType("time.Time"), gen.InitialHeight, gen.ChainID). - Return(initStateRoot, uint64(1024), nil).Once() + Return(initStateRoot, nil).Once() mockSeq.EXPECT().SetDAHeight(uint64(0)).Return().Once() require.NoError(t, exec.initializeState()) @@ -90,7 +90,7 @@ func TestLazyMode_ProduceBlockLogic(t *testing.T) { }).Once() mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.AnythingOfType("time.Time"), initStateRoot). - Return([]byte("new_root_1"), uint64(1024), nil).Once() + Return([]byte("new_root_1"), nil).Once() mockSeq.EXPECT().GetDAHeight().Return(uint64(0)).Once() @@ -114,7 +114,7 @@ func TestLazyMode_ProduceBlockLogic(t *testing.T) { }).Once() mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(2), mock.AnythingOfType("time.Time"), []byte("new_root_1")). - Return([]byte("new_root_2"), uint64(1024), nil).Once() + Return([]byte("new_root_2"), nil).Once() mockSeq.EXPECT().GetDAHeight().Return(uint64(0)).Once() @@ -187,7 +187,7 @@ func TestRegularMode_ProduceBlockLogic(t *testing.T) { // Initialize state initStateRoot := []byte("init_root") mockExec.EXPECT().InitChain(mock.Anything, mock.AnythingOfType("time.Time"), gen.InitialHeight, gen.ChainID). - Return(initStateRoot, uint64(1024), nil).Once() + Return(initStateRoot, nil).Once() mockSeq.EXPECT().SetDAHeight(uint64(0)).Return().Once() require.NoError(t, exec.initializeState()) @@ -205,7 +205,7 @@ func TestRegularMode_ProduceBlockLogic(t *testing.T) { }).Once() mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.AnythingOfType("time.Time"), initStateRoot). - Return([]byte("new_root_1"), uint64(1024), nil).Once() + Return([]byte("new_root_1"), nil).Once() mockSeq.EXPECT().GetDAHeight().Return(uint64(0)).Once() diff --git a/block/internal/executing/executor_logic_test.go b/block/internal/executing/executor_logic_test.go index fe5cfa098d..b69f602100 100644 --- a/block/internal/executing/executor_logic_test.go +++ b/block/internal/executing/executor_logic_test.go @@ -94,7 +94,7 @@ func TestProduceBlock_EmptyBatch_SetsEmptyDataHash(t *testing.T) { // Expect InitChain to be called initStateRoot := []byte("init_root") mockExec.EXPECT().InitChain(mock.Anything, mock.AnythingOfType("time.Time"), gen.InitialHeight, gen.ChainID). - Return(initStateRoot, uint64(1024), nil).Once() + Return(initStateRoot, nil).Once() mockSeq.EXPECT().SetDAHeight(uint64(0)).Return().Once() // initialize state (creates genesis block in store and sets state) @@ -112,7 +112,7 @@ func TestProduceBlock_EmptyBatch_SetsEmptyDataHash(t *testing.T) { // executor ExecuteTxs called with empty txs and previous state root mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.AnythingOfType("time.Time"), initStateRoot). - Return([]byte("new_root"), uint64(1024), nil).Once() + Return([]byte("new_root"), nil).Once() mockSeq.EXPECT().GetDAHeight().Return(uint64(0)).Once() @@ -182,7 +182,7 @@ func TestPendingLimit_SkipsProduction(t *testing.T) { require.NoError(t, err) mockExec.EXPECT().InitChain(mock.Anything, mock.AnythingOfType("time.Time"), gen.InitialHeight, gen.ChainID). - Return([]byte("i0"), uint64(1024), nil).Once() + Return([]byte("i0"), nil).Once() mockSeq.EXPECT().SetDAHeight(uint64(0)).Return().Once() require.NoError(t, exec.initializeState()) @@ -198,7 +198,7 @@ func TestPendingLimit_SkipsProduction(t *testing.T) { }).Once() // ExecuteTxs with empty mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.AnythingOfType("time.Time"), []byte("i0")). - Return([]byte("i1"), uint64(1024), nil).Once() + Return([]byte("i1"), nil).Once() mockSeq.EXPECT().GetDAHeight().Return(uint64(0)).Once() @@ -229,7 +229,7 @@ func TestExecutor_executeTxsWithRetry(t *testing.T) { name: "success on first attempt", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte("new-hash"), uint64(0), nil).Once() + Return([]byte("new-hash"), nil).Once() }, expectSuccess: true, expectHash: []byte("new-hash"), @@ -238,9 +238,9 @@ func TestExecutor_executeTxsWithRetry(t *testing.T) { name: "success on second attempt", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte(nil), uint64(0), errors.New("temporary failure")).Once() + Return([]byte(nil), errors.New("temporary failure")).Once() exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte("new-hash"), uint64(0), nil).Once() + Return([]byte("new-hash"), nil).Once() }, expectSuccess: true, expectHash: []byte("new-hash"), @@ -249,9 +249,9 @@ func TestExecutor_executeTxsWithRetry(t *testing.T) { name: "success on third attempt", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte(nil), uint64(0), errors.New("temporary failure")).Times(2) + Return([]byte(nil), errors.New("temporary failure")).Times(2) exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte("new-hash"), uint64(0), nil).Once() + Return([]byte("new-hash"), nil).Once() }, expectSuccess: true, expectHash: []byte("new-hash"), @@ -260,7 +260,7 @@ func TestExecutor_executeTxsWithRetry(t *testing.T) { name: "failure after max retries", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte(nil), uint64(0), errors.New("persistent failure")).Times(common.MaxRetriesBeforeHalt) + Return([]byte(nil), errors.New("persistent failure")).Times(common.MaxRetriesBeforeHalt) }, expectSuccess: false, expectError: "failed to execute transactions", @@ -269,7 +269,7 @@ func TestExecutor_executeTxsWithRetry(t *testing.T) { name: "context cancelled during retry", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte(nil), uint64(0), errors.New("temporary failure")).Once() + Return([]byte(nil), errors.New("temporary failure")).Once() }, expectSuccess: false, expectError: "context cancelled during retry", diff --git a/block/internal/executing/executor_restart_test.go b/block/internal/executing/executor_restart_test.go index 0df53d8e9f..a99325df67 100644 --- a/block/internal/executing/executor_restart_test.go +++ b/block/internal/executing/executor_restart_test.go @@ -72,7 +72,7 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { // Initialize state for first executor initStateRoot := []byte("init_root") mockExec1.EXPECT().InitChain(mock.Anything, mock.AnythingOfType("time.Time"), gen.InitialHeight, gen.ChainID). - Return(initStateRoot, uint64(1024), nil).Once() + Return(initStateRoot, nil).Once() mockSeq1.EXPECT().SetDAHeight(uint64(0)).Return().Once() require.NoError(t, exec1.initializeState()) @@ -91,7 +91,7 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { }).Once() mockExec1.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.AnythingOfType("time.Time"), initStateRoot). - Return([]byte("new_root_1"), uint64(1024), nil).Once() + Return([]byte("new_root_1"), nil).Once() mockSeq1.EXPECT().GetDAHeight().Return(uint64(0)).Once() @@ -208,7 +208,7 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { // The executor should be called to apply the pending block mockExec2.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(2), mock.AnythingOfType("time.Time"), currentState2.AppHash). - Return([]byte("new_root_2"), uint64(1024), nil).Once() + Return([]byte("new_root_2"), nil).Once() mockSeq2.EXPECT().GetDAHeight().Return(uint64(0)).Once() @@ -294,7 +294,7 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { initStateRoot := []byte("init_root") mockExec1.EXPECT().InitChain(mock.Anything, mock.AnythingOfType("time.Time"), gen.InitialHeight, gen.ChainID). - Return(initStateRoot, uint64(1024), nil).Once() + Return(initStateRoot, nil).Once() mockSeq1.EXPECT().SetDAHeight(uint64(0)).Return().Once() require.NoError(t, exec1.initializeState()) @@ -312,7 +312,7 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { }).Once() mockExec1.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.AnythingOfType("time.Time"), initStateRoot). - Return([]byte("new_root_1"), uint64(1024), nil).Once() + Return([]byte("new_root_1"), nil).Once() mockSeq1.EXPECT().GetDAHeight().Return(uint64(0)).Once() @@ -368,7 +368,7 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { }).Once() mockExec2.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(2), mock.AnythingOfType("time.Time"), []byte("new_root_1")). - Return([]byte("new_root_2"), uint64(1024), nil).Once() + Return([]byte("new_root_2"), nil).Once() mockSeq2.EXPECT().GetDAHeight().Return(uint64(0)).Once() diff --git a/block/internal/syncing/syncer.go b/block/internal/syncing/syncer.go index 0ea9d03dda..821a5018dd 100644 --- a/block/internal/syncing/syncer.go +++ b/block/internal/syncing/syncer.go @@ -260,7 +260,7 @@ func (s *Syncer) initializeState() error { if err != nil { // Initialize new chain state for a fresh full node (no prior state on disk) // Mirror executor initialization to ensure AppHash matches headers produced by the sequencer. - stateRoot, _, initErr := s.exec.InitChain( + stateRoot, initErr := s.exec.InitChain( s.ctx, s.genesis.StartTime, s.genesis.InitialHeight, @@ -693,7 +693,7 @@ func (s *Syncer) ApplyBlock(ctx context.Context, header types.Header, data *type // NOTE: the function retries the execution client call regardless of the error. Some execution clients errors are irrecoverable, and will eventually halt the node, as expected. func (s *Syncer) executeTxsWithRetry(ctx context.Context, rawTxs [][]byte, header types.Header, currentState types.State) ([]byte, error) { for attempt := 1; attempt <= common.MaxRetriesBeforeHalt; attempt++ { - newAppHash, _, err := s.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), currentState.AppHash) + newAppHash, err := s.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), currentState.AppHash) if err != nil { if attempt == common.MaxRetriesBeforeHalt { return nil, fmt.Errorf("failed to execute transactions: %w", err) diff --git a/block/internal/syncing/syncer_benchmark_test.go b/block/internal/syncing/syncer_benchmark_test.go index e2b6f6e51f..67025517cc 100644 --- a/block/internal/syncing/syncer_benchmark_test.go +++ b/block/internal/syncing/syncer_benchmark_test.go @@ -98,7 +98,7 @@ func newBenchFixture(b *testing.B, totalHeights uint64, shuffledTx bool, daDelay time.Sleep(execDelay) } }). - Return([]byte("app"), uint64(1024), nil).Maybe() + Return([]byte("app"), nil).Maybe() // Build syncer with mocks s := NewSyncer( diff --git a/block/internal/syncing/syncer_forced_inclusion_test.go b/block/internal/syncing/syncer_forced_inclusion_test.go index 0b16deadb2..95148bc2a7 100644 --- a/block/internal/syncing/syncer_forced_inclusion_test.go +++ b/block/internal/syncing/syncer_forced_inclusion_test.go @@ -362,7 +362,7 @@ func TestVerifyForcedInclusionTxs_AllTransactionsIncluded(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() @@ -436,7 +436,7 @@ func TestVerifyForcedInclusionTxs_MissingTransactions(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() @@ -540,7 +540,7 @@ func TestVerifyForcedInclusionTxs_PartiallyIncluded(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() @@ -648,7 +648,7 @@ func TestVerifyForcedInclusionTxs_NoForcedTransactions(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() @@ -715,7 +715,7 @@ func TestVerifyForcedInclusionTxs_NamespaceNotConfigured(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() @@ -782,7 +782,7 @@ func TestVerifyForcedInclusionTxs_DeferralWithinEpoch(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() @@ -906,7 +906,7 @@ func TestVerifyForcedInclusionTxs_MaliciousAfterEpochEnd(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() @@ -995,7 +995,7 @@ func TestVerifyForcedInclusionTxs_SmoothingExceedsEpoch(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain"). - Return([]byte("app0"), uint64(1024), nil).Once() + Return([]byte("app0"), nil).Once() client := testmocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() diff --git a/block/internal/syncing/syncer_test.go b/block/internal/syncing/syncer_test.go index 554e80f636..c62f0d67f4 100644 --- a/block/internal/syncing/syncer_test.go +++ b/block/internal/syncing/syncer_test.go @@ -113,7 +113,7 @@ func TestSyncer_validateBlock_DataHashMismatch(t *testing.T) { cfg := config.DefaultConfig() gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), uint64(1024), nil).Once() + mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() s := NewSyncer( st, @@ -163,7 +163,7 @@ func TestProcessHeightEvent_SyncsAndUpdatesState(t *testing.T) { gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), uint64(1024), nil).Once() + mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() errChan := make(chan error, 1) s := NewSyncer( @@ -191,7 +191,7 @@ func TestProcessHeightEvent_SyncsAndUpdatesState(t *testing.T) { // Expect ExecuteTxs call for height 1 mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.Anything, lastState.AppHash). - Return([]byte("app1"), uint64(1024), nil).Once() + Return([]byte("app1"), nil).Once() evt := common.DAHeightEvent{Header: hdr, Data: data, DaHeight: 1} s.processHeightEvent(&evt) @@ -217,7 +217,7 @@ func TestSequentialBlockSync(t *testing.T) { gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), uint64(1024), nil).Once() + mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() errChan := make(chan error, 1) s := NewSyncer( @@ -243,7 +243,7 @@ func TestSequentialBlockSync(t *testing.T) { _, hdr1 := makeSignedHeaderBytes(t, gen.ChainID, 1, addr, pub, signer, st0.AppHash, data1, st0.LastHeaderHash) // Expect ExecuteTxs call for height 1 mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.Anything, st0.AppHash). - Return([]byte("app1"), uint64(1024), nil).Once() + Return([]byte("app1"), nil).Once() evt1 := common.DAHeightEvent{Header: hdr1, Data: data1, DaHeight: 10} s.processHeightEvent(&evt1) @@ -252,7 +252,7 @@ func TestSequentialBlockSync(t *testing.T) { _, hdr2 := makeSignedHeaderBytes(t, gen.ChainID, 2, addr, pub, signer, st1.AppHash, data2, st1.LastHeaderHash) // Expect ExecuteTxs call for height 2 mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(2), mock.Anything, st1.AppHash). - Return([]byte("app2"), uint64(1024), nil).Once() + Return([]byte("app2"), nil).Once() evt2 := common.DAHeightEvent{Header: hdr2, Data: data2, DaHeight: 11} s.processHeightEvent(&evt2) @@ -378,7 +378,7 @@ func TestSyncLoopPersistState(t *testing.T) { // with n da blobs fetched var prevHeaderHash, prevAppHash []byte - prevAppHash, _, _ = execution.NewDummyExecutor().InitChain(t.Context(), gen.StartTime, gen.DAStartHeight, gen.ChainID) + prevAppHash, _ = execution.NewDummyExecutor().InitChain(t.Context(), gen.StartTime, gen.DAStartHeight, gen.ChainID) for i := range numBlocks { chainHeight, daHeight := gen.InitialHeight+i, i+myDAHeightOffset blockTime := gen.StartTime.Add(time.Duration(chainHeight+1) * time.Second) @@ -496,7 +496,7 @@ func TestSyncer_executeTxsWithRetry(t *testing.T) { name: "success on first attempt", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte("new-hash"), uint64(0), nil).Once() + Return([]byte("new-hash"), nil).Once() }, expectSuccess: true, expectHash: []byte("new-hash"), @@ -505,9 +505,9 @@ func TestSyncer_executeTxsWithRetry(t *testing.T) { name: "success on second attempt", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte(nil), uint64(0), errors.New("temporary failure")).Once() + Return([]byte(nil), errors.New("temporary failure")).Once() exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte("new-hash"), uint64(0), nil).Once() + Return([]byte("new-hash"), nil).Once() }, expectSuccess: true, expectHash: []byte("new-hash"), @@ -516,9 +516,9 @@ func TestSyncer_executeTxsWithRetry(t *testing.T) { name: "success on third attempt", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte(nil), uint64(0), errors.New("temporary failure")).Times(2) + Return([]byte(nil), errors.New("temporary failure")).Times(2) exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte("new-hash"), uint64(0), nil).Once() + Return([]byte("new-hash"), nil).Once() }, expectSuccess: true, expectHash: []byte("new-hash"), @@ -527,7 +527,7 @@ func TestSyncer_executeTxsWithRetry(t *testing.T) { name: "failure after max retries", setupMock: func(exec *testmocks.MockExecutor) { exec.On("ExecuteTxs", mock.Anything, mock.Anything, uint64(100), mock.Anything, mock.Anything). - Return([]byte(nil), uint64(0), errors.New("persistent failure")).Times(common.MaxRetriesBeforeHalt) + Return([]byte(nil), errors.New("persistent failure")).Times(common.MaxRetriesBeforeHalt) }, expectSuccess: false, expectError: "failed to execute transactions", diff --git a/core/execution/dummy.go b/core/execution/dummy.go index e5c50ebd5b..559f067901 100644 --- a/core/execution/dummy.go +++ b/core/execution/dummy.go @@ -14,12 +14,11 @@ import ( // DummyExecutor //--------------------- -// DummyExecutor is a dummy implementation of the DummyExecutor interface for testing +// DummyExecutor is a dummy implementation of the Executor interface for testing type DummyExecutor struct { mu sync.RWMutex // Add mutex for thread safety stateRoot []byte pendingRoots map[uint64][]byte - maxBytes uint64 injectedTxs [][]byte } @@ -28,23 +27,22 @@ func NewDummyExecutor() *DummyExecutor { return &DummyExecutor{ stateRoot: []byte{1, 2, 3}, pendingRoots: make(map[uint64][]byte), - maxBytes: 1000000, } } // InitChain initializes the chain state with the given genesis time, initial height, and chain ID. -// It returns the state root hash, the maximum byte size, and an error if the initialization fails. -func (e *DummyExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { +// It returns the state root hash and an error if the initialization fails. +func (e *DummyExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { e.mu.Lock() defer e.mu.Unlock() hash := sha512.New() hash.Write(e.stateRoot) e.stateRoot = hash.Sum(nil) - return e.stateRoot, e.maxBytes, nil + return e.stateRoot, nil } -// GetTxs returns the list of transactions (types.Tx) within the DummyExecutor instance and an error if any. +// GetTxs returns the list of transactions within the DummyExecutor instance and an error if any. func (e *DummyExecutor) GetTxs(context.Context) ([][]byte, error) { e.mu.RLock() defer e.mu.RUnlock() @@ -63,7 +61,7 @@ func (e *DummyExecutor) InjectTx(tx []byte) { } // ExecuteTxs simulate execution of transactions. -func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) { +func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) { e.mu.Lock() defer e.mu.Unlock() @@ -75,7 +73,7 @@ func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeigh pending := hash.Sum(nil) e.pendingRoots[blockHeight] = pending e.removeExecutedTxs(txs) - return pending, e.maxBytes, nil + return pending, nil } // SetFinal marks block at given height as finalized. @@ -104,3 +102,16 @@ func (e *DummyExecutor) GetStateRoot() []byte { return e.stateRoot } + +// GetExecutionInfo returns execution layer parameters. +// For DummyExecutor, returns MaxGas=0 indicating no gas-based filtering. +func (e *DummyExecutor) GetExecutionInfo(ctx context.Context, height uint64) (ExecutionInfo, error) { + return ExecutionInfo{MaxGas: 0}, nil +} + +// FilterDATransactions validates and filters force-included transactions from DA. +// For DummyExecutor, all transactions are considered valid (no parsing/gas checks). +func (e *DummyExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + // DummyExecutor doesn't do any filtering - return all txs as valid + return txs, nil, nil +} diff --git a/core/execution/dummy_test.go b/core/execution/dummy_test.go index 5e55afc3eb..f6be3d400b 100644 --- a/core/execution/dummy_test.go +++ b/core/execution/dummy_test.go @@ -19,10 +19,6 @@ func TestNewDummyExecutor(t *testing.T) { t.Errorf("Expected initial stateRoot to be [1, 2, 3], got %v", executor.stateRoot) } - if executor.maxBytes != 1000000 { - t.Errorf("Expected maxBytes to be 1000000, got %d", executor.maxBytes) - } - if executor.pendingRoots == nil { t.Error("pendingRoots map was not initialized") } @@ -42,16 +38,12 @@ func TestInitChain(t *testing.T) { initialStateRoot := make([]byte, len(executor.stateRoot)) copy(initialStateRoot, executor.stateRoot) - stateRoot, maxBytes, err := executor.InitChain(ctx, genesisTime, initialHeight, chainID) + stateRoot, err := executor.InitChain(ctx, genesisTime, initialHeight, chainID) if err != nil { t.Fatalf("InitChain returned error: %v", err) } - if maxBytes != executor.maxBytes { - t.Errorf("Expected maxBytes %d, got %d", executor.maxBytes, maxBytes) - } - if bytes.Equal(stateRoot, initialStateRoot) { t.Error("stateRoot should have changed after InitChain") } @@ -139,16 +131,12 @@ func TestExecuteTxs(t *testing.T) { prevStateRoot := executor.GetStateRoot() txsToExecute := [][]byte{tx1, tx3} - newStateRoot, maxBytes, err := executor.ExecuteTxs(ctx, txsToExecute, blockHeight, timestamp, prevStateRoot) + newStateRoot, err := executor.ExecuteTxs(ctx, txsToExecute, blockHeight, timestamp, prevStateRoot) if err != nil { t.Fatalf("ExecuteTxs returned error: %v", err) } - if maxBytes != executor.maxBytes { - t.Errorf("Expected maxBytes %d, got %d", executor.maxBytes, maxBytes) - } - if bytes.Equal(newStateRoot, prevStateRoot) { t.Error("stateRoot should have changed after ExecuteTxs") } @@ -179,7 +167,7 @@ func TestSetFinal(t *testing.T) { prevStateRoot := executor.GetStateRoot() txs := [][]byte{[]byte("tx1"), []byte("tx2")} - pendingRoot, _, _ := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) + pendingRoot, _ := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) // Set the block as final err := executor.SetFinal(ctx, blockHeight) @@ -304,7 +292,7 @@ func TestExecuteTxsAndSetFinal(t *testing.T) { timestamp := time.Now() txs := [][]byte{[]byte(string(rune(i)))} - _, _, err := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) + _, err := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) if err != nil { t.Fatalf("ExecuteTxs returned error: %v", err) } @@ -410,7 +398,7 @@ func TestExecuteTxsWithInvalidPrevStateRoot(t *testing.T) { timestamp := time.Now() txs := [][]byte{[]byte("tx1"), []byte("tx2")} - newStateRoot, maxBytes, err := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, invalidPrevStateRoot) + newStateRoot, err := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, invalidPrevStateRoot) // The dummy executor doesn't validate the previous state root, so it should still work // This is a characteristic of the dummy implementation @@ -422,10 +410,6 @@ func TestExecuteTxsWithInvalidPrevStateRoot(t *testing.T) { t.Error("Expected non-empty state root even with invalid prevStateRoot") } - if maxBytes != executor.maxBytes { - t.Errorf("Expected maxBytes %d, got %d", executor.maxBytes, maxBytes) - } - // Verify that the pending root was stored if _, exists := executor.pendingRoots[blockHeight]; !exists { t.Errorf("Expected pending root to be stored for height %d", blockHeight) diff --git a/core/execution/execution.go b/core/execution/execution.go index 5085ebe578..917e505161 100644 --- a/core/execution/execution.go +++ b/core/execution/execution.go @@ -5,11 +5,18 @@ import ( "time" ) +// ExecutionInfo contains execution layer parameters that may change per block. +type ExecutionInfo struct { + // MaxGas is the maximum gas allowed for transactions in a block. + // For non-gas-based execution layers, this should be 0. + MaxGas uint64 +} + // Executor defines the interface that execution clients must implement to be compatible with Evolve. // This interface enables the separation between consensus and execution layers, allowing for modular // and pluggable execution environments. // -// Note: if you are modifying this interface, ensure that all implementations are compatible (evm, abci, protobuf/grpc, etc.. +// Note: if you are modifying this interface, ensure that all implementations are compatible (evm, abci, protobuf/grpc, etc.) type Executor interface { // InitChain initializes a new blockchain instance with genesis parameters. // Requirements: @@ -17,7 +24,6 @@ type Executor interface { // - Must validate and store genesis parameters for future reference // - Must ensure idempotency (repeated calls with identical parameters should return same results) // - Must return error if genesis parameters are invalid - // - Must return maxBytes indicating maximum allowed bytes for a set of transactions in a block // // Parameters: // - ctx: Context for timeout/cancellation control @@ -27,9 +33,8 @@ type Executor interface { // // Returns: // - stateRoot: Hash representing initial state - // - maxBytes: Maximum allowed bytes for transactions in a block // - err: Any initialization errors - InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) (stateRoot []byte, maxBytes uint64, err error) + InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) (stateRoot []byte, err error) // GetTxs fetches available transactions from the execution layer's mempool. // Requirements: @@ -44,7 +49,7 @@ type Executor interface { // - ctx: Context for timeout/cancellation control // // Returns: - // - []types.Tx: Slice of valid transactions + // - [][]byte: Slice of valid transactions // - error: Any errors during transaction retrieval GetTxs(ctx context.Context) ([][]byte, error) @@ -66,9 +71,8 @@ type Executor interface { // // Returns: // - updatedStateRoot: New state root after executing transactions - // - maxBytes: Maximum allowed transaction size (may change with protocol updates) // - err: Any execution errors - ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, maxBytes uint64, err error) + ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, err error) // SetFinal marks a block as finalized at the specified height. // Requirements: @@ -85,6 +89,42 @@ type Executor interface { // Returns: // - error: Any errors during finalization SetFinal(ctx context.Context, blockHeight uint64) error + + // GetExecutionInfo returns current execution layer parameters. + // The height parameter allows querying info for a specific block height. + // Use height=0 to get parameters for the next block (based on latest state). + // + // For non-gas-based execution layers, return ExecutionInfo{MaxGas: 0}. + // + // Parameters: + // - ctx: Context for timeout/cancellation control + // - height: Block height to query (0 for next block parameters) + // + // Returns: + // - info: Current execution parameters + // - error: Any errors during retrieval + GetExecutionInfo(ctx context.Context, height uint64) (ExecutionInfo, error) + + // FilterDATransactions validates and filters force-included transactions from DA. + // It performs execution-layer specific validation (e.g., EVM tx parsing, gas checks) + // and returns transactions that are valid and fit within the gas limit. + // + // The function filters out: + // - Invalid/unparseable transactions (gibberish) + // - Transactions that would exceed the cumulative gas limit + // + // For non-gas-based execution layers, return all valid transactions with nil remainingTxs. + // + // Parameters: + // - ctx: Context for timeout/cancellation control + // - txs: Raw transactions from DA to validate + // - maxGas: Maximum cumulative gas allowed for these transactions + // + // Returns: + // - validTxs: Transactions that passed validation and fit within maxGas + // - remainingTxs: Valid transactions that didn't fit due to gas limit (for re-queuing) + // - err: Any errors during filtering (not validation errors, which result in filtering) + FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) (validTxs [][]byte, remainingTxs [][]byte, err error) } // HeightProvider is an optional interface that execution clients can implement diff --git a/execution/evm/execution.go b/execution/evm/execution.go index c310af06d7..83cb2c605b 100644 --- a/execution/evm/execution.go +++ b/execution/evm/execution.go @@ -262,9 +262,9 @@ func (c *EngineClient) SetLogger(l zerolog.Logger) { } // InitChain initializes the blockchain with the given genesis parameters -func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { +func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { if initialHeight != 1 { - return nil, 0, fmt.Errorf("initialHeight must be 1, got %d", initialHeight) + return nil, fmt.Errorf("initialHeight must be 1, got %d", initialHeight) } // Acknowledge the genesis block with retry logic for SYNCING status @@ -294,17 +294,17 @@ func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, ini return nil }, MaxPayloadStatusRetries, InitialRetryBackoff, "InitChain") if err != nil { - return nil, 0, err + return nil, err } - _, stateRoot, gasLimit, _, err := c.getBlockInfo(ctx, 0) + _, stateRoot, _, _, err := c.getBlockInfo(ctx, 0) if err != nil { - return nil, 0, fmt.Errorf("failed to get block info: %w", err) + return nil, fmt.Errorf("failed to get block info: %w", err) } c.initialHeight = initialHeight - return stateRoot[:], gasLimit, nil + return stateRoot[:], nil } // GetTxs retrieves transactions from the current execution payload @@ -335,16 +335,16 @@ func (c *EngineClient) GetTxs(ctx context.Context) ([][]byte, error) { // - Checks for already-promoted blocks to enable idempotent execution // - Saves ExecMeta with payloadID after forkchoiceUpdatedV3 for crash recovery // - Updates ExecMeta to "promoted" after successful execution -func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, maxBytes uint64, err error) { +func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, err error) { // 1. Check for idempotent execution - stateRoot, payloadID, found, err := c.checkIdempotency(ctx, blockHeight, timestamp, txs) - if err != nil { - c.logger.Warn().Err(err).Uint64("height", blockHeight).Msg("ExecuteTxs: idempotency check failed") + stateRoot, payloadID, found, idempotencyErr := c.checkIdempotency(ctx, blockHeight, timestamp, txs) + if idempotencyErr != nil { + c.logger.Warn().Err(idempotencyErr).Uint64("height", blockHeight).Msg("ExecuteTxs: idempotency check failed") // Continue execution on error, as it might be transient } else if found { if stateRoot != nil { - return stateRoot, 0, nil + return stateRoot, nil } if payloadID != nil { // Found in-progress execution, attempt to resume @@ -354,16 +354,16 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight prevBlockHash, prevHeaderStateRoot, prevGasLimit, _, err := c.getBlockInfo(ctx, blockHeight-1) if err != nil { - return nil, 0, fmt.Errorf("failed to get block info: %w", err) + return nil, fmt.Errorf("failed to get block info: %w", err) } // It's possible that the prev state root passed in is nil if this is the first block. // If so, we can't do a comparison. Otherwise, we compare the roots. if len(prevStateRoot) > 0 && !bytes.Equal(prevStateRoot, prevHeaderStateRoot.Bytes()) { - return nil, 0, fmt.Errorf("prevStateRoot mismatch at height %d: consensus=%x execution=%x", blockHeight-1, prevStateRoot, prevHeaderStateRoot.Bytes()) + return nil, fmt.Errorf("prevStateRoot mismatch at height %d: consensus=%x execution=%x", blockHeight-1, prevStateRoot, prevHeaderStateRoot.Bytes()) } // 2. Prepare payload attributes - txsPayload := c.filterTransactions(ctx, txs, blockHeight) + txsPayload := c.filterTransactions(txs) // Cache parent block hash for safe-block lookups. c.cacheBlockHash(blockHeight-1, prevBlockHash) @@ -436,7 +436,7 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight return nil }, MaxPayloadStatusRetries, InitialRetryBackoff, "ExecuteTxs forkchoice") if err != nil { - return nil, 0, err + return nil, err } // Save ExecMeta with payloadID for crash recovery (Stage="started") @@ -686,7 +686,7 @@ func (c *EngineClient) ResumePayload(ctx context.Context, payloadIDBytes []byte) Str("payloadID", payloadID.String()). Msg("ResumePayload: attempting to resume in-progress payload") - stateRoot, _, err = c.processPayload(ctx, payloadID, nil) // txs = nil for resume + stateRoot, err = c.processPayload(ctx, payloadID, nil) // txs = nil for resume return stateRoot, err } @@ -755,20 +755,50 @@ func (c *EngineClient) checkIdempotency(ctx context.Context, height uint64, time return nil, nil, false, nil } -// filterTransactions filters out invalid transactions and formats them for the payload. -func (c *EngineClient) filterTransactions(ctx context.Context, txs [][]byte, blockHeight uint64) []string { - forceIncludedMask := execution.GetForceIncludedMask(ctx) - +// filterTransactions formats transactions for the payload. +// DA transactions should already be filtered via FilterDATransactions before reaching here. +// Mempool transactions are already validated when added to mempool. +func (c *EngineClient) filterTransactions(txs [][]byte) []string { validTxs := make([]string, 0, len(txs)) - for i, tx := range txs { + for _, tx := range txs { if len(tx) == 0 { continue } + validTxs = append(validTxs, "0x"+hex.EncodeToString(tx)) + } + return validTxs +} + +// GetExecutionInfo returns current execution layer parameters. +// Implements the ExecutionInfoProvider interface. +func (c *EngineClient) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + // If height is 0, get the latest block's gas limit (for next block) + if height == 0 { + header, err := c.ethClient.HeaderByNumber(ctx, nil) // nil = latest + if err != nil { + return execution.ExecutionInfo{}, fmt.Errorf("failed to get latest block: %w", err) + } + return execution.ExecutionInfo{MaxGas: header.GasLimit}, nil + } + + _, _, gasLimit, _, err := c.getBlockInfo(ctx, height) + if err != nil { + return execution.ExecutionInfo{}, fmt.Errorf("failed to get block info at height %d: %w", height, err) + } + return execution.ExecutionInfo{MaxGas: gasLimit}, nil +} - // Force-included transactions (from DA) MUST be validated as they come from untrusted sources - // Skip validation for mempool transactions (already validated when added to mempool) - if forceIncludedMask != nil && i < len(forceIncludedMask) && !forceIncludedMask[i] { - validTxs = append(validTxs, "0x"+hex.EncodeToString(tx)) +// FilterDATransactions validates and filters force-included transactions from DA. +// Implements the DATransactionFilter interface. +// It filters out: +// - Invalid/unparseable transactions (gibberish) +// - Transactions that would exceed the cumulative gas limit +func (c *EngineClient) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + validTxs := make([][]byte, 0, len(txs)) + var cumulativeGas uint64 + + for i, tx := range txs { + if len(tx) == 0 { continue } @@ -777,33 +807,61 @@ func (c *EngineClient) filterTransactions(ctx context.Context, txs [][]byte, blo if err := ethTx.UnmarshalBinary(tx); err != nil { c.logger.Debug(). Int("tx_index", i). - Uint64("block_height", blockHeight). Err(err). Str("tx_hex", "0x"+hex.EncodeToString(tx)). - Msg("filtering out invalid transaction (gibberish)") + Msg("filtering out invalid DA transaction (gibberish)") continue } - validTxs = append(validTxs, "0x"+hex.EncodeToString(tx)) - } + txGas := ethTx.Gas() + + // Check if this transaction would exceed the gas limit + if cumulativeGas+txGas > maxGas { + // Return remaining valid transactions that didn't fit + remainingTxs := make([][]byte, 0) + // Add this tx to remaining + remainingTxs = append(remainingTxs, tx) + // Check remaining txs for validity and add to remaining + for j := i + 1; j < len(txs); j++ { + if len(txs[j]) == 0 { + continue + } + var remainingEthTx types.Transaction + if err := remainingEthTx.UnmarshalBinary(txs[j]); err != nil { + // Skip gibberish in remaining too + c.logger.Debug(). + Int("tx_index", j). + Err(err). + Msg("filtering out invalid DA transaction in remaining (gibberish)") + continue + } + remainingTxs = append(remainingTxs, txs[j]) + } - if len(validTxs) < len(txs) { - c.logger.Debug(). - Int("total_txs", len(txs)). - Int("valid_txs", len(validTxs)). - Int("filtered_txs", len(txs)-len(validTxs)). - Uint64("block_height", blockHeight). - Msg("filtered out invalid transactions") + c.logger.Debug(). + Int("valid_txs", len(validTxs)). + Int("remaining_txs", len(remainingTxs)). + Uint64("cumulative_gas", cumulativeGas). + Uint64("max_gas", maxGas). + Msg("DA transactions exceeded gas limit, splitting batch") + + return validTxs, remainingTxs, nil + } + + cumulativeGas += txGas + validTxs = append(validTxs, tx) } - return validTxs + + // All transactions fit + return validTxs, nil, nil } // processPayload handles the common logic of getting, submitting, and finalizing a payload. -func (c *EngineClient) processPayload(ctx context.Context, payloadID engine.PayloadID, txs [][]byte) ([]byte, uint64, error) { +func (c *EngineClient) processPayload(ctx context.Context, payloadID engine.PayloadID, txs [][]byte) ([]byte, error) { // 1. Get Payload payloadResult, err := c.engineClient.GetPayload(ctx, payloadID) if err != nil { - return nil, 0, fmt.Errorf("get payload failed: %w", err) + return nil, fmt.Errorf("get payload failed: %w", err) } blockHeight := payloadResult.ExecutionPayload.Number @@ -833,7 +891,7 @@ func (c *EngineClient) processPayload(ctx context.Context, payloadID engine.Payl return nil }, MaxPayloadStatusRetries, InitialRetryBackoff, "processPayload newPayload") if err != nil { - return nil, 0, err + return nil, err } // 3. Update Forkchoice @@ -842,13 +900,13 @@ func (c *EngineClient) processPayload(ctx context.Context, payloadID engine.Payl err = c.setFinalWithHeight(ctx, blockHash, blockHeight, false) if err != nil { - return nil, 0, fmt.Errorf("forkchoice update failed: %w", err) + return nil, fmt.Errorf("forkchoice update failed: %w", err) } // 4. Save ExecMeta (Promoted) c.saveExecMeta(ctx, blockHeight, blockTimestamp, payloadID[:], blockHash[:], payloadResult.ExecutionPayload.StateRoot.Bytes(), txs, ExecStagePromoted) - return payloadResult.ExecutionPayload.StateRoot.Bytes(), payloadResult.ExecutionPayload.GasUsed, nil + return payloadResult.ExecutionPayload.StateRoot.Bytes(), nil } func (c *EngineClient) derivePrevRandao(blockHeight uint64) common.Hash { diff --git a/execution/grpc/client.go b/execution/grpc/client.go index 0afef07d78..b2e4421cce 100644 --- a/execution/grpc/client.go +++ b/execution/grpc/client.go @@ -44,8 +44,8 @@ func NewClient(url string, opts ...connect.ClientOption) *Client { // InitChain initializes a new blockchain instance with genesis parameters. // // This method sends an InitChain request to the remote execution service and -// returns the initial state root and maximum bytes allowed for transactions. -func (c *Client) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) (stateRoot []byte, maxBytes uint64, err error) { +// returns the initial state root. +func (c *Client) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) (stateRoot []byte, err error) { req := connect.NewRequest(&pb.InitChainRequest{ GenesisTime: timestamppb.New(genesisTime), InitialHeight: initialHeight, @@ -54,10 +54,10 @@ func (c *Client) InitChain(ctx context.Context, genesisTime time.Time, initialHe resp, err := c.client.InitChain(ctx, req) if err != nil { - return nil, 0, fmt.Errorf("grpc client: failed to init chain: %w", err) + return nil, fmt.Errorf("grpc client: failed to init chain: %w", err) } - return resp.Msg.StateRoot, resp.Msg.MaxBytes, nil + return resp.Msg.StateRoot, nil } // GetTxs fetches available transactions from the execution layer's mempool. @@ -80,7 +80,7 @@ func (c *Client) GetTxs(ctx context.Context) ([][]byte, error) { // This method sends transactions to the execution service for processing and // returns the updated state root after execution. The execution service ensures // deterministic execution and validates the state transition. -func (c *Client) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, maxBytes uint64, err error) { +func (c *Client) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, err error) { req := connect.NewRequest(&pb.ExecuteTxsRequest{ Txs: txs, BlockHeight: blockHeight, @@ -90,10 +90,10 @@ func (c *Client) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint6 resp, err := c.client.ExecuteTxs(ctx, req) if err != nil { - return nil, 0, fmt.Errorf("grpc client: failed to execute txs: %w", err) + return nil, fmt.Errorf("grpc client: failed to execute txs: %w", err) } - return resp.Msg.UpdatedStateRoot, resp.Msg.MaxBytes, nil + return resp.Msg.UpdatedStateRoot, nil } // SetFinal marks a block as finalized at the specified height. diff --git a/execution/grpc/client_test.go b/execution/grpc/client_test.go index f09b3ed679..01195d89ac 100644 --- a/execution/grpc/client_test.go +++ b/execution/grpc/client_test.go @@ -9,17 +9,17 @@ import ( // mockExecutor is a mock implementation of execution.Executor for testing type mockExecutor struct { - initChainFunc func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) + initChainFunc func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) getTxsFunc func(ctx context.Context) ([][]byte, error) - executeTxsFunc func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) + executeTxsFunc func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) setFinalFunc func(ctx context.Context, blockHeight uint64) error } -func (m *mockExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { +func (m *mockExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { if m.initChainFunc != nil { return m.initChainFunc(ctx, genesisTime, initialHeight, chainID) } - return []byte("mock_state_root"), 1000000, nil + return []byte("mock_state_root"), nil } func (m *mockExecutor) GetTxs(ctx context.Context) ([][]byte, error) { @@ -29,11 +29,11 @@ func (m *mockExecutor) GetTxs(ctx context.Context) ([][]byte, error) { return [][]byte{[]byte("tx1"), []byte("tx2")}, nil } -func (m *mockExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) { +func (m *mockExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) { if m.executeTxsFunc != nil { return m.executeTxsFunc(ctx, txs, blockHeight, timestamp, prevStateRoot) } - return []byte("updated_state_root"), 1000000, nil + return []byte("updated_state_root"), nil } func (m *mockExecutor) SetFinal(ctx context.Context, blockHeight uint64) error { @@ -46,13 +46,12 @@ func (m *mockExecutor) SetFinal(ctx context.Context, blockHeight uint64) error { func TestClient_InitChain(t *testing.T) { ctx := context.Background() expectedStateRoot := []byte("test_state_root") - expectedMaxBytes := uint64(2000000) genesisTime := time.Now() initialHeight := uint64(1) chainID := "test-chain" mockExec := &mockExecutor{ - initChainFunc: func(ctx context.Context, gt time.Time, ih uint64, cid string) ([]byte, uint64, error) { + initChainFunc: func(ctx context.Context, gt time.Time, ih uint64, cid string) ([]byte, error) { if !gt.Equal(genesisTime) { t.Errorf("expected genesis time %v, got %v", genesisTime, gt) } @@ -62,7 +61,7 @@ func TestClient_InitChain(t *testing.T) { if cid != chainID { t.Errorf("expected chain ID %s, got %s", chainID, cid) } - return expectedStateRoot, expectedMaxBytes, nil + return expectedStateRoot, nil }, } @@ -75,7 +74,7 @@ func TestClient_InitChain(t *testing.T) { client := NewClient(server.URL) // Test InitChain - stateRoot, maxBytes, err := client.InitChain(ctx, genesisTime, initialHeight, chainID) + stateRoot, err := client.InitChain(ctx, genesisTime, initialHeight, chainID) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -83,9 +82,6 @@ func TestClient_InitChain(t *testing.T) { if string(stateRoot) != string(expectedStateRoot) { t.Errorf("expected state root %s, got %s", expectedStateRoot, stateRoot) } - if maxBytes != expectedMaxBytes { - t.Errorf("expected max bytes %d, got %d", expectedMaxBytes, maxBytes) - } } func TestClient_GetTxs(t *testing.T) { @@ -130,10 +126,9 @@ func TestClient_ExecuteTxs(t *testing.T) { timestamp := time.Now() prevStateRoot := []byte("prev_state_root") expectedStateRoot := []byte("new_state_root") - expectedMaxBytes := uint64(3000000) mockExec := &mockExecutor{ - executeTxsFunc: func(ctx context.Context, txsIn [][]byte, bh uint64, ts time.Time, psr []byte) ([]byte, uint64, error) { + executeTxsFunc: func(ctx context.Context, txsIn [][]byte, bh uint64, ts time.Time, psr []byte) ([]byte, error) { if len(txsIn) != len(txs) { t.Errorf("expected %d txs, got %d", len(txs), len(txsIn)) } @@ -146,7 +141,7 @@ func TestClient_ExecuteTxs(t *testing.T) { if string(psr) != string(prevStateRoot) { t.Errorf("expected prev state root %s, got %s", prevStateRoot, psr) } - return expectedStateRoot, expectedMaxBytes, nil + return expectedStateRoot, nil }, } @@ -159,7 +154,7 @@ func TestClient_ExecuteTxs(t *testing.T) { client := NewClient(server.URL) // Test ExecuteTxs - stateRoot, maxBytes, err := client.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) + stateRoot, err := client.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -167,9 +162,6 @@ func TestClient_ExecuteTxs(t *testing.T) { if string(stateRoot) != string(expectedStateRoot) { t.Errorf("expected state root %s, got %s", expectedStateRoot, stateRoot) } - if maxBytes != expectedMaxBytes { - t.Errorf("expected max bytes %d, got %d", expectedMaxBytes, maxBytes) - } } func TestClient_SetFinal(t *testing.T) { diff --git a/execution/grpc/go.mod b/execution/grpc/go.mod index 21f02396d2..acd20a4cb3 100644 --- a/execution/grpc/go.mod +++ b/execution/grpc/go.mod @@ -12,3 +12,5 @@ require ( ) require golang.org/x/text v0.31.0 // indirect + +replace github.com/evstack/ev-node/core => ../../core diff --git a/execution/grpc/server.go b/execution/grpc/server.go index b19836889e..c277db1907 100644 --- a/execution/grpc/server.go +++ b/execution/grpc/server.go @@ -50,7 +50,7 @@ func (s *Server) InitChain( return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("chain_id is required")) } - stateRoot, maxBytes, err := s.executor.InitChain( + stateRoot, err := s.executor.InitChain( ctx, req.Msg.GenesisTime.AsTime(), req.Msg.InitialHeight, @@ -62,7 +62,6 @@ func (s *Server) InitChain( return connect.NewResponse(&pb.InitChainResponse{ StateRoot: stateRoot, - MaxBytes: maxBytes, }), nil } @@ -103,7 +102,7 @@ func (s *Server) ExecuteTxs( return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("prev_state_root is required")) } - updatedStateRoot, maxBytes, err := s.executor.ExecuteTxs( + updatedStateRoot, err := s.executor.ExecuteTxs( ctx, req.Msg.Txs, req.Msg.BlockHeight, @@ -116,7 +115,6 @@ func (s *Server) ExecuteTxs( return connect.NewResponse(&pb.ExecuteTxsResponse{ UpdatedStateRoot: updatedStateRoot, - MaxBytes: maxBytes, }), nil } diff --git a/execution/grpc/server_test.go b/execution/grpc/server_test.go index da028fb2f5..e2a01b4bc4 100644 --- a/execution/grpc/server_test.go +++ b/execution/grpc/server_test.go @@ -18,12 +18,11 @@ func TestServer_InitChain(t *testing.T) { initialHeight := uint64(1) chainID := "test-chain" expectedStateRoot := []byte("state_root") - expectedMaxBytes := uint64(1000000) tests := []struct { name string req *pb.InitChainRequest - mockFunc func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) + mockFunc func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) wantErr bool wantCode connect.Code }{ @@ -34,8 +33,8 @@ func TestServer_InitChain(t *testing.T) { InitialHeight: initialHeight, ChainId: chainID, }, - mockFunc: func(ctx context.Context, gt time.Time, ih uint64, cid string) ([]byte, uint64, error) { - return expectedStateRoot, expectedMaxBytes, nil + mockFunc: func(ctx context.Context, gt time.Time, ih uint64, cid string) ([]byte, error) { + return expectedStateRoot, nil }, wantErr: false, }, @@ -73,8 +72,8 @@ func TestServer_InitChain(t *testing.T) { InitialHeight: initialHeight, ChainId: chainID, }, - mockFunc: func(ctx context.Context, gt time.Time, ih uint64, cid string) ([]byte, uint64, error) { - return nil, 0, errors.New("init chain failed") + mockFunc: func(ctx context.Context, gt time.Time, ih uint64, cid string) ([]byte, error) { + return nil, errors.New("init chain failed") }, wantErr: true, wantCode: connect.CodeInternal, @@ -113,9 +112,6 @@ func TestServer_InitChain(t *testing.T) { if string(resp.Msg.StateRoot) != string(expectedStateRoot) { t.Errorf("expected state root %s, got %s", expectedStateRoot, resp.Msg.StateRoot) } - if resp.Msg.MaxBytes != expectedMaxBytes { - t.Errorf("expected max bytes %d, got %d", expectedMaxBytes, resp.Msg.MaxBytes) - } }) } } @@ -179,11 +175,6 @@ func TestServer_GetTxs(t *testing.T) { if len(resp.Msg.Txs) != len(expectedTxs) { t.Fatalf("expected %d txs, got %d", len(expectedTxs), len(resp.Msg.Txs)) } - for i, tx := range resp.Msg.Txs { - if string(tx) != string(expectedTxs[i]) { - t.Errorf("tx %d: expected %s, got %s", i, expectedTxs[i], tx) - } - } }) } } @@ -195,12 +186,11 @@ func TestServer_ExecuteTxs(t *testing.T) { timestamp := time.Now() prevStateRoot := []byte("prev_state_root") expectedStateRoot := []byte("new_state_root") - expectedMaxBytes := uint64(2000000) tests := []struct { name string req *pb.ExecuteTxsRequest - mockFunc func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) + mockFunc func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) wantErr bool wantCode connect.Code }{ @@ -212,8 +202,8 @@ func TestServer_ExecuteTxs(t *testing.T) { Timestamp: timestamppb.New(timestamp), PrevStateRoot: prevStateRoot, }, - mockFunc: func(ctx context.Context, txs [][]byte, bh uint64, ts time.Time, psr []byte) ([]byte, uint64, error) { - return expectedStateRoot, expectedMaxBytes, nil + mockFunc: func(ctx context.Context, t [][]byte, bh uint64, ts time.Time, psr []byte) ([]byte, error) { + return expectedStateRoot, nil }, wantErr: false, }, @@ -255,8 +245,8 @@ func TestServer_ExecuteTxs(t *testing.T) { Timestamp: timestamppb.New(timestamp), PrevStateRoot: prevStateRoot, }, - mockFunc: func(ctx context.Context, txs [][]byte, bh uint64, ts time.Time, psr []byte) ([]byte, uint64, error) { - return nil, 0, errors.New("execute txs failed") + mockFunc: func(ctx context.Context, t [][]byte, bh uint64, ts time.Time, psr []byte) ([]byte, error) { + return nil, errors.New("execute txs failed") }, wantErr: true, wantCode: connect.CodeInternal, @@ -295,9 +285,6 @@ func TestServer_ExecuteTxs(t *testing.T) { if string(resp.Msg.UpdatedStateRoot) != string(expectedStateRoot) { t.Errorf("expected state root %s, got %s", expectedStateRoot, resp.Msg.UpdatedStateRoot) } - if resp.Msg.MaxBytes != expectedMaxBytes { - t.Errorf("expected max bytes %d, got %d", expectedMaxBytes, resp.Msg.MaxBytes) - } }) } } @@ -324,8 +311,10 @@ func TestServer_SetFinal(t *testing.T) { wantErr: false, }, { - name: "missing block height", - req: &pb.SetFinalRequest{}, + name: "missing block height", + req: &pb.SetFinalRequest{ + BlockHeight: 0, + }, wantErr: true, wantCode: connect.CodeInvalidArgument, }, diff --git a/node/execution_test.go b/node/execution_test.go index ea4e67cce6..336b36c126 100644 --- a/node/execution_test.go +++ b/node/execution_test.go @@ -35,27 +35,24 @@ func TestBasicExecutionFlow(t *testing.T) { // Define expected state and parameters expectedInitialStateRoot := []byte("initial state root") - expectedMaxBytes := uint64(1024) expectedNewStateRoot := []byte("new state root") blockHeight := uint64(1) chainID := "test-chain" // Set expectations on the mock executor mockExec.On("InitChain", mock.Anything, mock.AnythingOfType("time.Time"), blockHeight, chainID). - Return(expectedInitialStateRoot, expectedMaxBytes, nil).Once() + Return(expectedInitialStateRoot, nil).Once() mockExec.On("ExecuteTxs", mock.Anything, txs, blockHeight, mock.AnythingOfType("time.Time"), expectedInitialStateRoot). - Return(expectedNewStateRoot, expectedMaxBytes, nil).Once() + Return(expectedNewStateRoot, nil).Once() mockExec.On("SetFinal", mock.Anything, blockHeight). Return(nil).Once() // Call helper functions with the mock executor - stateRoot, maxBytes := initializeChain(t, mockExec, t.Context()) + stateRoot := initializeChain(t, mockExec, t.Context()) require.Equal(expectedInitialStateRoot, stateRoot) - require.Equal(expectedMaxBytes, maxBytes) - newStateRoot, newMaxBytes := executeTransactions(t, mockExec, t.Context(), txs, stateRoot, maxBytes) + newStateRoot := executeTransactions(t, mockExec, t.Context(), txs, stateRoot) require.Equal(expectedNewStateRoot, newStateRoot) - require.Equal(expectedMaxBytes, newMaxBytes) finalizeExecution(t, mockExec, t.Context()) @@ -98,23 +95,21 @@ func getTransactions(t *testing.T, executor coreexecutor.Executor, ctx context.C return txs } -func initializeChain(t *testing.T, executor coreexecutor.Executor, ctx context.Context) ([]byte, uint64) { +func initializeChain(t *testing.T, executor coreexecutor.Executor, ctx context.Context) []byte { genesisTime := time.Now() initialHeight := uint64(1) chainID := "test-chain" - stateRoot, maxBytes, err := executor.InitChain(ctx, genesisTime, initialHeight, chainID) + stateRoot, err := executor.InitChain(ctx, genesisTime, initialHeight, chainID) require.NoError(t, err) - require.Greater(t, maxBytes, uint64(0)) - return stateRoot, maxBytes + return stateRoot } -func executeTransactions(t *testing.T, executor coreexecutor.Executor, ctx context.Context, txs [][]byte, stateRoot types.Hash, maxBytes uint64) ([]byte, uint64) { +func executeTransactions(t *testing.T, executor coreexecutor.Executor, ctx context.Context, txs [][]byte, stateRoot types.Hash) []byte { blockHeight := uint64(1) timestamp := time.Now() - newStateRoot, newMaxBytes, err := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, stateRoot) + newStateRoot, err := executor.ExecuteTxs(ctx, txs, blockHeight, timestamp, stateRoot) require.NoError(t, err) - require.Greater(t, newMaxBytes, uint64(0)) - return newStateRoot, newMaxBytes + return newStateRoot } func finalizeExecution(t *testing.T, executor coreexecutor.Executor, ctx context.Context) { diff --git a/pkg/sequencers/single/sequencer.go b/pkg/sequencers/single/sequencer.go index 07c32c835e..7960c24dce 100644 --- a/pkg/sequencers/single/sequencer.go +++ b/pkg/sequencers/single/sequencer.go @@ -15,6 +15,7 @@ import ( "github.com/rs/zerolog" "github.com/evstack/ev-node/block" + "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/pkg/config" datypes "github.com/evstack/ev-node/pkg/da/types" @@ -50,6 +51,11 @@ type Sequencer struct { // Cached forced inclusion transactions from the current epoch cachedForcedInclusionTxs [][]byte + + // DA transaction filtering support (optional) + // When set, forced inclusion transactions are filtered by gas limit + txFilter execution.DATransactionFilter + infoProvider execution.ExecutionInfoProvider } // NewSequencer creates a new Single Sequencer @@ -116,6 +122,15 @@ func NewSequencer( return s, nil } +// SetDATransactionFilter sets the optional DA transaction filter and execution info provider. +// When set, forced inclusion transactions will be filtered by gas limit before being included in batches. +// This should be called after NewSequencer and before Start if filtering is desired. +func (c *Sequencer) SetDATransactionFilter(filter execution.DATransactionFilter, infoProvider execution.ExecutionInfoProvider) { + c.txFilter = filter + c.infoProvider = infoProvider + c.logger.Info().Msg("DA transaction filter configured for gas-based filtering") +} + // getInitialDAStartHeight retrieves the DA height of the first included chain height from store. func (c *Sequencer) getInitialDAStartHeight(ctx context.Context) uint64 { if daStartHeight := c.daStartHeight.Load(); daStartHeight != 0 { @@ -201,18 +216,66 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB // Process forced inclusion transactions from checkpoint position forcedTxs := c.processForcedInclusionTxsFromCheckpoint(req.MaxBytes) + // Apply gas-based filtering if filter is configured + var filteredForcedTxs [][]byte + var remainingGasFilteredTxs [][]byte + if c.txFilter != nil && c.infoProvider != nil && len(forcedTxs) > 0 { + // Get current gas limit from execution layer + info, err := c.infoProvider.GetExecutionInfo(ctx, 0) // 0 = latest/next block + if err != nil { + c.logger.Warn().Err(err).Msg("failed to get execution info for gas filtering, proceeding without gas filter") + filteredForcedTxs = forcedTxs + } else if info.MaxGas > 0 { + // Filter by gas limit + filteredForcedTxs, remainingGasFilteredTxs, err = c.txFilter.FilterDATransactions(ctx, forcedTxs, info.MaxGas) + if err != nil { + c.logger.Warn().Err(err).Msg("failed to filter DA transactions by gas, proceeding without gas filter") + filteredForcedTxs = forcedTxs + } else { + c.logger.Debug(). + Int("input_forced_txs", len(forcedTxs)). + Int("filtered_forced_txs", len(filteredForcedTxs)). + Int("remaining_for_next_block", len(remainingGasFilteredTxs)). + Uint64("max_gas", info.MaxGas). + Msg("filtered forced inclusion transactions by gas limit") + } + } else { + // MaxGas is 0, no gas-based filtering + filteredForcedTxs = forcedTxs + } + } else { + filteredForcedTxs = forcedTxs + } + // Calculate size used by forced inclusion transactions forcedTxsSize := 0 - for _, tx := range forcedTxs { + for _, tx := range filteredForcedTxs { forcedTxsSize += len(tx) } // Update checkpoint after consuming forced inclusion transactions + // Only advance checkpoint by the number of txs actually included (after gas filtering) + // Note: gibberish txs filtered by FilterDATransactions are permanently skipped, + // but gas-filtered valid txs (remainingGasFilteredTxs) stay in cache for next block if daHeight > 0 || len(forcedTxs) > 0 { - c.checkpoint.TxIndex += uint64(len(forcedTxs)) + // Count how many txs we're actually consuming from the checkpoint + // This is: filteredForcedTxs + (original forcedTxs - filteredForcedTxs - remainingGasFilteredTxs) + // The difference (original - filtered - remaining) represents gibberish that was filtered out + txsConsumed := uint64(len(forcedTxs) - len(remainingGasFilteredTxs)) + c.checkpoint.TxIndex += txsConsumed + + // If we have remaining gas-filtered txs, don't advance to next epoch yet + // These will be picked up in the next GetNextBatch call + if len(remainingGasFilteredTxs) > 0 { + // Update cached txs to only contain the remaining ones + c.cachedForcedInclusionTxs = remainingGasFilteredTxs + c.checkpoint.TxIndex = 0 // Reset index since we're replacing the cache - // If we've consumed all transactions from this DA epoch, move to next - if c.checkpoint.TxIndex >= uint64(len(c.cachedForcedInclusionTxs)) { + c.logger.Debug(). + Int("remaining_txs", len(remainingGasFilteredTxs)). + Msg("keeping gas-filtered transactions for next block") + } else if c.checkpoint.TxIndex >= uint64(len(c.cachedForcedInclusionTxs)) { + // If we've consumed all transactions from this DA epoch, move to next c.checkpoint.DAHeight = daHeight + 1 c.checkpoint.TxIndex = 0 c.cachedForcedInclusionTxs = nil @@ -227,12 +290,15 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB } c.logger.Debug(). - Int("forced_tx_count", len(forcedTxs)). + Int("forced_tx_count", len(filteredForcedTxs)). Uint64("checkpoint_da_height", c.checkpoint.DAHeight). Uint64("checkpoint_tx_index", c.checkpoint.TxIndex). Msg("processed forced inclusion transactions and updated checkpoint") } + // Use filtered forced txs for the rest of the function + forcedTxs = filteredForcedTxs + batch, err := c.queue.Next(ctx) if err != nil { return nil, err diff --git a/pkg/sequencers/single/sequencer_test.go b/pkg/sequencers/single/sequencer_test.go index b73d49bfd6..8273461b93 100644 --- a/pkg/sequencers/single/sequencer_test.go +++ b/pkg/sequencers/single/sequencer_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/evstack/ev-node/core/execution" ds "github.com/ipfs/go-datastore" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -1008,3 +1009,195 @@ func TestSequencer_GetNextBatch_EmptyDABatch_IncreasesDAHeight(t *testing.T) { assert.Equal(t, uint64(102), seq.checkpoint.DAHeight) assert.Equal(t, uint64(0), seq.checkpoint.TxIndex) } + +// mockDATransactionFilter is a mock implementation of execution.DATransactionFilter +type mockDATransactionFilter struct { + filterFunc func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) +} + +func (m *mockDATransactionFilter) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + if m.filterFunc != nil { + return m.filterFunc(ctx, txs, maxGas) + } + return txs, nil, nil +} + +// mockExecutionInfoProvider is a mock implementation of execution.ExecutionInfoProvider +type mockExecutionInfoProvider struct { + maxGas uint64 + err error +} + +func (m *mockExecutionInfoProvider) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + return execution.ExecutionInfo{MaxGas: m.maxGas}, m.err +} + +func TestSequencer_GetNextBatch_WithGasFiltering(t *testing.T) { + db := ds.NewMapDatastore() + logger := zerolog.Nop() + + gen := genesis.Genesis{ + ChainID: "test-gas-filter", + DAStartHeight: 100, + DAEpochForcedInclusion: 1, + } + + mockDA := newMockFullDAClient(t) + + // Setup DA to return forced inclusion transactions + forcedTxs := [][]byte{ + []byte("tx1-low-gas"), // Will fit + []byte("tx2-low-gas"), // Will fit + []byte("tx3-high-gas"), // Won't fit due to gas + } + + mockDA.MockClient.On("GetBlobs", mock.Anything, mock.Anything, mock.Anything). + Return(forcedTxs, nil).Maybe() + mockDA.MockClient.On("GetBlobsAtHeight", mock.Anything, mock.Anything, mock.Anything). + Return(forcedTxs, nil).Maybe() + mockDA.MockClient.On("HasForcedInclusionNamespace").Return(true).Maybe() + mockDA.MockClient.On("GetForcedInclusionNamespace").Return([]byte("forced")).Maybe() + mockDA.MockClient.On("MaxBlobSize", mock.Anything).Return(uint64(1000000), nil).Maybe() + + seq, err := NewSequencer( + logger, + db, + mockDA, + config.DefaultConfig(), + []byte("test-gas-filter"), + 1000, + gen, + ) + require.NoError(t, err) + + // Configure the gas filter mock + filterCallCount := 0 + mockFilter := &mockDATransactionFilter{ + filterFunc: func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + filterCallCount++ + // Simulate: first 2 txs fit, third one doesn't + if len(txs) >= 3 { + return txs[:2], txs[2:], nil + } + return txs, nil, nil + }, + } + + mockInfoProvider := &mockExecutionInfoProvider{ + maxGas: 1000000, // 1M gas limit + } + + // Set the filter + seq.SetDATransactionFilter(mockFilter, mockInfoProvider) + + // Manually set up cached forced txs to simulate DA fetch + seq.cachedForcedInclusionTxs = forcedTxs + seq.checkpoint.DAHeight = 100 + seq.checkpoint.TxIndex = 0 + seq.SetDAHeight(100) + + ctx := context.Background() + req := coresequencer.GetNextBatchRequest{ + Id: []byte("test-gas-filter"), + MaxBytes: 1000000, + } + + // First call should return filtered txs (only first 2) + resp, err := seq.GetNextBatch(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Batch) + + // Should have 2 forced txs (the ones that fit within gas limit) + assert.Equal(t, 2, len(resp.Batch.Transactions)) + assert.True(t, resp.Batch.ForceIncludedMask[0]) + assert.True(t, resp.Batch.ForceIncludedMask[1]) + + // The remaining tx should be cached for next block + assert.Equal(t, 1, len(seq.cachedForcedInclusionTxs)) + assert.Equal(t, uint64(0), seq.checkpoint.TxIndex) // Reset because we replaced the cache + + // Filter should have been called + assert.Equal(t, 1, filterCallCount) + + // Second call should return the remaining tx + mockFilter.filterFunc = func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + filterCallCount++ + // Now all remaining txs fit + return txs, nil, nil + } + + resp, err = seq.GetNextBatch(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Batch) + + // Should have the remaining forced tx + assert.Equal(t, 1, len(resp.Batch.Transactions)) + assert.True(t, resp.Batch.ForceIncludedMask[0]) + + // Cache should now be empty, DA height should advance + assert.Nil(t, seq.cachedForcedInclusionTxs) + assert.Equal(t, uint64(101), seq.checkpoint.DAHeight) +} + +func TestSequencer_GetNextBatch_GasFilterError(t *testing.T) { + db := ds.NewMapDatastore() + logger := zerolog.Nop() + + gen := genesis.Genesis{ + ChainID: "test-gas-error", + DAStartHeight: 100, + DAEpochForcedInclusion: 1, + } + + mockDA := newMockFullDAClient(t) + mockDA.MockClient.On("HasForcedInclusionNamespace").Return(true).Maybe() + mockDA.MockClient.On("GetForcedInclusionNamespace").Return([]byte("forced")).Maybe() + mockDA.MockClient.On("MaxBlobSize", mock.Anything).Return(uint64(1000000), nil).Maybe() + + seq, err := NewSequencer( + logger, + db, + mockDA, + config.DefaultConfig(), + []byte("test-gas-error"), + 1000, + gen, + ) + require.NoError(t, err) + + // Configure filter that returns error + mockFilter := &mockDATransactionFilter{ + filterFunc: func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + return nil, nil, errors.New("filter error") + }, + } + + mockInfoProvider := &mockExecutionInfoProvider{ + maxGas: 1000000, + } + + seq.SetDATransactionFilter(mockFilter, mockInfoProvider) + + // Set up cached txs + forcedTxs := [][]byte{[]byte("tx1"), []byte("tx2")} + seq.cachedForcedInclusionTxs = forcedTxs + seq.checkpoint.DAHeight = 100 + seq.checkpoint.TxIndex = 0 + seq.SetDAHeight(100) + + ctx := context.Background() + req := coresequencer.GetNextBatchRequest{ + Id: []byte("test-gas-error"), + MaxBytes: 1000000, + } + + // Should succeed but fall back to unfiltered txs on error + resp, err := seq.GetNextBatch(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + + // Should have all txs (filter error means no filtering) + assert.Equal(t, 2, len(resp.Batch.Transactions)) +} diff --git a/pkg/telemetry/executor_tracing.go b/pkg/telemetry/executor_tracing.go index ef19b430d1..b408ee1d56 100644 --- a/pkg/telemetry/executor_tracing.go +++ b/pkg/telemetry/executor_tracing.go @@ -25,7 +25,7 @@ func WithTracingExecutor(inner coreexec.Executor) coreexec.Executor { } } -func (t *tracedExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { +func (t *tracedExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { ctx, span := t.tracer.Start(ctx, "Executor.InitChain", trace.WithAttributes( attribute.String("chain.id", chainID), @@ -35,12 +35,12 @@ func (t *tracedExecutor) InitChain(ctx context.Context, genesisTime time.Time, i ) defer span.End() - stateRoot, maxBytes, err := t.inner.InitChain(ctx, genesisTime, initialHeight, chainID) + stateRoot, err := t.inner.InitChain(ctx, genesisTime, initialHeight, chainID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } - return stateRoot, maxBytes, err + return stateRoot, err } func (t *tracedExecutor) GetTxs(ctx context.Context) ([][]byte, error) { @@ -57,7 +57,7 @@ func (t *tracedExecutor) GetTxs(ctx context.Context) ([][]byte, error) { return txs, err } -func (t *tracedExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) { +func (t *tracedExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) { ctx, span := t.tracer.Start(ctx, "Executor.ExecuteTxs", trace.WithAttributes( attribute.Int("tx.count", len(txs)), @@ -67,12 +67,12 @@ func (t *tracedExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeig ) defer span.End() - stateRoot, maxBytes, err := t.inner.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) + stateRoot, err := t.inner.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } - return stateRoot, maxBytes, err + return stateRoot, err } func (t *tracedExecutor) SetFinal(ctx context.Context, blockHeight uint64) error { @@ -105,3 +105,52 @@ func (t *tracedExecutor) GetLatestHeight(ctx context.Context) (uint64, error) { } return height, err } + +// If the underlying executor implements ExecutionInfoProvider, forward it while tracing. +func (t *tracedExecutor) GetExecutionInfo(ctx context.Context, height uint64) (coreexec.ExecutionInfo, error) { + eip, ok := t.inner.(coreexec.ExecutionInfoProvider) + if !ok { + return coreexec.ExecutionInfo{}, nil + } + ctx, span := t.tracer.Start(ctx, "Executor.GetExecutionInfo", + trace.WithAttributes(attribute.Int64("height", int64(height))), + ) + defer span.End() + + info, err := eip.GetExecutionInfo(ctx, height) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } else { + span.SetAttributes(attribute.Int64("max_gas", int64(info.MaxGas))) + } + return info, err +} + +// If the underlying executor implements DATransactionFilter, forward it while tracing. +func (t *tracedExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + filter, ok := t.inner.(coreexec.DATransactionFilter) + if !ok { + // If not implemented, return all transactions as valid (no filtering) + return txs, nil, nil + } + ctx, span := t.tracer.Start(ctx, "Executor.FilterDATransactions", + trace.WithAttributes( + attribute.Int("input_tx_count", len(txs)), + attribute.Int64("max_gas", int64(maxGas)), + ), + ) + defer span.End() + + validTxs, remainingTxs, err := filter.FilterDATransactions(ctx, txs, maxGas) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } else { + span.SetAttributes( + attribute.Int("valid_tx_count", len(validTxs)), + attribute.Int("remaining_tx_count", len(remainingTxs)), + ) + } + return validTxs, remainingTxs, err +} diff --git a/pkg/telemetry/executor_tracing_test.go b/pkg/telemetry/executor_tracing_test.go index 9a79ba2f62..d3698ccc80 100644 --- a/pkg/telemetry/executor_tracing_test.go +++ b/pkg/telemetry/executor_tracing_test.go @@ -49,17 +49,15 @@ func TestWithTracingExecutor_InitChain_Success(t *testing.T) { initialHeight := uint64(1) chainID := "test-chain" expectedStateRoot := []byte("state-root") - expectedMaxBytes := uint64(1000000) mockExec.EXPECT(). InitChain(mock.Anything, genesisTime, initialHeight, chainID). - Return(expectedStateRoot, expectedMaxBytes, nil) + Return(expectedStateRoot, nil) - stateRoot, maxBytes, err := traced.InitChain(ctx, genesisTime, initialHeight, chainID) + stateRoot, err := traced.InitChain(ctx, genesisTime, initialHeight, chainID) require.NoError(t, err) require.Equal(t, expectedStateRoot, stateRoot) - require.Equal(t, expectedMaxBytes, maxBytes) // verify span was created spans := sr.Ended() @@ -89,9 +87,9 @@ func TestWithTracingExecutor_InitChain_Error(t *testing.T) { mockExec.EXPECT(). InitChain(mock.Anything, genesisTime, initialHeight, chainID). - Return(nil, uint64(0), expectedErr) + Return(nil, expectedErr) - _, _, err := traced.InitChain(ctx, genesisTime, initialHeight, chainID) + _, err := traced.InitChain(ctx, genesisTime, initialHeight, chainID) require.Error(t, err) require.Equal(t, expectedErr, err) @@ -179,18 +177,16 @@ func TestWithTracingExecutor_ExecuteTxs_Success(t *testing.T) { blockHeight := uint64(100) timestamp := time.Now() prevStateRoot := []byte("prev-state") - expectedStateRoot := []byte("new-state") - expectedMaxBytes := uint64(1000000) + expectedStateRoot := []byte("new-state-root") mockExec.EXPECT(). ExecuteTxs(mock.Anything, txs, blockHeight, timestamp, prevStateRoot). - Return(expectedStateRoot, expectedMaxBytes, nil) + Return(expectedStateRoot, nil) - stateRoot, maxBytes, err := traced.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) + stateRoot, err := traced.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) require.NoError(t, err) require.Equal(t, expectedStateRoot, stateRoot) - require.Equal(t, expectedMaxBytes, maxBytes) // verify span spans := sr.Ended() @@ -220,9 +216,9 @@ func TestWithTracingExecutor_ExecuteTxs_Error(t *testing.T) { mockExec.EXPECT(). ExecuteTxs(mock.Anything, txs, blockHeight, timestamp, prevStateRoot). - Return(nil, uint64(0), expectedErr) + Return(nil, expectedErr) - _, _, err := traced.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) + _, err := traced.ExecuteTxs(ctx, txs, blockHeight, timestamp, prevStateRoot) require.Error(t, err) require.Equal(t, expectedErr, err) diff --git a/test/mocks/execution.go b/test/mocks/execution.go index 08ab86389a..25ef0e8d4d 100644 --- a/test/mocks/execution.go +++ b/test/mocks/execution.go @@ -39,7 +39,7 @@ func (_m *MockExecutor) EXPECT() *MockExecutor_Expecter { } // ExecuteTxs provides a mock function for the type MockExecutor -func (_mock *MockExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) { +func (_mock *MockExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) { ret := _mock.Called(ctx, txs, blockHeight, timestamp, prevStateRoot) if len(ret) == 0 { @@ -47,9 +47,8 @@ func (_mock *MockExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHe } var r0 []byte - var r1 uint64 - var r2 error - if returnFunc, ok := ret.Get(0).(func(context.Context, [][]byte, uint64, time.Time, []byte) ([]byte, uint64, error)); ok { + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, [][]byte, uint64, time.Time, []byte) ([]byte, error)); ok { return returnFunc(ctx, txs, blockHeight, timestamp, prevStateRoot) } if returnFunc, ok := ret.Get(0).(func(context.Context, [][]byte, uint64, time.Time, []byte) []byte); ok { @@ -59,17 +58,12 @@ func (_mock *MockExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHe r0 = ret.Get(0).([]byte) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, [][]byte, uint64, time.Time, []byte) uint64); ok { + if returnFunc, ok := ret.Get(1).(func(context.Context, [][]byte, uint64, time.Time, []byte) error); ok { r1 = returnFunc(ctx, txs, blockHeight, timestamp, prevStateRoot) } else { - r1 = ret.Get(1).(uint64) - } - if returnFunc, ok := ret.Get(2).(func(context.Context, [][]byte, uint64, time.Time, []byte) error); ok { - r2 = returnFunc(ctx, txs, blockHeight, timestamp, prevStateRoot) - } else { - r2 = ret.Error(2) + r1 = ret.Error(1) } - return r0, r1, r2 + return r0, r1 } // MockExecutor_ExecuteTxs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecuteTxs' @@ -120,12 +114,12 @@ func (_c *MockExecutor_ExecuteTxs_Call) Run(run func(ctx context.Context, txs [] return _c } -func (_c *MockExecutor_ExecuteTxs_Call) Return(updatedStateRoot []byte, maxBytes uint64, err error) *MockExecutor_ExecuteTxs_Call { - _c.Call.Return(updatedStateRoot, maxBytes, err) +func (_c *MockExecutor_ExecuteTxs_Call) Return(updatedStateRoot []byte, err error) *MockExecutor_ExecuteTxs_Call { + _c.Call.Return(updatedStateRoot, err) return _c } -func (_c *MockExecutor_ExecuteTxs_Call) RunAndReturn(run func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error)) *MockExecutor_ExecuteTxs_Call { +func (_c *MockExecutor_ExecuteTxs_Call) RunAndReturn(run func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error)) *MockExecutor_ExecuteTxs_Call { _c.Call.Return(run) return _c } @@ -193,7 +187,7 @@ func (_c *MockExecutor_GetTxs_Call) RunAndReturn(run func(ctx context.Context) ( } // InitChain provides a mock function for the type MockExecutor -func (_mock *MockExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { +func (_mock *MockExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { ret := _mock.Called(ctx, genesisTime, initialHeight, chainID) if len(ret) == 0 { @@ -201,9 +195,8 @@ func (_mock *MockExecutor) InitChain(ctx context.Context, genesisTime time.Time, } var r0 []byte - var r1 uint64 - var r2 error - if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time, uint64, string) ([]byte, uint64, error)); ok { + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time, uint64, string) ([]byte, error)); ok { return returnFunc(ctx, genesisTime, initialHeight, chainID) } if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time, uint64, string) []byte); ok { @@ -213,17 +206,12 @@ func (_mock *MockExecutor) InitChain(ctx context.Context, genesisTime time.Time, r0 = ret.Get(0).([]byte) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, time.Time, uint64, string) uint64); ok { + if returnFunc, ok := ret.Get(1).(func(context.Context, time.Time, uint64, string) error); ok { r1 = returnFunc(ctx, genesisTime, initialHeight, chainID) } else { - r1 = ret.Get(1).(uint64) - } - if returnFunc, ok := ret.Get(2).(func(context.Context, time.Time, uint64, string) error); ok { - r2 = returnFunc(ctx, genesisTime, initialHeight, chainID) - } else { - r2 = ret.Error(2) + r1 = ret.Error(1) } - return r0, r1, r2 + return r0, r1 } // MockExecutor_InitChain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InitChain' @@ -268,12 +256,12 @@ func (_c *MockExecutor_InitChain_Call) Run(run func(ctx context.Context, genesis return _c } -func (_c *MockExecutor_InitChain_Call) Return(stateRoot []byte, maxBytes uint64, err error) *MockExecutor_InitChain_Call { - _c.Call.Return(stateRoot, maxBytes, err) +func (_c *MockExecutor_InitChain_Call) Return(stateRoot []byte, err error) *MockExecutor_InitChain_Call { + _c.Call.Return(stateRoot, err) return _c } -func (_c *MockExecutor_InitChain_Call) RunAndReturn(run func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error)) *MockExecutor_InitChain_Call { +func (_c *MockExecutor_InitChain_Call) RunAndReturn(run func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error)) *MockExecutor_InitChain_Call { _c.Call.Return(run) return _c } diff --git a/test/mocks/height_aware_executor.go b/test/mocks/height_aware_executor.go index 8d8e7d60aa..8e22487cc0 100644 --- a/test/mocks/height_aware_executor.go +++ b/test/mocks/height_aware_executor.go @@ -30,9 +30,9 @@ type MockHeightAwareExecutor struct { } // InitChain implements the Executor interface. -func (m *MockHeightAwareExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, uint64, error) { +func (m *MockHeightAwareExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { args := m.Called(ctx, genesisTime, initialHeight, chainID) - return args.Get(0).([]byte), args.Get(1).(uint64), args.Error(2) + return args.Get(0).([]byte), args.Error(1) } // GetTxs implements the Executor interface. @@ -42,9 +42,9 @@ func (m *MockHeightAwareExecutor) GetTxs(ctx context.Context) ([][]byte, error) } // ExecuteTxs implements the Executor interface. -func (m *MockHeightAwareExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, uint64, error) { +func (m *MockHeightAwareExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) { args := m.Called(ctx, txs, blockHeight, timestamp, prevStateRoot) - return args.Get(0).([]byte), args.Get(1).(uint64), args.Error(2) + return args.Get(0).([]byte), args.Error(1) } // SetFinal implements the Executor interface. From 77bc171d8e364efccece074c2dadeac022568068 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 14:33:19 +0100 Subject: [PATCH 2/3] updates --- apps/evm/cmd/run.go | 9 +- apps/testapp/kv/kvexecutor.go | 28 ++ core/execution/execution.go | 16 + execution/grpc/client.go | 38 +++ execution/grpc/server.go | 36 +++ pkg/sequencers/single/sequencer.go | 23 +- pkg/sequencers/single/sequencer_test.go | 73 ++--- pkg/telemetry/executor_tracing.go | 17 +- proto/evnode/v1/execution.proto | 37 +++ test/mocks/execution.go | 149 ++++++++++ test/mocks/height_aware_executor.go | 20 ++ types/pb/evnode/v1/execution.pb.go | 279 ++++++++++++++++-- .../evnode/v1/v1connect/execution.connect.go | 70 ++++- 13 files changed, 697 insertions(+), 98 deletions(-) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index 26a6752113..21d3be548d 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -194,13 +194,8 @@ func createSequencer( return nil, fmt.Errorf("failed to create single sequencer: %w", err) } - // Configure DA transaction filter if executor supports it - if filter, ok := executor.(execution.DATransactionFilter); ok { - if infoProvider, ok := executor.(execution.ExecutionInfoProvider); ok { - sequencer.SetDATransactionFilter(filter, infoProvider) - logger.Info().Msg("DA transaction filter configured for gas-based filtering") - } - } + // Configure executor for DA transaction gas-based filtering + sequencer.SetExecutor(executor) logger.Info(). Str("forced_inclusion_namespace", nodeConfig.DA.GetForcedInclusionNamespace()). diff --git a/apps/testapp/kv/kvexecutor.go b/apps/testapp/kv/kvexecutor.go index 7ddf4cf588..b2ae9ae3ea 100644 --- a/apps/testapp/kv/kvexecutor.go +++ b/apps/testapp/kv/kvexecutor.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/evstack/ev-node/core/execution" "github.com/evstack/ev-node/pkg/store" ds "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/query" @@ -422,3 +423,30 @@ func (k *KVExecutor) Rollback(ctx context.Context, height uint64) error { func getTxKey(height uint64, txKey string) ds.Key { return heightKeyPrefix.Child(ds.NewKey(fmt.Sprintf("%d/%s", height, txKey))) } + +// GetExecutionInfo returns execution layer parameters. +// For KVExecutor, returns MaxGas=0 indicating no gas-based filtering. +func (k *KVExecutor) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + return execution.ExecutionInfo{MaxGas: 0}, nil +} + +// FilterDATransactions validates and filters force-included transactions from DA. +// For KVExecutor, all transactions are considered valid (no gas-based filtering). +// Invalid transactions (not in key=value format) are filtered out. +func (k *KVExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + // KVExecutor doesn't do gas filtering but does basic validation + validTxs := make([][]byte, 0, len(txs)) + for _, tx := range txs { + if len(tx) == 0 { + continue // Skip empty transactions + } + // Basic format validation: must be key=value + parts := strings.SplitN(string(tx), "=", 2) + if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" { + continue // Filter out malformed transactions + } + validTxs = append(validTxs, tx) + } + // No gas-based filtering, so no remaining transactions + return validTxs, nil, nil +} diff --git a/core/execution/execution.go b/core/execution/execution.go index 917e505161..6d7c492aff 100644 --- a/core/execution/execution.go +++ b/core/execution/execution.go @@ -142,3 +142,19 @@ type HeightProvider interface { // - error: Any errors during height retrieval GetLatestHeight(ctx context.Context) (uint64, error) } + +// ExecutionInfoProvider is an interface for components that can provide execution layer parameters. +// This is useful for type assertions when an Executor implementation supports gas-based filtering. +type ExecutionInfoProvider interface { + // GetExecutionInfo returns current execution layer parameters. + // See Executor.GetExecutionInfo for full documentation. + GetExecutionInfo(ctx context.Context, height uint64) (ExecutionInfo, error) +} + +// DATransactionFilter is an interface for components that can filter DA transactions. +// This is useful for type assertions when an Executor implementation supports gas-based filtering. +type DATransactionFilter interface { + // FilterDATransactions validates and filters force-included transactions from DA. + // See Executor.FilterDATransactions for full documentation. + FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) (validTxs [][]byte, remainingTxs [][]byte, err error) +} diff --git a/execution/grpc/client.go b/execution/grpc/client.go index b2e4421cce..dd3d4c1bb9 100644 --- a/execution/grpc/client.go +++ b/execution/grpc/client.go @@ -112,3 +112,41 @@ func (c *Client) SetFinal(ctx context.Context, blockHeight uint64) error { return nil } + +// GetExecutionInfo returns current execution layer parameters. +// +// This method retrieves execution parameters such as the block gas limit +// from the remote execution service. +func (c *Client) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + req := connect.NewRequest(&pb.GetExecutionInfoRequest{ + Height: height, + }) + + resp, err := c.client.GetExecutionInfo(ctx, req) + if err != nil { + return execution.ExecutionInfo{}, fmt.Errorf("grpc client: failed to get execution info: %w", err) + } + + return execution.ExecutionInfo{ + MaxGas: resp.Msg.MaxGas, + }, nil +} + +// FilterDATransactions validates and filters force-included transactions from DA. +// +// This method sends DA transactions to the remote execution service for validation +// and gas-based filtering. It returns transactions that are valid and fit within +// the gas limit, plus any remaining valid transactions for re-queuing. +func (c *Client) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + req := connect.NewRequest(&pb.FilterDATransactionsRequest{ + Txs: txs, + MaxGas: maxGas, + }) + + resp, err := c.client.FilterDATransactions(ctx, req) + if err != nil { + return nil, nil, fmt.Errorf("grpc client: failed to filter DA transactions: %w", err) + } + + return resp.Msg.ValidTxs, resp.Msg.RemainingTxs, nil +} diff --git a/execution/grpc/server.go b/execution/grpc/server.go index c277db1907..276c8fde08 100644 --- a/execution/grpc/server.go +++ b/execution/grpc/server.go @@ -136,3 +136,39 @@ func (s *Server) SetFinal( return connect.NewResponse(&pb.SetFinalResponse{}), nil } + +// GetExecutionInfo handles the GetExecutionInfo RPC request. +// +// It returns current execution layer parameters such as the block gas limit. +func (s *Server) GetExecutionInfo( + ctx context.Context, + req *connect.Request[pb.GetExecutionInfoRequest], +) (*connect.Response[pb.GetExecutionInfoResponse], error) { + info, err := s.executor.GetExecutionInfo(ctx, req.Msg.Height) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get execution info: %w", err)) + } + + return connect.NewResponse(&pb.GetExecutionInfoResponse{ + MaxGas: info.MaxGas, + }), nil +} + +// FilterDATransactions handles the FilterDATransactions RPC request. +// +// It validates and filters force-included transactions from DA, returning +// transactions that are valid and fit within the gas limit. +func (s *Server) FilterDATransactions( + ctx context.Context, + req *connect.Request[pb.FilterDATransactionsRequest], +) (*connect.Response[pb.FilterDATransactionsResponse], error) { + validTxs, remainingTxs, err := s.executor.FilterDATransactions(ctx, req.Msg.Txs, req.Msg.MaxGas) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to filter DA transactions: %w", err)) + } + + return connect.NewResponse(&pb.FilterDATransactionsResponse{ + ValidTxs: validTxs, + RemainingTxs: remainingTxs, + }), nil +} diff --git a/pkg/sequencers/single/sequencer.go b/pkg/sequencers/single/sequencer.go index 7960c24dce..3e40d9e235 100644 --- a/pkg/sequencers/single/sequencer.go +++ b/pkg/sequencers/single/sequencer.go @@ -52,10 +52,10 @@ type Sequencer struct { // Cached forced inclusion transactions from the current epoch cachedForcedInclusionTxs [][]byte - // DA transaction filtering support (optional) + // Executor for DA transaction filtering (optional) // When set, forced inclusion transactions are filtered by gas limit - txFilter execution.DATransactionFilter - infoProvider execution.ExecutionInfoProvider + // using the executor's GetExecutionInfo and FilterDATransactions methods + executor execution.Executor } // NewSequencer creates a new Single Sequencer @@ -122,13 +122,12 @@ func NewSequencer( return s, nil } -// SetDATransactionFilter sets the optional DA transaction filter and execution info provider. +// SetExecutor sets the optional executor for DA transaction filtering. // When set, forced inclusion transactions will be filtered by gas limit before being included in batches. // This should be called after NewSequencer and before Start if filtering is desired. -func (c *Sequencer) SetDATransactionFilter(filter execution.DATransactionFilter, infoProvider execution.ExecutionInfoProvider) { - c.txFilter = filter - c.infoProvider = infoProvider - c.logger.Info().Msg("DA transaction filter configured for gas-based filtering") +func (c *Sequencer) SetExecutor(executor execution.Executor) { + c.executor = executor + c.logger.Info().Msg("Executor configured for DA transaction gas-based filtering") } // getInitialDAStartHeight retrieves the DA height of the first included chain height from store. @@ -216,18 +215,18 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB // Process forced inclusion transactions from checkpoint position forcedTxs := c.processForcedInclusionTxsFromCheckpoint(req.MaxBytes) - // Apply gas-based filtering if filter is configured + // Apply gas-based filtering if executor is configured var filteredForcedTxs [][]byte var remainingGasFilteredTxs [][]byte - if c.txFilter != nil && c.infoProvider != nil && len(forcedTxs) > 0 { + if c.executor != nil && len(forcedTxs) > 0 { // Get current gas limit from execution layer - info, err := c.infoProvider.GetExecutionInfo(ctx, 0) // 0 = latest/next block + info, err := c.executor.GetExecutionInfo(ctx, 0) // 0 = latest/next block if err != nil { c.logger.Warn().Err(err).Msg("failed to get execution info for gas filtering, proceeding without gas filter") filteredForcedTxs = forcedTxs } else if info.MaxGas > 0 { // Filter by gas limit - filteredForcedTxs, remainingGasFilteredTxs, err = c.txFilter.FilterDATransactions(ctx, forcedTxs, info.MaxGas) + filteredForcedTxs, remainingGasFilteredTxs, err = c.executor.FilterDATransactions(ctx, forcedTxs, info.MaxGas) if err != nil { c.logger.Warn().Err(err).Msg("failed to filter DA transactions by gas, proceeding without gas filter") filteredForcedTxs = forcedTxs diff --git a/pkg/sequencers/single/sequencer_test.go b/pkg/sequencers/single/sequencer_test.go index 8273461b93..63581a317c 100644 --- a/pkg/sequencers/single/sequencer_test.go +++ b/pkg/sequencers/single/sequencer_test.go @@ -1010,26 +1010,40 @@ func TestSequencer_GetNextBatch_EmptyDABatch_IncreasesDAHeight(t *testing.T) { assert.Equal(t, uint64(0), seq.checkpoint.TxIndex) } -// mockDATransactionFilter is a mock implementation of execution.DATransactionFilter -type mockDATransactionFilter struct { - filterFunc func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) +// mockExecutor is a mock implementation of execution.Executor for testing gas filtering +type mockExecutor struct { + maxGas uint64 + getInfoErr error + filterFunc func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) + filterCallCount int } -func (m *mockDATransactionFilter) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { - if m.filterFunc != nil { - return m.filterFunc(ctx, txs, maxGas) - } - return txs, nil, nil +func (m *mockExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { + return []byte("state-root"), nil +} + +func (m *mockExecutor) GetTxs(ctx context.Context) ([][]byte, error) { + return nil, nil } -// mockExecutionInfoProvider is a mock implementation of execution.ExecutionInfoProvider -type mockExecutionInfoProvider struct { - maxGas uint64 - err error +func (m *mockExecutor) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) { + return []byte("new-state-root"), nil } -func (m *mockExecutionInfoProvider) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { - return execution.ExecutionInfo{MaxGas: m.maxGas}, m.err +func (m *mockExecutor) SetFinal(ctx context.Context, blockHeight uint64) error { + return nil +} + +func (m *mockExecutor) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + return execution.ExecutionInfo{MaxGas: m.maxGas}, m.getInfoErr +} + +func (m *mockExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + m.filterCallCount++ + if m.filterFunc != nil { + return m.filterFunc(ctx, txs, maxGas) + } + return txs, nil, nil } func TestSequencer_GetNextBatch_WithGasFiltering(t *testing.T) { @@ -1070,11 +1084,10 @@ func TestSequencer_GetNextBatch_WithGasFiltering(t *testing.T) { ) require.NoError(t, err) - // Configure the gas filter mock - filterCallCount := 0 - mockFilter := &mockDATransactionFilter{ + // Configure the executor mock + mockExec := &mockExecutor{ + maxGas: 1000000, // 1M gas limit filterFunc: func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { - filterCallCount++ // Simulate: first 2 txs fit, third one doesn't if len(txs) >= 3 { return txs[:2], txs[2:], nil @@ -1083,12 +1096,8 @@ func TestSequencer_GetNextBatch_WithGasFiltering(t *testing.T) { }, } - mockInfoProvider := &mockExecutionInfoProvider{ - maxGas: 1000000, // 1M gas limit - } - - // Set the filter - seq.SetDATransactionFilter(mockFilter, mockInfoProvider) + // Set the executor + seq.SetExecutor(mockExec) // Manually set up cached forced txs to simulate DA fetch seq.cachedForcedInclusionTxs = forcedTxs @@ -1118,11 +1127,10 @@ func TestSequencer_GetNextBatch_WithGasFiltering(t *testing.T) { assert.Equal(t, uint64(0), seq.checkpoint.TxIndex) // Reset because we replaced the cache // Filter should have been called - assert.Equal(t, 1, filterCallCount) + assert.Equal(t, 1, mockExec.filterCallCount) // Second call should return the remaining tx - mockFilter.filterFunc = func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { - filterCallCount++ + mockExec.filterFunc = func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { // Now all remaining txs fit return txs, nil, nil } @@ -1167,18 +1175,15 @@ func TestSequencer_GetNextBatch_GasFilterError(t *testing.T) { ) require.NoError(t, err) - // Configure filter that returns error - mockFilter := &mockDATransactionFilter{ + // Configure executor that returns filter error + mockExec := &mockExecutor{ + maxGas: 1000000, filterFunc: func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { return nil, nil, errors.New("filter error") }, } - mockInfoProvider := &mockExecutionInfoProvider{ - maxGas: 1000000, - } - - seq.SetDATransactionFilter(mockFilter, mockInfoProvider) + seq.SetExecutor(mockExec) // Set up cached txs forcedTxs := [][]byte{[]byte("tx1"), []byte("tx2")} diff --git a/pkg/telemetry/executor_tracing.go b/pkg/telemetry/executor_tracing.go index b408ee1d56..91137c4502 100644 --- a/pkg/telemetry/executor_tracing.go +++ b/pkg/telemetry/executor_tracing.go @@ -106,18 +106,14 @@ func (t *tracedExecutor) GetLatestHeight(ctx context.Context) (uint64, error) { return height, err } -// If the underlying executor implements ExecutionInfoProvider, forward it while tracing. +// GetExecutionInfo forwards to the inner executor with tracing. func (t *tracedExecutor) GetExecutionInfo(ctx context.Context, height uint64) (coreexec.ExecutionInfo, error) { - eip, ok := t.inner.(coreexec.ExecutionInfoProvider) - if !ok { - return coreexec.ExecutionInfo{}, nil - } ctx, span := t.tracer.Start(ctx, "Executor.GetExecutionInfo", trace.WithAttributes(attribute.Int64("height", int64(height))), ) defer span.End() - info, err := eip.GetExecutionInfo(ctx, height) + info, err := t.inner.GetExecutionInfo(ctx, height) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -127,13 +123,8 @@ func (t *tracedExecutor) GetExecutionInfo(ctx context.Context, height uint64) (c return info, err } -// If the underlying executor implements DATransactionFilter, forward it while tracing. +// FilterDATransactions forwards to the inner executor with tracing. func (t *tracedExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { - filter, ok := t.inner.(coreexec.DATransactionFilter) - if !ok { - // If not implemented, return all transactions as valid (no filtering) - return txs, nil, nil - } ctx, span := t.tracer.Start(ctx, "Executor.FilterDATransactions", trace.WithAttributes( attribute.Int("input_tx_count", len(txs)), @@ -142,7 +133,7 @@ func (t *tracedExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, ) defer span.End() - validTxs, remainingTxs, err := filter.FilterDATransactions(ctx, txs, maxGas) + validTxs, remainingTxs, err := t.inner.FilterDATransactions(ctx, txs, maxGas) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) diff --git a/proto/evnode/v1/execution.proto b/proto/evnode/v1/execution.proto index 587b0e608e..bc08a1b69b 100644 --- a/proto/evnode/v1/execution.proto +++ b/proto/evnode/v1/execution.proto @@ -18,6 +18,12 @@ service ExecutorService { // SetFinal marks a block as finalized at the specified height rpc SetFinal(SetFinalRequest) returns (SetFinalResponse) {} + + // GetExecutionInfo returns current execution layer parameters + rpc GetExecutionInfo(GetExecutionInfoRequest) returns (GetExecutionInfoResponse) {} + + // FilterDATransactions validates and filters force-included transactions from DA + rpc FilterDATransactions(FilterDATransactionsRequest) returns (FilterDATransactionsResponse) {} } // InitChainRequest contains the genesis parameters for chain initialization @@ -86,3 +92,34 @@ message SetFinalRequest { message SetFinalResponse { // Empty response, errors are returned via gRPC status } + +// GetExecutionInfoRequest requests execution layer parameters +message GetExecutionInfoRequest { + // Block height to query (0 for next block parameters) + uint64 height = 1; +} + +// GetExecutionInfoResponse contains execution layer parameters +message GetExecutionInfoResponse { + // Maximum gas allowed for transactions in a block + // For non-gas-based execution layers, this should be 0 + uint64 max_gas = 1; +} + +// FilterDATransactionsRequest contains transactions to validate and filter +message FilterDATransactionsRequest { + // Raw transactions from DA to validate + repeated bytes txs = 1; + + // Maximum cumulative gas allowed for these transactions + uint64 max_gas = 2; +} + +// FilterDATransactionsResponse contains the filtered transactions +message FilterDATransactionsResponse { + // Transactions that passed validation and fit within maxGas + repeated bytes valid_txs = 1; + + // Valid transactions that didn't fit due to gas limit (for re-queuing) + repeated bytes remaining_txs = 2; +} diff --git a/test/mocks/execution.go b/test/mocks/execution.go index 25ef0e8d4d..3c748cc6b6 100644 --- a/test/mocks/execution.go +++ b/test/mocks/execution.go @@ -8,6 +8,7 @@ import ( "context" "time" + "github.com/evstack/ev-node/core/execution" mock "github.com/stretchr/testify/mock" ) @@ -124,6 +125,154 @@ func (_c *MockExecutor_ExecuteTxs_Call) RunAndReturn(run func(ctx context.Contex return _c } +// FilterDATransactions provides a mock function for the type MockExecutor +func (_mock *MockExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + ret := _mock.Called(ctx, txs, maxGas) + + if len(ret) == 0 { + panic("no return value specified for FilterDATransactions") + } + + var r0 [][]byte + var r1 [][]byte + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, [][]byte, uint64) ([][]byte, [][]byte, error)); ok { + return returnFunc(ctx, txs, maxGas) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, [][]byte, uint64) [][]byte); ok { + r0 = returnFunc(ctx, txs, maxGas) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([][]byte) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, [][]byte, uint64) [][]byte); ok { + r1 = returnFunc(ctx, txs, maxGas) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([][]byte) + } + } + if returnFunc, ok := ret.Get(2).(func(context.Context, [][]byte, uint64) error); ok { + r2 = returnFunc(ctx, txs, maxGas) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// MockExecutor_FilterDATransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FilterDATransactions' +type MockExecutor_FilterDATransactions_Call struct { + *mock.Call +} + +// FilterDATransactions is a helper method to define mock.On call +// - ctx context.Context +// - txs [][]byte +// - maxGas uint64 +func (_e *MockExecutor_Expecter) FilterDATransactions(ctx interface{}, txs interface{}, maxGas interface{}) *MockExecutor_FilterDATransactions_Call { + return &MockExecutor_FilterDATransactions_Call{Call: _e.mock.On("FilterDATransactions", ctx, txs, maxGas)} +} + +func (_c *MockExecutor_FilterDATransactions_Call) Run(run func(ctx context.Context, txs [][]byte, maxGas uint64)) *MockExecutor_FilterDATransactions_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 [][]byte + if args[1] != nil { + arg1 = args[1].([][]byte) + } + var arg2 uint64 + if args[2] != nil { + arg2 = args[2].(uint64) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockExecutor_FilterDATransactions_Call) Return(validTxs [][]byte, remainingTxs [][]byte, err error) *MockExecutor_FilterDATransactions_Call { + _c.Call.Return(validTxs, remainingTxs, err) + return _c +} + +func (_c *MockExecutor_FilterDATransactions_Call) RunAndReturn(run func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error)) *MockExecutor_FilterDATransactions_Call { + _c.Call.Return(run) + return _c +} + +// GetExecutionInfo provides a mock function for the type MockExecutor +func (_mock *MockExecutor) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + ret := _mock.Called(ctx, height) + + if len(ret) == 0 { + panic("no return value specified for GetExecutionInfo") + } + + var r0 execution.ExecutionInfo + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uint64) (execution.ExecutionInfo, error)); ok { + return returnFunc(ctx, height) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uint64) execution.ExecutionInfo); ok { + r0 = returnFunc(ctx, height) + } else { + r0 = ret.Get(0).(execution.ExecutionInfo) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uint64) error); ok { + r1 = returnFunc(ctx, height) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockExecutor_GetExecutionInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetExecutionInfo' +type MockExecutor_GetExecutionInfo_Call struct { + *mock.Call +} + +// GetExecutionInfo is a helper method to define mock.On call +// - ctx context.Context +// - height uint64 +func (_e *MockExecutor_Expecter) GetExecutionInfo(ctx interface{}, height interface{}) *MockExecutor_GetExecutionInfo_Call { + return &MockExecutor_GetExecutionInfo_Call{Call: _e.mock.On("GetExecutionInfo", ctx, height)} +} + +func (_c *MockExecutor_GetExecutionInfo_Call) Run(run func(ctx context.Context, height uint64)) *MockExecutor_GetExecutionInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uint64 + if args[1] != nil { + arg1 = args[1].(uint64) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockExecutor_GetExecutionInfo_Call) Return(executionInfo execution.ExecutionInfo, err error) *MockExecutor_GetExecutionInfo_Call { + _c.Call.Return(executionInfo, err) + return _c +} + +func (_c *MockExecutor_GetExecutionInfo_Call) RunAndReturn(run func(ctx context.Context, height uint64) (execution.ExecutionInfo, error)) *MockExecutor_GetExecutionInfo_Call { + _c.Call.Return(run) + return _c +} + // GetTxs provides a mock function for the type MockExecutor func (_mock *MockExecutor) GetTxs(ctx context.Context) ([][]byte, error) { ret := _mock.Called(ctx) diff --git a/test/mocks/height_aware_executor.go b/test/mocks/height_aware_executor.go index 8e22487cc0..c16660210f 100644 --- a/test/mocks/height_aware_executor.go +++ b/test/mocks/height_aware_executor.go @@ -6,6 +6,7 @@ import ( "context" "time" + "github.com/evstack/ev-node/core/execution" "github.com/stretchr/testify/mock" ) @@ -58,3 +59,22 @@ func (m *MockHeightAwareExecutor) GetLatestHeight(ctx context.Context) (uint64, args := m.Called(ctx) return args.Get(0).(uint64), args.Error(1) } + +// GetExecutionInfo implements the Executor interface. +func (m *MockHeightAwareExecutor) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + args := m.Called(ctx, height) + return args.Get(0).(execution.ExecutionInfo), args.Error(1) +} + +// FilterDATransactions implements the Executor interface. +func (m *MockHeightAwareExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + args := m.Called(ctx, txs, maxGas) + var validTxs, remainingTxs [][]byte + if args.Get(0) != nil { + validTxs = args.Get(0).([][]byte) + } + if args.Get(1) != nil { + remainingTxs = args.Get(1).([][]byte) + } + return validTxs, remainingTxs, args.Error(2) +} diff --git a/types/pb/evnode/v1/execution.pb.go b/types/pb/evnode/v1/execution.pb.go index 1b0d1188a2..12b462dc71 100644 --- a/types/pb/evnode/v1/execution.pb.go +++ b/types/pb/evnode/v1/execution.pb.go @@ -435,6 +435,209 @@ func (*SetFinalResponse) Descriptor() ([]byte, []int) { return file_evnode_v1_execution_proto_rawDescGZIP(), []int{7} } +// GetExecutionInfoRequest requests execution layer parameters +type GetExecutionInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Block height to query (0 for next block parameters) + Height uint64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetExecutionInfoRequest) Reset() { + *x = GetExecutionInfoRequest{} + mi := &file_evnode_v1_execution_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetExecutionInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetExecutionInfoRequest) ProtoMessage() {} + +func (x *GetExecutionInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_evnode_v1_execution_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetExecutionInfoRequest.ProtoReflect.Descriptor instead. +func (*GetExecutionInfoRequest) Descriptor() ([]byte, []int) { + return file_evnode_v1_execution_proto_rawDescGZIP(), []int{8} +} + +func (x *GetExecutionInfoRequest) GetHeight() uint64 { + if x != nil { + return x.Height + } + return 0 +} + +// GetExecutionInfoResponse contains execution layer parameters +type GetExecutionInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Maximum gas allowed for transactions in a block + // For non-gas-based execution layers, this should be 0 + MaxGas uint64 `protobuf:"varint,1,opt,name=max_gas,json=maxGas,proto3" json:"max_gas,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetExecutionInfoResponse) Reset() { + *x = GetExecutionInfoResponse{} + mi := &file_evnode_v1_execution_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetExecutionInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetExecutionInfoResponse) ProtoMessage() {} + +func (x *GetExecutionInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_evnode_v1_execution_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetExecutionInfoResponse.ProtoReflect.Descriptor instead. +func (*GetExecutionInfoResponse) Descriptor() ([]byte, []int) { + return file_evnode_v1_execution_proto_rawDescGZIP(), []int{9} +} + +func (x *GetExecutionInfoResponse) GetMaxGas() uint64 { + if x != nil { + return x.MaxGas + } + return 0 +} + +// FilterDATransactionsRequest contains transactions to validate and filter +type FilterDATransactionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Raw transactions from DA to validate + Txs [][]byte `protobuf:"bytes,1,rep,name=txs,proto3" json:"txs,omitempty"` + // Maximum cumulative gas allowed for these transactions + MaxGas uint64 `protobuf:"varint,2,opt,name=max_gas,json=maxGas,proto3" json:"max_gas,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FilterDATransactionsRequest) Reset() { + *x = FilterDATransactionsRequest{} + mi := &file_evnode_v1_execution_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FilterDATransactionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FilterDATransactionsRequest) ProtoMessage() {} + +func (x *FilterDATransactionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_evnode_v1_execution_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FilterDATransactionsRequest.ProtoReflect.Descriptor instead. +func (*FilterDATransactionsRequest) Descriptor() ([]byte, []int) { + return file_evnode_v1_execution_proto_rawDescGZIP(), []int{10} +} + +func (x *FilterDATransactionsRequest) GetTxs() [][]byte { + if x != nil { + return x.Txs + } + return nil +} + +func (x *FilterDATransactionsRequest) GetMaxGas() uint64 { + if x != nil { + return x.MaxGas + } + return 0 +} + +// FilterDATransactionsResponse contains the filtered transactions +type FilterDATransactionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Transactions that passed validation and fit within maxGas + ValidTxs [][]byte `protobuf:"bytes,1,rep,name=valid_txs,json=validTxs,proto3" json:"valid_txs,omitempty"` + // Valid transactions that didn't fit due to gas limit (for re-queuing) + RemainingTxs [][]byte `protobuf:"bytes,2,rep,name=remaining_txs,json=remainingTxs,proto3" json:"remaining_txs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FilterDATransactionsResponse) Reset() { + *x = FilterDATransactionsResponse{} + mi := &file_evnode_v1_execution_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FilterDATransactionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FilterDATransactionsResponse) ProtoMessage() {} + +func (x *FilterDATransactionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_evnode_v1_execution_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FilterDATransactionsResponse.ProtoReflect.Descriptor instead. +func (*FilterDATransactionsResponse) Descriptor() ([]byte, []int) { + return file_evnode_v1_execution_proto_rawDescGZIP(), []int{11} +} + +func (x *FilterDATransactionsResponse) GetValidTxs() [][]byte { + if x != nil { + return x.ValidTxs + } + return nil +} + +func (x *FilterDATransactionsResponse) GetRemainingTxs() [][]byte { + if x != nil { + return x.RemainingTxs + } + return nil +} + var File_evnode_v1_execution_proto protoreflect.FileDescriptor const file_evnode_v1_execution_proto_rawDesc = "" + @@ -461,13 +664,25 @@ const file_evnode_v1_execution_proto_rawDesc = "" + "\tmax_bytes\x18\x02 \x01(\x04R\bmaxBytes\"4\n" + "\x0fSetFinalRequest\x12!\n" + "\fblock_height\x18\x01 \x01(\x04R\vblockHeight\"\x12\n" + - "\x10SetFinalResponse2\xb0\x02\n" + + "\x10SetFinalResponse\"1\n" + + "\x17GetExecutionInfoRequest\x12\x16\n" + + "\x06height\x18\x01 \x01(\x04R\x06height\"3\n" + + "\x18GetExecutionInfoResponse\x12\x17\n" + + "\amax_gas\x18\x01 \x01(\x04R\x06maxGas\"H\n" + + "\x1bFilterDATransactionsRequest\x12\x10\n" + + "\x03txs\x18\x01 \x03(\fR\x03txs\x12\x17\n" + + "\amax_gas\x18\x02 \x01(\x04R\x06maxGas\"`\n" + + "\x1cFilterDATransactionsResponse\x12\x1b\n" + + "\tvalid_txs\x18\x01 \x03(\fR\bvalidTxs\x12#\n" + + "\rremaining_txs\x18\x02 \x03(\fR\fremainingTxs2\xfa\x03\n" + "\x0fExecutorService\x12H\n" + "\tInitChain\x12\x1b.evnode.v1.InitChainRequest\x1a\x1c.evnode.v1.InitChainResponse\"\x00\x12?\n" + "\x06GetTxs\x12\x18.evnode.v1.GetTxsRequest\x1a\x19.evnode.v1.GetTxsResponse\"\x00\x12K\n" + "\n" + "ExecuteTxs\x12\x1c.evnode.v1.ExecuteTxsRequest\x1a\x1d.evnode.v1.ExecuteTxsResponse\"\x00\x12E\n" + - "\bSetFinal\x12\x1a.evnode.v1.SetFinalRequest\x1a\x1b.evnode.v1.SetFinalResponse\"\x00B/Z-github.com/evstack/ev-node/types/pb/evnode/v1b\x06proto3" + "\bSetFinal\x12\x1a.evnode.v1.SetFinalRequest\x1a\x1b.evnode.v1.SetFinalResponse\"\x00\x12]\n" + + "\x10GetExecutionInfo\x12\".evnode.v1.GetExecutionInfoRequest\x1a#.evnode.v1.GetExecutionInfoResponse\"\x00\x12i\n" + + "\x14FilterDATransactions\x12&.evnode.v1.FilterDATransactionsRequest\x1a'.evnode.v1.FilterDATransactionsResponse\"\x00B/Z-github.com/evstack/ev-node/types/pb/evnode/v1b\x06proto3" var ( file_evnode_v1_execution_proto_rawDescOnce sync.Once @@ -481,34 +696,42 @@ func file_evnode_v1_execution_proto_rawDescGZIP() []byte { return file_evnode_v1_execution_proto_rawDescData } -var file_evnode_v1_execution_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_evnode_v1_execution_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_evnode_v1_execution_proto_goTypes = []any{ - (*InitChainRequest)(nil), // 0: evnode.v1.InitChainRequest - (*InitChainResponse)(nil), // 1: evnode.v1.InitChainResponse - (*GetTxsRequest)(nil), // 2: evnode.v1.GetTxsRequest - (*GetTxsResponse)(nil), // 3: evnode.v1.GetTxsResponse - (*ExecuteTxsRequest)(nil), // 4: evnode.v1.ExecuteTxsRequest - (*ExecuteTxsResponse)(nil), // 5: evnode.v1.ExecuteTxsResponse - (*SetFinalRequest)(nil), // 6: evnode.v1.SetFinalRequest - (*SetFinalResponse)(nil), // 7: evnode.v1.SetFinalResponse - (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*InitChainRequest)(nil), // 0: evnode.v1.InitChainRequest + (*InitChainResponse)(nil), // 1: evnode.v1.InitChainResponse + (*GetTxsRequest)(nil), // 2: evnode.v1.GetTxsRequest + (*GetTxsResponse)(nil), // 3: evnode.v1.GetTxsResponse + (*ExecuteTxsRequest)(nil), // 4: evnode.v1.ExecuteTxsRequest + (*ExecuteTxsResponse)(nil), // 5: evnode.v1.ExecuteTxsResponse + (*SetFinalRequest)(nil), // 6: evnode.v1.SetFinalRequest + (*SetFinalResponse)(nil), // 7: evnode.v1.SetFinalResponse + (*GetExecutionInfoRequest)(nil), // 8: evnode.v1.GetExecutionInfoRequest + (*GetExecutionInfoResponse)(nil), // 9: evnode.v1.GetExecutionInfoResponse + (*FilterDATransactionsRequest)(nil), // 10: evnode.v1.FilterDATransactionsRequest + (*FilterDATransactionsResponse)(nil), // 11: evnode.v1.FilterDATransactionsResponse + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp } var file_evnode_v1_execution_proto_depIdxs = []int32{ - 8, // 0: evnode.v1.InitChainRequest.genesis_time:type_name -> google.protobuf.Timestamp - 8, // 1: evnode.v1.ExecuteTxsRequest.timestamp:type_name -> google.protobuf.Timestamp - 0, // 2: evnode.v1.ExecutorService.InitChain:input_type -> evnode.v1.InitChainRequest - 2, // 3: evnode.v1.ExecutorService.GetTxs:input_type -> evnode.v1.GetTxsRequest - 4, // 4: evnode.v1.ExecutorService.ExecuteTxs:input_type -> evnode.v1.ExecuteTxsRequest - 6, // 5: evnode.v1.ExecutorService.SetFinal:input_type -> evnode.v1.SetFinalRequest - 1, // 6: evnode.v1.ExecutorService.InitChain:output_type -> evnode.v1.InitChainResponse - 3, // 7: evnode.v1.ExecutorService.GetTxs:output_type -> evnode.v1.GetTxsResponse - 5, // 8: evnode.v1.ExecutorService.ExecuteTxs:output_type -> evnode.v1.ExecuteTxsResponse - 7, // 9: evnode.v1.ExecutorService.SetFinal:output_type -> evnode.v1.SetFinalResponse - 6, // [6:10] is the sub-list for method output_type - 2, // [2:6] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 12, // 0: evnode.v1.InitChainRequest.genesis_time:type_name -> google.protobuf.Timestamp + 12, // 1: evnode.v1.ExecuteTxsRequest.timestamp:type_name -> google.protobuf.Timestamp + 0, // 2: evnode.v1.ExecutorService.InitChain:input_type -> evnode.v1.InitChainRequest + 2, // 3: evnode.v1.ExecutorService.GetTxs:input_type -> evnode.v1.GetTxsRequest + 4, // 4: evnode.v1.ExecutorService.ExecuteTxs:input_type -> evnode.v1.ExecuteTxsRequest + 6, // 5: evnode.v1.ExecutorService.SetFinal:input_type -> evnode.v1.SetFinalRequest + 8, // 6: evnode.v1.ExecutorService.GetExecutionInfo:input_type -> evnode.v1.GetExecutionInfoRequest + 10, // 7: evnode.v1.ExecutorService.FilterDATransactions:input_type -> evnode.v1.FilterDATransactionsRequest + 1, // 8: evnode.v1.ExecutorService.InitChain:output_type -> evnode.v1.InitChainResponse + 3, // 9: evnode.v1.ExecutorService.GetTxs:output_type -> evnode.v1.GetTxsResponse + 5, // 10: evnode.v1.ExecutorService.ExecuteTxs:output_type -> evnode.v1.ExecuteTxsResponse + 7, // 11: evnode.v1.ExecutorService.SetFinal:output_type -> evnode.v1.SetFinalResponse + 9, // 12: evnode.v1.ExecutorService.GetExecutionInfo:output_type -> evnode.v1.GetExecutionInfoResponse + 11, // 13: evnode.v1.ExecutorService.FilterDATransactions:output_type -> evnode.v1.FilterDATransactionsResponse + 8, // [8:14] is the sub-list for method output_type + 2, // [2:8] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_evnode_v1_execution_proto_init() } @@ -522,7 +745,7 @@ func file_evnode_v1_execution_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_evnode_v1_execution_proto_rawDesc), len(file_evnode_v1_execution_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/types/pb/evnode/v1/v1connect/execution.connect.go b/types/pb/evnode/v1/v1connect/execution.connect.go index 9d9652eaf8..fc6a08d391 100644 --- a/types/pb/evnode/v1/v1connect/execution.connect.go +++ b/types/pb/evnode/v1/v1connect/execution.connect.go @@ -44,6 +44,12 @@ const ( // ExecutorServiceSetFinalProcedure is the fully-qualified name of the ExecutorService's SetFinal // RPC. ExecutorServiceSetFinalProcedure = "/evnode.v1.ExecutorService/SetFinal" + // ExecutorServiceGetExecutionInfoProcedure is the fully-qualified name of the ExecutorService's + // GetExecutionInfo RPC. + ExecutorServiceGetExecutionInfoProcedure = "/evnode.v1.ExecutorService/GetExecutionInfo" + // ExecutorServiceFilterDATransactionsProcedure is the fully-qualified name of the ExecutorService's + // FilterDATransactions RPC. + ExecutorServiceFilterDATransactionsProcedure = "/evnode.v1.ExecutorService/FilterDATransactions" ) // ExecutorServiceClient is a client for the evnode.v1.ExecutorService service. @@ -56,6 +62,10 @@ type ExecutorServiceClient interface { ExecuteTxs(context.Context, *connect.Request[v1.ExecuteTxsRequest]) (*connect.Response[v1.ExecuteTxsResponse], error) // SetFinal marks a block as finalized at the specified height SetFinal(context.Context, *connect.Request[v1.SetFinalRequest]) (*connect.Response[v1.SetFinalResponse], error) + // GetExecutionInfo returns current execution layer parameters + GetExecutionInfo(context.Context, *connect.Request[v1.GetExecutionInfoRequest]) (*connect.Response[v1.GetExecutionInfoResponse], error) + // FilterDATransactions validates and filters force-included transactions from DA + FilterDATransactions(context.Context, *connect.Request[v1.FilterDATransactionsRequest]) (*connect.Response[v1.FilterDATransactionsResponse], error) } // NewExecutorServiceClient constructs a client for the evnode.v1.ExecutorService service. By @@ -93,15 +103,29 @@ func NewExecutorServiceClient(httpClient connect.HTTPClient, baseURL string, opt connect.WithSchema(executorServiceMethods.ByName("SetFinal")), connect.WithClientOptions(opts...), ), + getExecutionInfo: connect.NewClient[v1.GetExecutionInfoRequest, v1.GetExecutionInfoResponse]( + httpClient, + baseURL+ExecutorServiceGetExecutionInfoProcedure, + connect.WithSchema(executorServiceMethods.ByName("GetExecutionInfo")), + connect.WithClientOptions(opts...), + ), + filterDATransactions: connect.NewClient[v1.FilterDATransactionsRequest, v1.FilterDATransactionsResponse]( + httpClient, + baseURL+ExecutorServiceFilterDATransactionsProcedure, + connect.WithSchema(executorServiceMethods.ByName("FilterDATransactions")), + connect.WithClientOptions(opts...), + ), } } // executorServiceClient implements ExecutorServiceClient. type executorServiceClient struct { - initChain *connect.Client[v1.InitChainRequest, v1.InitChainResponse] - getTxs *connect.Client[v1.GetTxsRequest, v1.GetTxsResponse] - executeTxs *connect.Client[v1.ExecuteTxsRequest, v1.ExecuteTxsResponse] - setFinal *connect.Client[v1.SetFinalRequest, v1.SetFinalResponse] + initChain *connect.Client[v1.InitChainRequest, v1.InitChainResponse] + getTxs *connect.Client[v1.GetTxsRequest, v1.GetTxsResponse] + executeTxs *connect.Client[v1.ExecuteTxsRequest, v1.ExecuteTxsResponse] + setFinal *connect.Client[v1.SetFinalRequest, v1.SetFinalResponse] + getExecutionInfo *connect.Client[v1.GetExecutionInfoRequest, v1.GetExecutionInfoResponse] + filterDATransactions *connect.Client[v1.FilterDATransactionsRequest, v1.FilterDATransactionsResponse] } // InitChain calls evnode.v1.ExecutorService.InitChain. @@ -124,6 +148,16 @@ func (c *executorServiceClient) SetFinal(ctx context.Context, req *connect.Reque return c.setFinal.CallUnary(ctx, req) } +// GetExecutionInfo calls evnode.v1.ExecutorService.GetExecutionInfo. +func (c *executorServiceClient) GetExecutionInfo(ctx context.Context, req *connect.Request[v1.GetExecutionInfoRequest]) (*connect.Response[v1.GetExecutionInfoResponse], error) { + return c.getExecutionInfo.CallUnary(ctx, req) +} + +// FilterDATransactions calls evnode.v1.ExecutorService.FilterDATransactions. +func (c *executorServiceClient) FilterDATransactions(ctx context.Context, req *connect.Request[v1.FilterDATransactionsRequest]) (*connect.Response[v1.FilterDATransactionsResponse], error) { + return c.filterDATransactions.CallUnary(ctx, req) +} + // ExecutorServiceHandler is an implementation of the evnode.v1.ExecutorService service. type ExecutorServiceHandler interface { // InitChain initializes a new blockchain instance with genesis parameters @@ -134,6 +168,10 @@ type ExecutorServiceHandler interface { ExecuteTxs(context.Context, *connect.Request[v1.ExecuteTxsRequest]) (*connect.Response[v1.ExecuteTxsResponse], error) // SetFinal marks a block as finalized at the specified height SetFinal(context.Context, *connect.Request[v1.SetFinalRequest]) (*connect.Response[v1.SetFinalResponse], error) + // GetExecutionInfo returns current execution layer parameters + GetExecutionInfo(context.Context, *connect.Request[v1.GetExecutionInfoRequest]) (*connect.Response[v1.GetExecutionInfoResponse], error) + // FilterDATransactions validates and filters force-included transactions from DA + FilterDATransactions(context.Context, *connect.Request[v1.FilterDATransactionsRequest]) (*connect.Response[v1.FilterDATransactionsResponse], error) } // NewExecutorServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -167,6 +205,18 @@ func NewExecutorServiceHandler(svc ExecutorServiceHandler, opts ...connect.Handl connect.WithSchema(executorServiceMethods.ByName("SetFinal")), connect.WithHandlerOptions(opts...), ) + executorServiceGetExecutionInfoHandler := connect.NewUnaryHandler( + ExecutorServiceGetExecutionInfoProcedure, + svc.GetExecutionInfo, + connect.WithSchema(executorServiceMethods.ByName("GetExecutionInfo")), + connect.WithHandlerOptions(opts...), + ) + executorServiceFilterDATransactionsHandler := connect.NewUnaryHandler( + ExecutorServiceFilterDATransactionsProcedure, + svc.FilterDATransactions, + connect.WithSchema(executorServiceMethods.ByName("FilterDATransactions")), + connect.WithHandlerOptions(opts...), + ) return "/evnode.v1.ExecutorService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case ExecutorServiceInitChainProcedure: @@ -177,6 +227,10 @@ func NewExecutorServiceHandler(svc ExecutorServiceHandler, opts ...connect.Handl executorServiceExecuteTxsHandler.ServeHTTP(w, r) case ExecutorServiceSetFinalProcedure: executorServiceSetFinalHandler.ServeHTTP(w, r) + case ExecutorServiceGetExecutionInfoProcedure: + executorServiceGetExecutionInfoHandler.ServeHTTP(w, r) + case ExecutorServiceFilterDATransactionsProcedure: + executorServiceFilterDATransactionsHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -201,3 +255,11 @@ func (UnimplementedExecutorServiceHandler) ExecuteTxs(context.Context, *connect. func (UnimplementedExecutorServiceHandler) SetFinal(context.Context, *connect.Request[v1.SetFinalRequest]) (*connect.Response[v1.SetFinalResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("evnode.v1.ExecutorService.SetFinal is not implemented")) } + +func (UnimplementedExecutorServiceHandler) GetExecutionInfo(context.Context, *connect.Request[v1.GetExecutionInfoRequest]) (*connect.Response[v1.GetExecutionInfoResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("evnode.v1.ExecutorService.GetExecutionInfo is not implemented")) +} + +func (UnimplementedExecutorServiceHandler) FilterDATransactions(context.Context, *connect.Request[v1.FilterDATransactionsRequest]) (*connect.Response[v1.FilterDATransactionsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("evnode.v1.ExecutorService.FilterDATransactions is not implemented")) +} From 987beb02a7775fc57fb90a2e3a5e75a568338e0a Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 16 Jan 2026 15:00:06 +0100 Subject: [PATCH 3/3] wip --- core/execution/execution.go | 56 ++++++++--------- execution/grpc/client.go | 3 + execution/grpc/client_test.go | 18 ++++-- execution/grpc/go.mod | 8 ++- execution/grpc/go.sum | 16 ++--- execution/grpc/server.go | 8 ++- pkg/sequencers/single/sequencer.go | 22 ++++--- pkg/sequencers/single/sequencer_test.go | 3 +- pkg/telemetry/executor_tracing.go | 9 ++- test/mocks/execution.go | 82 ------------------------- test/mocks/height_aware_executor.go | 6 +- 11 files changed, 86 insertions(+), 145 deletions(-) diff --git a/core/execution/execution.go b/core/execution/execution.go index 6d7c492aff..b2918f3f3c 100644 --- a/core/execution/execution.go +++ b/core/execution/execution.go @@ -104,27 +104,6 @@ type Executor interface { // - info: Current execution parameters // - error: Any errors during retrieval GetExecutionInfo(ctx context.Context, height uint64) (ExecutionInfo, error) - - // FilterDATransactions validates and filters force-included transactions from DA. - // It performs execution-layer specific validation (e.g., EVM tx parsing, gas checks) - // and returns transactions that are valid and fit within the gas limit. - // - // The function filters out: - // - Invalid/unparseable transactions (gibberish) - // - Transactions that would exceed the cumulative gas limit - // - // For non-gas-based execution layers, return all valid transactions with nil remainingTxs. - // - // Parameters: - // - ctx: Context for timeout/cancellation control - // - txs: Raw transactions from DA to validate - // - maxGas: Maximum cumulative gas allowed for these transactions - // - // Returns: - // - validTxs: Transactions that passed validation and fit within maxGas - // - remainingTxs: Valid transactions that didn't fit due to gas limit (for re-queuing) - // - err: Any errors during filtering (not validation errors, which result in filtering) - FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) (validTxs [][]byte, remainingTxs [][]byte, err error) } // HeightProvider is an optional interface that execution clients can implement @@ -143,18 +122,31 @@ type HeightProvider interface { GetLatestHeight(ctx context.Context) (uint64, error) } -// ExecutionInfoProvider is an interface for components that can provide execution layer parameters. -// This is useful for type assertions when an Executor implementation supports gas-based filtering. -type ExecutionInfoProvider interface { - // GetExecutionInfo returns current execution layer parameters. - // See Executor.GetExecutionInfo for full documentation. - GetExecutionInfo(ctx context.Context, height uint64) (ExecutionInfo, error) -} - -// DATransactionFilter is an interface for components that can filter DA transactions. -// This is useful for type assertions when an Executor implementation supports gas-based filtering. +// DATransactionFilter is an optional interface that execution clients can implement +// to support gas-based filtering of force-included transactions from DA. +// +// When implemented, the sequencer will use this to filter DA transactions by gas limit +// before including them in blocks. If not implemented, all DA transactions are included +// without gas-based filtering. type DATransactionFilter interface { // FilterDATransactions validates and filters force-included transactions from DA. - // See Executor.FilterDATransactions for full documentation. + // It performs execution-layer specific validation (e.g., EVM tx parsing, gas checks) + // and returns transactions that are valid and fit within the gas limit. + // + // The function filters out: + // - Invalid/unparseable transactions (gibberish) + // - Transactions that would exceed the cumulative gas limit + // + // For non-gas-based execution layers, return all valid transactions with nil remainingTxs. + // + // Parameters: + // - ctx: Context for timeout/cancellation control + // - txs: Raw transactions from DA to validate + // - maxGas: Maximum cumulative gas allowed for these transactions + // + // Returns: + // - validTxs: Transactions that passed validation and fit within maxGas + // - remainingTxs: Valid transactions that didn't fit due to gas limit (for re-queuing) + // - err: Any errors during filtering (not validation errors, which result in filtering) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) (validTxs [][]byte, remainingTxs [][]byte, err error) } diff --git a/execution/grpc/client.go b/execution/grpc/client.go index dd3d4c1bb9..b222003d8e 100644 --- a/execution/grpc/client.go +++ b/execution/grpc/client.go @@ -17,6 +17,9 @@ import ( // Ensure Client implements the execution.Executor interface var _ execution.Executor = (*Client)(nil) +// Ensure Client implements the optional DATransactionFilter interface +var _ execution.DATransactionFilter = (*Client)(nil) + // Client is a gRPC client that implements the execution.Executor interface. // It communicates with a remote execution service via gRPC using Connect-RPC. type Client struct { diff --git a/execution/grpc/client_test.go b/execution/grpc/client_test.go index 01195d89ac..f1b8c12c39 100644 --- a/execution/grpc/client_test.go +++ b/execution/grpc/client_test.go @@ -5,14 +5,17 @@ import ( "net/http/httptest" "testing" "time" + + "github.com/evstack/ev-node/core/execution" ) // mockExecutor is a mock implementation of execution.Executor for testing type mockExecutor struct { - initChainFunc func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) - getTxsFunc func(ctx context.Context) ([][]byte, error) - executeTxsFunc func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) - setFinalFunc func(ctx context.Context, blockHeight uint64) error + initChainFunc func(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) + getTxsFunc func(ctx context.Context) ([][]byte, error) + executeTxsFunc func(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) ([]byte, error) + setFinalFunc func(ctx context.Context, blockHeight uint64) error + getExecutionInfoFunc func(ctx context.Context, height uint64) (execution.ExecutionInfo, error) } func (m *mockExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) { @@ -43,6 +46,13 @@ func (m *mockExecutor) SetFinal(ctx context.Context, blockHeight uint64) error { return nil } +func (m *mockExecutor) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { + if m.getExecutionInfoFunc != nil { + return m.getExecutionInfoFunc(ctx, height) + } + return execution.ExecutionInfo{MaxGas: 0}, nil +} + func TestClient_InitChain(t *testing.T) { ctx := context.Background() expectedStateRoot := []byte("test_state_root") diff --git a/execution/grpc/go.mod b/execution/grpc/go.mod index acd20a4cb3..c33c943673 100644 --- a/execution/grpc/go.mod +++ b/execution/grpc/go.mod @@ -7,10 +7,12 @@ require ( connectrpc.com/grpcreflect v1.3.0 github.com/evstack/ev-node v1.0.0-beta.11 github.com/evstack/ev-node/core v1.0.0-beta.5 - golang.org/x/net v0.47.0 - google.golang.org/protobuf v1.36.10 + golang.org/x/net v0.49.0 + google.golang.org/protobuf v1.36.11 ) -require golang.org/x/text v0.31.0 // indirect +require golang.org/x/text v0.33.0 // indirect replace github.com/evstack/ev-node/core => ../../core + +replace github.com/evstack/ev-node => ../.. diff --git a/execution/grpc/go.sum b/execution/grpc/go.sum index 4eb05c9c82..6cdec090b8 100644 --- a/execution/grpc/go.sum +++ b/execution/grpc/go.sum @@ -2,15 +2,11 @@ connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= -github.com/evstack/ev-node v1.0.0-beta.11 h1:3g4Ja2mP3HVGt3Q6rOvLExAlU24r+mEcpq0tUiKPPmw= -github.com/evstack/ev-node v1.0.0-beta.11/go.mod h1:BNQb29H/34/PgOB25Tn3+SuVhjdfZs3GsQz++bo8iHQ= -github.com/evstack/ev-node/core v1.0.0-beta.5 h1:lgxE8XiF3U9pcFgh7xuKMgsOGvLBGRyd9kc9MR4WL0o= -github.com/evstack/ev-node/core v1.0.0-beta.5/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/execution/grpc/server.go b/execution/grpc/server.go index 276c8fde08..d780755dad 100644 --- a/execution/grpc/server.go +++ b/execution/grpc/server.go @@ -158,11 +158,17 @@ func (s *Server) GetExecutionInfo( // // It validates and filters force-included transactions from DA, returning // transactions that are valid and fit within the gas limit. +// Returns an error if the executor does not implement DATransactionFilter. func (s *Server) FilterDATransactions( ctx context.Context, req *connect.Request[pb.FilterDATransactionsRequest], ) (*connect.Response[pb.FilterDATransactionsResponse], error) { - validTxs, remainingTxs, err := s.executor.FilterDATransactions(ctx, req.Msg.Txs, req.Msg.MaxGas) + filter, ok := s.executor.(execution.DATransactionFilter) + if !ok { + return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("executor does not support DA transaction filtering")) + } + + validTxs, remainingTxs, err := filter.FilterDATransactions(ctx, req.Msg.Txs, req.Msg.MaxGas) if err != nil { return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to filter DA transactions: %w", err)) } diff --git a/pkg/sequencers/single/sequencer.go b/pkg/sequencers/single/sequencer.go index 3e40d9e235..0f528c40fc 100644 --- a/pkg/sequencers/single/sequencer.go +++ b/pkg/sequencers/single/sequencer.go @@ -53,9 +53,11 @@ type Sequencer struct { cachedForcedInclusionTxs [][]byte // Executor for DA transaction filtering (optional) - // When set, forced inclusion transactions are filtered by gas limit - // using the executor's GetExecutionInfo and FilterDATransactions methods + // When set, forced inclusion transactions are filtered by gas limit. + // GetExecutionInfo is called directly on the executor. + // FilterDATransactions is called via type assertion to DATransactionFilter. executor execution.Executor + txFilter execution.DATransactionFilter // cached type assertion result } // NewSequencer creates a new Single Sequencer @@ -127,7 +129,13 @@ func NewSequencer( // This should be called after NewSequencer and before Start if filtering is desired. func (c *Sequencer) SetExecutor(executor execution.Executor) { c.executor = executor - c.logger.Info().Msg("Executor configured for DA transaction gas-based filtering") + // Check if executor implements the optional DATransactionFilter interface + if filter, ok := executor.(execution.DATransactionFilter); ok { + c.txFilter = filter + c.logger.Info().Msg("Executor configured for DA transaction gas-based filtering") + } else { + c.logger.Info().Msg("Executor configured (no DA transaction filtering support)") + } } // getInitialDAStartHeight retrieves the DA height of the first included chain height from store. @@ -215,18 +223,18 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB // Process forced inclusion transactions from checkpoint position forcedTxs := c.processForcedInclusionTxsFromCheckpoint(req.MaxBytes) - // Apply gas-based filtering if executor is configured + // Apply gas-based filtering if executor and filter are configured var filteredForcedTxs [][]byte var remainingGasFilteredTxs [][]byte - if c.executor != nil && len(forcedTxs) > 0 { + if c.executor != nil && c.txFilter != nil && len(forcedTxs) > 0 { // Get current gas limit from execution layer info, err := c.executor.GetExecutionInfo(ctx, 0) // 0 = latest/next block if err != nil { c.logger.Warn().Err(err).Msg("failed to get execution info for gas filtering, proceeding without gas filter") filteredForcedTxs = forcedTxs } else if info.MaxGas > 0 { - // Filter by gas limit - filteredForcedTxs, remainingGasFilteredTxs, err = c.executor.FilterDATransactions(ctx, forcedTxs, info.MaxGas) + // Filter by gas limit using the DATransactionFilter interface + filteredForcedTxs, remainingGasFilteredTxs, err = c.txFilter.FilterDATransactions(ctx, forcedTxs, info.MaxGas) if err != nil { c.logger.Warn().Err(err).Msg("failed to filter DA transactions by gas, proceeding without gas filter") filteredForcedTxs = forcedTxs diff --git a/pkg/sequencers/single/sequencer_test.go b/pkg/sequencers/single/sequencer_test.go index 63581a317c..ae797a96f4 100644 --- a/pkg/sequencers/single/sequencer_test.go +++ b/pkg/sequencers/single/sequencer_test.go @@ -1010,7 +1010,8 @@ func TestSequencer_GetNextBatch_EmptyDABatch_IncreasesDAHeight(t *testing.T) { assert.Equal(t, uint64(0), seq.checkpoint.TxIndex) } -// mockExecutor is a mock implementation of execution.Executor for testing gas filtering +// mockExecutor is a mock implementation of execution.Executor and the optional +// execution.DATransactionFilter interface for testing gas filtering type mockExecutor struct { maxGas uint64 getInfoErr error diff --git a/pkg/telemetry/executor_tracing.go b/pkg/telemetry/executor_tracing.go index 91137c4502..0dca905fcf 100644 --- a/pkg/telemetry/executor_tracing.go +++ b/pkg/telemetry/executor_tracing.go @@ -123,8 +123,13 @@ func (t *tracedExecutor) GetExecutionInfo(ctx context.Context, height uint64) (c return info, err } -// FilterDATransactions forwards to the inner executor with tracing. +// FilterDATransactions forwards to the inner executor with tracing if it implements DATransactionFilter. func (t *tracedExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { + filter, ok := t.inner.(coreexec.DATransactionFilter) + if !ok { + // If not implemented, return all transactions as valid (no filtering) + return txs, nil, nil + } ctx, span := t.tracer.Start(ctx, "Executor.FilterDATransactions", trace.WithAttributes( attribute.Int("input_tx_count", len(txs)), @@ -133,7 +138,7 @@ func (t *tracedExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, ) defer span.End() - validTxs, remainingTxs, err := t.inner.FilterDATransactions(ctx, txs, maxGas) + validTxs, remainingTxs, err := filter.FilterDATransactions(ctx, txs, maxGas) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) diff --git a/test/mocks/execution.go b/test/mocks/execution.go index 3c748cc6b6..646a161cad 100644 --- a/test/mocks/execution.go +++ b/test/mocks/execution.go @@ -125,88 +125,6 @@ func (_c *MockExecutor_ExecuteTxs_Call) RunAndReturn(run func(ctx context.Contex return _c } -// FilterDATransactions provides a mock function for the type MockExecutor -func (_mock *MockExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { - ret := _mock.Called(ctx, txs, maxGas) - - if len(ret) == 0 { - panic("no return value specified for FilterDATransactions") - } - - var r0 [][]byte - var r1 [][]byte - var r2 error - if returnFunc, ok := ret.Get(0).(func(context.Context, [][]byte, uint64) ([][]byte, [][]byte, error)); ok { - return returnFunc(ctx, txs, maxGas) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, [][]byte, uint64) [][]byte); ok { - r0 = returnFunc(ctx, txs, maxGas) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([][]byte) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, [][]byte, uint64) [][]byte); ok { - r1 = returnFunc(ctx, txs, maxGas) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).([][]byte) - } - } - if returnFunc, ok := ret.Get(2).(func(context.Context, [][]byte, uint64) error); ok { - r2 = returnFunc(ctx, txs, maxGas) - } else { - r2 = ret.Error(2) - } - return r0, r1, r2 -} - -// MockExecutor_FilterDATransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FilterDATransactions' -type MockExecutor_FilterDATransactions_Call struct { - *mock.Call -} - -// FilterDATransactions is a helper method to define mock.On call -// - ctx context.Context -// - txs [][]byte -// - maxGas uint64 -func (_e *MockExecutor_Expecter) FilterDATransactions(ctx interface{}, txs interface{}, maxGas interface{}) *MockExecutor_FilterDATransactions_Call { - return &MockExecutor_FilterDATransactions_Call{Call: _e.mock.On("FilterDATransactions", ctx, txs, maxGas)} -} - -func (_c *MockExecutor_FilterDATransactions_Call) Run(run func(ctx context.Context, txs [][]byte, maxGas uint64)) *MockExecutor_FilterDATransactions_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 [][]byte - if args[1] != nil { - arg1 = args[1].([][]byte) - } - var arg2 uint64 - if args[2] != nil { - arg2 = args[2].(uint64) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *MockExecutor_FilterDATransactions_Call) Return(validTxs [][]byte, remainingTxs [][]byte, err error) *MockExecutor_FilterDATransactions_Call { - _c.Call.Return(validTxs, remainingTxs, err) - return _c -} - -func (_c *MockExecutor_FilterDATransactions_Call) RunAndReturn(run func(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error)) *MockExecutor_FilterDATransactions_Call { - _c.Call.Return(run) - return _c -} - // GetExecutionInfo provides a mock function for the type MockExecutor func (_mock *MockExecutor) GetExecutionInfo(ctx context.Context, height uint64) (execution.ExecutionInfo, error) { ret := _mock.Called(ctx, height) diff --git a/test/mocks/height_aware_executor.go b/test/mocks/height_aware_executor.go index c16660210f..a8a6556fdb 100644 --- a/test/mocks/height_aware_executor.go +++ b/test/mocks/height_aware_executor.go @@ -24,8 +24,8 @@ func NewMockHeightAwareExecutor(t interface { return mockExec } -// MockHeightAwareExecutor is a mock that implements both Executor and HeightProvider interfaces. -// This allows testing code that needs an executor with height awareness capability. +// MockHeightAwareExecutor is a mock that implements Executor, HeightProvider, and DATransactionFilter interfaces. +// This allows testing code that needs an executor with height awareness and DA transaction filtering capability. type MockHeightAwareExecutor struct { mock.Mock } @@ -66,7 +66,7 @@ func (m *MockHeightAwareExecutor) GetExecutionInfo(ctx context.Context, height u return args.Get(0).(execution.ExecutionInfo), args.Error(1) } -// FilterDATransactions implements the Executor interface. +// FilterDATransactions implements the optional DATransactionFilter interface. func (m *MockHeightAwareExecutor) FilterDATransactions(ctx context.Context, txs [][]byte, maxGas uint64) ([][]byte, [][]byte, error) { args := m.Called(ctx, txs, maxGas) var validTxs, remainingTxs [][]byte