Skip to content

Conversation

@m-Peter
Copy link
Collaborator

@m-Peter m-Peter commented Nov 24, 2025

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:

  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the standards mentioned here.
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

Summary by CodeRabbit

  • Bug Fixes

    • Improved duplicate-transaction prevention by tracking recent per-account submissions; repeated nonces are rejected with clearer "already submitted" errors.
    • Adjusted submission timing and batching logic to reduce false duplicates and improve submission reliability.
  • Tests

    • Updated transaction-submission tests to validate duplicate detection, renamed the test, changed batching interval to 2.5s, and added a short indexing wait.
  • Refactor

    • Pooling and concurrency behavior reworked to improve batching reliability and throughput.
  • Chores

    • Removed an obsolete exported duplicate-error symbol from the public API.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

Replaces 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

Cohort / File(s) Summary
Error definition removal
models/errors/errors.go
Removed exported ErrDuplicateTransaction variable; other error declarations unchanged.
EOA activity metadata & dedup logic
services/requester/batch_tx_pool.go
Added eoaActivityMetadata { lastSubmission time.Time, txNonces []uint64 }; changed BatchTxPool.eoaActivity LRU value type to eoaActivityMetadata; added eoaActivityCacheSize, eoaActivityCacheTTL, maxTrackedTxNoncesPerEOA; removed txHash from pooledEvmTx; Add() now checks per-EOA txNonces to reject duplicate nonces, updates lastSubmission and appends/prunes txNonces on success; updated NewBatchTxPool imports and public type assertion.
Single-tx signing adjustments
services/requester/single_tx_pool.go
Removed mux sync.Mutex field and helper fetchSigningAccountKey; replaced key retrieval with keystore.Take() in buildTransaction; updated interface-implementation assertion form.
Duplicate submission test & batching interval
tests/tx_batching_test.go
Renamed test to Test_TransactionSubmissionWithPreviouslySubmittedTransactions; changed test address and submission sequence to nonces [0,1,2,3,2,3,4,5]; collects successes and duplicate errors and asserts two duplicate error messages referencing specific nonces; adjusted TxBatchInterval in setup to 2.5s and related validation/wait timing.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Extra attention:
    • services/requester/batch_tx_pool.go — LRU value-type change, duplicate-nonce logic, metadata lifecycle and pruning.
    • tests/tx_batching_test.go — test sequencing, duplicate error assertions, and timing/batch-interval adjustments.
    • services/requester/single_tx_pool.go — removal of mutex and changed key retrieval/signing path.
    • models/errors/errors.go — confirm no remaining references to removed exported error.

Possibly related issues

  • #857 — Matches: both implement per-EOA in-memory tracking to detect/reject duplicate submissions; this PR adds eoaActivityMetadata and nonce-based dedup.
  • #922 — Related: requests gateway-level rejection of invalid EVM txs; this PR's duplicate-nonce rejection partially addresses the objective of rejecting invalid/duplicate transactions before submission.

Possibly related PRs

  • #916 — Overlaps on duplicate-prevention in BatchTxPool Add flow; similar intent though different dedup approach.
  • #835 — Related changes to BatchTxPool eoaActivity handling and submission flow; similar cache/type and submission adjustments.
  • #772 — Related keystore/signing changes: both replace previous key-fetch logic with the new keystore.Take()/AccountKey pattern.

Suggested reviewers

  • zhangchiqing
  • janezpodhostnik
  • peterargue

Poem

🐇 I watched nonces hop in a tidy row,

