Skip to content

Commit 041fbdf

Browse files
Use restrictedaddr.HashStore in HashedAddressChecker
1 parent a589caa commit 041fbdf

File tree

2 files changed

+65
-98
lines changed

2 files changed

+65
-98
lines changed

txfilter/hashed_filter.go

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,43 @@ import (
88
"sync/atomic"
99

1010
"github.com/ethereum/go-ethereum/common"
11-
"github.com/ethereum/go-ethereum/common/lru"
1211
"github.com/ethereum/go-ethereum/core/state"
13-
"github.com/ethereum/go-ethereum/crypto"
12+
13+
"github.com/offchainlabs/nitro/restrictedaddr"
1414
)
1515

1616
// HashedAddressChecker is a global, shared address checker that filters
17-
// transactions by comparing hashed addresses against a precomputed hash list.
18-
//
19-
// Hashing is treated as expensive and amortised across all transactions via
20-
// a shared LRU cache. The checker itself is stateless from the StateDB
21-
// perspective; all per-transaction bookkeeping lives in HashedAddressCheckerState.
17+
// transactions using a HashStore. Hashing and caching are delegated to
18+
// the HashStore; this checker only manages async execution and per-tx
19+
// aggregation.
2220
type HashedAddressChecker struct {
23-
filteredHashSet map[common.Hash]struct{}
24-
hashCache *lru.Cache[common.Address, common.Hash]
25-
salt []byte
26-
21+
store *restrictedaddr.HashStore
2722
workChan chan workItem
2823
}
2924

3025
// HashedAddressCheckerState tracks address filtering for a single transaction.
31-
// It aggregates asynchronous hash checks initiated by TouchAddress and blocks
26+
// It aggregates asynchronous checks initiated by TouchAddress and blocks
3227
// in IsFiltered until all submitted checks complete.
3328
type HashedAddressCheckerState struct {
34-
checker *HashedAddressChecker
35-
36-
// filtered is set to true if any checked address hash appears in filtered HashSet.
29+
checker *HashedAddressChecker
3730
filtered atomic.Bool
38-
39-
// pending tracks the number of outstanding hash checks for this transaction.
40-
pending sync.WaitGroup
31+
pending sync.WaitGroup
4132
}
4233

43-
// workItem is a helper struct representing a single address hashing request associated with
44-
// a specific transaction state.
4534
type workItem struct {
4635
addr common.Address
4736
state *HashedAddressCheckerState
4837
}
4938

