Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
bf57e61
feat(compliance-chains): s3 polling + hash-store
mahdy-nasr Jan 13, 2026
74b4d97
remove address-checker+add lru-cache
mahdy-nasr Jan 13, 2026
aec4930
fix lint and add changelog
mahdy-nasr Jan 14, 2026
5ff641e
move restrictedaddr service to execution node
mahdy-nasr Jan 16, 2026
359c41b
move restrictedaddr service to execution node 2
mahdy-nasr Jan 16, 2026
ddaf146
apply suggestions and enhnace s3 download to use memory buffer
mahdy-nasr Jan 20, 2026
064f48a
Fix address-filter configci
mahdy-nasr Jan 20, 2026
0670977
Remove un-needed md file
mahdy-nasr Jan 20, 2026
8633730
rename addressfilter and change copyright
mahdy-nasr Jan 21, 2026
a6e26b1
update changelog
mahdy-nasr Jan 21, 2026
cbb44bd
enhance PR, fix issues
mahdy-nasr Jan 21, 2026
80fefb0
Add hashed address filter
MishkaRogachev Jan 13, 2026
054b70e
Use restrictedaddr.HashStore in HashedAddressChecker
MishkaRogachev Jan 14, 2026
d0453f3
Minor improvments and test
MishkaRogachev Jan 15, 2026
2370090
Move HashedAddressChecker to addressfilter
MishkaRogachev Jan 21, 2026
fa8249a
update changelog and move service enable check mechanism
mahdy-nasr Jan 22, 2026
78ac7d5
update changelog 2
mahdy-nasr Jan 22, 2026
a374160
Merge remote-tracking branch 'origin/s3-scensorship-resistant' into t…
MishkaRogachev Jan 22, 2026
cf1115d
Move checker to Initialize and review fixes
MishkaRogachev Jan 22, 2026
8eaaa0a
Use StopWaiter for HashedAddressChecker
MishkaRogachev Jan 26, 2026
6eeabb0
Remove legacy StaticAsyncChecker
MishkaRogachev Jan 26, 2026
4dcec58
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev Jan 29, 2026
4b90c0b
Fix post-merge issues
MishkaRogachev Jan 29, 2026
2c5be00
Add AddressChecker to FilterService and assing it in execution init
MishkaRogachev Jan 29, 2026
d08e556
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev Jan 29, 2026
c90fa95
Don't fallback to synchronous TouchAddress call, block instead
MishkaRogachev Feb 2, 2026
858d0db
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev Feb 3, 2026
e2a68b8
Merge branch 'master' into transaction-address-filter-hashed-address-…
MishkaRogachev Feb 3, 2026
6dd7358
Move address filter to gethexec & other review fixes
MishkaRogachev Feb 5, 2026
82e6790
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev Feb 5, 2026
1fb96f3
Post-merge fix
MishkaRogachev Feb 5, 2026
145cd1c
Fixup TestConfig_Validate
MishkaRogachev Feb 5, 2026
ba91f18
Merge branch 'master' into transaction-address-filter-hashed-address-…
MishkaRogachev Feb 6, 2026
f1f30bc
Merge branch 'master' into transaction-address-filter-hashed-address-…
MishkaRogachev Feb 7, 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
112 changes: 112 additions & 0 deletions addressfilter/address_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2026, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package addressfilter

import (
"sync"
"sync/atomic"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
)

// Default parameters for HashedAddressChecker, used in NewDefaultHashedAddressChecker
const (
defaultRestrictedAddrWorkerCount = 4
defaultRestrictedAddrQueueSize = 8192
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a standard config struct and have config values for these. Also, remove NewDefaultHashedAddressChecker.
We will want to be able to play with these without having to compile a new binary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


// HashedAddressChecker is a global, shared address checker that filters
// transactions using a HashStore. Hashing and caching are delegated to
// the HashStore; this checker only manages async execution and per-tx
// aggregation.
type HashedAddressChecker struct {
store *HashStore
workChan chan workItem
}

// HashedAddressCheckerState tracks address filtering for a single transaction.
// 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
}

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

// NewHashedAddressChecker constructs a new checker backed by a HashStore.
func NewHashedAddressChecker(
store *HashStore,
workerCount int,
queueSize int,
) *HashedAddressChecker {
if store == nil {
panic("HashStore cannot be nil")
}

c := &HashedAddressChecker{
store: store,
workChan: make(chan workItem, queueSize),
}

for range workerCount {
go c.worker()
}

return c
}

func NewDefaultHashedAddressChecker(store *HashStore) *HashedAddressChecker {
return NewHashedAddressChecker(
store,
defaultRestrictedAddrWorkerCount,
defaultRestrictedAddrQueueSize,
)
}

func (c *HashedAddressChecker) NewTxState() state.AddressCheckerState {
return &HashedAddressCheckerState{
checker: c,
}
}

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

// worker runs for the lifetime of the checker; workChan is never closed.
func (c *HashedAddressChecker) worker() {
for item := range c.workChan {
c.processAddress(item.addr, item.state)
}
}

