Skip to content

Commit 688a889

Browse files
Add prechecker address filtering via dry-run ProduceBlockAdvanced
Add address filtering to the prechecker by running the full block production pipeline on a throwaway statedb. For each RPC-submitted transaction, when filtering is enabled, the prechecker calls ProduceBlockAdvanced(dryRun=true) through a thin PrefiltererSequencingHooks adapter that implements the same TouchAddress/IsAddressFiltered logic the sequencer uses. This catches direct address touches, direct redeems, contract-triggered redeems (where a contract internally calls ArbRetryableTx.redeem), event-filter hits, and cascading redeem chains -- all with zero coupling to retryable internals. The approach is unified: every tx goes through ProduceBlockAdvanced rather than trying to detect redeem-related transactions statically. A regular contract can call ArbRetryableTx.redeem(ticketId) internally, making static detection impossible. Running the real block processor as a black box sidesteps this entirely. Key design decisions: - No isRedeemTx gate: the unified approach runs ALL txs through ProduceBlockAdvanced when filtering is enabled. A static gate on To == 0x6e misses contract-triggered redeems. The only gate is addressChecker != nil. - No statedb.Copy(): each PublishTransaction opens its own statedb via bc.StateAt, never uses it after the dry-run, and ProduceBlockAdvanced doesn't call Commit. The statedb is passed directly and GC'd on return. This eliminates the main cost objection to running the full pipeline. - dryRun mode: a new bool parameter on ProduceBlockAdvanced causes an early return after the tx processing loop, skipping FinalizeBlock (which calls IntermediateRoot -- likely the dominant fixed overhead per call), receipt construction, block assembly, and balance delta checks. The prechecker only needs hooks.filtered, not a valid block. - Forwarder-only wiring: on the sequencer node, the sequencer already returns ErrArbTxFilter synchronously from real block production so the prechecker dry-run is redundant. The addressChecker/eventFilter are only wired to TxPreChecker on forwarder nodes (which don't run a sequencer and relay to a remote sequencer after prechecking). - Config refactor: TransactionFilteringConfig is moved from SequencerConfig to a top-level execution.transaction-filtering.* namespace. Forwarders don't have a Sequencer component, so they couldn't access the filter config when it lived under execution.sequencer. The new location makes it available to any node role.
1 parent 66d78e5 commit 688a889

File tree

10 files changed

+756
-88
lines changed

10 files changed

+756
-88
lines changed

arbos/block_processor.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ func ProduceBlock(
198198
hooks := NewNoopSequencingHooks(txes)
199199

200200
return ProduceBlockAdvanced(
201-
message.Header, delayedMessagesRead, lastBlockHeader, statedb, chainContext, hooks, isMsgForPrefetch, runCtx, exposeMultiGas,
201+
message.Header, delayedMessagesRead, lastBlockHeader, statedb, chainContext, hooks, isMsgForPrefetch, runCtx, exposeMultiGas, false,
202202
)
203203
}
204204

@@ -213,6 +213,7 @@ func ProduceBlockAdvanced(
213213
isMsgForPrefetch bool,
214214
runCtx *core.MessageRunContext,
215215
exposeMultiGas bool,
216+
dryRun bool,
216217
) (*types.Block, types.Receipts, error) {
217218

218219
arbState, err := arbosState.OpenSystemArbosState(statedb, nil, false)
@@ -638,6 +639,13 @@ func ProduceBlockAdvanced(
638639
}
639640
}
640641

642+
if dryRun {
643+
// Filtering decisions are already recorded in the hooks.
644+
// Skip all post-loop finalization -- the caller only needs
645+
// hooks state, not a valid block.
646+
return nil, nil, nil
647+
}
648+
641649
// Flush deferred Finalise for the last clean group
642650
if activeGroupCP != nil {
643651
statedb.Finalise(true)

contracts-local/src/mocks/AddressFilterTest.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ contract AddressFilterTest {
6969
return address(uint160(uint256(hash)));
7070
}
7171

72+
/// @notice Redeems a retryable ticket by calling ArbRetryableTx.redeem()
73+
function redeemTicket(
74+
bytes32 ticketId
75+
) external {
76+
// ArbRetryableTx lives at address 110 (0x6e)
77+
(bool success,) = address(110).call(abi.encodeWithSignature("redeem(bytes32)", ticketId));
78+
require(success, "redeem failed");
79+
}
80+
7281
/// @notice Selfdestructs this contract and sends balance to beneficiary
7382
function selfDestructTo(
7483
address payable beneficiary

execution/gethexec/executionengine.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ func (s *ExecutionEngine) sequenceTransactionsWithBlockMutex(header *arbostypes.
683683
false,
684684
core.NewMessageCommitContext(s.wasmTargets),
685685
s.exposeMultiGas,
686+
false,
686687
)
687688
if err != nil {
688689
return nil, err
@@ -908,6 +909,7 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith
908909
isMsgForPrefetch,
909910
runCtx,
910911
s.exposeMultiGas,
912+
false,
911913
)
912914
if err != nil {
913915
return nil, nil, nil, err

execution/gethexec/node.go

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import (
3434
"github.com/offchainlabs/nitro/consensus"
3535
"github.com/offchainlabs/nitro/consensus/consensusrpcclient"
3636
"github.com/offchainlabs/nitro/execution"
37+
"github.com/offchainlabs/nitro/execution/gethexec/addressfilter"
38+
"github.com/offchainlabs/nitro/execution/gethexec/eventfilter"
3739
executionrpcserver "github.com/offchainlabs/nitro/execution/rpcserver"
3840
"github.com/offchainlabs/nitro/solgen/go/precompilesgen"
3941
"github.com/offchainlabs/nitro/util"
@@ -118,25 +120,26 @@ func TxIndexerConfigAddOptions(prefix string, f *pflag.FlagSet) {
118120
}
119121

120122
type Config struct {
121-
ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"`
122-
Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"`
123-
RecordingDatabase BlockRecorderConfig `koanf:"recording-database"`
124-
TxPreChecker TxPreCheckerConfig `koanf:"tx-pre-checker" reload:"hot"`
125-
Forwarder ForwarderConfig `koanf:"forwarder"`
126-
ForwardingTarget string `koanf:"forwarding-target"`
127-
SecondaryForwardingTarget []string `koanf:"secondary-forwarding-target"`
128-
Caching CachingConfig `koanf:"caching"`
129-
RPC arbitrum.Config `koanf:"rpc"`
130-
TxIndexer TxIndexerConfig `koanf:"tx-indexer"`
131-
EnablePrefetchBlock bool `koanf:"enable-prefetch-block"`
132-
SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"`
133-
StylusTarget StylusTargetConfig `koanf:"stylus-target"`
134-
BlockMetadataApiCacheSize uint64 `koanf:"block-metadata-api-cache-size"`
135-
BlockMetadataApiBlocksLimit uint64 `koanf:"block-metadata-api-blocks-limit"`
136-
VmTrace LiveTracingConfig `koanf:"vmtrace"`
137-
ExposeMultiGas bool `koanf:"expose-multi-gas"`
138-
RPCServer rpcserver.Config `koanf:"rpc-server"`
139-
ConsensusRPCClient rpcclient.ClientConfig `koanf:"consensus-rpc-client" reload:"hot"`
123+
ParentChainReader headerreader.Config `koanf:"parent-chain-reader" reload:"hot"`
124+
Sequencer SequencerConfig `koanf:"sequencer" reload:"hot"`
125+
RecordingDatabase BlockRecorderConfig `koanf:"recording-database"`
126+
TxPreChecker TxPreCheckerConfig `koanf:"tx-pre-checker" reload:"hot"`
127+
TransactionFiltering TransactionFilteringConfig `koanf:"transaction-filtering" reload:"hot"`
128+
Forwarder ForwarderConfig `koanf:"forwarder"`
129+
ForwardingTarget string `koanf:"forwarding-target"`
130+
SecondaryForwardingTarget []string `koanf:"secondary-forwarding-target"`
131+
Caching CachingConfig `koanf:"caching"`
132+
RPC arbitrum.Config `koanf:"rpc"`
133+
TxIndexer TxIndexerConfig `koanf:"tx-indexer"`
134+
EnablePrefetchBlock bool `koanf:"enable-prefetch-block"`
135+
SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"`
136+
StylusTarget StylusTargetConfig `koanf:"stylus-target"`
137+
BlockMetadataApiCacheSize uint64 `koanf:"block-metadata-api-cache-size"`
138+
BlockMetadataApiBlocksLimit uint64 `koanf:"block-metadata-api-blocks-limit"`
139+
VmTrace LiveTracingConfig `koanf:"vmtrace"`
140+
ExposeMultiGas bool `koanf:"expose-multi-gas"`
141+
RPCServer rpcserver.Config `koanf:"rpc-server"`
142+
ConsensusRPCClient rpcclient.ClientConfig `koanf:"consensus-rpc-client" reload:"hot"`
140143

141144
forwardingTarget string
142145
}
@@ -168,6 +171,9 @@ func (c *Config) Validate() error {
168171
if err := c.ConsensusRPCClient.Validate(); err != nil {
169172
return fmt.Errorf("error validating ConsensusRPCClient config: %w", err)
170173
}
174+
if err := c.TransactionFiltering.Validate(); err != nil {
175+
return err
176+
}
171177
return nil
172178
}
173179

@@ -181,6 +187,7 @@ func ConfigAddOptions(prefix string, f *pflag.FlagSet) {
181187
f.StringSlice(prefix+".secondary-forwarding-target", ConfigDefault.SecondaryForwardingTarget, "secondary transaction forwarding target URL")
182188
AddOptionsForNodeForwarderConfig(prefix+".forwarder", f)
183189
TxPreCheckerConfigAddOptions(prefix+".tx-pre-checker", f)
190+
TransactionFilteringConfigAddOptions(prefix+".transaction-filtering", f)
184191
CachingConfigAddOptions(prefix+".caching", f)
185192
SyncMonitorConfigAddOptions(prefix+".sync-monitor", f)
186193
f.Bool(prefix+".enable-prefetch-block", ConfigDefault.EnablePrefetchBlock, "enable prefetching of blocks")
@@ -217,6 +224,7 @@ var ConfigDefault = Config{
217224
ForwardingTarget: "",
218225
SecondaryForwardingTarget: []string{},
219226
TxPreChecker: DefaultTxPreCheckerConfig,
227+
TransactionFiltering: DefaultTransactionFilteringConfig,
220228
Caching: DefaultCachingConfig,
221229
Forwarder: DefaultNodeForwarderConfig,
222230
SyncMonitor: DefaultSyncMonitorConfig,
@@ -262,6 +270,8 @@ type ExecutionNode struct {
262270
started atomic.Bool
263271
bulkBlockMetadataFetcher *BulkBlockMetadataFetcher
264272
consensusRPCClient *consensusrpcclient.ConsensusRPCClient
273+
addressFilterService *addressfilter.FilterService
274+
eventFilter *eventfilter.EventFilter
265275
}
266276

267277
func CreateExecutionNode(
@@ -318,6 +328,23 @@ func CreateExecutionNode(
318328
}
319329
}
320330

331+
ef, err := eventfilter.NewEventFilterFromConfig(config.TransactionFiltering.EventFilter)
332+
if err != nil {
333+
return nil, err
334+
}
335+
addressFilterService, err := addressfilter.NewFilterService(ctx, &config.TransactionFiltering.AddressFilter)
336+
if err != nil {
337+
return nil, fmt.Errorf("failed to create address filter service: %w", err)
338+
}
339+
340+
if config.Sequencer.Enable && config.TransactionFiltering.TransactionFiltererRPCClient.URL != "" {
341+
filtererConfigFetcher := func() *rpcclient.ClientConfig {
342+
return &configFetcher.Get().TransactionFiltering.TransactionFiltererRPCClient
343+
}
344+
transactionFiltererRPCClient := NewTransactionFiltererRPCClient(filtererConfigFetcher)
345+
execEngine.SetTransactionFiltererRPCClient(transactionFiltererRPCClient)
346+
}
347+
321348
txprecheckConfigFetcher := func() *TxPreCheckerConfig { return &configFetcher.Get().TxPreChecker }
322349

323350
txPreChecker := NewTxPreChecker(txPublisher, l2BlockChain, txprecheckConfigFetcher)
@@ -371,6 +398,8 @@ func CreateExecutionNode(
371398
ParentChainReader: parentChainReader,
372399
ClassicOutbox: classicOutbox,
373400
bulkBlockMetadataFetcher: bulkBlockMetadataFetcher,
401+
addressFilterService: addressFilterService,
402+
eventFilter: ef,
374403
}
375404

376405
if config.ConsensusRPCClient.URL != "" {
@@ -458,6 +487,11 @@ func (n *ExecutionNode) Initialize(ctx context.Context) error {
458487
if err != nil {
459488
return fmt.Errorf("error initializing transaction publisher: %w", err)
460489
}
490+
if n.addressFilterService != nil {
491+
if err = n.addressFilterService.Initialize(ctx); err != nil {
492+
return fmt.Errorf("error initializing address filter service: %w", err)
493+
}
494+
}
461495
err = n.Backend.APIBackend().SetSyncBackend(n.SyncMonitor)
462496
if err != nil {
463497
return fmt.Errorf("error setting sync backend: %w", err)
@@ -488,6 +522,24 @@ func (n *ExecutionNode) Start(ctxIn context.Context) error {
488522
return fmt.Errorf("error starting execution engine: %w", err)
489523
}
490524

525+
if n.addressFilterService != nil {
526+
n.addressFilterService.Start(ctx)
527+
checker := n.addressFilterService.GetAddressChecker()
528+
if n.Sequencer != nil {
529+
n.ExecEngine.SetAddressChecker(checker)
530+
} else {
531+
n.TxPreChecker.SetAddressChecker(checker)
532+
}
533+
}
534+
if n.eventFilter != nil {
535+
if n.Sequencer != nil {
536+
n.ExecEngine.SetEventFilter(n.eventFilter)
537+
n.Sequencer.SetEventFilter(n.eventFilter)
538+
} else {
539+
n.TxPreChecker.SetEventFilter(n.eventFilter)
540+
}
541+
}
542+
491543
err = n.TxPublisher.Start(ctx)
492544
if err != nil {
493545
return fmt.Errorf("error starting transaction puiblisher: %w", err)
@@ -509,6 +561,9 @@ func (n *ExecutionNode) StopAndWait() {
509561
if n.TxPublisher.Started() {
510562
n.TxPublisher.StopAndWait()
511563
}
564+
if n.addressFilterService != nil {
565+
n.addressFilterService.StopAndWait()
566+
}
512567
n.Recorder.OrderlyShutdown()
513568
if n.ParentChainReader != nil && n.ParentChainReader.Started() {
514569
n.ParentChainReader.StopAndWait()
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2026, Offchain Labs, Inc.
2+
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md
3+
4+
package gethexec
5+
6+
import (
7+
"github.com/ethereum/go-ethereum/arbitrum_types"
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/core"
10+
"github.com/ethereum/go-ethereum/core/state"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
"github.com/ethereum/go-ethereum/params"
13+
14+
"github.com/offchainlabs/nitro/arbos"
15+
"github.com/offchainlabs/nitro/arbos/arbosState"
16+
"github.com/offchainlabs/nitro/execution/gethexec/eventfilter"
17+
)
18+
19+
// PrefiltererSequencingHooks implements arbos.SequencingHooks for the
20+
// prechecker's dry-run filtering. It feeds a single candidate tx into
21+
// ProduceBlockAdvanced and collects address filtering results.
22+
type PrefiltererSequencingHooks struct {
23+
tx *types.Transaction
24+
delivered bool
25+
txError error
26+
filtered bool
27+
eventFilter *eventfilter.EventFilter
28+
}
29+
30+
func (h *PrefiltererSequencingHooks) NextTxToSequence() (*types.Transaction, *arbitrum_types.ConditionalOptions, error) {
31+
if h.delivered {
32+
return nil, nil, nil
33+
}
34+
h.delivered = true
35+
return h.tx, nil, nil
36+
}
37+
38+
func (h *PrefiltererSequencingHooks) DiscardInvalidTxsEarly() bool {
39+
return true
40+
}
41+
42+
func (h *PrefiltererSequencingHooks) PreTxFilter(
43+
_ *params.ChainConfig,
44+
_ *types.Header,
45+
statedb *state.StateDB,
46+
_ *arbosState.ArbosState,
47+
tx *types.Transaction,
48+
_ *arbitrum_types.ConditionalOptions,
49+
sender common.Address,
50+
_ *arbos.L1Info,
51+
) error {
52+
statedb.TouchAddress(sender)
53+
if tx.To() != nil {
54+
statedb.TouchAddress(*tx.To())
55+
}
56+
if statedb.IsAddressFiltered() {
57+
h.filtered = true
58+
return state.ErrArbTxFilter
59+
}
60+
return nil
61+
}
62+
63+
func (h *PrefiltererSequencingHooks) PostTxFilter(
64+
_ *types.Header,
65+
statedb *state.StateDB,
66+
_ *arbosState.ArbosState,
67+
_ *types.Transaction,
68+
sender common.Address,
69+
_ uint64,
70+
_ *core.ExecutionResult,
71+
) error {
72+
// Inline event filtering with actual sender, matching the real sequencer's
73+
// postTxFilter. Do NOT use applyEventFilter here -- it passes
74+
// common.Address{} as sender, which would miss sender-dependent rules.
75+
if h.eventFilter != nil {
76+
logs := statedb.GetCurrentTxLogs()
77+
for _, l := range logs {
78+
for _, addr := range h.eventFilter.AddressesForFiltering(l.Topics, l.Data, l.Address, sender) {
79+
statedb.TouchAddress(addr)
80+
}
81+
}
82+
}
83+
// The real sequencer's postTxFilter also checks statedb.IsTxFiltered(),
84+
// which is the onchain per-tx-hash filter for delayed messages. We omit
85+
// it here because the prechecker only processes RPC-submitted txs, never
86+
// delayed messages, so IsTxFiltered() would never fire.
87+
if statedb.IsAddressFiltered() {
88+
h.filtered = true
89+
return state.ErrArbTxFilter
90+
}
91+
return nil
92+
}
93+
94+
func (h *PrefiltererSequencingHooks) RedeemFilter(statedb *state.StateDB) error {
95+
applyEventFilter(h.eventFilter, statedb)
96+
if statedb.IsAddressFiltered() {
97+
h.filtered = true
98+
return state.ErrArbTxFilter
99+
}
100+
return nil
101+
}
102+
103+
func (h *PrefiltererSequencingHooks) InsertLastTxError(err error) {
104+
h.txError = err
105+
}
106+
107+
func (h *PrefiltererSequencingHooks) ReportGroupRevert(err error) {
108+
h.filtered = true
109+
}
110+
111+
func (h *PrefiltererSequencingHooks) BlockFilter(
112+
_ *types.Header, _ *state.StateDB, _ types.Transactions, _ types.Receipts,
113+
) error {
114+
return nil
115+
}

0 commit comments

Comments
 (0)