Skip to content

Address filtering for redeems#4352

Closed
Tristan-Wilson wants to merge 25 commits intomasterfrom
filter-submit-retryable
Closed

Address filtering for redeems#4352
Tristan-Wilson wants to merge 25 commits intomasterfrom
filter-submit-retryable

Conversation

@Tristan-Wilson
Copy link
Copy Markdown
Member

@Tristan-Wilson Tristan-Wilson commented Feb 9, 2026

Overview

This PR extends the address filtering system to handle retryable transactions
end-to-end. The existing filtering infrastructure covers regular L2 transactions
(via PostTxFilter and IsAddressFiltered), but retryables introduce several
gaps where funds or execution can reach filtered addresses unchecked.

There are four distinct challenges, each with its own solution:

  1. Filtered retryable submissions -- the retryable's outer fields
    (Beneficiary, FeeRefundAddr, RetryTo) may name filtered addresses. Solution:
    redirect funds to filteredFundsRecipient, still create the ticket, skip
    auto-redeem.

  2. Cascading redeems that touch filtered addresses -- the retryable's inner
    execution (auto-redeem or manual redeem) may CALL, CREATE, SELFDESTRUCT to,
    or emit events involving filtered addresses. This is the hardest problem
    because redeems are generated inside the STF and can't simply be dropped
    without causing consensus divergence. Solution: checkpoint-and-revert the
    entire transaction group.

  3. Event filter wiring for delayed messages -- the event filter (log-based
    address detection for Transfer, TransferSingle, TransferBatch) was only wired
    into the sequencer's postTxFilter, not into the delayed message path.
    Solution: plumb the event filter through to DelayedFilteringSequencingHooks.

  4. Delayed manual redeem filtering -- a signed L2 tx sent via the delayed
    inbox that calls ArbRetryableTx.redeem() needs the same protection as
    auto-redeems. Handled by the same group revert mechanism via
    DelayedFilteringSequencingHooks.RedeemFilter and ReportGroupRevert.


1. Filtered retryable submissions

Problem

PostTxFilter touches sender and tx.To() but not the retryable-specific
fields (Beneficiary, FeeRefundAddr, RetryTo). When the onchain filter
contains the tx hash, StartTxHook had no handling for the retryable case, so
funds would flow to filtered addresses.

Retryable submissions are L1 delayed messages -- they are force-included from L1
and must be processed. The user's ETH is already locked in the L1 bridge;
processing always mints it on L2 via MintBalance in StartTxHook. Rejecting
the submission would leave funds stuck in escrow with an unreachable beneficiary.

Solution

In StartTxHook for ArbitrumSubmitRetryableTx, check
IsFilteredFree(ticketId). When the tx hash is in the onchain filter:

  1. Beneficiary and FeeRefundAddr are redirected to
    filteredFundsRecipient (a new ArbOS state field with ArbOwner precompile
    accessors). Falls back to networkFeeAccount if no recipient is configured.
  2. The retryable ticket is still created -- with the redirected beneficiary.
  3. Auto-redeem scheduling is skipped entirely (the RetryData calldata may
    target filtered addresses).
  4. ErrFilteredTx is set as result.Err so that PostTxFilter can detect the
    tx was already handled by the onchain filter and skip re-halting the delayed
    sequencer.

The redirected beneficiary can later manually redeem if the inner execution is
clean, or the retryable expires and funds go to the redirected beneficiary.

A new touchRetryableAddresses() helper touches Beneficiary, FeeRefundAddr,
RetryTo, and their de-aliased versions (InverseRemapL1Address) in
PostTxFilter. This ensures the address filter detects retryable-specific
fields during the initial tentative pass (before the tx hash is in the onchain
filter).

Design decisions

  • Redirect instead of reject. L1 delayed messages cannot be dropped -- the
    user's ETH is locked in the L1 bridge and will be minted on L2 when processed.
    Redirecting Beneficiary/FeeRefundAddr keeps the
    retryable alive with funds flowing to a safe recipient.

  • Skip auto-redeem for filtered retryables. The RetryData calldata may
    target filtered addresses. Skipping the auto-redeem prevents the inner
    execution from touching those addresses. The redirected beneficiary can
    manually redeem if the inner execution is clean.

  • ErrFilteredTx marker in result.Err. Without this marker, PostTxFilter
    sees the original (still-filtered) Beneficiary via touchRetryableAddresses
    and re-halts the delayed sequencer. The error signals that the onchain filter
    already handled this tx.

  • De-aliased address touching. The L1 Inbox aliases contract addresses for
    Beneficiary and FeeRefundAddr. We touch both the aliased and original
    (InverseRemapL1Address) versions so filtering catches the L1 address.

  • GetInner() deep copy. The tx.Beneficiary mutation does not affect the tx
    hash used elsewhere because GetInner() returns a deep copy.

Implementation

  • arbos/tx_processor.go: StartTxHook for ArbitrumSubmitRetryableTx
    checks IsFilteredFree on the ticketId. If filtered, redirects
    FeeRefundAddr and Beneficiary to filteredFundsRecipient, sets
    ErrFilteredTx as result error, and skips auto-redeem scheduling. Removed
    unnecessary ArbOSVersion >= ArbosVersion_TransactionFiltering check from the
    IsFilteredFree call (matches RevertedTxHook pattern).

  • execution/gethexec/executionengine.go: Added touchRetryableAddresses to
    PostTxFilter: touches Beneficiary, FeeRefundAddr, RetryTo and their
    de-aliased versions.