I kept the newest steps and let old echoes go.
When twins tried to dance I gently said "nope",
Trimmed the tails, saved the beat, and hopped with hope.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly summarizes the main change: rejecting transactions already submitted to the tx pool, which aligns with implementing duplicate transaction validation.
Linked Issues check ✅ Passed The PR implements tracking of submitted transaction nonces to reject duplicates [#922], addressing the invalid transaction rejection mechanism requested in the linked issue.
Out of Scope Changes check ✅ Passed Changes include removing ErrDuplicateTransaction, refactoring BatchTxPool with eoaActivityMetadata, removing mutex from SingleTxPool, and test updates - all related to implementing duplicate transaction rejection.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch mpeter/submitted-tx-validations

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 goals

Using eoaActivityMetadata with a capped txHashes slice and a 10s eoaActivityCacheTTL matches 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 deriving eoaActivityCacheTTL from config instead of hardcoding 10 * 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 succeeds

The duplicate check via slices.Contains(lastActivity.txHashes, txHash) is straightforward and efficient with the 15-hash cap. One nuance is that you always update lastActivity and add the hash to eoaActivity, even if submitSingleTransaction (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 err

This 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 rejection

The 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

setupGatewayNode now sets TxBatchInterval to 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

📥 Commits

Reviewing files that changed from the base of the PR and between bbeb971 and f7a3610.

📒 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.go
  • tests/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

ErrFailedTransaction and ErrInvalidTransaction are composed correctly, and removing a dedicated duplicate error in favor of generic ErrInvalid for pre-submission checks is consistent with the new batch pool behavior, assuming all references to the old duplicate error were cleaned up.

@m-Peter m-Peter force-pushed the mpeter/submitted-tx-validations branch from f7a3610 to 582d698 Compare November 24, 2025 16:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 interval

You’re hard-coding eoaActivityCacheTTL to 10 seconds while batching behavior still depends on the configurable TxBatchInterval. 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 TxBatchInterval is 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 eoaActivityCacheTTL to TxBatchInterval (e.g., max(TxBatchInterval, 10s)), or
  • Document explicitly that duplicate protection is limited to a fixed 10-second horizon.

170-170: Consider extracting 15 into a named constant for tracked hashes

The pruning logic is clear, but 15 as 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

📥 Commits

Reviewing files that changed from the base of the PR and between f7a3610 and 582d698.

📒 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 consistent

Using eoaActivityMetadata{submittedAt, txHashes} in an expirable.LRU keyed by EOA, plus the var _ TxPool = (*BatchTxPool)(nil) assertion, cleanly encapsulates:

  • Batching recency (submittedAt vs TxBatchInterval), and
  • Short-horizon duplicate detection (txHashes bounded by cache TTL and array size).

The wiring in NewBatchTxPool (shared SingleTxPool, map for pooledTxs, 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 — %s formatting is correct for common.Hash

The review comment mischaracterizes how Go's fmt package formats common.Hash. The Hash type implements a custom Format method that handles the 's' verb by writing the hex representation with a "0x" prefix. Since Hash implements fmt.Formatter, the %s verb will produce the proper hex string output (e.g., "0x1234..."), not the artifact mentioned in the comment.

The String method 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.

@m-Peter m-Peter force-pushed the mpeter/submitted-tx-validations branch from 582d698 to 86c6bda Compare November 24, 2025 16:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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‑up

The new flow in Add:

  • Computes txHash and rejects when found && slices.Contains(lastActivity.txHashes, txHash), so only byte‑identical re‑submissions from the same EOA are blocked.
  • Uses lastActivity.submittedAt with TxBatchInterval to decide whether to batch or send individually, and
  • On success, records submittedAt = time.Now(), appends txHash, and prunes to the last maxTrackedTxHashesPerEOA before writing back to eoaActivity.

This cleanly achieves the “reject already‑submitted txs within a short, bounded window” goal and keeps batching behavior intact, including the first‑time !found case (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 the Publish call 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 reference

Setting TxBatchInterval to time.Millisecond * 2500 in setupGatewayNode and documenting 2.5s in the “recent interval” test comment keeps the test environment aligned with mainnet behavior and with the batching logic in BatchTxPool. The only thing to watch for is that the comment in Test_MultipleTransactionSubmissionsWithinNonRecentInterval still 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 strict

The 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] and errors[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 the errors slice) rather than bound to specific indices.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 582d698 and 86c6bda.

📒 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.go
  • services/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 good

Defining ErrInvalidTransaction as %w: %w over ErrInvalid and ErrFailedTransaction gives you a single sentinel while preserving errors.Is matches for both underlying categories. This keeps NewInvalidTransactionError’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 appropriate

Storing 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 on submittedAt vs TxBatchInterval) 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

@m-Peter m-Peter force-pushed the mpeter/submitted-tx-validations branch from 86c6bda to 62deb8b Compare November 25, 2025 16:59
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 86c6bda and 62deb8b.

📒 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.go
  • services/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 ErrDuplicateTransaction is appropriate since duplicate detection is now handled via per-EOA metadata in BatchTxPool, with the error constructed inline using errs.ErrInvalid.

tests/tx_batching_test.go (2)

620-622: Sleep-based synchronization for indexing.

The 2-second sleep runs concurrently with bootstrap.Run to 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: LocalIndexValidation enables 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 eoaActivityMetadata struct 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.Contains on a max-15 element slice is efficient. The error wraps errs.ErrInvalid with 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]) > 0 ensures 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:

  1. Only updates on successful submission (after error check)
  2. Uses FIFO pruning (drops oldest hash first)
  3. Stores the updated value back to the cache