50-
// NewHashedAddressChecker constructs a new checker for a given hash list.
51-
// The hash list is copied into an immutable set.
39+
// NewHashedAddressChecker constructs a new checker backed by a HashStore.
5240
func NewHashedAddressChecker(
53-
hashes []common.Hash,
54-
salt []byte,
55-
hashCacheSize int,
41+
store *restrictedaddr.HashStore,
5642
workerCount int,
5743
queueSize int,
5844
) *HashedAddressChecker {
59-
hashSet := make(map[common.Hash]struct{}, len(hashes))
60-
for _, h := range hashes {
61-
hashSet[h] = struct{}{}
62-
}
63-
64-
cache := lru.NewCache[common.Address, common.Hash](hashCacheSize)
65-
6645
c := &HashedAddressChecker{
67-
filteredHashSet: hashSet,
68-
hashCache: cache,
69-
salt: salt,
70-
workChan: make(chan workItem, queueSize),
46+
store: store,
47+
workChan: make(chan workItem, queueSize),
7148
}
7249

7350
for i := 0; i < workerCount; i++ {
@@ -83,19 +60,11 @@ func (c *HashedAddressChecker) NewTxState() state.AddressCheckerState {
8360
}
8461
}
8562

86-
// worker runs for the lifetime of the checker; workCh is never closed.
63+
// worker runs for the lifetime of the checker; workChan is never closed.
8764
func (c *HashedAddressChecker) worker() {
8865
for item := range c.workChan {
89-
// First, check the LRU cache for a precomputed hash.
90-
hash, ok := c.hashCache.Get(item.addr)
91-
if !ok {
92-
hash = crypto.Keccak256Hash(item.addr.Bytes(), c.salt)
93-
c.hashCache.Add(item.addr, hash)
94-
}
95-
96-
// Second, check if the computed hash is in the filtered set.
97-
_, filtered := c.filteredHashSet[hash]
98-
item.state.report(filtered)
66+
restricted := c.store.IsRestricted(item.addr)
67+
item.state.report(restricted)
9968
}
10069
}
10170

txfilter/hashed_filter_test.go

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,102 +4,99 @@
44
package txfilter
55

66
import (
7+
"crypto/sha256"
78
"sync"
89
"testing"
910

11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
1014
"github.com/ethereum/go-ethereum/common"
11-
"github.com/ethereum/go-ethereum/crypto"
15+
16+
"github.com/offchainlabs/nitro/restrictedaddr"
1217
)
1318

19+
func mustState(t *testing.T, s any) *HashedAddressCheckerState {
20+
t.Helper()
21+
state, ok := s.(*HashedAddressCheckerState)
22+
require.Truef(t, ok, "unexpected AddressCheckerState type %T", s)
23+
return state
24+
}
25+
1426
func TestHashedAddressCheckerSimple(t *testing.T) {
1527
salt := []byte("test-salt")
1628

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

20-
filteredHash := crypto.Keccak256Hash(addrFiltered.Bytes(), salt)
32+
store := restrictedaddr.NewHashStore()
33+
34+
filteredHash := sha256.Sum256(append(salt, addrFiltered.Bytes()...))
35+
store.Load(salt, [][32]byte{filteredHash}, "test")
2136

2237
checker := NewHashedAddressChecker(
23-
[]common.Hash{filteredHash},
24-
salt,
25-
/* hashCacheSize */ 16,
38+
store,
2639
/* workerCount */ 2,
2740
/* queueSize */ 8,
2841
)
2942

3043
// Tx 1: filtered address
31-
//nolint:errcheck
32-
state1 := checker.NewTxState().(*HashedAddressCheckerState)
44+
state1 := mustState(t, checker.NewTxState())
3345
state1.TouchAddress(addrFiltered)
34-
35-
if !state1.IsFiltered() {
36-
t.Fatalf("expected transaction to be filtered")
37-
}
46+
assert.True(t, state1.IsFiltered(), "expected transaction to be filtered")
3847

3948
// Tx 2: allowed address
40-
//nolint:errcheck
41-
state2 := checker.NewTxState().(*HashedAddressCheckerState)
49+
state2 := mustState(t, checker.NewTxState())
4250
state2.TouchAddress(addrAllowed)
43-
44-
if state2.IsFiltered() {
45-
t.Fatalf("expected transaction NOT to be filtered")
46-
}
51+
assert.False(t, state2.IsFiltered(), "expected transaction NOT to be filtered")
4752

4853
// Tx 3: mixed addresses
49-
//nolint:errcheck
50-
state3 := checker.NewTxState().(*HashedAddressCheckerState)
54+
state3 := mustState(t, checker.NewTxState())
5155
state3.TouchAddress(addrAllowed)
5256
state3.TouchAddress(addrFiltered)
57+
assert.True(t, state3.IsFiltered(), "expected transaction with mixed addresses to be filtered")
5358

54-
if !state3.IsFiltered() {
55-
t.Fatalf("expected transaction with mixed addresses to be filtered")
56-
}
57-
58-
// Tx 4: reuse hash cache across txs
59-
// Touch the same filtered address again; this must hit the hash cache
60-
//nolint:errcheck
61-
state4 := checker.NewTxState().(*HashedAddressCheckerState)
59+
// Tx 4: reuse HashStore cache across txs
60+
state4 := mustState(t, checker.NewTxState())
6261
state4.TouchAddress(addrFiltered)
63-
64-
if !state4.IsFiltered() {
65-
t.Fatalf("expected cached filtered address to still be filtered")
66-
}
62+
assert.True(t, state4.IsFiltered(), "expected cached filtered address to still be filtered")
6763

6864
// Tx 5: queue overflow should not panic and must be conservative
69-
// Create a checker with zero queue size to force drops
7065
overflowChecker := NewHashedAddressChecker(
71-
[]common.Hash{filteredHash},
72-
salt,
73-
/* hashCacheSize */ 16,
66+
store,
7467
/* workerCount */ 1,
7568
/* queueSize */ 0,
7669
)
7770

78-
//nolint:errcheck
79-
overflowState := overflowChecker.NewTxState().(*HashedAddressCheckerState)
71+
overflowState := mustState(t, overflowChecker.NewTxState())
8072
overflowState.TouchAddress(addrFiltered)
8173

82-
// Queue is full, work is dropped; result may be false, but must not panic
83-
_ = overflowState.IsFiltered()
74+
// false negative allowed
75+
assert.False(
76+
t,
77+
overflowState.IsFiltered(),
78+
"expected overflowed check to be unfiltered (false negative allowed)",
79+
)
8480
}
8581

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

8985
const filteredCount = 500
9086
filteredAddrs := make([]common.Address, filteredCount)
91-
filteredHashes := make([]common.Hash, filteredCount)
87+
filteredHashes := make([][32]byte, filteredCount)
9288

9389
for i := range filteredAddrs {
9490
addr := common.BytesToAddress([]byte{byte(i + 1)})
9591
filteredAddrs[i] = addr
96-
filteredHashes[i] = crypto.Keccak256Hash(addr.Bytes(), salt)
92+
filteredHashes[i] = sha256.Sum256(append(salt, addr.Bytes()...))
9793
}
9894

95+
store := restrictedaddr.NewHashStore()
96+
store.Load(salt, filteredHashes, "heavy")
97+
9998
checker := NewHashedAddressChecker(
100-
filteredHashes,
101-
salt,
102-
/* hashCacheSize */ 256,
99+
store,
103100
/* workerCount */ 4,
104101
/* queueSize */ 32,
105102
)
@@ -116,14 +113,13 @@ func TestHashedAddressCheckerHeavy(t *testing.T) {
116113
go func(tx int) {
117114
defer wg.Done()
118115

119-
//nolint:errcheck
120-
state := checker.NewTxState().(*HashedAddressCheckerState)
116+
state := mustState(t, checker.NewTxState())
121117

122118
for i := range touchesPerTx {
123119
if i%10 == 0 {
124120
state.TouchAddress(filteredAddrs[i%filteredCount])
125121
} else {
126-
addr := common.BytesToAddress([]byte{byte(200 + i)})
122+
addr := common.BytesToAddress([]byte{byte(200 + i*tx)})
127123
state.TouchAddress(addr)
128124
}
129125
}
@@ -135,15 +131,17 @@ func TestHashedAddressCheckerHeavy(t *testing.T) {
135131
wg.Wait()
136132
close(results)
137133

138-
// Post-conditions
139134
filteredTxs := 0
140135
for r := range results {
141136
if r {
142137
filteredTxs++
143138
}
144139
}
145140

146-
if filteredTxs == 0 {
147-
t.Fatalf("expected at least some transactions to be filtered under load")
148-
}
141+
assert.Greater(
142+
t,
143+
filteredTxs,
144+
0,
145+
"expected at least some transactions to be filtered under load",
146+
)
149147
}

0 commit comments

Comments
 (0)