Conversation
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 Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4352 +/- ##
==========================================
- Coverage 34.73% 32.76% -1.98%
==========================================
Files 489 493 +4
Lines 58125 58457 +332
==========================================
- Hits 20190 19151 -1039
- Misses 34347 35960 +1613
+ Partials 3588 3346 -242 |
❌ 393 Tests Failed:
View the top 3 failed tests by shortest run time
📣 Thoughts on this report? Let Codecov know! | Powered by Codecov |
|
Removing myself from assigned set while this is in draft. |
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
8178bf1 to
66d78e5
Compare
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) { |
There was a problem hiding this comment.
(as discussed)
use revertGroupCheckpoint even for non-redeems. They'll just be a group of size 1.
There was a problem hiding this comment.
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
arbos/block_processor.go
Outdated
| } | ||
|
|
||
| // Take group checkpoint before processing user tx | ||
| if isUserTx { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Every non-redeem now gets a group checkpoint, fixed in 696d550
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) |
There was a problem hiding this comment.
(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
There was a problem hiding this comment.
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.
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
…ios-for-address-filtering
| // 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 { |
There was a problem hiding this comment.
I don;t think this if (or even bool input) is necessary? can redeems can use the same logic here?
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I also think that DesequenceLastTx could return a boolean (true/false) that says if the tx was really desequenced and that'll replace DiscardInvalidTxsEarly
There was a problem hiding this comment.
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 (replacingInsertLastTxError(nil))TxFailed(error)- records failure (replacingInsertLastTxError(err)andReportGroupRevert(err)CanDiscardTx() bool- this is just a renaming ofDiscardInvalidTxsEarly()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
| // 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. |
There was a problem hiding this comment.
good, very necessary comment. thanks!
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Removed the separate hooks var and checking for ArbitrumInternalTxType in the impls 60b5372
arbos/block_processor.go
Outdated
| blockGasLeft = activeGroupCP.blockGasLeft | ||
| expectedBalanceDelta.Set(activeGroupCP.expectedBalanceDelta) | ||
| complete = complete[:activeGroupCP.completeLen] | ||
| receipts = receipts[:activeGroupCP.receiptsLen] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I tried it out and I think it looks slightly nicer because I could roll the statedb.Finalize in with it ca7d4d9
…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.
…ios-for-address-filtering
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.
…ios-for-address-filtering
…os-for-address-filtering add retrylabe tests scenarios for address filtering
Overview
This PR extends the address filtering system to handle retryable transactions
end-to-end. The existing filtering infrastructure covers regular L2 transactions
(via
PostTxFilterandIsAddressFiltered), but retryables introduce severalgaps where funds or execution can reach filtered addresses unchecked.
There are four distinct challenges, each with its own solution:
Filtered retryable submissions -- the retryable's outer fields
(Beneficiary, FeeRefundAddr, RetryTo) may name filtered addresses. Solution:
redirect funds to
filteredFundsRecipient, still create the ticket, skipauto-redeem.
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.
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.Delayed manual redeem filtering -- a signed L2 tx sent via the delayed
inbox that calls
ArbRetryableTx.redeem()needs the same protection asauto-redeems. Handled by the same group revert mechanism via
DelayedFilteringSequencingHooks.RedeemFilterandReportGroupRevert.1. Filtered retryable submissions
Problem
PostTxFiltertouchessenderandtx.To()but not the retryable-specificfields (
Beneficiary,FeeRefundAddr,RetryTo). When the onchain filtercontains the tx hash,
StartTxHookhad no handling for the retryable case, sofunds 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
MintBalanceinStartTxHook. Rejectingthe submission would leave funds stuck in escrow with an unreachable beneficiary.
Solution
In
StartTxHookforArbitrumSubmitRetryableTx, checkIsFilteredFree(ticketId). When the tx hash is in the onchain filter:BeneficiaryandFeeRefundAddrare redirected tofilteredFundsRecipient(a new ArbOS state field with ArbOwner precompileaccessors). Falls back to
networkFeeAccountif no recipient is configured.RetryDatacalldata maytarget filtered addresses).
ErrFilteredTxis set asresult.Errso thatPostTxFiltercan detect thetx 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 touchesBeneficiary,FeeRefundAddr,RetryTo, and their de-aliased versions (InverseRemapL1Address) inPostTxFilter. This ensures the address filter detects retryable-specificfields 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
RetryDatacalldata maytarget 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,
PostTxFiltersees the original (still-filtered) Beneficiary via
touchRetryableAddressesand 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.Beneficiarymutation does not affect the txhash used elsewhere because
GetInner()returns a deep copy.Implementation
arbos/tx_processor.go:
StartTxHookforArbitrumSubmitRetryableTxchecks
IsFilteredFreeon the ticketId. If filtered, redirectsFeeRefundAddrandBeneficiarytofilteredFundsRecipient, setsErrFilteredTxas result error, and skips auto-redeem scheduling. Removedunnecessary
ArbOSVersion >= ArbosVersion_TransactionFilteringcheck from theIsFilteredFreecall (matchesRevertedTxHookpattern).execution/gethexec/executionengine.go: Added
touchRetryableAddressestoPostTxFilter: touchesBeneficiary,FeeRefundAddr,RetryToand theirde-aliased versions.
2. Cascading redeem checkpoint-and-revert
Problem
When a retryable is redeemed (auto or manual), the
ArbitrumRetryTxruns withhooks = nilin the block processor, soPostTxFilternever fires. The EVMexecution 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
CreateRetryablerunsin
StartTxHook, and the auto-redeem is scheduled byStartTxHook(via theRedeemScheduledevent) and executed as a follow-on transaction in the sameblock. 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 consensusdivergence.
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 newRedeemFiltermethod on theSequencingHooksinterfaceis called after each redeem executes -- it runs the event filter on logs and
checks
IsAddressFiltered(). If any redeem touches a filtered address,RedeemFilterreturnsErrArbTxFilter, and the entire group is reverted to thesnapshot.
The originating tx hash is reported to the sequencing hooks via
ReportGroupRevert, which halts the delayed sequencer (for delayed messages) orreturns 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
ProduceBlockAdvancedviaScheduledTxes(). Dropping only the redeem woulddiverge 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()promotesdirtyStoragetopendingStorage(not journaled), clears the journal, and zeros the refundcounter. After
Finalise,RevertToSnapshotcannot undo past that boundary.We skip
Finalisewhile a group checkpoint is active so the entire group canbe cleanly reverted.
Finaliseis flushed at group boundaries (before thenext user tx or at end of block).
SubRefund is needed for consensus correctness. Without
Finalisebetweentxs in a tentative group, the EVM refund counter leaks across tx boundaries.
calcRefund()readsstatedb.GetRefund()(cumulative counter), andPrepare()does NOT reset it. A leaked refund would causegasUsedvaluesthat differ from what per-tx
Finalisewould produce (the canonical gasaccounting behavior).
SubRefunddrains the counter to zero before each tx,mimicking what
Finalisewould do. It's journaled, so group revert undoes it.RedeemFilter via sequencingHooks not hooks.
hooksis intentionally nilfor 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.
RedeemFilteris called onsequencingHooks(the function parameter, alwaysnon-nil) directly to get only the narrow redeem filtering behavior.
Dropping redeems is safe. State reverts via
RevertToSnapshot. Theretryable ticket survives (
DeleteRetryableonly runs on successful redeem inEndTxHook). Ticket can be manually redeemed later or expires to beneficiary.This is a sequencing-level decision --
NoopSequencingHooks.RedeemFilterreturns nil during replay/validation.
Checkpoints are unconditional. Every user tx gets a checkpoint regardless
of whether filtering is active.
statedb.Snapshot()is a lightweightoperation (appends to the journal's revision list). Avoiding a
SupportsGroupRevertcheck simplifies the code and eliminates a class ofbugs where the optimization flag gets out of sync.
Minimal interface additions. Only two new methods on
SequencingHooks:RedeemFilterandReportGroupRevert.NoopSequencingHooksprovides no-opdefaults so non-filtering paths pay no cost.
Implementation
go-ethereum (submodule bump): Added
skipFinalise boolparameter toApplyTransactionWithEVMandApplyTransactionWithResultFilter. Existingcallers (
Process,ApplyTransaction, etc.) passfalseexplicitly. Whentrue, skips
Finalise()after a committed tx, keeping the journal anddirtyStorageintact for cross-tx snapshot revert.arbos/block_processor.go: Added
ErrFilteredCascadingRedeemerror typecarrying
OriginatingTxHash. AddedRedeemFilterandReportGroupReverttoSequencingHooksinterface, with no-op defaults onNoopSequencingHooks.Added
groupCheckpointstruct (snapshot ID, header gas, block gas, expectedbalance delta, complete/receipts lengths, userTxsProcessed, gethGas pool,
userTxHash) with
lint:require-exhaustive-initialization. AddedrevertToGroupCheckpointclosure: reverts statedb, resets non-statedb values,clears tx filter, reopens ArbOS state, calls
ReportGroupRevert. Checkpointtaken before each user tx,
skipFinalisepassed when group is active,Finaliseflushed at group boundaries.SubRefunddrains refund counterbefore each tx in a tentative group. On
ErrArbTxFilterfrom a redeem, callsrevertToGroupCheckpointand continues the loop. If the user tx itselffails, the group checkpoint is deactivated -- no redeems will be generated, so
there is nothing to revert. Added
isRedeemflag;RedeemFiltercalled viasequencingHooksdirectly (nothooks, which is nil for redeems).execution/gethexec/sequencer.go: Added
redeemFilterfield toFullSequencingHooksandMakeSequencingHooks.FullSequencingHooks.RedeemFilterdelegates to the sequencer's filter.FullSequencingHooks.ReportGroupRevertreplaces the lasttxErrorsentrywith the cascading redeem error, excluding the tx from the block and returning
the error to the RPC caller.
Sequencer.redeemFilterapplies event filter andchecks
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 inDelayedFilteringSequencingHooks.PostTxFilter. Delayed transactions that emitevents involving filtered addresses would pass through undetected.
Solution
Plumb the
eventFilterthrough toDelayedFilteringSequencingHooks:NewDelayedFilteringSequencingHooksnow accepts an*eventfilter.EventFilterparameter, stored on the struct.
PostTxFiltercallsapplyEventFilter()after touching sender/recipientaddresses, before the
IsAddressFiltered()check.RedeemFilteralso callsapplyEventFilter()so event-based addressdetection works for both direct delayed txs and their cascading redeems.
ExecutionEngine.SetEventFilterstores the filter;createBlockFromNextMessagepasses it to the hooks constructor.A shared
applyEventFilter()helper iterates the current tx's logs and callsTouchAddressfor each address returned byEventFilter.AddressesForFiltering.Implementation
eventFilterfield toExecutionEngineandDelayedFilteringSequencingHooks. AddedapplyEventFilterhelper. Plumbed through from sequencer construction viaSetEventFilter.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. Withoutit, delayed manual redeems could touch filtered addresses with no detection.
Solution
Handled by the same checkpoint-and-revert mechanism from section 2.
DelayedFilteringSequencingHooksimplements bothRedeemFilter(applies eventfilter, checks
IsAddressFiltered) andReportGroupRevert(extractsOriginatingTxHashfromErrFilteredCascadingRedeem, appends toFilteredTxHashesto trigger the delayed sequencer halt).Note that for delayed manual redeems, the
OriginatingTxHashreported is thehash of the L2 tx that called
redeem(), not the retryable's ticketId. This iscorrect -- the onchain filter needs the hash of the tx being replayed, not the
retryable it targets.
Implementation
DelayedFilteringSequencingHooks.RedeemFilterapplies event filter and checksIsAddressFiltered.ReportGroupRevertextractsOriginatingTxHashandappends it to
FilteredTxHashes, triggering the delayed sequencer halt.Other implementation notes
contracts-local/src/mocks/AddressFilterTest.sol
Made
selfDestructTofunctionpayableso tests can send ETH with the call.Tests
17 new tests added to
system_tests/delayed_message_filter_test.go.Shared helpers
setupRetryableFilterTest,submitRetryableViaL1, andverifyCascadingRedeemFilteredreduce duplication across the 17 tests. Existingtests updated to use
advanceL1ForDelayed(renamed fromadvanceAndWaitForDelayed, removes sleep in favor of explicit wait mechanisms).Filtered retryable submissions (section 1)
TestFilteredRetryableRedirectWithExplicitRecipient-- filtered beneficiary redirected to explicitfilteredFundsRecipientTestFilteredRetryableRedirectFallbackToNetworkFee-- filtered beneficiary falls back tonetworkFeeAccountwhen no recipient setTestFilteredRetryableNoRedirectWhenNotFiltered-- clean retryable passes through unaffectedTestFilteredRetryableWithCallValue-- redirection works correctly with non-zero call value in escrowTestFilteredRetryableSequencerDoesNotReHalt-- sequencer processes subsequent delayed messages after resolving a filtered retryableCascading redeem checkpoint-and-revert (section 2)
TestRetryableAutoRedeemCallsFilteredAddress-- auto-redeem CALLs filtered contract, group revertedTestRetryableAutoRedeemCreatesAtFilteredAddress-- auto-redeem CREATEs at filtered address, no contract deployedTestRetryableAutoRedeemSelfDestructsToFilteredAddress-- auto-redeem SELFDESTRUCTs to filtered beneficiary, no ETH transferredTestRetryableAutoRedeemStaticCallsFilteredAddress-- auto-redeem STATICCALLs filtered contract, group revertedTestRetryableAutoRedeemEmitsTransferToFilteredAddress-- event filter detects filtered address in log topic during redeemTestManualRedeemGroupRevert-- manual redeem via L2 tx (FullSequencingHooks path), error returned to RPC callerCascading redeem state safety (section 2)
TestRetryableGroupRevertDoesNotAffectCleanRetryable-- clean retryable in prior block unaffected by subsequent dirty retryableTestSequentialRetryableGroupReverts-- two dirty retryables trigger independent halt/resolve cyclesTestRetryableGroupRevertWithChainedRedeems-- chained redeem (A redeems B) with filtered inner execution, entire chain revertedTestRetryableGroupRevertSkipFinaliseSafety-- tentative storage writes fully rolled back after group revertDelayed manual redeem filtering (section 4)
TestDelayedManualRedeemGroupRevert-- manual redeem via delayed L2 tx, proves originatingTxHash != ticketIdEvent filter integration (section 3)
TestDelayedMessageFilterCatchesEventFilter-- event filter detects filtered address in Transfer event topic on delayed txpulls in OffchainLabs/go-ethereum#623
fixes NIT-4453