-
Notifications
You must be signed in to change notification settings - Fork 713
Add hashed address filter #4235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
tsahee
merged 34 commits into
master
from
transaction-address-filter-hashed-address-filter
Feb 10, 2026
+408
−151
Merged
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 74b4d97
remove address-checker+add lru-cache
mahdy-nasr aec4930
fix lint and add changelog
mahdy-nasr 5ff641e
move restrictedaddr service to execution node
mahdy-nasr 359c41b
move restrictedaddr service to execution node 2
mahdy-nasr ddaf146
apply suggestions and enhnace s3 download to use memory buffer
mahdy-nasr 064f48a
Fix address-filter configci
mahdy-nasr 0670977
Remove un-needed md file
mahdy-nasr 8633730
rename addressfilter and change copyright
mahdy-nasr a6e26b1
update changelog
mahdy-nasr cbb44bd
enhance PR, fix issues
mahdy-nasr 80fefb0
Add hashed address filter
MishkaRogachev 054b70e
Use restrictedaddr.HashStore in HashedAddressChecker
MishkaRogachev d0453f3
Minor improvments and test
MishkaRogachev 2370090
Move HashedAddressChecker to addressfilter
MishkaRogachev fa8249a
update changelog and move service enable check mechanism
mahdy-nasr 78ac7d5
update changelog 2
mahdy-nasr a374160
Merge remote-tracking branch 'origin/s3-scensorship-resistant' into t…
MishkaRogachev cf1115d
Move checker to Initialize and review fixes
MishkaRogachev 8eaaa0a
Use StopWaiter for HashedAddressChecker
MishkaRogachev 6eeabb0
Remove legacy StaticAsyncChecker
MishkaRogachev 4dcec58
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev 4b90c0b
Fix post-merge issues
MishkaRogachev 2c5be00
Add AddressChecker to FilterService and assing it in execution init
MishkaRogachev d08e556
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev c90fa95
Don't fallback to synchronous TouchAddress call, block instead
MishkaRogachev 858d0db
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev e2a68b8
Merge branch 'master' into transaction-address-filter-hashed-address-…
MishkaRogachev 6dd7358
Move address filter to gethexec & other review fixes
MishkaRogachev 82e6790
Merge remote-tracking branch 'origin/master' into transaction-address…
MishkaRogachev 1fb96f3
Post-merge fix
MishkaRogachev 145cd1c
Fixup TestConfig_Validate
MishkaRogachev ba91f18
Merge branch 'master' into transaction-address-filter-hashed-address-…
MishkaRogachev f1f30bc
Merge branch 'master' into transaction-address-filter-hashed-address-…
MishkaRogachev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ) | ||
|
|
||
| // 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() | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed