Skip to content

Add prechecker address filtering via dry-run ProduceBlockAdvanced#4382

Closed
Tristan-Wilson wants to merge 9 commits intofilter-submit-retryablefrom
prefilter-produceblock-dryrun
Closed

Add prechecker address filtering via dry-run ProduceBlockAdvanced#4382
Tristan-Wilson wants to merge 9 commits intofilter-submit-retryablefrom
prefilter-produceblock-dryrun

Conversation

@Tristan-Wilson
Copy link
Copy Markdown
Member

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.

fixes NIT-4344

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.
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 15.50388% with 109 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.92%. Comparing base (013a240) to head (94a69e5).
⚠️ Report is 63 commits behind head on filter-submit-retryable.

Additional details and impacted files
@@                     Coverage Diff                     @@
##           filter-submit-retryable    #4382      +/-   ##
===========================================================
+ Coverage                    32.83%   32.92%   +0.09%     
===========================================================
  Files                          489      490       +1     
  Lines                        58211    58292      +81     
===========================================================
+ Hits                         19111    19193      +82     
+ Misses                       35760    35709      -51     
- Partials                      3340     3390      +50     

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 13, 2026

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
4254 6 4248 0
View the top 3 failed tests by shortest run time
TestNitroNodeVersionAlerter
Stack Traces | 0.240s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
INFO [02-19|18:44:11.901] Imported new potential chain segment     number=494 hash=f991bc..17f5a7 blocks=1  txs=1   mgas=0.021 elapsed=1.663ms      mgasps=12.624   triediffs=659.76KiB triedirty=211.64KiB
INFO [02-19|18:44:11.901] Chain head was updated                   number=494 hash=f991bc..17f5a7 root=2c7ca9..3aa96a elapsed="69.41µs"
INFO [02-19|18:44:11.905] Submitted transaction                    hash=0x7722ddb5ec84d4925bca4c5671345035c5e08cea57bdb78ce143d2e5d3bdd876 from=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 nonce=79  recipient=0x0C709F340F0BB2e361229e345B7e26999d0969Ab value=1
INFO [02-19|18:44:11.906] Deploying validator wallet creator contract
INFO [02-19|18:44:11.908] Submitted transaction                    hash=0xeae1916e6a01f17201e24ae1d1b73a45df7a3c8c748829e525e41c3cc4a71af7 from=0xaF24Ca6c2831f4d4F629418b50C227DF0885613A nonce=430 recipient=0x7E23C8862920797d81916d62c274dd9217113e28 value=1,000,000,000,000
INFO [02-19|18:44:11.908] Starting work on payload                 id=0x0346b35b13765c10
INFO [02-19|18:44:11.909] Updated payload                          id=0x0346b35b13765c10 number=495 hash=c75607..8451dc txs=1   withdrawals=0 gas=21000     fees=0.0021         root=5c5a87..6f53f9 elapsed="709.864µs"
INFO [02-19|18:44:11.910] Stopping work on payload                 id=0x0346b35b13765c10 reason=delivery
INFO [02-19|18:44:11.911] Imported new potential chain segment     number=495 hash=c75607..8451dc blocks=1  txs=1   mgas=0.021 elapsed=1.262ms      mgasps=16.628   triediffs=660.00KiB triedirty=212.12KiB
INFO [02-19|18:44:11.911] Chain head was updated                   number=495 hash=c75607..8451dc root=5c5a87..6f53f9 elapsed="54.401µs"
INFO [02-19|18:44:11.912] Submitted contract creation              hash=0x94381394c53f7b46c6d8a9026b0db383dad621827f6b2be7e14d883203b7e993 from=0x57Ff0F473737a1c161bfF9efDF016F7991585088 nonce=21  contract=0x5E6d7135B0a5F12bF935A83B7d5b5e4a8BFf112d value=0
INFO [02-19|18:44:11.914] Starting work on payload                 id=0x0330251e5ca98b2f
INFO [02-19|18:44:11.914] Updated payload                          id=0x0330251e5ca98b2f number=23  hash=3e6f57..c49f07 txs=1   withdrawals=0 gas=3,088,290 fees=3.08829e-06    root=11bb74..daf683 elapsed="812.415µs"
INFO [02-19|18:44:11.915] Stopping work on payload                 id=0x0330251e5ca98b2f reason=delivery
INFO [02-19|18:44:11.915] Submitted transaction                    hash=0xbafd73e6768db6fc6a6c6da8bd01b02d8a76df2b304e13312924ee5dc0154bd7 from=0xaF24Ca6c2831f4d4F629418b50C227DF0885613A nonce=431 recipient=0x7E23C8862920797d81916d62c274dd9217113e28 value=1,000,000,000,000
INFO [02-19|18:44:11.915] Submitted transaction                    hash=0xc9729c0a5acdfc7a7434452e9175f9a77f4ed48ebcec06bab4227090b6db477f from=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 nonce=80  recipient=0x0C709F340F0BB2e361229e345B7e26999d0969Ab value=1
INFO [02-19|18:44:11.916] Imported new potential chain segment     number=23  hash=3e6f57..c49f07 blocks=1  txs=1   mgas=3.088 elapsed=1.315ms      mgasps=2347.271 triediffs=84.71KiB  triedirty=0.00B
INFO [02-19|18:44:11.916] Starting work on payload                 id=0x03314f856dcf57fa
INFO [02-19|18:44:11.917] Chain head was updated                   number=23  hash=3e6f57..c49f07 root=11bb74..daf683 elapsed="269.263µs"
--- FAIL: TestNitroNodeVersionAlerter (0.24s)
TestBlocksReExecutorCommitState
Stack Traces | 2.070s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
INFO [02-19|18:36:50.192] 
INFO [02-19|18:36:50.192] Pre-Merge hard forks (block based):
INFO [02-19|18:36:50.192]  - Homestead:                   #0       
INFO [02-19|18:36:50.192]  - Tangerine Whistle (EIP 150): #0       
INFO [02-19|18:36:50.192]  - Spurious Dragon/1 (EIP 155): #0       
INFO [02-19|18:36:50.192]  - Spurious Dragon/2 (EIP 158): #0       
INFO [02-19|18:36:50.192]  - Byzantium:                   #0       
INFO [02-19|18:36:50.192]  - Constantinople:              #0       
INFO [02-19|18:36:50.192]  - Petersburg:                  #0       
INFO [02-19|18:36:50.192]  - Istanbul:                    #0       
INFO [02-19|18:36:50.192]  - Muir Glacier:                #0       
INFO [02-19|18:36:50.192]  - Berlin:                      #0       
INFO [02-19|18:36:50.192]  - London:                      #0       
INFO [02-19|18:36:50.192]  - Arrow Glacier:               #0       
INFO [02-19|18:36:50.192]  - Gray Glacier:                #0       
INFO [02-19|18:36:50.192] 
INFO [02-19|18:36:50.192] Merge configured:
INFO [02-19|18:36:50.192]  - Total terminal difficulty:  0
INFO [02-19|18:36:50.192] 
--- FAIL: TestBlocksReExecutorCommitState (2.07s)
TestVersion40
Stack Traces | 11.610s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
        github.com/offchainlabs/nitro/system_tests.Require(0xc012069500, {0x40c0a60, 0xc0e4045e30}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/system_tests/common_test.go:2075 +0x5d
        github.com/offchainlabs/nitro/system_tests.testPrecompiles(0xc012069500, 0x28, {0xc096823df8, 0x5, 0x39?})
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:94 +0x371
        github.com/offchainlabs/nitro/system_tests.TestVersion40(0xc012069500?)
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:71 +0x64b
        testing.tRunner(0xc012069500, 0x3d39378)
        	/opt/hostedtoolcache/go/1.25.6/x64/src/testing/testing.go:1934 +0xea
        created by testing.(*T).Run in goroutine 1
        	/opt/hostedtoolcache/go/1.25.6/x64/src/testing/testing.go:1997 +0x465
        
    precompile_inclusion_test.go:94: �[31;1m [] execution aborted (timeout = 5s) �[0;0m
INFO [02-19|18:42:48.487] Writing cached state to disk             block=1  hash=403f8e..95a6f6 root=d45d64..4e7811
INFO [02-19|18:42:48.487] Persisted trie from memory database      nodes=23 flushnodes=0 size=3.61KiB  flushsize=0.00B time="116.207µs" flushtime=0s gcnodes=0 gcsize=0.00B gctime="1.542µs"  livenodes=0   livesize=0.00B
INFO [02-19|18:42:48.487] Writing cached state to disk             block=1  hash=403f8e..95a6f6 root=d45d64..4e7811
INFO [02-19|18:42:48.487] Persisted trie from memory database      nodes=0  flushnodes=0 size=0.00B    flushsize=0.00B time="1.192µs"   flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0   livesize=0.00B
INFO [02-19|18:42:48.487] Writing snapshot state to disk           root=28fb26..40a768
INFO [02-19|18:42:48.487] Persisted trie from memory database      nodes=0  flushnodes=0 size=0.00B    flushsize=0.00B time=742ns       flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0   livesize=0.00B
INFO [02-19|18:42:48.487] Blockchain stopped
--- FAIL: TestVersion40 (11.61s)

📣 Thoughts on this report? Let Codecov know! | Powered by Codecov

Copy link
Copy Markdown
Contributor

@MishkaRogachev MishkaRogachev left a comment

Choose a reason for hiding this comment

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

Overall the approach using hooks ProduceBlockAdvanced in dry-run mode makes a lot of sense, I just have a few small questions

Comment thread execution/gethexec/node.go Outdated
}
}

ef, err := eventfilter.NewEventFilterFromConfig(config.TransactionFiltering.EventFilter)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: ef -> eventFilter
btw, should we merge event filter to (address) FilterService? I can start a dedicated PR for that

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes to merging eventfilter into FilterService.

Comment thread execution/gethexec/node.go
Comment thread execution/gethexec/prefilterer_hooks.go Outdated
Comment thread execution/gethexec/tx_pre_checker.go
Comment thread system_tests/prechecker_filter_test.go Outdated
}
select {
case <-timeoutCtx.Done():
t.Fatalf("forwarder did not reach block %d within timeout", targetBlock)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Require(t, err) since testify/require is already used in this test

Tristan-Wilson and others added 2 commits February 18, 2026 15:05
- Rename ef -> eventFilter in node.go
- Add comment explaining intentional else (avoid double filtering)
- Remove unused txError field from PrefiltererSequencingHooks
- Add isRedeem bool param to PostTxFilter to match interface
- Add defensive nil check for addressChecker in dryRunFilter
- Use require.NoError instead of t.Fatalf in test helper
Copy link
Copy Markdown
Contributor

@MishkaRogachev MishkaRogachev left a comment

Choose a reason for hiding this comment

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

Looks good, only the changelog remains

Comment thread execution/gethexec/node.go Outdated
Comment thread execution/gethexec/node.go
Comment thread execution/gethexec/prefilterer_hooks.go Outdated
Comment thread execution/gethexec/prefilterer_hooks.go Outdated
Comment thread execution/gethexec/prefilterer_hooks.go Outdated
@diegoximenes diegoximenes assigned tsahee and unassigned diegoximenes Feb 20, 2026
Copy link
Copy Markdown
Contributor

@tsahee tsahee left a comment

Choose a reason for hiding this comment

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

initial. Need another look

logs := db.GetCurrentTxLogs()
for _, l := range logs {
for _, addr := range ef.AddressesForFiltering(l.Topics, l.Data, l.Address, common.Address{}) {
for _, addr := range ef.AddressesForFiltering(l.Topics, l.Data, l.Address, sender) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's remove sender from here, from AddressForFiltering where it's not used, and from the chain of calls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants