Skip to content

Commit 2547bb2

Browse files
crStivzsfelfoldi
andauthored
eth/fetcher: Fix flaky TestTransactionForgotten test using mock clock (#31468)
Fixes #31169 The TestTransactionForgotten test was flaky due to real time dependencies. This PR: - Replaces real time with mock clock for deterministic timing control - Adds precise state checks at timeout boundaries - Verifies underpriced cache states and cleanup - Improves test reliability by controlling transaction timestamps - Adds checks for transaction re-enqueueing behavior The changes ensure consistent test behavior without timing-related flakiness. --------- Co-authored-by: Zsolt Felfoldi <[email protected]>
1 parent 0c2ad07 commit 2547bb2

File tree

3 files changed

+99
-20
lines changed

3 files changed

+99
-20
lines changed

eth/fetcher/tx_fetcher.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,22 +202,23 @@ type TxFetcher struct {
202202
fetchTxs func(string, []common.Hash) error // Retrieves a set of txs from a remote peer
203203
dropPeer func(string) // Drops a peer in case of announcement violation
204204

205-
step chan struct{} // Notification channel when the fetcher loop iterates
206-
clock mclock.Clock // Time wrapper to simulate in tests
207-
rand *mrand.Rand // Randomizer to use in tests instead of map range loops (soft-random)
205+
step chan struct{} // Notification channel when the fetcher loop iterates
206+
clock mclock.Clock // Monotonic clock or simulated clock for tests
207+
realTime func() time.Time // Real system time or simulated time for tests
208+
rand *mrand.Rand // Randomizer to use in tests instead of map range loops (soft-random)
208209
}
209210

210211
// NewTxFetcher creates a transaction fetcher to retrieve transaction
211212
// based on hash announcements.
212213
func NewTxFetcher(hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string)) *TxFetcher {
213-
return NewTxFetcherForTests(hasTx, addTxs, fetchTxs, dropPeer, mclock.System{}, nil)
214+
return NewTxFetcherForTests(hasTx, addTxs, fetchTxs, dropPeer, mclock.System{}, time.Now, nil)
214215
}
215216

