Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
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
136 changes: 136 additions & 0 deletions addressfilter/address_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2026, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package addressfilter

import (
"context"
"sync"
"sync/atomic"

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

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

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

// 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 {
stopwaiter.StopWaiter
store *HashStore
workChan chan workItem
workerCount int
}

// 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),
workerCount: workerCount,
}

return c
}

func (c *HashedAddressChecker) Start(ctx context.Context) {
c.StopWaiter.Start(ctx, c)

for i := 0; i < c.workerCount; i++ {
c.LaunchThread(func(ctx context.Context) {
c.worker(ctx)
})
}
}

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(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case item := <-c.workChan:
c.processAddress(item.addr, item.state)
}
}
}

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

// If the checker is stopped, process synchronously
if s.checker.Stopped() {
s.checker.processAddress(addr, s)
return
}

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()
}
140 changes: 140 additions & 0 deletions addressfilter/address_checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2026, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package addressfilter

import (
"context"
"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")

Check failure on line 34 in addressfilter/address_checker_test.go

View workflow job for this annotation

GitHub Actions / fast / Lint and Build

store.Load undefined (type *HashStore has no field or method Load)

checker := NewDefaultHashedAddressChecker(store)
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")

// 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")

Check failure on line 92 in addressfilter/address_checker_test.go

View workflow job for this annotation

GitHub Actions / fast / Lint and Build

store.Load undefined (type *HashStore has no field or method Load) (typecheck)

checker := NewDefaultHashedAddressChecker(store)
checker.Start(context.Background())

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