Skip to content

Commit 0d4147a

Browse files
Address filtering for retryable submissions, redeems, and delayed events
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
1 parent 10838b6 commit 0d4147a

File tree

6 files changed

+1008
-27
lines changed

6 files changed

+1008
-27
lines changed

arbos/block_processor.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ type SequencingHooks interface {
116116
DiscardInvalidTxsEarly() bool
117117
PreTxFilter(*params.ChainConfig, *types.Header, *state.StateDB, *arbosState.ArbosState, *types.Transaction, *arbitrum_types.ConditionalOptions, common.Address, *L1Info) error
118118
PostTxFilter(*types.Header, *state.StateDB, *arbosState.ArbosState, *types.Transaction, common.Address, uint64, *core.ExecutionResult) error
119+
RedeemFilter(*state.StateDB) error
119120
BlockFilter(*types.Header, *state.StateDB, types.Transactions, types.Receipts) error
120121
InsertLastTxError(error)
121122
}
@@ -153,6 +154,10 @@ func (n *NoopSequencingHooks) BlockFilter(header *types.Header, db *state.StateD
153154
return nil
154155
}
155156

157+
func (n *NoopSequencingHooks) RedeemFilter(db *state.StateDB) error {
158+
return nil
159+
}
160+
156161
func (n *NoopSequencingHooks) InsertLastTxError(err error) {}
157162