func (s *HashedAddressCheckerState) TouchAddress(addr common.Address) {
s.pending.Add(1)
select {
case s.checker.workChan <- workItem{addr: addr, state: s}:
// ok
default:
// queue full: process synchronously to avoid dropping
s.checker.processAddress(addr, s)
}
}

func (s *HashedAddressCheckerState) report(filtered bool) {
if filtered {
s.filtered.Store(true)
}
s.pending.Done()
}

func (s *HashedAddressCheckerState) IsFiltered() bool {
s.pending.Wait()
return s.filtered.Load()
}
137 changes: 137 additions & 0 deletions addressfilter/address_checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2026, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package addressfilter

import (
"crypto/sha256"
"sync"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

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

func mustState(t *testing.T, s any) *HashedAddressCheckerState {
t.Helper()
state, ok := s.(*HashedAddressCheckerState)
require.Truef(t, ok, "unexpected AddressCheckerState type %T", s)
return state
}

func TestHashedAddressCheckerSimple(t *testing.T) {
salt := []byte("test-salt")

addrFiltered := common.HexToAddress("0x000000000000000000000000000000000000dead")
addrAllowed := common.HexToAddress("0x000000000000000000000000000000000000beef")

store := NewHashStore()

hash := sha256.Sum256(append(salt, addrFiltered.Bytes()...))
store.Load(salt, []common.Hash{hash}, "test")

checker := NewDefaultHashedAddressChecker(store)

// Tx 1: filtered address
state1 := mustState(t, checker.NewTxState())
state1.TouchAddress(addrFiltered)
assert.True(t, state1.IsFiltered(), "expected transaction to be filtered")

// Tx 2: allowed address
state2 := mustState(t, checker.NewTxState())
state2.TouchAddress(addrAllowed)
assert.False(t, state2.IsFiltered(), "expected transaction NOT to be filtered")

// 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
state4 := mustState(t, checker.NewTxState())
state4.TouchAddress(addrFiltered)
assert.True(t, state4.IsFiltered(), "expected cached filtered address to still be filtered")

// Tx 5: queue overflow should not panic and must be conservative
overflowChecker := NewHashedAddressChecker(
store,
/* workerCount */ 1,
/* queueSize */ 0,
)

// Tx 5: synchronous call
overflowState := mustState(t, overflowChecker.NewTxState())
overflowState.TouchAddress(addrFiltered)

assert.True(
t,
overflowState.IsFiltered(),
"expected cached filtered address to still be filtered",
)
}

func TestHashedAddressCheckerHeavy(t *testing.T) {
salt := []byte("heavy-salt")

const filteredCount = 500
filteredAddrs := make([]common.Address, filteredCount)
filteredHashes := make([]common.Hash, filteredCount)

for i := range filteredAddrs {
addr := common.BytesToAddress([]byte{byte(i + 1)})
filteredAddrs[i] = addr
filteredHashes[i] = sha256.Sum256(append(salt, addr.Bytes()...))
}

store := NewHashStore()
store.Load(salt, filteredHashes, "heavy")

checker := NewDefaultHashedAddressChecker(store)

const txCount = 100
const touchesPerTx = 100

results := make(chan bool, txCount)

var wg sync.WaitGroup
wg.Add(txCount)

for tx := range txCount {
go func(tx int) {
defer wg.Done()

state := mustState(t, checker.NewTxState())

for i := range touchesPerTx {
if i%10 == 0 {
state.TouchAddress(filteredAddrs[i%filteredCount])
} else {
addr := common.BytesToAddress([]byte{byte(200 + i*tx)})
state.TouchAddress(addr)
}
}

results <- state.IsFiltered()
}(tx)
}

wg.Wait()
close(results)

filteredTxs := 0
for r := range results {
if r {
filteredTxs++
}
}

assert.Greater(
t,
filteredTxs,
0,
"expected at least some transactions to be filtered under load",
)
}
46 changes: 46 additions & 0 deletions addressfilter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2026, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package addressfilter

import (
"errors"
"time"

"github.com/spf13/pflag"

"github.com/offchainlabs/nitro/util/s3syncer"
)

type Config struct {
Enable bool `koanf:"enable"`
S3 s3syncer.Config `koanf:"s3"`
PollInterval time.Duration `koanf:"poll-interval"`
}

var DefaultConfig = Config{
Enable: false,
PollInterval: 5 * time.Minute,
}

func ConfigAddOptions(prefix string, f *pflag.FlagSet) {
f.Bool(prefix+".enable", DefaultConfig.Enable, "enable restricted address synchronization service")
s3syncer.ConfigAddOptions(prefix+".s3", f)
f.Duration(prefix+".poll-interval", DefaultConfig.PollInterval, "interval between polling S3 for hash list updates")
}

func (c *Config) Validate() error {
if !c.Enable {
return nil
}

if err := c.S3.Validate(); err != nil {
return err
}

if c.PollInterval <= 0 {
return errors.New("restricted-addr.poll-interval must be positive")
}

return nil
}
Loading
Loading