216217
// NewTxFetcherForTests is a testing method to mock out the realtime clock with
217218
// a simulated version and the internal randomness with a deterministic one.
218219
func NewTxFetcherForTests(
219220
hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string),
220-
clock mclock.Clock, rand *mrand.Rand) *TxFetcher {
221+
clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher {
221222
return &TxFetcher{
222223
notify: make(chan *txAnnounce),
223224
cleanup: make(chan *txDelivery),
@@ -237,6 +238,7 @@ func NewTxFetcherForTests(
237238
fetchTxs: fetchTxs,
238239
dropPeer: dropPeer,
239240
clock: clock,
241+
realTime: realTime,
240242
rand: rand,
241243
}
242244
}
@@ -293,7 +295,7 @@ func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []c
293295
// isKnownUnderpriced reports whether a transaction hash was recently found to be underpriced.
294296
func (f *TxFetcher) isKnownUnderpriced(hash common.Hash) bool {
295297
prevTime, ok := f.underpriced.Peek(hash)
296-
if ok && prevTime.Before(time.Now().Add(-maxTxUnderpricedTimeout)) {
298+
if ok && prevTime.Before(f.realTime().Add(-maxTxUnderpricedTimeout)) {
297299
f.underpriced.Remove(hash)
298300
return false
299301
}

eth/fetcher/tx_fetcher_test.go

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,9 +2150,22 @@ func containsHashInAnnounces(slice []announce, hash common.Hash) bool {
21502150
return false
21512151
}
21522152

2153-
// Tests that a transaction is forgotten after the timeout.
2153+
// TestTransactionForgotten verifies that underpriced transactions are properly
2154+
// forgotten after the timeout period, testing both the exact timeout boundary
2155+
// and the cleanup of the underpriced cache.
21542156
func TestTransactionForgotten(t *testing.T) {
2155-
fetcher := NewTxFetcher(
2157+
// Test ensures that underpriced transactions are properly forgotten after a timeout period,
2158+
// including checks for timeout boundary and cache cleanup.
2159+
t.Parallel()
2160+
2161+
// Create a mock clock for deterministic time control
2162+
mockClock := new(mclock.Simulated)
2163+
mockTime := func() time.Time {
2164+
nanoTime := int64(mockClock.Now())
2165+
return time.Unix(nanoTime/1000000000, nanoTime%1000000000)
2166+
}
2167+
2168+
fetcher := NewTxFetcherForTests(
21562169
func(common.Hash) bool { return false },
21572170
func(txs []*types.Transaction) []error {
21582171
errs := make([]error, len(txs))
@@ -2163,24 +2176,83 @@ func TestTransactionForgotten(t *testing.T) {
21632176
},
21642177
func(string, []common.Hash) error { return nil },
21652178
func(string) {},
2179+
mockClock,
2180+
mockTime,
2181+
rand.New(rand.NewSource(0)), // Use fixed seed for deterministic behavior
21662182
)
21672183
fetcher.Start()
21682184
defer fetcher.Stop()
2169-
// Create one TX which is 5 minutes old, and one which is recent
2170-
tx1 := types.NewTx(&types.LegacyTx{Nonce: 0})
2171-
tx1.SetTime(time.Now().Add(-maxTxUnderpricedTimeout - 1*time.Second))
2172-
tx2 := types.NewTx(&types.LegacyTx{Nonce: 1})
21732185

2174-
// Enqueue both in the fetcher. They will be immediately tagged as underpriced
2175-
if err := fetcher.Enqueue("asdf", []*types.Transaction{tx1, tx2}, false); err != nil {
2186+
// Create two test transactions with the same timestamp
2187+
tx1 := types.NewTransaction(0, common.Address{}, big.NewInt(100), 21000, big.NewInt(1), nil)
2188+
tx2 := types.NewTransaction(1, common.Address{}, big.NewInt(100), 21000, big.NewInt(1), nil)
2189+
2190+
now := mockTime()
2191+
tx1.SetTime(now)
2192+
tx2.SetTime(now)
2193+
2194+
// Initial state: both transactions should be marked as underpriced
2195+
if err := fetcher.Enqueue("peer", []*types.Transaction{tx1, tx2}, false); err != nil {
21762196
t.Fatal(err)
21772197
}
2178-
// isKnownUnderpriced should trigger removal of the first tx (no longer be known underpriced)
2179-
if fetcher.isKnownUnderpriced(tx1.Hash()) {
2180-
t.Fatal("transaction should be forgotten by now")
2198+
if !fetcher.isKnownUnderpriced(tx1.Hash()) {
2199+
t.Error("tx1 should be underpriced")
2200+
}
2201+
if !fetcher.isKnownUnderpriced(tx2.Hash()) {
2202+
t.Error("tx2 should be underpriced")
2203+
}
2204+
2205+
// Verify cache size
2206+
if size := fetcher.underpriced.Len(); size != 2 {
2207+
t.Errorf("wrong underpriced cache size: got %d, want %d", size, 2)
2208+
}
2209+
2210+
// Just before timeout: transactions should still be underpriced
2211+
mockClock.Run(maxTxUnderpricedTimeout - time.Second)
2212+
if !fetcher.isKnownUnderpriced(tx1.Hash()) {
2213+
t.Error("tx1 should still be underpriced before timeout")
2214+
}
2215+
if !fetcher.isKnownUnderpriced(tx2.Hash()) {
2216+
t.Error("tx2 should still be underpriced before timeout")
2217+
}
2218+
2219+
// Exactly at timeout boundary: transactions should still be present
2220+
mockClock.Run(time.Second)
2221+
if !fetcher.isKnownUnderpriced(tx1.Hash()) {
2222+
t.Error("tx1 should be present exactly at timeout")
21812223
}
2182-
// isKnownUnderpriced should not trigger removal of the second
21832224
if !fetcher.isKnownUnderpriced(tx2.Hash()) {
2184-
t.Fatal("transaction should be known underpriced")
2225+
t.Error("tx2 should be present exactly at timeout")
2226+
}
2227+
2228+
// After timeout: transactions should be forgotten
2229+
mockClock.Run(time.Second)
2230+
if fetcher.isKnownUnderpriced(tx1.Hash()) {
2231+
t.Error("tx1 should be forgotten after timeout")
2232+
}
2233+
if fetcher.isKnownUnderpriced(tx2.Hash()) {
2234+
t.Error("tx2 should be forgotten after timeout")
2235+
}
2236+
2237+
// Verify cache is empty
2238+
if size := fetcher.underpriced.Len(); size != 0 {
2239+
t.Errorf("wrong underpriced cache size after timeout: got %d, want 0", size)
2240+
}
2241+
2242+
// Re-enqueue tx1 with updated timestamp
2243+
tx1.SetTime(mockTime())
2244+
if err := fetcher.Enqueue("peer", []*types.Transaction{tx1}, false); err != nil {
2245+
t.Fatal(err)
2246+
}
2247+
if !fetcher.isKnownUnderpriced(tx1.Hash()) {
2248+
t.Error("tx1 should be underpriced after re-enqueueing with new timestamp")
2249+
}
2250+
if fetcher.isKnownUnderpriced(tx2.Hash()) {
2251+
t.Error("tx2 should remain forgotten")
2252+
}
2253+
2254+
// Verify final cache state
2255+
if size := fetcher.underpriced.Len(); size != 1 {
2256+
t.Errorf("wrong final underpriced cache size: got %d, want 1", size)
21852257
}
21862258
}

tests/fuzzers/txfetcher/txfetcher_fuzzer.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ func fuzz(input []byte) int {
8484
},
8585
func(string, []common.Hash) error { return nil },
8686
nil,
87-
clock, rand,
87+
clock,
88+
func() time.Time {
89+
nanoTime := int64(clock.Now())
90+
return time.Unix(nanoTime/1000000000, nanoTime%1000000000)
91+
},
92+
rand,
8893
)
8994
f.Start()
9095
defer f.Stop()

0 commit comments

Comments
 (0)