158163
func NewNoopSequencingHooks(txes types.Transactions) *NoopSequencingHooks {
@@ -254,12 +259,14 @@ func ProduceBlockAdvanced(
254259
var options *arbitrum_types.ConditionalOptions
255260
var hooks SequencingHooks
256261
isUserTx := false
262+
isRedeem := false
257263
if firstTx != nil {
258264
tx = firstTx
259265
firstTx = nil
260266
} else if len(redeems) > 0 {
261267
tx = redeems[0]
262268
redeems = redeems[1:]
269+
isRedeem = true
263270

264271
retry, ok := (tx.GetInner()).(*types.ArbitrumRetryTx)
265272
if !ok {
@@ -375,6 +382,14 @@ func ProduceBlockAdvanced(
375382
if hooks != nil {
376383
return hooks.PostTxFilter(header, statedb, arbState, tx, sender, dataGas, result)
377384
}
385+
// hooks is intentionally nil for redeems - it gates sequencer policies
386+
// (PreTxFilter, PostTxFilter, nonce checking, error tracking) that don't
387+
// apply to protocol-scheduled transactions. RedeemFilter is called on
388+
// sequencingHooks directly to get the narrow redeem filtering behavior
389+
// without enabling those other policies.
390+
if isRedeem {
391+
return sequencingHooks.RedeemFilter(statedb)
392+
}
378393
return nil
379394
},
380395
)

arbos/tx_processor.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,36 @@ func (p *TxProcessor) StartTxHook() (endTxNow bool, multiGasUsed multigas.MultiG
202202
defer (startTracer())()
203203
statedb := evm.StateDB
204204
ticketId := underlyingTx.Hash()
205+
205206
escrow := retryables.RetryableEscrowAddress(ticketId)
206207
networkFeeAccount, _ := p.state.NetworkFeeAccount()
207208
from := tx.From
208209
scenario := util.TracingDuringEVM
209210

211+
// Check if this transaction is in the onchain filter. If so, redirect FeeRefundAddr
212+
// and Beneficiary to the filteredFundsRecipient (or networkFeeAccount as fallback).
213+
// filteredErr is set as result.Err so that PostTxFilter can detect the tx was
214+
// already handled by the onchain filter and skip halting.
215+
var filteredErr error
216+
isFiltered := false
217+
if p.state.ArbOSVersion() >= params.ArbosVersion_TransactionFiltering &&
218+
p.state.FilteredTransactions().IsFilteredFree(ticketId) {
219+
recipient, err := p.state.FilteredFundsRecipientOrDefault()
220+
if err != nil {
221+
return true, multigas.ZeroGas(), err, nil
222+
}
223+
// For symmetry with other filtered tx paths, deletion from the onchain filter
224+
// is handled by the external tx authority service rather than here.
225+
// Note: deletion here *would* be committed despite the ErrFilteredTx in
226+
// result.Err, because endTxNow=true means the outer error is nil and state
227+
// is not reverted. May move to direct deletion here in future.
228+
// p.state.FilteredTransactions().DeleteFree(ticketId)
229+
tx.FeeRefundAddr = recipient
230+
tx.Beneficiary = recipient
231+
isFiltered = true
232+
filteredErr = &core.ErrFilteredTx{TxHash: ticketId}
233+
}
234+
210235
// mint funds with the deposit, then charge fees later
211236
availableRefund := new(big.Int).Set(tx.DepositValue)
212237
takeFunds(availableRefund, tx.RetryValue)
@@ -306,7 +331,7 @@ func (p *TxProcessor) StartTxHook() (endTxNow bool, multiGasUsed multigas.MultiG
306331
// should never happen as from's balance should be at least availableRefund at this point
307332
log.Error("failed to transfer gasCostRefund", "err", err)
308333
}
309-
return true, multigas.ZeroGas(), nil, ticketId.Bytes()
334+
return true, multigas.ZeroGas(), filteredErr, ticketId.Bytes()
310335
}
311336

312337
// pay for the retryable's gas and update the pools
@@ -323,15 +348,15 @@ func (p *TxProcessor) StartTxHook() (endTxNow bool, multiGasUsed multigas.MultiG
323348
infraCost = takeFunds(networkCost, infraCost)
324349
if err := transfer(&tx.From, &infraFeeAccount, infraCost, tracing.BalanceIncreaseInfraFee); err != nil {
325350
log.Error("failed to transfer gas cost to infrastructure fee account", "err", err)
326-
return true, multigas.ZeroGas(), nil, ticketId.Bytes()
351+
return true, multigas.ZeroGas(), filteredErr, ticketId.Bytes()
327352
}
328353
}
329354
}
330355
if arbmath.BigGreaterThan(networkCost, common.Big0) {
331356
if err := transfer(&tx.From, &networkFeeAccount, networkCost, tracing.BalanceIncreaseNetworkFee); err != nil {
332357
// should be impossible because we just checked the tx.From balance
333358
log.Error("failed to transfer gas cost to network fee account", "err", err)
334-
return true, multigas.ZeroGas(), nil, ticketId.Bytes()
359+
return true, multigas.ZeroGas(), filteredErr, ticketId.Bytes()
335360
}
336361
}
337362

@@ -348,6 +373,14 @@ func (p *TxProcessor) StartTxHook() (endTxNow bool, multiGasUsed multigas.MultiG
348373
availableRefund.Add(availableRefund, withheldGasFunds)
349374
availableRefund.Add(availableRefund, withheldSubmissionFee)
350375

376+
// For filtered retryables, skip auto-redeem scheduling. The retryable
377+
// is created with a redirected beneficiary who can redeem manually.
378+
// This prevents the auto-redeem from executing calls against
379+
// potentially filtered addresses.
380+
if isFiltered {
381+
return true, multigas.L2CalldataGas(usergas), filteredErr, ticketId.Bytes()
382+
}
383+
351384
// emit RedeemScheduled event
352385
retryTxInner, err := retryable.MakeTx(
353386
underlyingTx.ChainId(),

contracts-local/src/mocks/AddressFilterTest.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ contract AddressFilterTest {
7272
/// @notice Selfdestructs this contract and sends balance to beneficiary
7373
function selfDestructTo(
7474
address payable beneficiary
75-
) external {
75+
) external payable {
7676
selfdestruct(beneficiary);
7777
}
7878

execution/gethexec/executionengine.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ import (
4646
"github.com/offchainlabs/nitro/arbos/filteredTransactions"
4747
"github.com/offchainlabs/nitro/arbos/l1pricing"
4848
"github.com/offchainlabs/nitro/arbos/programs"
49+
arbosutil "github.com/offchainlabs/nitro/arbos/util"
4950
"github.com/offchainlabs/nitro/arbutil"
5051
"github.com/offchainlabs/nitro/consensus"
5152
"github.com/offchainlabs/nitro/execution"
53+
"github.com/offchainlabs/nitro/execution/gethexec/eventfilter"
5254
"github.com/offchainlabs/nitro/util/arbmath"
5355
"github.com/offchainlabs/nitro/util/containers"
5456
"github.com/offchainlabs/nitro/util/sharedmetrics"
@@ -95,10 +97,14 @@ var ErrDelayedTxFiltered = errors.New("delayed transaction filtered")
9597
type DelayedFilteringSequencingHooks struct {
9698
arbos.NoopSequencingHooks
9799
FilteredTxHashes []common.Hash
100+
eventFilter *eventfilter.EventFilter
98101
}
99102

100-
func NewDelayedFilteringSequencingHooks(txes types.Transactions) *DelayedFilteringSequencingHooks {
101-
return &DelayedFilteringSequencingHooks{NoopSequencingHooks: *arbos.NewNoopSequencingHooks(txes)}
103+
func NewDelayedFilteringSequencingHooks(txes types.Transactions, ef *eventfilter.EventFilter) *DelayedFilteringSequencingHooks {
104+
return &DelayedFilteringSequencingHooks{
105+
NoopSequencingHooks: *arbos.NewNoopSequencingHooks(txes),
106+
eventFilter: ef,
107+
}
102108
}
103109

104110
// PostTxFilter touches To/From addresses and checks IsAddressFiltered.
@@ -109,6 +115,8 @@ func (f *DelayedFilteringSequencingHooks) PostTxFilter(header *types.Header, db
109115
if tx.To() != nil {
110116
db.TouchAddress(*tx.To())
111117
}
118+
touchRetryableAddresses(db, tx)
119+
applyEventFilter(f.eventFilter, db)
112120

113121
if db.IsAddressFiltered() {
114122
// If the STF already handled this tx via the onchain filter mechanism,
@@ -124,6 +132,42 @@ func (f *DelayedFilteringSequencingHooks) PostTxFilter(header *types.Header, db
124132
return nil
125133
}
126134

135+
func (f *DelayedFilteringSequencingHooks) RedeemFilter(db *state.StateDB) error {
136+
applyEventFilter(f.eventFilter, db)
137+
if db.IsAddressFiltered() {
138+
return state.ErrArbTxFilter
139+
}
140+
return nil
141+
}
142+
143+
func applyEventFilter(ef *eventfilter.EventFilter, db *state.StateDB) {
144+
if ef == nil {
145+
return
146+
}
147+
logs := db.GetCurrentTxLogs()
148+
for _, l := range logs {
149+
for _, addr := range ef.AddressesForFiltering(l.Topics, l.Data, l.Address, common.Address{}) {
150+
db.TouchAddress(addr)
151+
}
152+
}
153+
}
154+
155+
// touchRetryableAddresses touches addresses from retryable inner fields
156+
// (Beneficiary, FeeRefundAddr, RetryTo) so the address filter can detect them.
157+
// Also touches de-aliased versions to catch L1 contract addresses that were
158+
// aliased by the Inbox contract.
159+
func touchRetryableAddresses(db *state.StateDB, tx *types.Transaction) {
160+
if inner, ok := tx.GetInner().(*types.ArbitrumSubmitRetryableTx); ok {
161+
db.TouchAddress(inner.Beneficiary)
162+
db.TouchAddress(inner.FeeRefundAddr)
163+
if inner.RetryTo != nil {
164+
db.TouchAddress(*inner.RetryTo)
165+
}
166+
db.TouchAddress(arbosutil.InverseRemapL1Address(inner.Beneficiary))
167+
db.TouchAddress(arbosutil.InverseRemapL1Address(inner.FeeRefundAddr))
168+
}
169+
}
170+
127171
type L1PriceDataOfMsg struct {
128172
callDataUnits uint64
129173
cummulativeCallDataUnits uint64
@@ -170,6 +214,7 @@ type ExecutionEngine struct {
170214
runningMaintenance atomic.Bool
171215

172216
addressChecker state.AddressChecker
217+
eventFilter *eventfilter.EventFilter
173218
}
174219

175220
func NewL1PriceData() *L1PriceData {
@@ -828,7 +873,7 @@ func (s *ExecutionEngine) createBlockFromNextMessage(msg *arbostypes.MessageWith
828873
log.Warn("error parsing incoming message for filtering", "err", err)
829874
txes = types.Transactions{}
830875
}
831-
filteringHooks := NewDelayedFilteringSequencingHooks(txes)
876+
filteringHooks := NewDelayedFilteringSequencingHooks(txes, s.eventFilter)
832877

833878
block, receipts, err := arbos.ProduceBlockAdvanced(
834879
msg.Message.Header,
@@ -1238,6 +1283,10 @@ func (s *ExecutionEngine) SetAddressChecker(checker state.AddressChecker) {
12381283
s.addressChecker = checker
12391284
}
12401285

1286+
func (s *ExecutionEngine) SetEventFilter(ef *eventfilter.EventFilter) {
1287+
s.eventFilter = ef
1288+
}
1289+
12411290
func (s *ExecutionEngine) IsTxHashInOnchainFilter(txHash common.Hash) (bool, error) {
12421291
currentHeader, err := s.getCurrentHeader()
12431292
if err != nil {

execution/gethexec/sequencer.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ func NewSequencer(execEngine *ExecutionEngine, l1Reader *headerreader.HeaderRead
517517
}
518518
s.Pause()
519519
execEngine.EnableReorgSequencing()
520+
execEngine.SetEventFilter(eventFilter)
520521
return s, nil
521522
}
522523

@@ -783,6 +784,7 @@ func (s *Sequencer) postTxFilter(header *types.Header, statedb *state.StateDB, _
783784
}
784785
}
785786
}
787+
touchRetryableAddresses(statedb, tx)
786788

787789
if statedb.IsTxFiltered() || statedb.IsAddressFiltered() {
788790
return state.ErrArbTxFilter
@@ -810,6 +812,14 @@ func (s *Sequencer) postTxFilter(header *types.Header, statedb *state.StateDB, _
810812
return nil
811813
}
812814

815+
func (s *Sequencer) redeemFilter(db *state.StateDB) error {
816+
applyEventFilter(s.eventFilter, db)
817+
if db.IsAddressFiltered() {
818+
return state.ErrArbTxFilter
819+
}
820+
return nil
821+
}
822+
813823
func (s *Sequencer) CheckHealth(ctx context.Context) error {
814824
pauseChan, forwarder := s.GetPauseAndForwarder()
815825
if forwarder != nil {
@@ -962,6 +972,7 @@ type FullSequencingHooks struct {
962972
txErrors []error
963973
preTxFilter func(*params.ChainConfig, *types.Header, *state.StateDB, *arbosState.ArbosState, *types.Transaction, *arbitrum_types.ConditionalOptions, common.Address, *arbos.L1Info) error
964974
postTxFilter func(*types.Header, *state.StateDB, *arbosState.ArbosState, *types.Transaction, common.Address, uint64, *core.ExecutionResult) error
975+
redeemFilter func(*state.StateDB) error
965976
blockFilter func(*types.Header, *state.StateDB, types.Transactions, types.Receipts) error
966977
}
967978

@@ -1068,6 +1079,13 @@ func (s *FullSequencingHooks) PostTxFilter(header *types.Header, db *state.State
10681079
return nil
10691080
}
10701081

1082+
func (s *FullSequencingHooks) RedeemFilter(db *state.StateDB) error {
1083+
if s.redeemFilter != nil {
1084+
return s.redeemFilter(db)
1085+
}
1086+
return nil
1087+
}
1088+
10711089
func (s *FullSequencingHooks) BlockFilter(header *types.Header, db *state.StateDB, transactions types.Transactions, receipts types.Receipts) error {
10721090
if s.blockFilter != nil {
10731091
return s.blockFilter(header, db, transactions, receipts)
@@ -1080,6 +1098,7 @@ func MakeSequencingHooks(
10801098
maxSequencedTxsSize int,
10811099
preTxFilter func(*params.ChainConfig, *types.Header, *state.StateDB, *arbosState.ArbosState, *types.Transaction, *arbitrum_types.ConditionalOptions, common.Address, *arbos.L1Info) error,
10821100
postTxFilter func(*types.Header, *state.StateDB, *arbosState.ArbosState, *types.Transaction, common.Address, uint64, *core.ExecutionResult) error,
1101+
redeemFilter func(*state.StateDB) error,
10831102
blockFilter func(*types.Header, *state.StateDB, types.Transactions, types.Receipts) error,
10841103
) *FullSequencingHooks {
10851104
res := &FullSequencingHooks{
@@ -1089,6 +1108,7 @@ func MakeSequencingHooks(
10891108
maxSequencedTxsSize: maxSequencedTxsSize,
10901109
preTxFilter: preTxFilter,
10911110
postTxFilter: postTxFilter,
1111+
redeemFilter: redeemFilter,
10921112
blockFilter: blockFilter,
10931113
}
10941114
return res
@@ -1113,6 +1133,7 @@ func MakeZeroTxSizeSequencingHooksForTesting(
11131133
0,
11141134
preTxFilter,
11151135
postTxFilter,
1136+
nil,
11161137
blockFilter,
11171138
)
11181139
}
@@ -1370,6 +1391,7 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) {
13701391
s.config().MaxTxDataSize,
13711392
s.preTxFilter,
13721393
s.postTxFilter,
1394+
s.redeemFilter,
13731395
nil,
13741396
)
13751397

0 commit comments

Comments
 (0)