Add prechecker address filtering via dry-run ProduceBlockAdvanced#4382
Add prechecker address filtering via dry-run ProduceBlockAdvanced#4382Tristan-Wilson wants to merge 9 commits intofilter-submit-retryablefrom
Conversation
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 Report❌ Patch coverage is 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 |
❌ 6 Tests Failed:
View the top 3 failed tests by shortest run time
📣 Thoughts on this report? Let Codecov know! | Powered by Codecov |
MishkaRogachev
left a comment
There was a problem hiding this comment.
Overall the approach using hooks ProduceBlockAdvanced in dry-run mode makes a lot of sense, I just have a few small questions
| } | ||
| } | ||
|
|
||
| ef, err := eventfilter.NewEventFilterFromConfig(config.TransactionFiltering.EventFilter) |
There was a problem hiding this comment.
nit: ef -> eventFilter
btw, should we merge event filter to (address) FilterService? I can start a dedicated PR for that
There was a problem hiding this comment.
Yes to merging eventfilter into FilterService.
| } | ||
| select { | ||
| case <-timeoutCtx.Done(): | ||
| t.Fatalf("forwarder did not reach block %d within timeout", targetBlock) |
There was a problem hiding this comment.
nit: Require(t, err) since testify/require is already used in this test
- 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
MishkaRogachev
left a comment
There was a problem hiding this comment.
Looks good, only the changelog remains
tsahee
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Let's remove sender from here, from AddressForFiltering where it's not used, and from the chain of calls
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