Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d5ebca5
Update TouchAddress and IsFiltered to propagate filtering reasons
MishkaRogachev Mar 24, 2026
820a86e
Add the changelog
MishkaRogachev Mar 24, 2026
ba6fdef
Review fixes
MishkaRogachev Mar 31, 2026
04052f8
Merge remote-tracking branch 'origin/master' into update-touchaddress…
MishkaRogachev Mar 31, 2026
f1d7a67
Assign proper FilterSetId
MishkaRogachev Apr 1, 2026
6819ceb
Fix a comment in TestHashedAddressCheckerSimple
MishkaRogachev Apr 1, 2026
e78fde4
Merge remote-tracking branch 'origin/master' into update-touchaddress…
MishkaRogachev Apr 7, 2026
850f79f
Post-merge and review fixes
MishkaRogachev Apr 7, 2026
bf50495
Update AddressesForFiltering signature to use records by value
MishkaRogachev Apr 7, 2026
46d603f
Remove unneeded duplicate-addresses test case
MishkaRogachev Apr 7, 2026
e3a14e5
Remove TODO comment
MishkaRogachev Apr 8, 2026
592e13f
Remove FilterSetID from FilteredAddressRecord and put back lint:requi…
MishkaRogachev Apr 8, 2026
46d1081
Merge branch 'master' into update-touchaddress-and-hashedaddresscheck…
MishkaRogachev Apr 8, 2026
185c424
Fill EventRuleMatchfor FilterReason
MishkaRogachev Apr 8, 2026
ed4a2b9
Merge remote-tracking branch 'origin/master' into update-touchaddress…
MishkaRogachev Apr 9, 2026
1817c15
Merge remote-tracking branch 'origin/master' into update-touchaddress…
MishkaRogachev Apr 13, 2026
e860cec
Change FilteredTxReport's PositionInBlock to int
MishkaRogachev Apr 13, 2026
eb45735
Merge remote-tracking branch 'origin/master' into update-touchaddress…
MishkaRogachev Apr 14, 2026
a1bb61a
Change FilteredTxReport's PositionInBlock to uint
MishkaRogachev Apr 14, 2026
f49ed70
Change PositionInBlock once again, to uint64
MishkaRogachev Apr 15, 2026
273b75e
Merge branch 'master' into update-touchaddress-and-hashedaddresscheck…
MishkaRogachev Apr 16, 2026
654d6a4
Update go-ethereum pin
MishkaRogachev Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions arbos/tx_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/holiman/uint256"

"github.com/ethereum/go-ethereum/arbitrum/filter"
"github.com/ethereum/go-ethereum/arbitrum/multigas"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
Expand Down Expand Up @@ -81,8 +82,14 @@ func (p *TxProcessor) PushContract(contract *vm.Contract) {
}

// Record touched addresses for tx filtering
p.evm.StateDB.TouchAddress(contract.Address())
p.evm.StateDB.TouchAddress(contract.Caller())
p.evm.StateDB.TouchAddress(&filter.FilteredAddressRecord{
Address: contract.Address(),
FilterReason: filter.FilterReason{Reason: filter.ReasonContractAddress, EventRuleMatch: nil},
})
p.evm.StateDB.TouchAddress(&filter.FilteredAddressRecord{
Address: contract.Caller(),
FilterReason: filter.FilterReason{Reason: filter.ReasonContractCaller, EventRuleMatch: nil},
})
}