Note: When !found, eoaActivity is the zero-value of eoaActivityMetadata (empty txHashes slice, zero lastSubmission), which is correctly populated before Add.


217-220: Map swap pattern is thread-safe.

The pattern of assigning txsGroupedByAddress := t.pooledTxs and then creating a new map t.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 since Add() 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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_000 and maxTrackedTxHashesPerEOA = 15 look 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 rejection

The 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 eoaActivityCache exists.


129-142: Duplicate tx hash rejection logic is sound; consider observability

The slices.Contains(eoaActivity.txHashes, txHash) check correctly rejects re‑submitted txs for the same EOA and wraps them as errs.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

📥 Commits

Reviewing files that changed from the base of the PR and between 62deb8b and c6b18b1.

📒 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

fmt is used only for the wrapped invalid‑tx error; no issues here.


36-39: Per‑EOA activity metadata struct is well‑factored

Bundling lastSubmission and txHashes into eoaActivityMetadata keeps 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 + txMux for batching and eoaActivityCache for 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

eoaActivityCache uses config.EOAActivityCacheTTL while batching decisions use config.TxBatchInterval. To avoid surprising behavior (e.g., metadata expiring while we still expect batching / duplicate protection), it would be good to ensure EOAActivityCacheTTL >= TxBatchInterval and document that contract in config or a comment.


161-188: Add() batching vs single‑submission control flow looks correct

The three scenarios based on found and time.Since(eoaActivity.lastSubmission) are consistent, and the hasBatch pattern correctly appends to any existing batch while falling back to a single submission when none exists. err handling is safe in all branches (only set when calling submitSingleTransaction, and checked once afterward).


193-203: Metadata update and hash list trimming behave as intended

Updating eoaActivity only after a successful add/submit and then appending txHash (while trimming to the last maxTrackedTxHashesPerEOA) matches the “track last N recent hashes” requirement and keeps memory bounded. The logic to drop the oldest entry via txHashes[1:] is simple and correct given the small fixed N.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 to pooledTxs[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 submitSingleTransaction for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 856da05 and 33d6624.

📒 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.go
  • tests/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 eoaActivityMetadata type 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 eoaActivityMetadata and the configured TTL.


129-140: LGTM!

The duplicate nonce detection is straightforward and correct. Using slices.Contains is 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.pooledTxs with a new empty map under the lock, subsequent Add() 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] where 2 and 3 are 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.

  • LocalIndexValidation is 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 Eventually or 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(
Copy link
Contributor

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)

Comment on lines +192 to +193
eoaActivity.lastSubmission = time.Now()
eoaActivity.txNonces = append(eoaActivity.txNonces, nonce)
Copy link
Contributor

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?

Comment on lines +194 to +198
// 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:]
}
Copy link
Contributor

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

Suggested change
// 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
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add additional TX validations to reject invalid txs

3 participants