-
Notifications
You must be signed in to change notification settings - Fork 12
Reject transactions that have already been submitted to the tx pool #924
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
base: main
Are you sure you want to change the base?
Conversation
WalkthroughReplaces per-EOA time tracking with an expirable LRU storing eoaActivityMetadata (lastSubmission + recent tx nonces), removes exported ErrDuplicateTransaction, adds per-EOA duplicate-nonce rejection and trimming, updates BatchTxPool/SingleTxPool internals and tests (batch interval, duplicate assertions). Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant BatchTxPool
participant EOACache as "EOA Activity Cache"
User->>BatchTxPool: Add(tx {eoa, nonce})
BatchTxPool->>EOACache: Get(eoa)
alt metadata not found
EOACache-->>BatchTxPool: nil
BatchTxPool->>EOACache: Store {lastSubmission=now, txNonces=[nonce]}
BatchTxPool-->>User: Success
else metadata present
EOACache-->>BatchTxPool: {lastSubmission, txNonces}
BatchTxPool->>BatchTxPool: if nonce in txNonces -> reject
alt duplicate nonce
BatchTxPool-->>User: Error "a tx with nonce X has already been submitted"
else new nonce
BatchTxPool->>EOACache: Update lastSubmission=now, append nonce (trim to max)
BatchTxPool-->>User: Success (batched or immediate)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 0
🧹 Nitpick comments (4)
services/requester/batch_tx_pool.go (2)
26-29: EOA activity metadata and cache TTL align with duplicate-detection goalsUsing
eoaActivityMetadatawith a cappedtxHashesslice and a 10seoaActivityCacheTTLmatches the “last 15 tx hashes over ~10s” requirement and keeps lookup cost bounded. If you expect operators to tune the duplicate window over time, consider derivingeoaActivityCacheTTLfrom config instead of hardcoding10 * time.Second, but the current choice is reasonable for now.Also applies to: 36-39, 59-60, 89-93
135-149: Consider recording EOA activity only when submission succeedsThe duplicate check via
slices.Contains(lastActivity.txHashes, txHash)is straightforward and efficient with the 15-hash cap. One nuance is that you always updatelastActivityand add the hash toeoaActivity, even ifsubmitSingleTransaction(or upstream build/send) fails. That means a client retrying after a transient internal error can be rejected as “already submitted” for up to the TTL, even though the transaction never made it to Flow.If you want duplicate protection to apply only to successfully accepted submissions (single or future batched), you can gate the metadata update on
err == nil:- // Update metadata for the last EOA activity - lastActivity.submittedAt = time.Now() - lastActivity.txHashes = append(lastActivity.txHashes, txHash) - // To avoid the slice of hashes from growing indefinitely, - // maintain only a handful of the last tx hashes. - if len(lastActivity.txHashes) > 15 { - lastActivity.txHashes = lastActivity.txHashes[1:] - } - - t.eoaActivity.Add(from, lastActivity) - - return err + // Update metadata for the last EOA activity only on successful add/submit. + if err == nil { + lastActivity.submittedAt = time.Now() + lastActivity.txHashes = append(lastActivity.txHashes, txHash) + // To avoid the slice of hashes from growing indefinitely, + // maintain only a handful of the last tx hashes. + if len(lastActivity.txHashes) > 15 { + lastActivity.txHashes = lastActivity.txHashes[1:] + } + + t.eoaActivity.Add(from, lastActivity) + } + + return errThis keeps the rejection behavior focused on truly duplicated submissions rather than transient failures.
Also applies to: 170-170, 175-175, 179-189
tests/tx_batching_test.go (2)
517-568: Duplicate-submission test accurately exercises hash-based rejectionThe nonce pattern and assertions (exactly two errors, each containing the expected “already submitted” hash) give good coverage of the new hash-based duplicate detection while still verifying that all non-duplicate transactions execute successfully. The only minor trade-off is that the test is now coupled to specific precomputed hashes; if chain ID or signing details change, you’ll need to update those literals. If you want to reduce that coupling, you could derive the expected duplicate hashes inline from the first submissions before asserting on the error messages.
334-335: Update comments to reflect the new 2.5s TxBatchInterval in tests
setupGatewayNodenow setsTxBatchIntervalto 2.5 seconds, but the explanatory comments in the “recent” and “non-recent” interval tests still mention 2 seconds / 3 seconds, which can be misleading when someone tweaks the interval again.Consider updating the comments to reference the actual configured value instead of hardcoded numbers, for example:
- // For the E2E tests the `cfg.TxBatchInterval` is equal - // to 2 seconds. + // For the E2E tests, `cfg.TxBatchInterval` is currently + // set in setupGatewayNode (2.5 seconds at the time of writing).and similarly for the “3 seconds” wording in the non-recent test.
Also applies to: 448-449, 609-610
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
models/errors/errors.go(1 hunks)services/requester/batch_tx_pool.go(6 hunks)tests/tx_batching_test.go(3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.
Applied to files:
services/requester/batch_tx_pool.go
📚 Learning: 2025-06-17T10:29:35.941Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/pool.go:202-214
Timestamp: 2025-06-17T10:29:35.941Z
Learning: In Flow EVM Gateway's transaction pool (services/requester/pool.go), signing keys acquired with keystore.Take() are intentionally held until the transaction is sealed, not released immediately after sending. The key release happens asynchronously when sealed transactions are ingested via event_subscriber.go NotifyTransaction() -> keystore unsafeUnlockKey() -> key.Done(). This pattern ensures transaction integrity by preventing key reuse before transaction finalization.
Applied to files:
services/requester/batch_tx_pool.gotests/tx_batching_test.go
📚 Learning: 2025-11-12T13:21:59.080Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 890
File: tests/integration_test.go:692-707
Timestamp: 2025-11-12T13:21:59.080Z
Learning: In the flow-evm-gateway integration tests (tests/integration_test.go), the gateway's RPC server continues to operate independently even after the context passed to bootstrap.Run() is cancelled. Context cancellation affects upstream dependencies (like the Emulator acting as an Access Node) but does not immediately shut down the RPC server, allowing tests to verify backup failover scenarios.
Applied to files:
tests/tx_batching_test.go
🧬 Code graph analysis (1)
services/requester/batch_tx_pool.go (3)
services/requester/single_tx_pool.go (1)
SingleTxPool(28-41)services/requester/tx_pool.go (1)
TxPool(16-18)models/errors/errors.go (1)
ErrInvalid(23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test
🔇 Additional comments (1)
models/errors/errors.go (1)
31-32: Transaction error constants remain coherent after duplicate-error removal
ErrFailedTransactionandErrInvalidTransactionare composed correctly, and removing a dedicated duplicate error in favor of genericErrInvalidfor pre-submission checks is consistent with the new batch pool behavior, assuming all references to the old duplicate error were cleaned up.
f7a3610 to
582d698
Compare
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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
services/requester/batch_tx_pool.go (2)
26-29: Confirm EOA activity TTL vs configurable batch intervalYou’re hard-coding
eoaActivityCacheTTLto 10 seconds while batching behavior still depends on the configurableTxBatchInterval. That’s probably fine (issue description mentions a 10-second window), but it means:
- Duplicate detection will never look back further than 10 seconds, even if
TxBatchIntervalis larger.- Memory usage is capped by 10k EOAs × 15 hashes for that 10s window.
If operators ever run with a much larger
TxBatchInterval, you may want to either:
- Tie
eoaActivityCacheTTLtoTxBatchInterval(e.g.,max(TxBatchInterval, 10s)), or- Document explicitly that duplicate protection is limited to a fixed 10-second horizon.
170-170: Consider extracting15into a named constant for tracked hashesThe pruning logic is clear, but
15as a literal is a bit magic:if err == nil { lastActivity.submittedAt = time.Now() lastActivity.txHashes = append(lastActivity.txHashes, txHash) if len(lastActivity.txHashes) > 15 { lastActivity.txHashes = lastActivity.txHashes[1:] } t.eoaActivity.Add(from, lastActivity) }To make the intent self‑documenting and adjustable, consider:
-const ( - eoaActivityCacheSize = 10_000 - eoaActivityCacheTTL = time.Second * 10 -) +const ( + eoaActivityCacheSize = 10_000 + eoaActivityCacheTTL = 10 * time.Second + maxTrackedTxHashesPerEOA = 15 +)- if err == nil { - lastActivity.submittedAt = time.Now() - lastActivity.txHashes = append(lastActivity.txHashes, txHash) - if len(lastActivity.txHashes) > 15 { - lastActivity.txHashes = lastActivity.txHashes[1:] - } - - t.eoaActivity.Add(from, lastActivity) - } + if err == nil { + lastActivity.submittedAt = time.Now() + lastActivity.txHashes = append(lastActivity.txHashes, txHash) + if len(lastActivity.txHashes) > maxTrackedTxHashesPerEOA { + lastActivity.txHashes = lastActivity.txHashes[1:] + } + + t.eoaActivity.Add(from, lastActivity) + }Pure readability/maintainability improvement; behavior stays the same.
Also applies to: 179-190
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
models/errors/errors.go(1 hunks)services/requester/batch_tx_pool.go(6 hunks)tests/tx_batching_test.go(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- models/errors/errors.go
- tests/tx_batching_test.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.
Applied to files:
services/requester/batch_tx_pool.go
📚 Learning: 2025-06-17T10:29:35.941Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/pool.go:202-214
Timestamp: 2025-06-17T10:29:35.941Z
Learning: In Flow EVM Gateway's transaction pool (services/requester/pool.go), signing keys acquired with keystore.Take() are intentionally held until the transaction is sealed, not released immediately after sending. The key release happens asynchronously when sealed transactions are ingested via event_subscriber.go NotifyTransaction() -> keystore unsafeUnlockKey() -> key.Done(). This pattern ensures transaction integrity by preventing key reuse before transaction finalization.
Applied to files:
services/requester/batch_tx_pool.go
🧬 Code graph analysis (1)
services/requester/batch_tx_pool.go (3)
services/requester/single_tx_pool.go (1)
SingleTxPool(28-41)services/requester/tx_pool.go (1)
TxPool(16-18)models/errors/errors.go (1)
ErrInvalid(23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test
🔇 Additional comments (2)
services/requester/batch_tx_pool.go (2)
36-39: EOA activity metadata + LRU wiring looks consistentUsing
eoaActivityMetadata{submittedAt, txHashes}in anexpirable.LRUkeyed by EOA, plus thevar _ TxPool = (*BatchTxPool)(nil)assertion, cleanly encapsulates:
- Batching recency (
submittedAtvsTxBatchInterval), and- Short-horizon duplicate detection (
txHashesbounded by cache TTL and array size).The wiring in
NewBatchTxPool(sharedSingleTxPool, map forpooledTxs, mutex, and the expirable LRU) looks coherent with existing concurrency patterns and should behave deterministically under load.Also applies to: 57-60, 62-62, 89-99
135-148: I'll verify the review comment by examining the codebase and checking the Hash type behavior.Now let me verify the code file exists and check the actual implementation:
Incorrect review comment —
%sformatting is correct forcommon.HashThe review comment mischaracterizes how Go's
fmtpackage formatscommon.Hash. TheHashtype implements a customFormatmethod that handles the's'verb by writing the hex representation with a"0x"prefix. SinceHashimplementsfmt.Formatter, the%sverb will produce the proper hex string output (e.g.,"0x1234..."), not the artifact mentioned in the comment.The
Stringmethod implements the stringer interface and produces identical output. The current code is already correct; no changes are needed.Likely an incorrect or invalid review comment.
582d698 to
86c6bda
Compare
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.
Actionable comments posted: 0
🧹 Nitpick comments (3)
services/requester/batch_tx_pool.go (1)
136-149: Duplicate‑submission check and metadata update align with design; consider Publish ordering as a small follow‑upThe new flow in
Add:
- Computes
txHashand rejects whenfound && slices.Contains(lastActivity.txHashes, txHash), so only byte‑identical re‑submissions from the same EOA are blocked.- Uses
lastActivity.submittedAtwithTxBatchIntervalto decide whether to batch or send individually, and- On success, records
submittedAt = time.Now(), appendstxHash, and prunes to the lastmaxTrackedTxHashesPerEOAbefore writing back toeoaActivity.This cleanly achieves the “reject already‑submitted txs within a short, bounded window” goal and keeps batching behavior intact, including the first‑time
!foundcase (where the zero‑value metadata is safely initialized on success).One minor consideration:
t.txPublisher.Publish(tx)is still called before the dedup check, so duplicate submissions that are rejected at this point will still generate “pending transaction” events downstream. If you don’t want observers to see events for txs that are immediately rejected, you could move thePublishcall to just after this validation block.Also applies to: 171-177, 180-191
tests/tx_batching_test.go (2)
335-336: TxBatchInterval test configuration and comments mostly consistent; one stale referenceSetting
TxBatchIntervaltotime.Millisecond * 2500insetupGatewayNodeand documenting 2.5s in the “recent interval” test comment keeps the test environment aligned with mainnet behavior and with the batching logic inBatchTxPool. The only thing to watch for is that the comment inTest_MultipleTransactionSubmissionsWithinNonRecentIntervalstill mentions a 2‑second interval; if you want the docs fully accurate, updating that comment to 2.5 seconds would avoid future confusion.Also applies to: 609-610
517-568: Duplicate‑submission test correctly exercises the new behavior; assertions are quite strictThe new
Test_TransactionSubmissionWithPreviouslySubmittedTransactions:
- Uses a nonce pattern
[0,1,2,3,2,3,4,5]to create two exact‑hash duplicates and verifies that:
- Exactly two submissions fail, and
- Both failures contain the expected “a tx with hash 0x… has already been submitted” messages.
- Then ensures all successfully submitted txs receive successful receipts.
This is a solid end‑to‑end validation of the new dedup logic and confirms that non‑duplicate txs still execute. The only potential nit is test brittleness: it assumes both duplicate submissions fail and that their errors appear in
errors[0]anderrors[1]with those exact hash strings. If you ever change signing parameters or error formatting, the test will need updates. As a small robustness improvement, you could make the assertions order‑insensitive (e.g., check that the two expected substrings appear somewhere in theerrorsslice) rather than bound to specific indices.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
models/errors/errors.go(1 hunks)services/requester/batch_tx_pool.go(6 hunks)tests/tx_batching_test.go(4 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-12T13:21:59.080Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 890
File: tests/integration_test.go:692-707
Timestamp: 2025-11-12T13:21:59.080Z
Learning: In the flow-evm-gateway integration tests (tests/integration_test.go), the gateway's RPC server continues to operate independently even after the context passed to bootstrap.Run() is cancelled. Context cancellation affects upstream dependencies (like the Emulator acting as an Access Node) but does not immediately shut down the RPC server, allowing tests to verify backup failover scenarios.
Applied to files:
tests/tx_batching_test.go
📚 Learning: 2025-06-17T10:29:35.941Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/pool.go:202-214
Timestamp: 2025-06-17T10:29:35.941Z
Learning: In Flow EVM Gateway's transaction pool (services/requester/pool.go), signing keys acquired with keystore.Take() are intentionally held until the transaction is sealed, not released immediately after sending. The key release happens asynchronously when sealed transactions are ingested via event_subscriber.go NotifyTransaction() -> keystore unsafeUnlockKey() -> key.Done(). This pattern ensures transaction integrity by preventing key reuse before transaction finalization.
Applied to files:
tests/tx_batching_test.goservices/requester/batch_tx_pool.go
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.
Applied to files:
services/requester/batch_tx_pool.go
🧬 Code graph analysis (1)
services/requester/batch_tx_pool.go (4)
services/requester/single_tx_pool.go (1)
SingleTxPool(28-41)services/requester/tx_pool.go (1)
TxPool(16-18)api/pool.go (1)
TxPool(8-8)models/errors/errors.go (1)
ErrInvalid(23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test
🔇 Additional comments (2)
models/errors/errors.go (1)
31-32: Composed ErrInvalidTransaction sentinel looks goodDefining
ErrInvalidTransactionas%w: %woverErrInvalidandErrFailedTransactiongives you a single sentinel while preservingerrors.Ismatches for both underlying categories. This keepsNewInvalidTransactionError’s wrapping behavior consistent without introducing new error types.services/requester/batch_tx_pool.go (1)
26-30: Per‑EOA activity metadata and bounded dedup window look appropriateStoring
eoaActivityMetadata{submittedAt, txHashes}in a 10k‑entry expirable LRU, with a 10s TTL and at most 15 hashes per EOA, gives you a clear, bounded dedup window (~150k hashes max in memory) and keeps the batching heuristics (based onsubmittedAtvsTxBatchInterval) independent from the cache’s eviction policy. This matches the PR intent of “last 15 hashes over ~10 seconds” without impacting the existing batching semantics.Also applies to: 37-40, 60-61, 90-94
86c6bda to
62deb8b
Compare
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.
Actionable comments posted: 0
🧹 Nitpick comments (1)
tests/tx_batching_test.go (1)
546-556: Hardcoded transaction hashes create fragile test assertions.These assertions rely on deterministic tx hashes derived from fixed inputs (private key, nonces, amounts). While this works, any change to the signing parameters or transaction structure could silently break the test without clear indication of why.
Consider extracting the expected hashes programmatically by signing the duplicate transactions upfront and storing their hashes, then comparing against those:
// Before the loop, pre-compute expected duplicate hashes expectedDupHashes := make(map[common.Hash]bool) for _, nonce := range []uint64{2, 3} { signed, _, _ := evmSign(big.NewInt(1_000_000_000), 23_500, eoaKey, nonce, &testAddr, nil) expectedDupHashes[signed.Hash()] = true } // Then in assertions, verify errors reference hashes in expectedDupHashes
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
models/errors/errors.go(1 hunks)services/requester/batch_tx_pool.go(5 hunks)tests/tx_batching_test.go(6 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-12T13:21:59.080Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 890
File: tests/integration_test.go:692-707
Timestamp: 2025-11-12T13:21:59.080Z
Learning: In the flow-evm-gateway integration tests (tests/integration_test.go), the gateway's RPC server continues to operate independently even after the context passed to bootstrap.Run() is cancelled. Context cancellation affects upstream dependencies (like the Emulator acting as an Access Node) but does not immediately shut down the RPC server, allowing tests to verify backup failover scenarios.
Applied to files:
tests/tx_batching_test.go
📚 Learning: 2025-06-17T10:29:35.941Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/pool.go:202-214
Timestamp: 2025-06-17T10:29:35.941Z
Learning: In Flow EVM Gateway's transaction pool (services/requester/pool.go), signing keys acquired with keystore.Take() are intentionally held until the transaction is sealed, not released immediately after sending. The key release happens asynchronously when sealed transactions are ingested via event_subscriber.go NotifyTransaction() -> keystore unsafeUnlockKey() -> key.Done(). This pattern ensures transaction integrity by preventing key reuse before transaction finalization.
Applied to files:
tests/tx_batching_test.goservices/requester/batch_tx_pool.go
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.
Applied to files:
services/requester/batch_tx_pool.go
🧬 Code graph analysis (2)
tests/tx_batching_test.go (1)
config/config.go (2)
TxStateValidation(37-37)LocalIndexValidation(40-40)
services/requester/batch_tx_pool.go (4)
services/requester/single_tx_pool.go (1)
SingleTxPool(28-41)services/requester/tx_pool.go (1)
TxPool(16-18)api/pool.go (1)
TxPool(8-8)models/errors/errors.go (1)
ErrInvalid(23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test
🔇 Additional comments (9)
models/errors/errors.go (1)
31-32: LGTM!The removal of
ErrDuplicateTransactionis appropriate since duplicate detection is now handled via per-EOA metadata inBatchTxPool, with the error constructed inline usingerrs.ErrInvalid.tests/tx_batching_test.go (2)
620-622: Sleep-based synchronization for indexing.The 2-second sleep runs concurrently with
bootstrap.Runto allow indexing to catch up. While this works, it adds to test execution time. If flakiness occurs, consider increasing the timeout or using a more deterministic readiness check.
607-609: LGTM!Configuration changes align with the new duplicate detection approach:
LocalIndexValidationenables local index-based validation, and the 2.5-second batch interval matches mainnet behavior.services/requester/batch_tx_pool.go (6)
26-30: LGTM!The constants provide sensible defaults: 10-second TTL for activity tracking, 15 tracked hashes per EOA, and 10,000 cache size. These values align with the PR objective to reduce noise from bot resubmissions.
37-40: LGTM!The
eoaActivityMetadatastruct cleanly encapsulates per-EOA tracking data. Using a value type with a small slice (max 15 hashes) ensures efficient cache operations.
138-148: Duplicate detection logic is correct.The check using
slices.Containson a max-15 element slice is efficient. The error wrapserrs.ErrInvalidwith an informative message including the transaction hash, which helps with debugging.
170-180: Good correctness fix for transaction ordering.The added check
len(t.pooledTxs[from]) > 0ensures that if there are pending pooled transactions for this EOA, subsequent transactions are added to the same batch rather than submitted individually. This prevents potential ordering issues where a "stale" transaction could race ahead of earlier pooled transactions.
191-200: Metadata update logic is sound.The pattern correctly:
- Only updates on successful submission (after error check)
- Uses FIFO pruning (drops oldest hash first)
- Stores the updated value back to the cache
Note: When
!found,eoaActivityis the zero-value ofeoaActivityMetadata(emptytxHashesslice, zerolastSubmission), which is correctly populated beforeAdd.
217-220: Map swap pattern is thread-safe.The pattern of assigning
txsGroupedByAddress := t.pooledTxsand then creating a new mapt.pooledTxs = make(...)under lock effectively transfers ownership of the old map (and its slice values) to the processing goroutine. This avoids the need for deep copying sinceAdd()will only write to the new map.Based on learnings about deep copying maps with slices, this swap approach is a valid alternative that achieves the same safety guarantees.
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.
Actionable comments posted: 0
🧹 Nitpick comments (3)
services/requester/batch_tx_pool.go (3)
26-29: Consider documenting / de‑magic‑numbering cache sizing constants
eoaActivityCacheSize = 10_000andmaxTrackedTxHashesPerEOA = 15look reasonable, but they’re opaque to a reader. A brief comment tying these to expected traffic patterns and the 10s window (or making them configurable) would make future tuning safer.
41-52: Top‑level comment could mention duplicate‑tx rejectionThe BatchTxPool comment still describes batching/ordering behavior well, but it doesn’t mention the new “recent hash tracking + duplicate rejection” behavior. Adding a short sentence about rejecting recently submitted tx hashes would help future readers understand why
eoaActivityCacheexists.
129-142: Duplicate tx hash rejection logic is sound; consider observabilityThe
slices.Contains(eoaActivity.txHashes, txHash)check correctly rejects re‑submitted txs for the same EOA and wraps them aserrs.ErrInvalid. One thing you might want is a metric/log counter (e.g., a dedicated “duplicate txs rejected” metric, or treating them as dropped) so operators can see how noisy certain EOAs/bots are; right now they’re silent from a metrics perspective.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
services/requester/batch_tx_pool.go(5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.
Applied to files:
services/requester/batch_tx_pool.go
📚 Learning: 2025-06-17T10:29:35.941Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/pool.go:202-214
Timestamp: 2025-06-17T10:29:35.941Z
Learning: In Flow EVM Gateway's transaction pool (services/requester/pool.go), signing keys acquired with keystore.Take() are intentionally held until the transaction is sealed, not released immediately after sending. The key release happens asynchronously when sealed transactions are ingested via event_subscriber.go NotifyTransaction() -> keystore unsafeUnlockKey() -> key.Done(). This pattern ensures transaction integrity by preventing key reuse before transaction finalization.
Applied to files:
services/requester/batch_tx_pool.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test
🔇 Additional comments (7)
services/requester/batch_tx_pool.go (7)
6-6: fmt import usage looks correct
fmtis used only for the wrapped invalid‑tx error; no issues here.
36-39: Per‑EOA activity metadata struct is well‑factoredBundling
lastSubmissionandtxHashesintoeoaActivityMetadatakeeps the Add() logic clear, and the zero value is safe on cache misses. No changes needed.
56-59: BatchTxPool fields align with intended behavior
pooledTxs+txMuxfor batching andeoaActivityCachefor per‑EOA activity/duplicate tracking is a clean separation of concerns. No structural or concurrency issues spotted here.
61-61: Nice use of interface assertion idiom
var _ TxPool = (*BatchTxPool)(nil)is the preferred compile‑time check; good improvement.
88-98: Verify EOAActivityCache TTL is compatible with batching interval
eoaActivityCacheusesconfig.EOAActivityCacheTTLwhile batching decisions useconfig.TxBatchInterval. To avoid surprising behavior (e.g., metadata expiring while we still expect batching / duplicate protection), it would be good to ensureEOAActivityCacheTTL >= TxBatchIntervaland document that contract in config or a comment.
161-188: Add() batching vs single‑submission control flow looks correctThe three scenarios based on
foundandtime.Since(eoaActivity.lastSubmission)are consistent, and thehasBatchpattern correctly appends to any existing batch while falling back to a single submission when none exists.errhandling is safe in all branches (only set when callingsubmitSingleTransaction, and checked once afterward).
193-203: Metadata update and hash list trimming behave as intendedUpdating
eoaActivityonly after a successful add/submit and then appendingtxHash(while trimming to the lastmaxTrackedTxHashesPerEOA) matches the “track last N recent hashes” requirement and keeps memory bounded. The logic to drop the oldest entry viatxHashes[1:]is simple and correct given the small fixed N.
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.
Actionable comments posted: 0
🧹 Nitpick comments (1)
services/requester/batch_tx_pool.go (1)
162-178: Minor TOCTOU window in batch checking.There's a small window between releasing the lock (line 172) and calling
submitSingleTransaction(line 177) where another goroutine could add transactions topooledTxs[from]. However, the impact is minimal—worst case is the transaction gets submitted individually when it could have been batched, which doesn't affect correctness.If you want to tighten this, you could hold the lock during
submitSingleTransactionfor Case 2, or restructure to check-and-submit atomically. Given the low impact, this is optional.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
services/requester/batch_tx_pool.go(5 hunks)tests/tx_batching_test.go(6 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.
Applied to files:
services/requester/batch_tx_pool.go
📚 Learning: 2025-06-17T10:29:35.941Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/pool.go:202-214
Timestamp: 2025-06-17T10:29:35.941Z
Learning: In Flow EVM Gateway's transaction pool (services/requester/pool.go), signing keys acquired with keystore.Take() are intentionally held until the transaction is sealed, not released immediately after sending. The key release happens asynchronously when sealed transactions are ingested via event_subscriber.go NotifyTransaction() -> keystore unsafeUnlockKey() -> key.Done(). This pattern ensures transaction integrity by preventing key reuse before transaction finalization.
Applied to files:
services/requester/batch_tx_pool.gotests/tx_batching_test.go
📚 Learning: 2025-11-12T13:21:59.080Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 890
File: tests/integration_test.go:692-707
Timestamp: 2025-11-12T13:21:59.080Z
Learning: In the flow-evm-gateway integration tests (tests/integration_test.go), the gateway's RPC server continues to operate independently even after the context passed to bootstrap.Run() is cancelled. Context cancellation affects upstream dependencies (like the Emulator acting as an Access Node) but does not immediately shut down the RPC server, allowing tests to verify backup failover scenarios.
Applied to files:
tests/tx_batching_test.go
🧬 Code graph analysis (1)
services/requester/batch_tx_pool.go (4)
services/requester/single_tx_pool.go (1)
SingleTxPool(28-40)services/requester/tx_pool.go (1)
TxPool(16-18)api/pool.go (1)
TxPool(8-8)models/errors/errors.go (1)
ErrInvalid(23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test
🔇 Additional comments (12)
services/requester/batch_tx_pool.go (8)
26-39: LGTM!The constants and
eoaActivityMetadatatype are well-structured for tracking per-EOA activity and recent transaction nonces.
53-61: LGTM!The struct fields and interface assertion are appropriate. Using
(*BatchTxPool)(nil)for interface assertion is idiomatic Go.
88-98: LGTM!The cache initialization properly uses
eoaActivityMetadataand the configured TTL.
129-140: LGTM!The duplicate nonce detection is straightforward and correct. Using
slices.Containsis appropriate given the small max size of 15.
191-200: Verify metadata update happens only on success.The metadata update correctly happens after confirming
err == nil(line 187). The nonce trimming at lines 196-198 keeps only the most recent 15 nonces by removing from the front—this is intentional behavior to bound memory while still catching recent duplicates.
217-220: LGTM!The map swap pattern is thread-safe here: by replacing
t.pooledTxswith a new empty map under the lock, subsequentAdd()calls operate on the new map while the batch processor iterates over the old one. Based on learnings, this avoids the deep-copy concern because no concurrent modifications occur on the same slices.
245-249: LGTM!Sorting by nonce before batch submission ensures correct execution order regardless of arrival order or any prior reordering.
283-312: LGTM!Clean implementation for single transaction submission with appropriate error handling and metrics recording.
tests/tx_batching_test.go (4)
335-335: LGTM!Comment correctly updated to reflect the new 2.5-second batch interval.
517-568: LGTM!The test correctly validates duplicate nonce rejection:
- Submits nonces
[0, 1, 2, 3, 2, 3, 4, 5]where2and3are intentional duplicates- Verifies exactly 2 errors with the expected messages
- Confirms the 6 successful transactions are executed
This effectively covers the new duplicate submission protection.
607-622: Configuration changes align with production settings.
LocalIndexValidationis appropriate for integration tests- The 2.5-second batch interval matches mainnet
- The 2-second sleep allows indexing to catch up
One minor concern: the fixed 2-second sleep could cause flakiness if indexing occasionally takes longer. Consider using
Eventuallyor a readiness check if tests become flaky.
449-449: LGTM!Comment correctly updated to match the 2.5-second batch interval.
| // Reject transactions that have already been submitted, | ||
| // as they are *likely* to fail. | ||
| if found && slices.Contains(eoaActivity.txNonces, nonce) { | ||
| return fmt.Errorf( |
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.
should we return an error here, or just silently drop the tx? What happens on a geth node?
IIRC, bots will commonly resubmit tx at a specific nonce to increase the gas price in an attempt to get it to be executed earlier in the block, or decrease the gas price to effectively cancel the tx. I'm not totally sure, but I suspect these bots would expect submitting the tx to succeed (even if it was never executed)
| eoaActivity.lastSubmission = time.Now() | ||
| eoaActivity.txNonces = append(eoaActivity.txNonces, nonce) |
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.
do we need to protect these writes? Is it possible another request is concurrently updating/reading them?
| // To avoid the slice of nonces from growing indefinitely, | ||
| // maintain only a handful of the last tx nonces. | ||
| if len(eoaActivity.txNonces) > maxTrackedTxNoncesPerEOA { | ||
| eoaActivity.txNonces = eoaActivity.txNonces[1:] | ||
| } |
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.
it should always be 1, but just in case
| // To avoid the slice of nonces from growing indefinitely, | |
| // maintain only a handful of the last tx nonces. | |
| if len(eoaActivity.txNonces) > maxTrackedTxNoncesPerEOA { | |
| eoaActivity.txNonces = eoaActivity.txNonces[1:] | |
| } | |
| // To avoid the slice of nonces from growing indefinitely, | |
| // keep only the last `maxTrackedTxNoncesPerEOA` nonces. | |
| if len(eoaActivity.txNonces) > maxTrackedTxNoncesPerEOA { | |
| firstKeep := len(eoaActivity.txNonces) - maxTrackedTxNoncesPerEOA | |
| eoaActivity.txNonces = eoaActivity.txNonces[firstKeep:] | |
| } |
| const eoaActivityCacheSize = 10_000 | ||
| const ( | ||
| eoaActivityCacheSize = 10_000 | ||
| maxTrackedTxNoncesPerEOA = 15 |
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.
is 15 enough? I think the data we'd need to cache is small enough we could use a larger value here without tying up too much memory.
Closes: #922
Description
To avoid the noise from all the invalid transactions from trading bots, we keep track of the last 15 submitted transaction hashes, on a 10 second interval, with the aim to reject incoming transactions that match any of the tracked hashes.
For contributor use:
masterbranchFiles changedin the Github PR explorerSummary by CodeRabbit
Bug Fixes
Tests
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.