2. Cascading redeem checkpoint-and-revert

Problem

When a retryable is redeemed (auto or manual), the ArbitrumRetryTx runs with
hooks = nil in the block processor, so PostTxFilter never fires. The EVM
execution can touch filtered addresses via CALL, CREATE, SELFDESTRUCT, or
STATICCALL, but nobody checks IsAddressFiltered() afterwards.

Simply dropping the redeem is not safe. The retryable's CreateRetryable runs
in StartTxHook, and the auto-redeem is scheduled by StartTxHook (via the
RedeemScheduled event) and executed as a follow-on transaction in the same
block. If we drop only the redeem at the sequencing level, the validator (which
doesn't run sequencing hooks) still executes it. The validator sees different state -- different receipts, different
gas accounting, different retryable numTries -- causing a consensus
divergence.

Solution

The block processor takes a statedb snapshot before each user transaction and
processes the entire group (user tx + all its auto-redeems) tentatively with
skipFinalise. A new RedeemFilter method on the SequencingHooks interface
is called after each redeem executes -- it runs the event filter on logs and
checks IsAddressFiltered(). If any redeem touches a filtered address,
RedeemFilter returns ErrArbTxFilter, and the entire group is reverted to the
snapshot.

The originating tx hash is reported to the sequencing hooks via
ReportGroupRevert, which halts the delayed sequencer (for delayed messages) or
returns an error to the RPC caller (for sequencer txs). The operator then adds
the tx hash to the onchain filter, and the submission re-processes through the
filtered retryable redirect path (section 1 above).

Design decisions

  • Revert the entire group, not just the redeem. The redeem executes as a
    follow-on transaction in the same block, generated inside
    ProduceBlockAdvanced via ScheduledTxes(). Dropping only the redeem would
    diverge from the validator path where the redeem always runs. Reverting the
    full group (user tx + all redeems) and re-processing with the onchain filter
    ensures both paths produce identical state.

  • skipFinalise is needed. StateDB.Finalise() promotes dirtyStorage to
    pendingStorage (not journaled), clears the journal, and zeros the refund
    counter. After Finalise, RevertToSnapshot cannot undo past that boundary.
    We skip Finalise while a group checkpoint is active so the entire group can
    be cleanly reverted. Finalise is flushed at group boundaries (before the
    next user tx or at end of block).

  • SubRefund is needed for consensus correctness. Without Finalise between
    txs in a tentative group, the EVM refund counter leaks across tx boundaries.
    calcRefund() reads statedb.GetRefund() (cumulative counter), and
    Prepare() does NOT reset it. A leaked refund would cause gasUsed values
    that differ from what per-tx Finalise would produce (the canonical gas
    accounting behavior). SubRefund drains the counter to zero before each tx,
    mimicking what Finalise would do. It's journaled, so group revert undoes it.

  • RedeemFilter via sequencingHooks not hooks. hooks is intentionally nil
    for redeems -- it gates sequencer policies (PreTxFilter nonce checking,
    PostTxFilter nonce cache updates/revert gas rejection, InsertLastTxError,
    DiscardInvalidTxsEarly) that don't apply to protocol-scheduled transactions.
    RedeemFilter is called on sequencingHooks (the function parameter, always
    non-nil) directly to get only the narrow redeem filtering behavior.

  • Dropping redeems is safe. State reverts via RevertToSnapshot. The
    retryable ticket survives (DeleteRetryable only runs on successful redeem in
    EndTxHook). Ticket can be manually redeemed later or expires to beneficiary.
    This is a sequencing-level decision -- NoopSequencingHooks.RedeemFilter
    returns nil during replay/validation.

  • Checkpoints are unconditional. Every user tx gets a checkpoint regardless
    of whether filtering is active. statedb.Snapshot() is a lightweight
    operation (appends to the journal's revision list). Avoiding a
    SupportsGroupRevert check simplifies the code and eliminates a class of
    bugs where the optimization flag gets out of sync.

  • Minimal interface additions. Only two new methods on SequencingHooks:
    RedeemFilter and ReportGroupRevert. NoopSequencingHooks provides no-op
    defaults so non-filtering paths pay no cost.

Implementation

  • go-ethereum (submodule bump): Added skipFinalise bool parameter to
    ApplyTransactionWithEVM and ApplyTransactionWithResultFilter. Existing
    callers (Process, ApplyTransaction, etc.) pass false explicitly. When
    true, skips Finalise() after a committed tx, keeping the journal and
    dirtyStorage intact for cross-tx snapshot revert.

  • arbos/block_processor.go: Added ErrFilteredCascadingRedeem error type
    carrying OriginatingTxHash. Added RedeemFilter and ReportGroupRevert to
    SequencingHooks interface, with no-op defaults on NoopSequencingHooks.
    Added groupCheckpoint struct (snapshot ID, header gas, block gas, expected
    balance delta, complete/receipts lengths, userTxsProcessed, gethGas pool,
    userTxHash) with lint:require-exhaustive-initialization. Added
    revertToGroupCheckpoint closure: reverts statedb, resets non-statedb values,
    clears tx filter, reopens ArbOS state, calls ReportGroupRevert. Checkpoint
    taken before each user tx, skipFinalise passed when group is active,
    Finalise flushed at group boundaries. SubRefund drains refund counter
    before each tx in a tentative group. On ErrArbTxFilter from a redeem, calls
    revertToGroupCheckpoint and continues the loop. If the user tx itself
    fails, the group checkpoint is deactivated -- no redeems will be generated, so
    there is nothing to revert. Added isRedeem flag; RedeemFilter called via
    sequencingHooks directly (not hooks, which is nil for redeems).

  • execution/gethexec/sequencer.go: Added redeemFilter field to
    FullSequencingHooks and MakeSequencingHooks.
    FullSequencingHooks.RedeemFilter delegates to the sequencer's filter.
    FullSequencingHooks.ReportGroupRevert replaces the last txErrors entry
    with the cascading redeem error, excluding the tx from the block and returning
    the error to the RPC caller. Sequencer.redeemFilter applies event filter and
    checks IsAddressFiltered.


3. Event filter wiring for delayed messages

Problem

The event filter (scanning Transfer, TransferSingle, TransferBatch log topics
for filtered addresses) only ran in the sequencer's postTxFilter, not in
DelayedFilteringSequencingHooks.PostTxFilter. Delayed transactions that emit
events involving filtered addresses would pass through undetected.

Solution

Plumb the eventFilter through to DelayedFilteringSequencingHooks:

  • NewDelayedFilteringSequencingHooks now accepts an *eventfilter.EventFilter
    parameter, stored on the struct.
  • PostTxFilter calls applyEventFilter() after touching sender/recipient
    addresses, before the IsAddressFiltered() check.
  • RedeemFilter also calls applyEventFilter() so event-based address
    detection works for both direct delayed txs and their cascading redeems.
  • ExecutionEngine.SetEventFilter stores the filter;
    createBlockFromNextMessage passes it to the hooks constructor.

A shared applyEventFilter() helper iterates the current tx's logs and calls
TouchAddress for each address returned by
EventFilter.AddressesForFiltering.

Implementation

  • execution/gethexec/executionengine.go: Added eventFilter field to
    ExecutionEngine and DelayedFilteringSequencingHooks. Added
    applyEventFilter helper. Plumbed through from sequencer construction via
    SetEventFilter.

4. Delayed manual redeem filtering

Problem

A signed L2 transaction sent via the delayed inbox that calls
ArbRetryableTx.redeem() needs the same protection as auto-redeems. Without
it, delayed manual redeems could touch filtered addresses with no detection.

Solution

Handled by the same checkpoint-and-revert mechanism from section 2.
DelayedFilteringSequencingHooks implements both RedeemFilter (applies event
filter, checks IsAddressFiltered) and ReportGroupRevert (extracts
OriginatingTxHash from ErrFilteredCascadingRedeem, appends to
FilteredTxHashes to trigger the delayed sequencer halt).

Note that for delayed manual redeems, the OriginatingTxHash reported is the
hash of the L2 tx that called redeem(), not the retryable's ticketId. This is
correct -- the onchain filter needs the hash of the tx being replayed, not the
retryable it targets.

Implementation

  • execution/gethexec/executionengine.go:
    DelayedFilteringSequencingHooks.RedeemFilter applies event filter and checks
    IsAddressFiltered. ReportGroupRevert extracts OriginatingTxHash and
    appends it to FilteredTxHashes, triggering the delayed sequencer halt.

Other implementation notes

contracts-local/src/mocks/AddressFilterTest.sol

Made selfDestructTo function payable so tests can send ETH with the call.

Tests

17 new tests added to system_tests/delayed_message_filter_test.go.

Shared helpers setupRetryableFilterTest, submitRetryableViaL1, and
verifyCascadingRedeemFiltered reduce duplication across the 17 tests. Existing
tests updated to use advanceL1ForDelayed (renamed from
advanceAndWaitForDelayed, removes sleep in favor of explicit wait mechanisms).

Filtered retryable submissions (section 1)

  • TestFilteredRetryableRedirectWithExplicitRecipient -- filtered beneficiary redirected to explicit filteredFundsRecipient
  • TestFilteredRetryableRedirectFallbackToNetworkFee -- filtered beneficiary falls back to networkFeeAccount when no recipient set
  • TestFilteredRetryableNoRedirectWhenNotFiltered -- clean retryable passes through unaffected
  • TestFilteredRetryableWithCallValue -- redirection works correctly with non-zero call value in escrow
  • TestFilteredRetryableSequencerDoesNotReHalt -- sequencer processes subsequent delayed messages after resolving a filtered retryable

Cascading redeem checkpoint-and-revert (section 2)

  • TestRetryableAutoRedeemCallsFilteredAddress -- auto-redeem CALLs filtered contract, group reverted
  • TestRetryableAutoRedeemCreatesAtFilteredAddress -- auto-redeem CREATEs at filtered address, no contract deployed
  • TestRetryableAutoRedeemSelfDestructsToFilteredAddress -- auto-redeem SELFDESTRUCTs to filtered beneficiary, no ETH transferred
  • TestRetryableAutoRedeemStaticCallsFilteredAddress -- auto-redeem STATICCALLs filtered contract, group reverted
  • TestRetryableAutoRedeemEmitsTransferToFilteredAddress -- event filter detects filtered address in log topic during redeem
  • TestManualRedeemGroupRevert -- manual redeem via L2 tx (FullSequencingHooks path), error returned to RPC caller

Cascading redeem state safety (section 2)

  • TestRetryableGroupRevertDoesNotAffectCleanRetryable -- clean retryable in prior block unaffected by subsequent dirty retryable
  • TestSequentialRetryableGroupReverts -- two dirty retryables trigger independent halt/resolve cycles
  • TestRetryableGroupRevertWithChainedRedeems -- chained redeem (A redeems B) with filtered inner execution, entire chain reverted
  • TestRetryableGroupRevertSkipFinaliseSafety -- tentative storage writes fully rolled back after group revert

Delayed manual redeem filtering (section 4)

  • TestDelayedManualRedeemGroupRevert -- manual redeem via delayed L2 tx, proves originatingTxHash != ticketId

Event filter integration (section 3)

  • TestDelayedMessageFilterCatchesEventFilter -- event filter detects filtered address in Transfer event topic on delayed tx

pulls in OffchainLabs/go-ethereum#623
fixes NIT-4453

Extend address filtering to cover ArbitrumSubmitRetryableTx, retryable
redeem execution, and event-based filtering in the delayed message path.

What was missing
----------------

ArbitrumSubmitRetryableTx filtering: PostTxFilter touches sender and tx.To()
but not the retryable-specific fields (Beneficiary, FeeRefundAddr, RetryTo).
When the onchain filter contains the tx hash, StartTxHook had no handling
for the retryable case, so funds would flow to filtered addresses.

Redeem inner execution filtering: When a retryable is redeemed (auto or
manual), the ArbitrumRetryTx runs with hooks = nil in the block processor,
so PostTxFilter never fires. The EVM execution touches filtered addresses
via PushContract/opSelfdestruct but nobody checks IsAddressFiltered()
afterwards.

Event filter in delayed path: The event filter (Transfer, TransferSingle,
TransferBatch log scanning) only ran in the sequencer's postTxFilter, not
in DelayedFilteringSequencingHooks.PostTxFilter.

Solution
--------

Filtered retryable redirect: In StartTxHook for ArbitrumSubmitRetryableTx,
when the tx hash is in the onchain filter, redirect Beneficiary and
FeeRefundAddr to a configurable filteredFundsRecipient (new ArbOS state
field, with ArbOwner precompile accessors, fallback to networkFeeAccount).
Skip auto-redeem scheduling. Set ErrFilteredTx as result.Err so PostTxFilter
knows to skip re-halting.

RedeemFilter: New RedeemFilter(*state.StateDB) error method on the
SequencingHooks interface. Called in the block processor's result filter
closure when the current tx is a redeem. Runs the event filter on logs then
checks IsAddressFiltered(). Returns ErrArbTxFilter to revert the snapshot
and drop the redeem from the block.

Delayed event filter: Pass the event filter to
DelayedFilteringSequencingHooks. Shared applyEventFilter() helper called in
both PostTxFilter and RedeemFilter.

PostTxFilter retryable field touching: New touchRetryableAddresses() helper
touches Beneficiary, FeeRefundAddr, RetryTo, and their de-aliased versions
(InverseRemapL1Address). Called in both sequencer and delayed PostTxFilter.

Design Decisions
----------------

Redirect instead of reject: Retryable submissions are L1 delayed messages
that cannot be rejected. Funds are already deposited on L2. Rejecting would
leave them stuck in escrow with an unreachable beneficiary.

Skip auto-redeem for filtered retryables: The RetryData calldata may target
filtered addresses. The redirected beneficiary can manually redeem if
appropriate.

ErrFilteredTx in result.Err: Without this marker, PostTxFilter sees the
original (still-filtered) Beneficiary via touchRetryableAddresses and
re-halts. The error signals that the onchain filter already handled this tx.

RedeemFilter via sequencingHooks not hooks: hooks is intentionally nil for
redeems - it gates sequencer policies (PreTxFilter nonce checking,
PostTxFilter nonce cache updates/revert gas rejection, InsertLastTxError,
DiscardInvalidTxsEarly) that don't apply to protocol-scheduled transactions.
RedeemFilter is called on sequencingHooks (the function parameter, always
non-nil) directly to get only the narrow redeem filtering behavior.

Dropping redeems is safe: State reverts via RevertToSnapshot. The retryable
ticket survives (DeleteRetryable only runs on successful redeem in
EndTxHook). Ticket can be manually redeemed later or expires to beneficiary.
This is a sequencing-level decision - NoopSequencingHooks.RedeemFilter
returns nil during replay/validation.

De-aliased address touching: The L1 Inbox aliases contract addresses for
Beneficiary and FeeRefundAddr. We touch both the aliased and original
(InverseRemapL1Address) versions so filtering catches the L1 address.

DeleteFree commented out: For symmetry with other filtered tx paths,
deletion from the onchain filter is handled by the external tx authority
service.

Tests (11 new):
---------------

Retryable redirect (halt-and-wait pattern):
- TestFilteredRetryableRedirectWithExplicitRecipient
- TestFilteredRetryableRedirectFallbackToNetworkFee
- TestFilteredRetryableNoRedirectWhenNotFiltered
- TestFilteredRetryableWithCallValue
- TestFilteredRetryableSequencerDoesNotReHalt

RedeemFilter (verify redeem dropped, ticket survives):
- TestRetryableAutoRedeemCallsFilteredAddress
- TestRetryableAutoRedeemCreatesAtFilteredAddress
- TestRetryableAutoRedeemSelfDestructsToFilteredAddress
- TestRetryableAutoRedeemStaticCallsFilteredAddress
- TestRetryableAutoRedeemEmitsTransferToFilteredAddress
- TestDelayedMessageFilterCatchesEventFilter

Delayed event filter:
- TestDelayedMessageFilterCatchesEventFilter
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 9, 2026

Codecov Report

❌ Patch coverage is 60.00000% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.99%. Comparing base (0b4a8fa) to head (219e906).
⚠️ Report is 45 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4352      +/-   ##
==========================================
- Coverage   34.73%   32.99%   -1.75%     
==========================================
  Files         489      493       +4     
  Lines       58125    58457     +332     
==========================================
- Hits        20190    19285     -905     
- Misses      34347    35799    +1452     
+ Partials     3588     3373     -215     

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 10, 2026

❌ 7 Tests Failed:

Tests completed Failed Passed Skipped
4293 7 4286 0
View the top 3 failed tests by shortest run time
TestValidationInputsAtWithWasmTarget
Stack Traces | 4.390s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
        	/opt/hostedtoolcache/go/1.25.6/x64/src/testing/testing.go:1997 +0x465
        
    validation_inputs_at_test.go:70: �[31;1m [] failed calculating position for validation: batch not found on L1 yet �[0;0m
INFO [02-25|14:22:52.981] InboxTracker                             sequencerBatchCount=5 messageCount=8  l1Block=32 l1Timestamp=2026-02-25T14:22:52+0000
INFO [02-25|14:22:52.982] Submitted transaction                    hash=0x92f6e314fbffc757c935a0f32cac79fd728793580d75e96a438cc64838862e00 from=0xb386a74Dcab67b66F8AC07B4f08365d37495Dd23 nonce=7  recipient=0xaa1695807a9fD9E660bBf79Dd0ddeE1B391d0Fa2 value=0
INFO [02-25|14:22:52.985] DataPoster sent transaction              nonce=7  hash=92f6e3..862e00 feeCap=19,733,247,140 tipCap=50,000,000 blobFeeCap=<nil> gas=166,744
INFO [02-25|14:22:52.986] BatchPoster: batch sent                  sequenceNumber=8 from=9 to=17 prevDelayed=1 currentDelayed=1 totalSegments=10 numBlobs=0
INFO [02-25|14:22:52.983] Starting work on payload                 id=0x03614b76beb1c361
INFO [02-25|14:22:52.987] Updated payload                          id=0x03614b76beb1c361 number=35 hash=7d36b8..9d35fc txs=1  withdrawals=0 gas=154,352    fees=7.7176e-06    root=fa1e98..08f821 elapsed=1.533ms
INFO [02-25|14:22:52.988] Stopping work on payload                 id=0x03614b76beb1c361 reason=delivery
INFO [02-25|14:22:52.989] Submitted transaction                    hash=0x5026a6f03f659b9b1add86c90da5a79c03033c04a1d71d3fc5ef2a0d5c41d19b from=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 nonce=11 recipient=0x0000000000000000000000000000000000000071 value=1,000,000,000,000,000,000
INFO [02-25|14:22:52.990] Submitted transaction                    hash=0xa01fd2a0d20a7c1b8e9199404170cb2f30db0420bab30baee1c968d8a2c4f4d5 from=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 nonce=22 recipient=0x457b1BA688E9854BDbed2f473F7510C476A3dA09 value=0
INFO [02-25|14:22:52.991] Log index head rendering in progress     firstblock=0 lastblock=1 processed=2 remaining=0 elapsed=1.834s
INFO [02-25|14:22:52.991] Log index head rendering finished        firstblock=0 lastblock=1 processed=2 elapsed=1.835s
INFO [02-25|14:22:52.993] InboxTracker                             sequencerBatchCount=5 messageCount=11 l1Block=31 l1Timestamp=2026-02-25T14:22:52+0000
�[90mTime to activate keccak: 702.123113ms�[0;0m
INFO [02-25|14:22:52.994] Ethereum protocol stopped
INFO [02-25|14:22:52.994] Transaction pool stopped
INFO [02-25|14:22:52.994] Persisting dirty state                   head=30 root=de1669..4bb994 layers=30
--- FAIL: TestValidationInputsAtWithWasmTarget (4.39s)
TestVersion40
Stack Traces | 11.570s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:94 +0x371
        github.com/offchainlabs/nitro/system_tests.TestVersion40(0xc026bf81c0?)
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:71 +0x64b
        testing.tRunner(0xc026bf81c0, 0x3d45f38)
        	/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-25|14:23:00.367] Updated payload                          id=0x03361ed6b088dd9b number=46 hash=8940f1..a8043a txs=2  withdrawals=0 gas=171,281    fees=0.00209941531  root=40594e..96febc elapsed=1.356ms
INFO [02-25|14:23:00.367] Writing cached state to disk             block=1  hash=025ee6..86c328 root=d45d64..4e7811
INFO [02-25|14:23:00.368] Persisted trie from memory database      nodes=23  flushnodes=0 size=3.61KiB   flushsize=0.00B time="115.005µs" flushtime=0s gcnodes=0 gcsize=0.00B gctime="2.775µs"  livenodes=0   livesize=0.00B
INFO [02-25|14:23:00.368] Writing cached state to disk             block=1  hash=025ee6..86c328 root=d45d64..4e7811
INFO [02-25|14:23:00.368] Persisted trie from memory database      nodes=0   flushnodes=0 size=0.00B     flushsize=0.00B time=481ns       flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0   livesize=0.00B
INFO [02-25|14:23:00.368] Writing snapshot state to disk           root=28fb26..40a768
INFO [02-25|14:23:00.368] Persisted trie from memory database      nodes=0   flushnodes=0 size=0.00B     flushsize=0.00B time=391ns       flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0   livesize=0.00B
INFO [02-25|14:23:00.368] Stopping work on payload                 id=0x03361ed6b088dd9b reason=delivery
INFO [02-25|14:23:00.368] Blockchain stopped
--- FAIL: TestVersion40 (11.57s)
WARN [02-25|14:23:00.369] Served eth_call                          reqid=12  duration=7.47600881s  err="execution aborted (timeout = 5s)"
TestVersion30
Stack Traces | 11.570s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
        	/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-25|14:23:00.369] Imported new potential chain segment     number=46 hash=8940f1..a8043a blocks=1  txs=2  mgas=0.171  elapsed=1.870ms      mgasps=91.593   triediffs=232.20KiB triedirty=0.00B
INFO [02-25|14:23:00.369] Chain head was updated                   number=46 hash=8940f1..a8043a root=40594e..96febc elapsed="115.706µs"
INFO [02-25|14:23:00.369] DataPoster sent transaction              nonce=12 hash=f8a656..427e2f feeCap=10,451,040,190 tipCap=50,000,000    blobFeeCap=<nil> gas=154,769
INFO [02-25|14:23:00.370] Writing cached state to disk             block=1  hash=b9c9f6..8a4312 root=8448b4..050cee
INFO [02-25|14:23:00.370] BatchPoster: batch sent                  sequenceNumber=13 from=26 to=27 prevDelayed=1 currentDelayed=1 totalSegments=3  numBlobs=0
INFO [02-25|14:23:00.370] Persisted trie from memory database      nodes=20  flushnodes=0 size=3.26KiB   flushsize=0.00B time="96.79µs"   flushtime=0s gcnodes=0 gcsize=0.00B gctime="2.565µs"  livenodes=0   livesize=0.00B
INFO [02-25|14:23:00.370] Writing cached state to disk             block=1  hash=b9c9f6..8a4312 root=8448b4..050cee
INFO [02-25|14:23:00.370] Persisted trie from memory database      nodes=0   flushnodes=0 size=0.00B     flushsize=0.00B time=501ns       flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0   livesize=0.00B
INFO [02-25|14:23:00.370] Writing snapshot state to disk           root=6b754c..7398ca
INFO [02-25|14:23:00.370] Persisted trie from memory database      nodes=0   flushnodes=0 size=0.00B     flushsize=0.00B time=381ns       flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0   livesize=0.00B
INFO [02-25|14:23:00.370] Submitted transaction                    hash=0xa7cb816e77dc5f50f9b7459bfc51a052816d8f587d728c7ae5f65142ad97fb90 from=0xb386a74Dcab67b66F8AC07B4f08365d37495Dd23 nonce=5  recipient=0xaa1695807a9fD9E660bBf79Dd0ddeE1B391d0Fa2 value=0
INFO [02-25|14:23:00.370] Blockchain stopped
INFO [02-25|14:23:00.370] DataPoster sent transaction              nonce=5  hash=a7cb81..97fb90 feeCap=13,282,087,920 tipCap=50,000,000    blobFeeCap=<nil> gas=154,389
INFO [02-25|14:23:00.370] BatchPoster: batch sent                  sequenceNumber=6  from=6  to=7  prevDelayed=1 currentDelayed=1 totalSegments=3  numBlobs=0
INFO [02-25|14:23:00.370] Submitted transaction                    hash=0x2a95f9aabe75203afdbe4284cebd2b46de36b622c24ea8c83764fc2930ca2774 from=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 nonce=26 recipient=0x457b1BA688E9854BDbed2f473F7510C476A3dA09 value=0
INFO [02-25|14:23:00.371] Submitted transaction                    hash=0xe3c051bb3e73a8cf3be560d8f759fa78e499cc165d0483bb543a028323f6bf02 from=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 nonce=23 recipient=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 value=1
--- FAIL: TestVersion30 (11.57s)

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

Base automatically changed from filtered-funds-recipient to master February 10, 2026 15:22
@Tristan-Wilson Tristan-Wilson marked this pull request as draft February 11, 2026 18:37
@diegoximenes
Copy link
Copy Markdown
Contributor

diegoximenes commented Feb 12, 2026

Removing myself from assigned set while this is in draft.

@diegoximenes diegoximenes removed their assignment Feb 12, 2026
When a retryable auto-redeem's inner execution touches a filtered address,
simply dropping the redeem causes consensus divergence: redeems are generated
inside ProduceBlockAdvanced via ScheduledTxes(), so during replay the redeem
re-executes (NoopSequencingHooks.RedeemFilter returns nil), producing a
different state root than the sequencer's block.

The fix is checkpoint-and-revert: take a state snapshot before each user tx
and process it with all its redeems tentatively (skipFinalise). If any redeem
triggers RedeemFilter, revert the entire group (user tx + all redeems) so the
redeem is never generated in the first place. Both sequencer and replay then
see the same block without the tx, maintaining consensus.

For the delayed path, the group revert reports the originating tx hash via
ReportGroupRevert, which halts the delayed sequencer. The the hash is added to
the onchain filter via the transaction-filterer service, and the
submission re-processes with redirected beneficiary and no auto-redeem.

Key design decisions:
- skipFinalise defers statedb.Finalise during tentative group processing so
  that RevertToSnapshot can cleanly undo the entire group across tx boundaries
  (Finalise destroys the journal and promotes dirtyStorage to pendingStorage,
  making cross-tx revert impossible)
- SubRefund drains the EVM refund counter before each tx in a tentative group,
  mimicking what Finalise normally does - without this, the leaked refund
  causes GasUsed divergence (consensus break). SubRefund is journaled so group
  revert restores it automatically
- ReportGroupRevert is a new SequencingHooks method that lets the block
  processor signal a group revert to the hooks layer without coupling to
  specific hook implementations
Comment thread arbos/block_processor.go Outdated
if err != nil {
// Cascading redeem filtering: if a redeem was filtered and we have an
// active group checkpoint, revert the entire group (user tx + all redeems)
if isRedeem && activeGroupCP != nil && errors.Is(err, state.ErrArbTxFilter) {
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.

(as discussed)
use revertGroupCheckpoint even for non-redeems. They'll just be a group of size 1.

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.

Any outer error now triggers revertToGroupCheckpoint unconditionally. revertToGroupCheckpoint is split from ReportGroupRevert so that ReportGroupRevert is only called for filter errors on redeems. Fixed in 696d550

Comment thread arbos/block_processor.go Outdated
}

// Take group checkpoint before processing user tx
if isUserTx {
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.

I think it'll work better if you start a group for any non-redeem.
The one other case is first tx, which can only revert if things go very wrong, and and never issues a redeem - but having a group for firstTx will make it easy to always revert groups.

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.

Every non-redeem now gets a group checkpoint, fixed in 696d550

Comment thread arbos/block_processor.go Outdated
// sequencingHooks directly to get the narrow redeem filtering behavior
// without enabling those other policies.
if isRedeem {
return sequencingHooks.RedeemFilter(statedb)
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.

(as discussed)
Try to still use hooks.PostTxFilter for redeem.
That will mean not setting hooks to nil, and replacing all the places of checking if hooks is nil with adding to hooks a function like "nextTxIsRedeem" or something and have hooks act accordingly. Possibly block_processotr can also just check directly isRedeem

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.

Redeems now go through PostTxFilter with isRedeem=true instead of the separate RedeemFilter. hooks is no longer set to nil for redeems; the 5 sites that used hooks != nil as a proxy for "is user tx" now check isUserTx directly. Fixed in 696d550

1. Universal group checkpoints: every non-redeem tx (including firstTx)
now gets a group checkpoint, not just user txs. Since activeGroupCP is
now always non-nil during tx processing, its nil guards on SubRefund,
SkipFinalise, and the error handling block are removed.

2. Uniform group revert for all errors: any outer error now triggers
revertToGroupCheckpoint, not just filter errors on redeems.
revertToGroupCheckpoint is split from ReportGroupRevert so callers
control reporting: filter errors on redeems call ReportGroupRevert
(replacing the user tx's nil txErrors entry with
ErrFilteredCascadingRedeem), while filter errors on user txs and
non-filter errors skip it (preserving the correct error already in
txErrors from InsertLastTxError).

3. Route redeems through PostTxFilter: redeems now use
hooks.PostTxFilter(isRedeem=true) instead of the separate RedeemFilter
method. hooks is no longer set to nil for redeems; the 5 sites that
used "hooks != nil" as a proxy for "is user tx" now check isUserTx
directly. RedeemFilter is removed from the SequencingHooks interface.
Tristan-Wilson and others added 4 commits February 17, 2026 19:25
The skipFinalise parameter was being passed unconditionally as true for
all ArbOS versions, which breaks consensus for pre-ArbOS 60 blocks. The
issue is that skipping Finalise between the submit-retryable tx and its
auto-redeem prevents the CreateZombieIfDeleted mechanism from firing:
without the intermediate Finalise, empty escrow accounts are never added
to stateObjectsDestruct, so the zombie preservation logic (needed for
ArbOS < 30) doesn't trigger. This causes empty retryable escrow accounts
to be deleted at end-of-block Finalise, diverging from the state produced
by existing nodes that call Finalise between every tx.

The group checkpoint mechanism (skipFinalise, SubRefund drain, group
revert) exists solely for the cascading redeem filtering feature, which
is gated behind ArbosVersion_TransactionFiltering (60). This commit gates
the entire mechanism behind the same version check, preserving the legacy
per-tx Finalise behavior for older ArbOS versions.

- Add useGroupCheckpoints flag based on ArbOS version >= 60
- Conditionally create group checkpoints and skip Finalise only when flag is set
- Restore original non-zero refund fatal error for older ArbOS versions
- Guard group revert logic behind activeGroupCP != nil check
@Tristan-Wilson Tristan-Wilson changed the base branch from master to filter-submit-retryable-outer-tx-only February 18, 2026 19:33
Base automatically changed from filter-submit-retryable-outer-tx-only to master February 19, 2026 01:16
@Tristan-Wilson Tristan-Wilson changed the title Address filtering for retryable submissions, redeems, and delayed events Address filtering for redeems Feb 19, 2026
Comment thread execution/gethexec/executionengine.go Outdated
// For non-redeems: touches To/From addresses, applies event filter, and collects
// tx hashes that touch filtered addresses but are not in the onchain filter.
func (f *DelayedFilteringSequencingHooks) PostTxFilter(header *types.Header, db *state.StateDB, a *arbosState.ArbosState, tx *types.Transaction, sender common.Address, dataGas uint64, result *core.ExecutionResult, isRedeem bool) error {
if isRedeem {
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.

I don;t think this if (or even bool input) is necessary? can redeems can use the same logic here?

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.

Got rid of the bool param in bd91bc3 and moved the gating of special logic for redeems/non redeems later in 63d3ef4

Comment thread arbos/block_processor.go Outdated
PostTxFilter(*types.Header, *state.StateDB, *arbosState.ArbosState, *types.Transaction, common.Address, uint64, *core.ExecutionResult, bool) error
BlockFilter(*types.Header, *state.StateDB, types.Transactions, types.Receipts) error
InsertLastTxError(error)
ReportGroupRevert(error)
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 use this chance to solidify the API a little.
InsertLastTxError and ReportGroupRevert should be merged to a single function.. something like DesequenceLastTx(err).
And now instead of api being "must call InsertLastTxError exactly once after every "NextTxToSequence" It'll just be "DesequenceLastTx only handles last NextTxToSequence". That'll require some restructure of other parts but worth it.

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.

I also think that DesequenceLastTx could return a boolean (true/false) that says if the tx was really desequenced and that'll replace DiscardInvalidTxsEarly

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.

I ended up with three methods instead of two, but shifted around the responsibilities in a way that I think is cleaner than the original.

  • TxSucceeded() - records success (replacing InsertLastTxError(nil))
  • TxFailed(error) - records failure (replacing InsertLastTxError(err) and ReportGroupRevert(err)
  • CanDiscardTx() bool - this is just a renaming of DiscardInvalidTxsEarly() to make it clear it's a static propery

I tried merging TxFailed and CanDiscardTx into a single DesequenceLastTx(error) bool as you suggested but I felt like the name was misleading because it didn't actually desequence anything, it just recorded the error and returned whether to discard the tx or not. I think splitting them makes it read more naturally.

Here's the change 6298406

Comment thread arbos/block_processor.go
// between txs so each starts with refund=0. A nonzero starting refund
// here would cause GasUsed divergence (consensus break). SubRefund
// drains the counter to 0, mimicking Finalise. It's journaled, so
// group revert undoes it.
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.

good, very necessary comment. thanks!

Comment thread arbos/block_processor.go Outdated
// older versions would break consensus by changing empty-account
// lifecycle behavior (CreateZombieIfDeleted depends on intermediate
// Finalise calls to populate stateObjectsDestruct).
useGroupCheckpoints := arbState.ArbOSVersion() >= params.ArbosVersion_TransactionFiltering
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.

if the problem is with zombie accounts, they were fixed after arbos 30, so keep the comment but use arbos30 as limit. I will want to see re-execution / validation of old arbos with that logic to make sure we're not introducing any new weird behavior

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.

Fixed in 746118f

The tests TestSubmitRetryableEmptyEscrowArbOS20 and TestSubmitRetryableEmptyEscrowArbOS30 fail without this gate and are passing now. Do you think this is enough or is there more you think we should test?

Comment thread arbos/block_processor.go Outdated
func(result *core.ExecutionResult) error {
if hooks != nil {
return hooks.PostTxFilter(header, statedb, arbState, tx, sender, dataGas, result)
if hooks != nil { // nil only for firstTx (ArbitrumInternalTxType)
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.

I really don't like that there is even a difference between "hooks" and "sequencinghooks". We should always use the same hooks in the loop.
We can just make sure PostTxFilter will always return nil for ArbitrumInternalTxType because we never want to filer these

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.

Removed the separate hooks var and checking for ArbitrumInternalTxType in the impls 60b5372

Comment thread arbos/block_processor.go Outdated
blockGasLeft = activeGroupCP.blockGasLeft
expectedBalanceDelta.Set(activeGroupCP.expectedBalanceDelta)
complete = complete[:activeGroupCP.completeLen]
receipts = receipts[:activeGroupCP.receiptsLen]
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.

I keep thinking if there's a way to do the opposite, instead of removing items from complete/receipts in case of failure, storing separate "current group" arrays and appending them to the main array in case of success.
not sure this is a better direction.. but sharing it here.

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.

I tried it out and I think it looks slightly nicer because I could roll the statedb.Finalize in with it ca7d4d9

mahdy-nasr and others added 15 commits February 23, 2026 11:52
…github.com:OffchainLabs/nitro into add-retryable-test-scenarios-for-address-filtering
PostTxFilter implementations can derive this from the tx type
(ArbitrumRetryTxType) instead of receiving it as a parameter.
Both implementations now run TouchAddress, alias de-aliasing, and event
filter application unconditionally for all tx types. The isRedeem early
return moves below the shared section, before non-redeem-specific logic
(nonce cache, revert gas rejection, filtered tx hash collection).
TestSubmitRetryableEmptyEscrowArbOS20 and ArbOS30 both pass.
Instead of appending to complete/receipts during tentative group
processing and truncating on revert, accumulate in separate
groupComplete/groupReceipts slices and promote them on commit.
Always use sequencingHooks directly. PostTxFilter implementations now
return nil for ArbitrumInternalTxType instead of relying on a nil
hooks guard at the call site.
Split the old InsertLastTxError/ReportGroupRevert/DiscardInvalidTxsEarly
into TxSucceeded/TxFailed/CanDiscardTx so success and failure are recorded
on separate code paths instead of always appending then overwriting.
…os-for-address-filtering

add retrylabe tests scenarios for address filtering
@Tristan-Wilson
Copy link
Copy Markdown
Member Author

This approach is too dependent on the internals of statedb and is thus too fragile to upstream changes. Back to the drawing board (at least we can keep the tests).

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.

4 participants