func (p *TxProcessor) PopContract() {
Expand Down
2 changes: 2 additions & 0 deletions changelog/mrogachev-nit-4642.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Internal
- Update TouchAddress to accept FilteredAddressRecord with filtering reason instead of just an address, and update IsFiltered to return filtered address records for use in transaction filtering reports.
44 changes: 25 additions & 19 deletions execution/gethexec/addressfilter/address_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ package addressfilter
import (
"context"
"sync"
"sync/atomic"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/arbitrum/filter"
"github.com/ethereum/go-ethereum/core/state"

"github.com/offchainlabs/nitro/util/stopwaiter"
Expand All @@ -29,14 +28,16 @@ type HashedAddressChecker struct {
// It aggregates asynchronous checks initiated by TouchAddress and blocks
// in IsFiltered until all submitted checks complete.
type HashedAddressCheckerState struct {
checker *HashedAddressChecker
filtered atomic.Bool
pending sync.WaitGroup
checker *HashedAddressChecker
mu sync.Mutex
filtered bool
filteredAddresses []filter.FilteredAddressRecord
pending sync.WaitGroup
}

type workItem struct {
addr common.Address
state *HashedAddressCheckerState
record *filter.FilteredAddressRecord
state *HashedAddressCheckerState
}

// NewHashedAddressChecker constructs a new checker backed by a HashStore.
Expand Down Expand Up @@ -74,9 +75,9 @@ func (c *HashedAddressChecker) NewTxState() state.AddressCheckerState {
}
}

func (c *HashedAddressChecker) processAddress(addr common.Address, state *HashedAddressCheckerState) {
restricted := c.store.IsRestricted(addr)
state.report(restricted)
func (c *HashedAddressChecker) processRecord(record *filter.FilteredAddressRecord, state *HashedAddressCheckerState) {
restricted := c.store.IsRestricted(record.Address)
state.report(record, restricted)
}

// worker runs for the lifetime of the checker; workChan is never closed.
Expand All @@ -86,37 +87,42 @@ func (c *HashedAddressChecker) worker(ctx context.Context) {
case <-ctx.Done():
return
case item := <-c.workChan:
c.processAddress(item.addr, item.state)
c.processRecord(item.record, item.state)
}
}
}

func (s *HashedAddressCheckerState) TouchAddress(addr common.Address) {
func (s *HashedAddressCheckerState) TouchAddress(record *filter.FilteredAddressRecord) {
s.pending.Add(1)

// If the checker is stopped, conservatively mark filtered
if s.checker.Stopped() {
s.report(true)
s.report(nil, true)
return
}
Comment thread
diegoximenes marked this conversation as resolved.

select {
case s.checker.workChan <- workItem{addr: addr, state: s}:
case s.checker.workChan <- workItem{record: record, state: s}:
// ok
Comment thread
diegoximenes marked this conversation as resolved.
case <-s.checker.GetContext().Done():
// shutting down, conservatively mark filtered
s.report(true)
s.report(nil, true)
Comment thread
diegoximenes marked this conversation as resolved.
}
}

func (s *HashedAddressCheckerState) report(filtered bool) {
func (s *HashedAddressCheckerState) report(record *filter.FilteredAddressRecord, filtered bool) {
if filtered {
s.filtered.Store(true)
s.mu.Lock()
s.filtered = true
if record != nil {
s.filteredAddresses = append(s.filteredAddresses, *record)
}
s.mu.Unlock()
}
s.pending.Done()
}

func (s *HashedAddressCheckerState) IsFiltered() bool {
func (s *HashedAddressCheckerState) IsFiltered() (bool, []filter.FilteredAddressRecord) {
s.pending.Wait()
return s.filtered.Load()
return s.filtered, s.filteredAddresses
Comment thread
diegoximenes marked this conversation as resolved.
}
69 changes: 50 additions & 19 deletions execution/gethexec/addressfilter/address_checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ethereum/go-ethereum/arbitrum/filter"
"github.com/ethereum/go-ethereum/common"
)

Expand All @@ -27,54 +28,83 @@ func TestHashedAddressCheckerSimple(t *testing.T) {
require.NoError(t, err, "failed to parse salt UUID")

addrFiltered := common.HexToAddress("0xddfAbCdc4D8FfC6d5beaf154f18B778f892A0740")
addrFiltered2 := common.HexToAddress("0xdead000000000000000000000000000000000001")
addrAllowed := common.HexToAddress("0x000000000000000000000000000000000000beef")

const cacheSize = 100
store := NewHashStore(cacheSize)

// These values are test values from the provider, to cross-check the salting/hashing algorithm.
hash := common.HexToHash("0x8fb74f22f0aed996e7548101ae1cea812ccdf86e7ad8a781eebea00f797ce4a6")
store.Store(uuid.New(), salt, []common.Hash{hash}, "test")
hash2 := common.HexToHash("0xe4c758332a0fe49872f79ae15d2e1c0d76daeb5a9b33578e7f11d3e2571dad1a")
store.Store(uuid.New(), salt, []common.Hash{hash, hash2}, "test")

checker := NewHashedAddressChecker(store, 4, 8192)
checker.Start(context.Background())

// Tx 1: filtered address
state1 := mustState(t, checker.NewTxState())
state1.TouchAddress(addrFiltered)
assert.True(t, state1.IsFiltered(), "expected transaction to be filtered")
state1.TouchAddress(&filter.FilteredAddressRecord{Address: addrFiltered, FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})
filtered1, records1 := state1.IsFiltered()
assert.True(t, filtered1, "expected transaction to be filtered")
require.Len(t, records1, 1)
assert.Equal(t, addrFiltered, records1[0].Address)
assert.Equal(t, filter.ReasonFrom, records1[0].Reason)

// Tx 2: allowed address
state2 := mustState(t, checker.NewTxState())
state2.TouchAddress(addrAllowed)
assert.False(t, state2.IsFiltered(), "expected transaction NOT to be filtered")
state2.TouchAddress(&filter.FilteredAddressRecord{Address: addrAllowed, FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})
filtered2, records2 := state2.IsFiltered()
assert.False(t, filtered2, "expected transaction NOT to be filtered")
assert.Empty(t, records2)

// Tx 3: mixed addresses
state3 := mustState(t, checker.NewTxState())
state3.TouchAddress(addrAllowed)
state3.TouchAddress(addrFiltered)
assert.True(t, state3.IsFiltered(), "expected transaction with mixed addresses to be filtered")

// Tx 4: reuse HashStore cache across txs
state3.TouchAddress(&filter.FilteredAddressRecord{Address: addrAllowed, FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})
state3.TouchAddress(&filter.FilteredAddressRecord{Address: addrFiltered, FilterReason: filter.FilterReason{Reason: filter.ReasonTo, EventRuleMatch: nil}})
filtered3, records3 := state3.IsFiltered()
assert.True(t, filtered3, "expected transaction with mixed addresses to be filtered")
require.Len(t, records3, 1)
Comment thread
diegoximenes marked this conversation as resolved.
assert.Equal(t, addrFiltered, records3[0].Address)
assert.Equal(t, filter.ReasonTo, records3[0].Reason)

// Tx 4: multiple filtered addresses
state4 := mustState(t, checker.NewTxState())
state4.TouchAddress(addrFiltered)
assert.True(t, state4.IsFiltered(), "expected cached filtered address to still be filtered")
state4.TouchAddress(&filter.FilteredAddressRecord{Address: addrFiltered, FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})
state4.TouchAddress(&filter.FilteredAddressRecord{Address: addrAllowed, FilterReason: filter.FilterReason{Reason: filter.ReasonTo, EventRuleMatch: nil}})
state4.TouchAddress(&filter.FilteredAddressRecord{Address: addrFiltered2, FilterReason: filter.FilterReason{Reason: filter.ReasonContractAddress, EventRuleMatch: nil}})
filtered4, records4 := state4.IsFiltered()
assert.True(t, filtered4, "expected transaction with multiple filtered addresses to be filtered")
require.Len(t, records4, 2)
recordsByAddr := make(map[common.Address]filter.FilteredAddressRecord)
for _, r := range records4 {
recordsByAddr[r.Address] = r
}
assert.Equal(t, filter.ReasonFrom, recordsByAddr[addrFiltered].Reason)
assert.Equal(t, filter.ReasonContractAddress, recordsByAddr[addrFiltered2].Reason)

// Tx 5: reuse HashStore cache across txs
state5 := mustState(t, checker.NewTxState())
state5.TouchAddress(&filter.FilteredAddressRecord{Address: addrFiltered, FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})
filtered5, _ := state5.IsFiltered()
assert.True(t, filtered5, "expected cached filtered address to still be filtered")

// Tx 5: queue overflow should not panic and must be conservative
// Tx 6: unbuffered channel (synchronous send) should not panic
overflowChecker := NewHashedAddressChecker(
store,
/* workerCount */ 1,
/* queueSize */ 0,
)
overflowChecker.Start(context.Background())

// Tx 5: synchronous call
// Tx 6: synchronous call
overflowState := mustState(t, overflowChecker.NewTxState())
overflowState.TouchAddress(addrFiltered)
overflowState.TouchAddress(&filter.FilteredAddressRecord{Address: addrFiltered, FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})

filtered6, _ := overflowState.IsFiltered()
assert.True(
t,
overflowState.IsFiltered(),
filtered6,
"expected cached filtered address to still be filtered",
)
}
Expand Down Expand Up @@ -117,14 +147,15 @@ func TestHashedAddressCheckerHeavy(t *testing.T) {

for i := range touchesPerTx {
if i%10 == 0 {
state.TouchAddress(filteredAddrs[i%filteredCount])
state.TouchAddress(&filter.FilteredAddressRecord{Address: filteredAddrs[i%filteredCount], FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})
} else {
addr := common.BytesToAddress([]byte{byte(200 + i*tx)})
state.TouchAddress(addr)
state.TouchAddress(&filter.FilteredAddressRecord{Address: addr, FilterReason: filter.FilterReason{Reason: filter.ReasonFrom, EventRuleMatch: nil}})
}
}

results <- state.IsFiltered()
filtered, _ := state.IsFiltered()
results <- filtered
}(tx)
}

Expand Down
31 changes: 31 additions & 0 deletions execution/gethexec/addressfilter/filter_report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2026, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package addressfilter

import (
"time"

"github.com/ethereum/go-ethereum/arbitrum/filter"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)

// lint:require-exhaustive-initialization
type DelayedReportData struct {
InboxRequestId common.Hash `json:"delayedInboxRequestId"`
}

// lint:require-exhaustive-initialization
type FilteredTxReport struct {
ID string `json:"id"`
TxHash common.Hash `json:"txHash"`
TxRLP hexutil.Bytes `json:"txRLP"`
FilteredAddresses []filter.FilteredAddressRecord `json:"filteredAddresses"`
BlockNumber uint64 `json:"blockNumber"`
ParentBlockHash common.Hash `json:"parentBlockHash"`
PositionInBlock uint64 `json:"positionInBlock"`
FilteredAt time.Time `json:"filteredAt"`
IsDelayed bool `json:"isDelayed"`
*DelayedReportData
}
Loading
Loading