Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b93225e
Adds TransactionFiltererRPCClient to SequencerConfig
diegoximenes Jan 29, 2026
235cfee
Moves TransactionFiltererRPClient to execution.Config
diegoximenes Jan 29, 2026
e250a63
TransactionFiltererRPCClient
diegoximenes Jan 29, 2026
6d37e9b
ExecutionEngine with TransactionFiltererRPCClient
diegoximenes Jan 29, 2026
638b7ee
Guard agains nil transactionFiltererRPCClient
diegoximenes Jan 29, 2026
e95384c
System tests use cmd/transaction-filterer
diegoximenes Jan 29, 2026
c42252f
Add comments
diegoximenes Jan 29, 2026
c9645de
Rename test setTransactionFiltererRPCClient to setTransactionFilterer…
diegoximenes Jan 29, 2026
45d1dd3
Simplifies NewTransactionFiltererRPCClient
diegoximenes Jan 29, 2026
b6eccb4
changelog
diegoximenes Jan 29, 2026
801ca8f
Fix changelog
diegoximenes Jan 29, 2026
b244782
Adds missing require.NoError
diegoximenes Jan 29, 2026
40ea668
Avoids concurrent txs with the same nonce
diegoximenes Jan 30, 2026
c5bca58
Sequencer calls transaction-filterer sequentially
diegoximenes Jan 30, 2026
cdd0916
Adds *testing.T as an argument for SetTransactionFiltererRPCClient
diegoximenes Jan 30, 2026
eb88a51
Adds addTxHashToOnChainFilter again
diegoximenes Jan 30, 2026
2559d24
Adds missing client.Close in transaction-filterer client
diegoximenes Feb 3, 2026
1f53f82
Adds missing client.Close in ConsensusRPCClient
diegoximenes Feb 3, 2026
bb75495
Adds missing client.Close in ExecutionRPCClient
diegoximenes Feb 3, 2026
5cc32a9
transactionfiltererclient.DefaultConfig
diegoximenes Feb 3, 2026
cb5c1ae
TransactionFilteringConfig
diegoximenes Feb 3, 2026
2ee93f0
Removes ExecutionEngine.SetTransactionFiltererRPCClient. Adds Transac…
diegoximenes Feb 3, 2026
6577316
Updates comment
diegoximenes Feb 3, 2026
7ef5272
Adds missing require.NoError(t, err)
diegoximenes Feb 3, 2026
9857f64
Fix lint issue
diegoximenes Feb 3, 2026
e85704a
Merge branch 'master' into sequencer_calls_transaction_filterer
diegoximenes Feb 3, 2026
4baedff
Moves TransactionFiltererRPCClient to gethexec package
diegoximenes Feb 5, 2026
207928a
Moves TransactionFiltering config to SequencerConfig
diegoximenes Feb 5, 2026
ad740b0
Moves FilterService to the Sequencer
diegoximenes Feb 5, 2026
9c5f489
Moves EventFilter to TransactionFilteringConfig
diegoximenes Feb 5, 2026
73c24bc
Fixes var name
diegoximenes Feb 5, 2026
fb68449
Merge branch 'master' into sequencer_calls_transaction_filterer
diegoximenes Feb 5, 2026
8200822
Fixes eventfilter in tests
diegoximenes Feb 5, 2026
d1ae878
Merge branch 'master' into sequencer_calls_transaction_filterer
diegoximenes Feb 6, 2026
f00b17c
Moves transactionfilterer namespace to gethexec
diegoximenes Feb 9, 2026
687e1d1
Fixes ExecutionEngine StopWaiter start/stop
diegoximenes Feb 9, 2026
8c207bb
Merge branch 'master' into sequencer_calls_transaction_filterer
diegoximenes Feb 9, 2026
d7479ee
Merge branch 'master' into sequencer_calls_transaction_filterer
diegoximenes Feb 10, 2026
10e8cf9
Fix import
diegoximenes Feb 10, 2026
a918047
Creates transactionFiltererRPCClient in NewSequencer
diegoximenes Feb 10, 2026
3cffec2
Simplifies NewExecutionEngine
diegoximenes Feb 10, 2026
7f334f1
Fixes order of execution and consensus rpc clients StopAndWait calls
diegoximenes Feb 10, 2026
5221bee
Merge branch 'master' into sequencer_calls_transaction_filterer
diegoximenes Feb 10, 2026
55fbbef
Merge branch 'master' into sequencer_calls_transaction_filterer
diegoximenes Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions arbnode/delayed_seq_reorg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ func TestSequencerReorgFromDelayed(t *testing.T) {

err = streamer.Start(ctx)
Require(t, err)
exec.Start(ctx)
err = exec.Start(ctx)
Require(t, err)
init, err := streamer.GetMessage(0)
Require(t, err)

Expand Down Expand Up @@ -225,7 +226,8 @@ func TestSequencerReorgFromLastDelayedMsg(t *testing.T) {

err = streamer.Start(ctx)
Require(t, err)
exec.Start(ctx)
err = exec.Start(ctx)
Require(t, err)
init, err := streamer.GetMessage(0)
Require(t, err)

Expand Down
2 changes: 1 addition & 1 deletion arbnode/delayed_sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ var TestDelayedSequencerConfig = DelayedSequencerConfig{
RequireFullFinality: false,
UseMergeFinality: false,
RescanInterval: time.Millisecond * 100,
FilteredTxFullRetryInterval: 30 * time.Second,
FilteredTxFullRetryInterval: 1 * time.Second,
}

func NewDelayedSequencer(l1Reader *headerreader.HeaderReader, reader *InboxReader, exec execution.ExecutionSequencer, coordinator *SeqCoordinator, config DelayedSequencerConfigFetcher) (*DelayedSequencer, error) {
Expand Down
5 changes: 3 additions & 2 deletions arbnode/inbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func NewTransactionStreamerForTest(t *testing.T, ctx context.Context, ownerAddre
}

transactionStreamerConfigFetcher := func() *TransactionStreamerConfig { return &DefaultTransactionStreamerConfig }
execEngine, err := gethexec.NewExecutionEngine(bc, 0, false)
execEngine, err := gethexec.NewExecutionEngine(bc, 0, false, nil)
if err != nil {
Fail(t, err)
}
Expand Down Expand Up @@ -183,7 +183,8 @@ func TestTransactionStreamer(t *testing.T) {

err := inbox.Start(ctx)
Require(t, err)
exec.Start(ctx)
err = exec.Start(ctx)
Require(t, err)

maxExpectedGasCost := big.NewInt(l2pricing.InitialBaseFeeWei)
maxExpectedGasCost.Mul(maxExpectedGasCost, big.NewInt(2100*2))
Expand Down
2 changes: 2 additions & 0 deletions changelog/diegoximenes-nit-4252.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Added
- Sequencer calls transaction-filterer command if delayed transaction was filtered
42 changes: 33 additions & 9 deletions cmd/transaction-filterer/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package api

import (
"context"
"sync"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
Expand All @@ -15,18 +17,22 @@ import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"

"github.com/offchainlabs/nitro/execution/gethexec"
"github.com/offchainlabs/nitro/solgen/go/precompilesgen"
)

const namespace = "transactionfilterer"

type TransactionFiltererAPI struct {
apiMutex sync.Mutex // avoids concurrent transactions with the same nonce
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a channel with a single sink to ensure transactions are treated one by one. We should eventually have a service with some more internal state that could e.g. make sure it's filter requests pass through, retries if needed etc.. no need to catch a lock while handling a request.

Copy link
Contributor Author

@diegoximenes diegoximenes Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If following a strategy like this, then adding a message queue layer between the sequencer and cmd/transaction-filterer is a way to move forward.
But it seems over engineering right now TBH.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree it can be done later


arbFilteredTransactionsManager *precompilesgen.ArbFilteredTransactionsManager
txOpts *bind.TransactOpts
}

// Filter adds the given transaction hash to the filtered transactions set, which is managed by the ArbFilteredTransactionsManager precompile.
func (t *TransactionFiltererAPI) Filter(ctx context.Context, txHashToFilter common.Hash) (common.Hash, error) {
t.apiMutex.Lock()
defer t.apiMutex.Unlock()

txOpts := *t.txOpts
txOpts.Context = ctx

Expand All @@ -41,19 +47,37 @@ func (t *TransactionFiltererAPI) Filter(ctx context.Context, txHashToFilter comm
}
}

// Only used for testing.
// Sequencer and TransactionFiltererAPI depend on each other, as a workaround for the egg/chicken problem,
// we set the sequencer client after both are created.
func (t *TransactionFiltererAPI) SetSequencerClient(_ *testing.T, sequencerClient *ethclient.Client) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using go testing package in production code is a bit anti-pattern I think. I can see the need for it here from the comment. But the impact of this is that go now will compile testing related package/runtime into production code. increase the binary size. and might cause some issues or affect the speed (I think)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using testing.T input in code that's only used for testing helps make sure it's not used anywhere outside of tests by mistakes. Binary size is irrelevant, having a function that can be called from tests and only from tests is useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, since no member function of testing.T is called I don't believe even binary size will increase

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My worries is not only the binary size, but the test-related runtime or execution.

arbFilteredTransactionsManager, err := precompilesgen.NewArbFilteredTransactionsManager(
types.ArbFilteredTransactionsManagerAddress,
sequencerClient,
)
if err != nil {
return err
}

t.apiMutex.Lock()
defer t.apiMutex.Unlock()
t.arbFilteredTransactionsManager = arbFilteredTransactionsManager
return nil
}

var DefaultStackConfig = node.Config{
DataDir: "", // ephemeral
HTTPPort: node.DefaultHTTPPort,
AuthAddr: node.DefaultAuthHost,
AuthPort: node.DefaultAuthPort,
AuthVirtualHosts: node.DefaultAuthVhosts,
HTTPModules: []string{namespace},
HTTPModules: []string{gethexec.TransactionFiltererNamespace},
HTTPHost: node.DefaultHTTPHost,
HTTPVirtualHosts: []string{"localhost"},
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
WSHost: node.DefaultWSHost,
WSPort: node.DefaultWSPort,
WSModules: []string{namespace},
WSModules: []string{gethexec.TransactionFiltererNamespace},
GraphQLVirtualHosts: []string{"localhost"},
P2P: p2p.Config{
ListenAddr: "",
Expand All @@ -66,31 +90,31 @@ func NewStack(
stackConfig *node.Config,
txOpts *bind.TransactOpts,
sequencerClient *ethclient.Client,
) (*node.Node, error) {
) (*node.Node, *TransactionFiltererAPI, error) {
stack, err := node.New(stackConfig)
if err != nil {
return nil, err
return nil, nil, err
}

arbFilteredTransactionsManager, err := precompilesgen.NewArbFilteredTransactionsManager(
types.ArbFilteredTransactionsManagerAddress,
sequencerClient,
)
if err != nil {
return nil, err
return nil, nil, err
}

api := &TransactionFiltererAPI{
arbFilteredTransactionsManager: arbFilteredTransactionsManager,
txOpts: txOpts,
}
apis := []rpc.API{{
Namespace: namespace,
Namespace: gethexec.TransactionFiltererNamespace,
Version: "1.0",
Service: api,
Public: true,
}}
stack.RegisterAPIs(apis)

return stack, nil
return stack, api, nil
}
2 changes: 1 addition & 1 deletion cmd/transaction-filterer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ func mainImpl() int {
return 1
}

stack, err := api.NewStack(&stackConf, txOpts, sequencerClient)
stack, _, err := api.NewStack(&stackConf, txOpts, sequencerClient)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating stack: %v\n", err)
return 1
Expand Down
5 changes: 5 additions & 0 deletions consensus/consensusrpcclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ func (c *ConsensusRPCClient) Start(ctx_in context.Context) error {
return c.client.Start(ctx)
}

func (c *ConsensusRPCClient) StopAndWait() {
c.StopWaiter.StopAndWait()
c.client.Close()
}

func convertError(err error) error {
if err == nil {
return nil
Expand Down
67 changes: 55 additions & 12 deletions execution/gethexec/executionengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ type ExecutionEngine struct {

runningMaintenance atomic.Bool

addressChecker state.AddressChecker
addressChecker state.AddressChecker
transactionFiltererRPCClient *TransactionFiltererRPCClient
}

func NewL1PriceData() *L1PriceData {
Expand All @@ -185,15 +186,22 @@ func init() {
}
}

func NewExecutionEngine(bc *core.BlockChain, syncTillBlock uint64, exposeMultiGas bool) (*ExecutionEngine, error) {
return &ExecutionEngine{
bc: bc,
resequenceChan: make(chan []*arbostypes.MessageWithMetadata),
newBlockNotifier: make(chan struct{}, 1),
cachedL1PriceData: NewL1PriceData(),
exposeMultiGas: exposeMultiGas,
syncTillBlock: syncTillBlock,
}, nil
func NewExecutionEngine(
bc *core.BlockChain,
syncTillBlock uint64,
exposeMultiGas bool,
transactionFiltererRPCClient *TransactionFiltererRPCClient,
) (*ExecutionEngine, error) {
execEngine := &ExecutionEngine{
bc: bc,
resequenceChan: make(chan []*arbostypes.MessageWithMetadata),
newBlockNotifier: make(chan struct{}, 1),
cachedL1PriceData: NewL1PriceData(),
exposeMultiGas: exposeMultiGas,
syncTillBlock: syncTillBlock,
transactionFiltererRPCClient: transactionFiltererRPCClient,
}
return execEngine, nil
}

func (s *ExecutionEngine) backlogCallDataUnits() uint64 {
Expand Down Expand Up @@ -846,6 +854,20 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith
}
// Check if any txs touched filtered addresses but are not in the onchain filter
if len(filteringHooks.FilteredTxHashes) > 0 {
if s.transactionFiltererRPCClient != nil {
s.LaunchThread(func(ctx context.Context) {
// Call transaction-filterer sequentially.
// To avoid nonce collisions when adding a tx to ArbFilteredTransactionsManager,
// transaction-filterer will process only one Filter call at a time.
for _, filteredTxHash := range filteringHooks.FilteredTxHashes {
_, err := s.transactionFiltererRPCClient.Filter(filteredTxHash).Await(ctx)
if err != nil {
log.Error("error reporting filtered tx to transaction-filterer", "filteredTxHash", filteredTxHash, "err", err)
}
}
})
}

return nil, nil, nil, &ErrFilteredDelayedMessage{
TxHashes: filteringHooks.FilteredTxHashes,
DelayedMsgIdx: msg.DelayedMessagesRead - 1,
Expand Down Expand Up @@ -1129,8 +1151,27 @@ func (s *ExecutionEngine) ArbOSVersionForMessageIndex(msgIdx arbutil.MessageInde
return containers.NewReadyPromise(extra.ArbOSFormatVersion, nil)
}

func (s *ExecutionEngine) Start(ctx_in context.Context) {
s.StopWaiter.Start(ctx_in, s)
func (s *ExecutionEngine) StopAndWait() {
if s.transactionFiltererRPCClient != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do this before the parent stopandwait.
Parent stopAndWait will kill parent context, and you'll end up with a non-ordered shutdown.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ExecutionEngine StopWaiter start/stop

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tsahee I am noop here regarding this pattern, but have question, why the order should be stopChild then stopParent?
I think parent might be in mid of thread sending request thru child but child already stoped, this will cause error during shutdown?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StopAndWait cancels the context, then waits for all threads to be done.
Child uses a context that's a child of parent context so once you call parent stopandwait, both parent and child context will be closed and all threads will end without particular order.
If parent creates threads that should be closed while child is still active - these threads should be done by a different child.

s.transactionFiltererRPCClient.StopAndWait()
}
s.StopWaiter.StopAndWait()
}

func (s *ExecutionEngine) Start(ctxIn context.Context) error {
s.StopWaiter.Start(ctxIn, s)

ctx, err := s.GetContextSafe()
if err != nil {
return err
}

if s.transactionFiltererRPCClient != nil {
err := s.transactionFiltererRPCClient.Start(ctx)
if err != nil {
return fmt.Errorf("failed to start transaction filterer RPC client: %w", err)
}
}

s.LaunchThread(func(ctx context.Context) {
for {
Expand Down Expand Up @@ -1186,6 +1227,8 @@ func (s *ExecutionEngine) Start(ctx_in context.Context) {
}
})
}

return nil
}

func (s *ExecutionEngine) ShouldTriggerMaintenance(trieLimitBeforeFlushMaintenance time.Duration) bool {
Expand Down
Loading
Loading