Skip to content

Rework BatchTxPool functionality to a legitimate Ethereum tx mempool - #986

Merged
vishalchangrani merged 25 commits into
mainfrom
mpeter/solidify-tx-mempool
Aug 20, 2026
Merged

Rework BatchTxPool functionality to a legitimate Ethereum tx mempool#986
vishalchangrani merged 25 commits into
mainfrom
mpeter/solidify-tx-mempool

Conversation

@m-Peter

@m-Peter m-Peter commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Closes: #983

Problem

Flow did not have a traditional EVM mempool. On standard EVM chains, when a wallet sends transactions out-of-nonce-sequence (e.g., nonces 5, 7, 6 in parallel), the mempool holds future-nonce transactions until the gap is filled. Flow EVM had no such pooling mechanism — a transaction whose nonce does not match the current account nonce is simply dropped.

The original BatchTxPool implementation partially addressed this by batching transactions that arrived from an EOA with "recent activity" (i.e., a prior transaction within TxBatchInterval, 2.5 seconds for mainnet). However, it still submitted the FIRST transaction from any burst immediately — before the rest of the burst had a chance to arrive. If that first transaction happened to carry a future nonce (due to parallel dispatch), it failed, and the gap it left caused all subsequent nonces in the batch to fail as well.

Fix

For all incoming transactions, we are now inspecting the EOA's current nonce in the local state index:

  1. If it matches the transaction nonce, we submit it right away, and record this activity in the EOA's dedicated queue, for use in future submissions.
  2. If the transaction nonce is higher, we check for any recent submissions to see if we can form a valid sequence. This could happen from in-flight transaction submission, that have not yet been indexed by the local state index. In this case we optimistically submit right away. and record this activity in the EOA's dedicated queue, for use in future submissions.
  3. If none of the above 2 conditions are met, we enqueue the transaction in the pool. The flush timer (TxBatchInterval) is the sole submission trigger. This guarantees that parallel transactions from the same wallet accumulate in the pool before being sorted by nonce and submitted atomically. When we process the per-EOA pool, we read again the current nonce from the local state index, any stale transactions are pruned, we attempt to select a list of transactions with sequential nonces (up to 5 transactions max), and we submit those in a batch. On successful submission, we record this activity in the EOA's dedicated queue, for use in future submissions. When there are submission errors, e.g. due to network, we add the transactions back to the pool as a retry mechanism. This is an important part to avoid gaps, which would require users to resubmit.

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

  • New Features

    • Improved transaction batching with nonce-aware queueing.
    • Eligible transactions can be submitted immediately.
    • Out-of-order transactions are held and released when nonce gaps are filled.
    • Batches are limited to five transactions for predictable processing.
    • Duplicate and in-flight nonce transactions are rejected.
    • Submission results now include Flow transaction IDs.
    • Accounts receive a clear error when their transaction pool is full.
  • Bug Fixes

    • Improved handling of failed submissions, stale transactions, and queue cleanup.
    • Transactions are preserved for retry when batch submission fails.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Batch transaction pooling now tracks transactions per EOA and nonce. It validates local nonces, rejects duplicates, submits eligible transactions immediately, batches sequential transactions, retries failed submissions, and adds regression coverage.

Changes

Nonce-aware transaction pooling

Layer / File(s) Summary
Per-EOA queue model and admission
services/requester/batch_tx_pool.go, models/errors/errors.go, services/requester/batch_tx_pool_test.go
BatchTxPool now stores transactions by EOA and nonce. It validates local nonces, rejects duplicates, enforces queue limits, supports bounded immediate submission, and queues nonce gaps. Unit tests cover queue selection, pruning, spacing, and rollback behavior.
Sequential batch flushing and integration
services/requester/batch_tx_pool.go, bootstrap/bootstrap.go, cmd/run/cmd.go, go.mod
The flush loop selects sequential batches of up to five transactions, retries failed submissions, records Flow transaction IDs, receives a local nonce provider, and deprecates the activity-cache TTL flag.
Batching regression coverage
tests/tx_batching_test.go
Tests cover shuffled nonce bursts, immediate submission, nonce gaps, batch-size limits, queued duplicates, in-flight nonce rejection, and gateway timing changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • #983 — Adds nonce-aware pooling behavior for burst submissions and duplicate or in-flight nonce handling.
  • #975 — Relates to transaction-pool rejection handling and nonce error reporting.

Possibly related PRs

Suggested labels: Bugfix

Suggested reviewers: peterargue, janezpodhostnik, zhangchiqing

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BatchTxPool
  participant NonceProvider
  participant Gateway

  Client->>BatchTxPool: Add transaction
  BatchTxPool->>NonceProvider: Read local EOA nonce
  NonceProvider-->>BatchTxPool: Return current nonce
  BatchTxPool->>BatchTxPool: Queue by nonce or select immediate submission
  BatchTxPool->>Gateway: Submit sequential transaction batch
  Gateway-->>BatchTxPool: Return Flow transaction ID or error
  BatchTxPool->>BatchTxPool: Requeue failed batches or record successful submission
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: reworking BatchTxPool into an Ethereum-style transaction mempool.
Linked Issues check ✅ Passed The implementation addresses issue #983 by tracking nonces, holding nonce gaps, rejecting duplicates, retrying failures, and testing pending-transaction scenarios.
Out of Scope Changes check ✅ Passed The dependency, configuration, CLI, error, and test changes support the BatchTxPool rework and the linked nonce-handling objective.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mpeter/solidify-tx-mempool

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.

@m-Peter
m-Peter force-pushed the mpeter/solidify-tx-mempool branch 3 times, most recently from 7abd408 to 911d22f Compare August 9, 2026 06:58
@m-Peter
m-Peter force-pushed the mpeter/solidify-tx-mempool branch from 911d22f to 8aa8dff Compare August 9, 2026 10:41
@m-Peter
m-Peter marked this pull request as ready for review August 9, 2026 11:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/requester/batch_tx_pool.go (1)

281-332: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Release txQueuesMux before FastPath submission.

Add() holds txQueuesMux while GetNextNonce() reads local state and then while submitSingleTransaction() sends a Flow transaction. All other Add() calls—any EOA—and the flush loop serialize behind the same mutex. Detach the fast-path batch like the flush path and re-acquire txQueuesMux after SendTransaction returns to record lastSubmittedAt and lastSubmittedNonce.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/requester/batch_tx_pool.go` around lines 281 - 332, Update Add() to
release txQueuesMux before the fast-path submitSingleTransaction call, avoiding
holding the global queue lock during nonce lookup and network submission.
Re-acquire the mutex after SendTransaction returns before updating
eoaQueue.lastSubmittedAt and lastSubmittedNonce, while preserving timeout, error
propagation, and submission metrics behavior.
🧹 Nitpick comments (6)
services/requester/batch_tx_pool.go (4)

535-551: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse eoaQueueEntry in eoaEnqueueTxs.

Both functions duplicate the get-or-create logic for a queue.

♻️ Proposed refactor
 func (t *BatchTxPool) eoaEnqueueTxs(address gethCommon.Address, txs []pooledEvmTx) *txQueue {
-	queue, ok := t.txQueues[address]
-	if !ok {
-		queue = &txQueue{
-			txs: make(map[uint64]pooledEvmTx),
-		}
-		t.txQueues[address] = queue
-	}
+	queue := t.eoaQueueEntry(address)
 	for _, tx := range txs {
 		queue.txs[tx.nonce] = tx
 	}
 
 	return queue
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/requester/batch_tx_pool.go` around lines 535 - 551, Update
eoaEnqueueTxs to reuse the existing eoaQueueEntry helper for the get-or-create
queue logic instead of duplicating lookup and initialization. Preserve the
subsequent transaction insertion into the returned queue and its current return
behavior.

425-425: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This assignment has no effect.

batch is the loop copy of the map value. Assigning batch.eoaQueue does not update txBatchByAddress, and batch is not read again in this iteration. eoaEnqueueTxs already performs the re-enqueue. Drop the assignment.

♻️ Proposed cleanup
-					batch.eoaQueue = t.eoaEnqueueTxs(address, batch.txs)
+					t.eoaEnqueueTxs(address, batch.txs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/requester/batch_tx_pool.go` at line 425, Remove the assignment to
batch.eoaQueue in the loop around txBatchByAddress; call eoaEnqueueTxs(address,
batch.txs) directly, since the loop-local batch copy is not persisted or reused.

194-203: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Break the loop at the first nonce gap.

The slice is sorted ascending. After tx.nonce != stateNonce, no later element can match, because nonces only increase. The loop still scans the remaining entries.

♻️ Proposed simplification
 	for _, tx := range txs {
 		if len(txSequence) >= maxTxBatch {
 			break
 		}
-		if tx.nonce == stateNonce {
-			txSequence = append(txSequence, tx)
-			stateNonce += 1
-		}
+		if tx.nonce != stateNonce {
+			break
+		}
+		txSequence = append(txSequence, tx)
+		stateNonce += 1
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/requester/batch_tx_pool.go` around lines 194 - 203, Update the loop
building txSequence to break immediately when tx.nonce differs from stateNonce,
since the sorted input cannot match afterward; retain the maxTxBatch limit and
sequential nonce append/increment behavior.

138-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the comment with the actual bound.

The check tx.nonce > maxEOAPoolSize+stateNonce bounds the nonce window, not the transaction count. The comment states "keep up to maxEOAPoolSize txs per EOA". Both descriptions coincide only when the queue is dense. Update the comment to describe a nonce window, or count entries to enforce a true size cap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/requester/batch_tx_pool.go` around lines 138 - 177, Update the
pool-limit comment in txQueue.pruneTxs to describe the enforced nonce window
from stateNonce through maxEOAPoolSize+stateNonce, rather than claiming a
transaction-count limit. Keep the existing nonce comparison and pruning behavior
unchanged.
bootstrap/bootstrap.go (1)

290-304: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the shared nonce provider construction.

The TxMemPoolMode branch on Lines 273-278 builds the same LocalNonceProvider with the same arguments. Create it once before the branch and pass it to whichever pool needs it. This keeps the two branches consistent if the provider gains a dependency later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bootstrap/bootstrap.go` around lines 290 - 304, Move the LocalNonceProvider
construction out of the TxMemPoolMode conditional so it is created once before
the branch. Reuse that shared nonceProvider when constructing either transaction
pool, including the NewBatchTxPool call, while preserving the existing
constructor arguments and branch behavior.
tests/tx_batching_test.go (1)

273-277: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the actual production constant.

The comment refers to TxMaxBatchSize. The pool defines the cap as maxTxBatch in services/requester/batch_tx_pool.go. Update the comment so a reader can find the source of the value that maxBatchSize duplicates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/tx_batching_test.go` around lines 273 - 277, Update the comment above
Test_BatchTxPool_BatchSizeCap to reference the production constant maxTxBatch
instead of TxMaxBatchSize, matching the cap defined by the batch transaction
pool while preserving the test’s existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/requester/batch_tx_pool.go`:
- Line 431: Update services/requester/batch_tx_pool.go at lines 431-431 and
476-486: in the flush loop, move TxPoolSubmission from the per-attempt path into
the successful-send branch, or give failed attempts a distinct reason; in the
send-failure handling, replace TransactionsDropped with a submission-failure
metric while the batch is re-enqueued, and call TransactionsDropped only when
the pool permanently discards the transaction.
- Around line 352-360: Update the block-view failure handling in the batch
transaction pool tick around nonceProvider.GetBlockView: replace logger.Fatal
with a non-terminating error log, then return from the current operation so this
tick is skipped and the next tick can retry.
- Around line 307-338: After a successful fast-path submission in the
eoaQueue.validNonce branch, delete the entry for tx.Nonce() from eoaQueue.txs
before updating submission metadata and returning. Keep the deletion limited to
the successful submission path so failed submissions retain their queued state.
- Around line 405-430: Update the batch detachment logic before unlocking
txQueuesMux to record the detached batch’s lastSubmittedAt and
lastSubmittedNonce immediately. Preserve the previous values, and restore them
in the submission-error path alongside re-enqueuing batch.txs; keep the
successful completion path from overwriting these reservation values.

In `@tests/tx_batching_test.go`:
- Around line 965-968: In the bootstrap synchronization sequence, move the
time.Sleep call below the <-bootstrapDone receive so the Gateway settle period
starts only after bootstrap reports readiness. Keep the existing two-second
delay and its explanatory comment with the sleep.
- Around line 248-259: Update the stale timing comment and replace the hardcoded
2500ms sleep in the transaction batching test with a multiple of the configured
cfg.TxBatchInterval, ensuring the delay spans at least one complete flush
interval and avoids asserting exactly on a single tick boundary.

---

Outside diff comments:
In `@services/requester/batch_tx_pool.go`:
- Around line 281-332: Update Add() to release txQueuesMux before the fast-path
submitSingleTransaction call, avoiding holding the global queue lock during
nonce lookup and network submission. Re-acquire the mutex after SendTransaction
returns before updating eoaQueue.lastSubmittedAt and lastSubmittedNonce, while
preserving timeout, error propagation, and submission metrics behavior.

---

Nitpick comments:
In `@bootstrap/bootstrap.go`:
- Around line 290-304: Move the LocalNonceProvider construction out of the
TxMemPoolMode conditional so it is created once before the branch. Reuse that
shared nonceProvider when constructing either transaction pool, including the
NewBatchTxPool call, while preserving the existing constructor arguments and
branch behavior.

In `@services/requester/batch_tx_pool.go`:
- Around line 535-551: Update eoaEnqueueTxs to reuse the existing eoaQueueEntry
helper for the get-or-create queue logic instead of duplicating lookup and
initialization. Preserve the subsequent transaction insertion into the returned
queue and its current return behavior.
- Line 425: Remove the assignment to batch.eoaQueue in the loop around
txBatchByAddress; call eoaEnqueueTxs(address, batch.txs) directly, since the
loop-local batch copy is not persisted or reused.
- Around line 194-203: Update the loop building txSequence to break immediately
when tx.nonce differs from stateNonce, since the sorted input cannot match
afterward; retain the maxTxBatch limit and sequential nonce append/increment
behavior.
- Around line 138-177: Update the pool-limit comment in txQueue.pruneTxs to
describe the enforced nonce window from stateNonce through
maxEOAPoolSize+stateNonce, rather than claiming a transaction-count limit. Keep
the existing nonce comparison and pruning behavior unchanged.

In `@tests/tx_batching_test.go`:
- Around line 273-277: Update the comment above Test_BatchTxPool_BatchSizeCap to
reference the production constant maxTxBatch instead of TxMaxBatchSize, matching
the cap defined by the batch transaction pool while preserving the test’s
existing behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78833572-8bf3-40ef-9bc7-6b4f918f48ca

📥 Commits

Reviewing files that changed from the base of the PR and between 1276961 and 8aa8dff.

📒 Files selected for processing (4)
  • bootstrap/bootstrap.go
  • go.mod
  • services/requester/batch_tx_pool.go
  • tests/tx_batching_test.go

Comment thread services/requester/batch_tx_pool.go Outdated
Comment thread services/requester/batch_tx_pool.go
Comment thread services/requester/batch_tx_pool.go
Comment thread services/requester/batch_tx_pool.go Outdated
Comment thread tests/tx_batching_test.go Outdated
Comment thread tests/tx_batching_test.go Outdated

@Kay-Zee Kay-Zee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the rework does what it sets out to do. I verified the three core behaviors against the code (and CI is green): an immediately-ready tx fast-paths synchronously out of Add (spacing always elapses for a fresh queue, validNonce checks the local state index, and unknown accounts read as nonce 0, so first-ever transactions qualify); a second tx from the same EOA inside TxBatchInterval never fast-paths, and the flush loop gates on the state index rather than the timer — a queued tx only enters a batch once the index confirms its predecessor executed, which is strictly stronger than time spacing; and selectSequentialNonces builds the run starting exactly at the state nonce and stops at the first gap, so pooled {5,6,8,9,10} at state nonce 5 submits exactly {5,6} and holds the rest until 7 lands. Test_BatchTxPool_GapHoldAndFill is that scenario end-to-end, and the failed-batch re-queue with reservation rollback means transient submission errors can't manufacture nonce gaps either.

Two inline items — one behavior gap worth a fix (in-flight duplicates), one comment that overstates what this pool does. Neither blocks the merge: the gap can't corrupt state, since the execution-layer nonce check makes double-execution impossible no matter how the pool behaves.

One nit: EOAActivityCacheTTL is dead config now — only the flag and the struct field remain.

// check; needs no index read).
eoaQueue := t.eoaQueueEntry(from)
if existing, ok := eoaQueue.txs[tx.Nonce()]; ok && existing.txHash == tx.Hash() {
return errs.ErrDuplicateTransaction

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dedup only covers the queue, and submitted txs are deleted from it — so a wallet retrying an identical tx after it fast-pathed gets it re-accepted, and if the index hasn't advanced it can be resubmitted: queued at the next tick (selectSequentialNonces picks it up while the state nonce is unchanged), or fast-pathed outright once spacing elapses, since validNonce(5, 5) still holds while the index lags. Concretely: 5 fast-paths, a resent 5 arrives, then 6 — at the next tick 5(resent) and 6 can go out together in one batch.

No double-execution is possible — run.cdc treats nonce-too-low as invalid, so the duplicate either gets skipped in the batch or fails the Cadence wrapper — but it costs a Cadence submission, possibly a failed wrapper tx, noisy metrics, and a success response where Ethereum clients expect already known / nonce too low.

Add already has both numbers in hand, so this can be closed at admission: reject txNonce < stateNonce as nonce-too-low, and stateNonce <= txNonce <= lastSubmittedNonce (when lastSubmittedAt is set) as ErrInFlightNonce — mirroring what TxMemPool does in tx_mempool.go. That also converts the flush-time pruneTxs silent drops (which happen after Add already returned success) into admission-time rejections the caller can actually see.

Worth noting this is inherently per-gateway: a retry hitting a different gateway replica can't be caught this way, and that's fine — the execution-layer nonce check is the real backstop there, same as multi-node Ethereum. Sticky per-EOA routing would make the per-gateway guard effective for realistic retry patterns.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, I missed that part.

Note that nonce-too-low is a check that applies to any pool implementation (SingleTxPool / BatchTxPool / TxMempool), so it already happens in the layers above, to avoid any lock-acquisition from the configured pool strategy, see here: https://github.com/onflow/flow-evm-gateway/blob/main/services/requester/requester.go#L602-L611 .

That being said, I added the check for rejecting in-flight nonces in 427224b .

Comment thread tests/tx_batching_test.go Outdated
// gap, not yet submitted) is rejected with ErrDuplicateTransaction.
//
// A fast-path-submitted tx instead becomes IN-FLIGHT, so resending it would
// surface as ErrInFlightNonce (see Test_TxMemPool_InFlightNonceRejection).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment overstates what BatchTxPool does: ErrInFlightNonce is only enforced in TxMemPool (tx_mempool.go) — this pool has no in-flight tracking, so a resent fast-pathed tx is currently re-accepted, not rejected (see my comment on the duplicate check in batch_tx_pool.go). As written, the test docs describe behavior the code doesn't have; either fix the comment, or fold in the admission-time guard and the claim becomes true.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point 👍 I added the appropriate logic and a dedicated test in 427224b .

@m-Peter

m-Peter commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

The --eoa-activity-cache-ttl CLI flag has been deprecated in 1a08967, and should be removed in a later version.

vishalchangrani and others added 3 commits August 11, 2026 19:28
…tests

- flush success log tagged as flushReasonPrefix (metric already correct)
- flush success/failure paths merge lastSubmittedNonce/At with max() so a
  concurrent Add() fast-path is never regressed by a stale ack or rollback
- eoaEnqueueTxs preserves an existing same-nonce entry: last-write-wins
  keeps a client's fresher payload over a re-queued failed batch
- rename maxEOAPoolSize -> maxNonceLookahead (that's what it enforces and
  add a real per-EOA size cap at admission time via maxEOAQueueSize +
  ErrTxPoolFull
- staleEntry no longer marks a freshly-created queue (zero lastSubmittedAt)
  as stale
- selectSequentialNonces walks the nonce-keyed map directly (O(k)) instead
  of sorting the full queue every tick
- add batch_tx_pool_test.go: txQueue primitives + rollback preserve-fresh
Solidify BatchTxPool: log tag, rollback race, size cap, unit tests
Comment thread services/requester/batch_tx_pool.go Outdated
Comment on lines +324 to +326
if !existsAtNonce && eoaQueue.size() >= maxEOAQueueSize {
return errs.ErrTxPoolFull
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check runs before the fast-path check below. With a full queue (50 txs held behind a missing head nonce), the gap-filling tx that would fast-path and unblock the whole queue is rejected with ErrTxPoolFull, and the queue can never drain on its own. The EOA is wedged until restart. Move the cap check to just before the enqueue, so fast-path-eligible txs bypass it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a legit scenario that could cause some serious issues, for high-volume dApps/bots. Fixed in bf4c366 .

Comment thread services/requester/batch_tx_pool.go Outdated
Comment on lines +457 to +461
eoaQueue := t.eoaEnqueueTxs(address, batch.txs)
if eoaQueue.lastSubmittedNonce == batch.txs[len(batch.txs)-1].nonce {
eoaQueue.lastSubmittedNonce = batch.lastSubmittedNonce
eoaQueue.lastSubmittedAt = batch.lastSubmittedAt
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed batches are re-enqueued with no attempt cap or TTL, so a deterministically failing submission is rebuilt and resent every TxBatchInterval forever, while higher nonces pile up behind it. Cap retries per tx and drop with a WARN + TransactionsDropped after N attempts.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although failing submission should largely be of transient networking issues, because we just use the client from flow-go-sdk to submit transactions to ANs, I have added a cap for retries in aafa055 , to be on the safe side.

}

t.eoaActivity.Add(from, time.Now())
eoaQueue.txs[tx.Nonce()] = userTx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A tx held behind a nonce gap that never fills stays in the pool silently until restart: within maxNonceLookahead it is never pruned, and a non-empty queue never goes stale. TxMemPool covers this with TxPoolTTL/collectExpired. Add an enqueuedAt + TTL with a submit-anyway or WARN-drop path so a stuck tx eventually gets an observable outcome.

@m-Peter m-Peter Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, we should be quite meticulous about not letting the queue grow without control. Added a enqueuedAt + TTL logic in 01bfc16 .

Comment on lines +311 to +316
// Reject an exact duplicate of a transaction already in the queue
// (cheapest check; needs no index read).
eoaQueue := t.eoaQueueEntry(from)
existing, existsAtNonce := eoaQueue.txs[tx.Nonce()]
if existsAtNonce && existing.txHash == tx.Hash() {
return errs.ErrDuplicateTransaction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: GetNextNonce runs above (line ~293) for every tx, so "needs no index read" is inaccurate, and pure duplicate retries still pay a view lookup. Move the duplicate and in-flight checks (queue state only) above the GetNextNonce call, or fix the comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 💯 Updated in f9c164b .

Comment thread services/requester/batch_tx_pool.go Outdated
Comment on lines +73 to +79
// in the local state index:
// 1. If it matches the transaction nonce, we submit it right away, and record
// this activity in the EOA's dedicated queue, for use in future submissions.
// 2. If the transaction nonce is higher, we check for any recent submissions
// to see if we can form a valid sequence. This could happen from in-flight
// transaction submission, that have not yet been index by the local state
// index. In this case we optimistically submit right away. and record this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: steps 1-2 say matching nonces are submitted "right away", but the fast path also requires spacingElapsed — a matching-nonce tx arriving within TxBatchInterval is enqueued, not submitted. Also two typos: "been index by" -> "been indexed by", and a stray period in "right away. and record".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, comments updated in 298b60e .

Comment thread services/requester/batch_tx_pool.go Outdated
// batchSubmission is a batch selected for submission, detached from the queue so
// the network call happens outside queueMux.
type batchSubmission struct {
from gethCommon.Address

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: batchSubmission.from is assigned but never read — the flush loop uses the map key everywhere.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, removed in 8941811 .

Comment thread services/requester/batch_tx_pool.go Outdated
Comment on lines +579 to +585
func (t *BatchTxPool) eoaEnqueueTxs(address gethCommon.Address, txs []pooledEvmTx) *txQueue {
queue, ok := t.txQueues[address]
if !ok {
queue = &txQueue{
txs: make(map[uint64]pooledEvmTx),
}
t.txQueues[address] = queue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this duplicates eoaQueueEntry's get-or-create block. Call eoaQueueEntry and keep only the re-enqueue loop.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, nice, I missed that.. Updated in 686ae8f .

batchTail := batch.txs[len(batch.txs)-1].nonce
if batchTail > batch.eoaQueue.lastSubmittedNonce {
batch.eoaQueue.lastSubmittedNonce = batchTail
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no test covers the ErrTxPoolFull admission path or the flush failure/success merge branches (rollback guard, non-regression merge). A unit test with a fake failing submitter plus a concurrent-Add interleave would cover both.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 3 dedicated unit tests in 6f1b87e .

@m-Peter
m-Peter force-pushed the mpeter/solidify-tx-mempool branch from a7d5981 to 01bfc16 Compare August 12, 2026 15:45

@janezpodhostnik janezpodhostnik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes look great! Thanks. Found 2 new things. the rest looks good.

}
queue.txs[tx.nonce] = tx
}
queue.retries += 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retries is never reset on success. A queue is only re-zeroed when it is evicted (empty and idle for 2x interval), so a continuously active EOA keeps its counter forever. After 5 cumulative failures, even transient blips spread far apart, every later failure drops the batch on the first attempt, which manufactures the nonce gap the retry mechanism exists to prevent.

@m-Peter m-Peter Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch indeed 💯 Fixed in a6844f1 .

Comment on lines +465 to +466
eoaQueue.lastSubmittedAt = time.Now()
eoaQueue.lastSubmittedNonce = txSequence[len(txSequence)-1].nonce

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reservation can regress lastSubmittedNonce below a concurrent fast-path advance. Scenario: batch {5,6,7} is in flight (marker=7), the submit outlives the interval so Add fast-paths nonce 8 (marker=8), the batch fails and is re-enqueued (rollback skipped since 8 != 7), then the next tick re-detaches {5,6,7} and overwrites the marker back to 7. A subsequent Add for nonce 8 now passes the in-flight guard (8 <= 7 is false) and validNonce (7+1), so nonce 8 is submitted twice. Make this non-regressing, like the success-path merge: only advance when the batch tail exceeds the current value.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@janezpodhostnik I'm having difficulty understanding the actual issue here, and the suggested fix.

If the submission of batch {5,6,7} outlives the interval, Add will fast-path nonce 8, which is going to fail with nonce too high: address 0xab, tx: 8, state: 5, because the expected nonce is 5, and the batch submission has not yet happened.

Assuming the batch fails, transactions {5,6,7} are re-enqueued, the next tick re-detaches {5,6,7} and overwrites lastSubmittedNonce back to 7, from 8. To me this is legitimate behavior, because lastSubmittedNonce = 8 doesn't necessarily mean the on-chain nonce advanced, only the marker in EVM Gateway did. In fact, since we haven't yet submitted the batch of {5,6,7}, we can be certain that the on-chain nonce is for sure not 8.

That being said, does your suggestion still hold?

// 2. If enough spacing has elapsed and the transaction nonce is higher, we check
// for any recent submissions to see if we can form a valid sequence. This could
// happen from in-flight transaction submission, that have not yet been indexed
// by the local state index. In this case we optimistically submit right away and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we have case one above is to optimize for the case of ordinary user sending transaction without brust, so that they don't have to wait for 2.5sec just to send a single tx.

But the problem we ran into last time is from case 2, where we received currentNonce + 1, we started waiting, and when the waiting is over, we submitted the batch right away hoping that this batch can be accepted as soon as the in-flight tx, which carries the currentNonce and sent previously, is accepted. However, we still ran into problem, I think it's because even though the single currentNonce tx is sent earlier, it ended up reordered later than the batch tx in the same block, which caused the batch tx to fail.

We'd better add a test case simulating that problem we ran into and verify.

Basically, we had hard control on how long is actually enough for the waiting before sending the batch tx, so that it can be very likely ordered after the single currentNonce tx. Maybe we should make this parameter configurable via flag and experiment with it. For instance, if we configure it with 2.5 sec initially and reduce it down slowly.

With this logic, I think a batch tx without any waiting can still be achieved by sending txs with a nonce gap first, and then send a no-op tx to fill the nonce gap to trigger the whole batch to be sent. Would DFNS send like this?

@m-Peter m-Peter Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we have case one above is to optimize for the case of ordinary user sending transaction without brust, so that they don't have to wait for 2.5sec just to send a single tx.

Exactly, and the reasoning behind it is this: with soft-finality it takes about 6-8 seconds until the local state index is updated after a particular transaction submission. For regular users this is enough time, we basically have the updated nonce in the local state index, by the time the user submits a subsequent transaction.

But the problem we ran into last time is from case 2, where we received currentNonce + 1, we started waiting, and when the waiting is over, we submitted the batch right away hoping that this batch can be accepted as soon as the in-flight tx, which carries the currentNonce and sent previously, is accepted. However, we still ran into problem, I think it's because even though the single currentNonce tx is sent earlier, it ended up reordered later than the batch tx in the same block, which caused the batch tx to fail.

The implementation that produced this issue, has a number of knobs & timers:

  1. TxCollectionWindow (defaults to 300 ms): Per-EOA sliding collection window for the transaction mempool. Resets on each arrival from the same EOA.
  2. TxSubmissionSpacing (defaults to 1.2 seconds): Minimum gap between consecutive Cadence submissions for the same EOA in the transaction mempool; also serves as the flush deadline for a continuously-fed collection window. Recommended ~1.5x the block production rate.

which makes it a bit harder to reason about the root cause of the issue.

The implementation proposed in this PR, has a single timer:

  1. TxBatchInterval (defaults to 2.5 seconds): Time interval upon which to submit the transaction batches to the Flow network.

to make sure that we don't cause any disruptions for burst dApps, we are more defensive in the waiting/queuing time. We start from the state nonce, and submit batches with sequential nonces. But we can't wait the 6-8 seconds it takes to index the state from the submitted batch, we sequentially update lastSubmittedNonce, so that we can further reduce to waiting time for burst dApps as well.

For that, the we'll run some live tests on testnet (evm-001) because with Emulator it's hard to simulate the collection topology and traffic that exists for testnet/mainnet.

Comment thread services/requester/batch_tx_pool.go Outdated
)
return err
}
t.logSubmission(from, []pooledEvmTx{userTx}, flushReasonFastPath, flowTxID)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are two cases that eoaQueue.validNonce would return true, can we distinguish them in the flushReasonFastPath logs?

  1. the tx's nonce is the localIndexedNonce + 1 (i.e. log as `fast-path: next unindexed nonce)
  2. the tx's nonce is the lastSubmittedNonce + 1 (i.e. log as `fast-path: next unsubmitted nonce)

So that if we ran into wrong nonce issue, it could confirm what case exactly is hitting.

maybe we can extract the logic in the if condition into a function to be reused by both cases?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would definitely help with debugging. Updated in dca6f94 .

Comment thread services/requester/batch_tx_pool.go Outdated
// has elapsed and the tx nonce is the next expected, we submit right
// away and update the `lastSubmittedAt` & `lastSubmittedNonce` fields,
// for classifying future submissions, that might arrive shortly.
if eoaQueue.spacingElapsed(time.Now(), t.config.TxBatchInterval) && eoaQueue.validNonce(tx.Nonce(), nonce) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an edge case where lastSubmittedAt < tx.Nonce() < nextIndexedNonce, which we should reject, but would be added to the queue?

Why would lastSubmittedAt < nextIndexedNonce? maybe because there is another gateway sending tx for this EOA as well.

So if lastSubmitted is 10, nextIndexedNonce is 20, and tx.Nonce() is 18, it ended up being added to the queue, but actually could be rejected.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there are prior validation checks at outer layers (the Requester for example), before even a transaction gets in the pool (regardless of which pool implementation is in use).

Two such checks are:

  1. Making sure that the sender has enough balance to cover gas fees + transfer value
  2. The transaction nonce is not lower than the nextIndexedNonce

Check 2. is done here: https://github.com/onflow/flow-evm-gateway/blob/main/services/requester/requester.go#L602-L611 . So in your example scenario above, the transaction would have been rejected even before reaching the pool, and an appropriate error message would be returned to the author.

Comment thread services/requester/batch_tx_pool.go Outdated
Comment thread services/requester/batch_tx_pool.go Outdated
Comment on lines +262 to +263
lastSubmittedAt time.Time
lastSubmittedNonce uint64

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add comments to this variable, I think they are used for reverting the queue's last submitted state in order to retry submission, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, you are right. Added comments in 1e2b22e .

Comment thread services/requester/batch_tx_pool.go Outdated
Comment on lines +507 to +510
if eoaQueue.lastSubmittedNonce == batch.txs[len(batch.txs)-1].nonce {
eoaQueue.lastSubmittedNonce = batch.lastSubmittedNonce
eoaQueue.lastSubmittedAt = batch.lastSubmittedAt
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this can be moved into eoaEnqueueTxs, since this is the only case we enqueue a batch of txs, so the lastSubmittedNonce should also be checked and updated. In this case, it doesn't need to return the eoaQueue any more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes more sense, and it will allow for better unit tests. Updated in 8ebe992 .

Comment thread tests/tx_batching_test.go
@@ -514,59 +975,6 @@ func Test_MultipleTransactionSubmissionsWithinNonRecentInterval(t *testing.T) {
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have a test case to simulate this case?
#983 (comment)

Basically, we need to verify that if an EOA have a series of txs with nonce [1-10] send to the GW in whichever order and spacing, the GW will never send to the AN two txs, which has consecutive nonce, within a batch interval. We could verify by making the mock tx receiver panic when receiving nonce 2 too quickly after reciving nonce 1, which is the case that caused both nonce 1 and 2 are included in the same block but in the wrong order.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea 👍 While adding such an E2E test in 250bfaf, I realized we had a small gap in processPooledTransactions(), that could manifest the case you mentioned above.

The case was this: We could be holding a batch [1-5], for some amount of time. When 0, it would be fast-pathed, but depending on the status of the timer in processPooledTransactions(), we could end up submitting the batch [1-5] in quite a short interval. So I added:

// skip processing for this EOA, if there was any recent activity
// from concurrent `Add()`
if !eoaQueue.spacingElapsed(time.Now(), t.config.TxBatchInterval) {
	continue
}

The batch would be picked up on the next tick, which I made more frequent now, to avoid extending the waiting time.

@m-Peter
m-Peter requested a review from zhangchiqing August 19, 2026 12:24
@vishalchangrani

Copy link
Copy Markdown
Contributor

Correction on prior testnet validation comment (deleted)

The earlier "Testnet validation" comment I posted was misleading and has been deleted. Here is the corrected picture.

What was wrong:

What's needed for a real testnet validation of this PR:

  • Reconfigure testnet flags: swap --tx-mempool-mode=true--tx-batch-mode=true --tx-batch-interval=2.5s. Same image (v1.5.5-mempool-rework-pr-986), no rebuild required. Then re-run the golden-path smoke suite.

Wedge-recovery validation (issue #983 comment 5112498568) cannot be done on testnet at all:

  • The wedge state is only visible while on-chain state stays at N (i.e., the Cadence wrapper reverted). Testnet cannot force that.
  • Even with --tx-batch-mode=true, validateTransactionWithState at services/requester/requester.go:224 intercepts every retry with ErrNonceTooLow as soon as tx1 mines and state indexes forward. That happens in ~1–3s regardless of the pool's staleEntry window (~10s), so the pool's eviction timing is masked.
  • Validation of the wedge fix has to be done in-repo with emu.DisableAutoMine(). See PR test(requester): add BatchTxPool wedge → recovery e2e test #988 (stacked on this one) which adds Test_BatchTxPool_WedgeRecovery — extends Test_BatchTxPool_InFlightNonceRejection to assert that a same-nonce retry succeeds after the TxBatchInterval * stalenessFactor window. Passes locally in ~16s.

Follow-up work when this ships:

  • Mainnet activation is a config change on the foundation gateway (same flag swap, same image).
  • The reconciliation-loop approach on vishal/mempool-reconcile-loop-sf is orthogonal — it also fixes the wedge but through GetTransactionResult polling rather than eviction; not part of this PR.

🤖 Generated with Claude Code

@vishalchangrani

Copy link
Copy Markdown
Contributor

Testnet validation — actual BatchTxPool code path

Reconfigured evm-001.devnet0.nodes.onflow.org:8000 to --tx-batch-mode=true --tx-batch-interval=2.5s (dropping --tx-mempool-mode=true), same image v1.5.5-mempool-rework-pr-986. This time the reworked BatchTxPool is actually on the request path.

Version gate: web3_clientVersion = flow-evm-gateway@v1.5.5-mempool-rework-pr-986

EOA: 0x061B63D29332e4de81bD9F51A48609824CD113a8

Scenario Result
fastpath — single tx, expected next nonce 1/1 mined status 1 in 4.6s ✅
burst -n 50 — 50 sequential nonces, concurrent send 41/50 mined; tail nonces 409..417 dropped by pool TTL ⚠️
gap — send N+5 first, then fill N..N+4 N+5 held while gap unfilled; all 6 mined in 14s once gap filled ✅
duplicate — resend identical bytes of a queued tx Second submission rejected: invalid: transaction already in pool

The burst 50 tail-drop

Mechanism:

  • TxBatchInterval = 2.5s gates consecutive submits per EOA (spacingElapsed in batch_tx_pool.go:141).
  • maxTxBatch = 5 caps each batch → 50 txs need 10 sequential batches.
  • maxQueueTTL = 30s prunes any queued tx that's been held past the TTL (pruneTxs, line 213-222).
  • 10 batches × 2.5s spacing + state indexing lag between batches ≈ 30+s to drain the queue, so tail nonces hit TTL before their batch can form.
  • Verified after the fact: on-chain nonce reached 408 (41 txs mined). Nonces 409..417 were dropped; the subsequent gap scenario re-signed 409..414 which then mined, leaving 415..417 permanently gapped on chain.

Context:

  • The old --tx-mempool-mode=true config used a 1.2s TxSubmissionSpacing default → same 50-tx burst previously got 50/50 in 15s.
  • The 2.5s --tx-batch-interval value is the mainnet convention (matches tests/tx_batching_test.go:1106), and stalenessFactor was tuned to it in 8529e5cc, so this is by design for the new pool — not a code regression in Rework BatchTxPool functionality to a legitimate Ethereum tx mempool #986.
  • Real-world DFNS traffic is sequential-over-time, not a 50-nonce burst, so this may not manifest on mainnet at current traffic patterns. But it does mean burst tolerance is lower with this pool at 2.5s spacing than with the old mempool at 1.2s spacing.

Follow-up questions this raises (not blockers for #986 itself):

  • Should mainnet activation use --tx-batch-interval=1.2s instead of 2.5s to preserve the previous burst tolerance? Trade-off: shorter staleEntry window (4.8s vs 10s wedge recovery).
  • Would raising maxQueueTTL (currently a hard-coded 30s constant) help without hurting the wedge-recovery semantics?

Coverage note

🤖 Generated with Claude Code

Reproduces the wedge state from #983 (Cadence wrapper reverts before
advancing state, leaving the pool's in-flight marker ahead of the
on-chain frontier) and asserts BatchTxPool clears it within
TxBatchInterval*stalenessFactor. Auto-mine is disabled so the pool's
staleEntry timing is exposed - otherwise validateTransactionWithState
preempts once state catches up, masking the pool's recovery.

This is the counterpart to Test_BatchTxPool_InFlightNonceRejection,
which only verifies the wedge state is entered, not that it recovers.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@zhangchiqing zhangchiqing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thanks for the update!

@vishalchangrani

Copy link
Copy Markdown
Contributor

Mainnet rollout status — first 24h clean ✅

evm-001.mainnet0.nodes.onflow.org:8000 was bumped to v1.5.5-mempool-rework-pr-986 (from v1.5.0-batch-fix-with-sf-and-reduce-spend) yesterday, same flags (--tx-batch-mode=true --tx-batch-interval=2.5s). No issues observed over the first ~24h under real DFNS traffic.

Prometheus snapshot (network="mainnet0", last 24h):

Metric Value
evm_gateway_transactions_dropped_total (Δ) 0
evm_gateway_rate_limited_transactions_total (Δ) 0
evm_gateway_txpool_queued_transactions 0 now, max 4 over 24h
evm_gateway_txpool_submissions_total (Δ) 5,173 fast-path / 1,678 consecutive-prefix (~76% fast-path)
evm_gateway_available_signing_keys min 1996 / 2000 (peak 4 in flight)
Indexing EVM ↔ Cadence height moving 1:1 throughout
Operator balance drift ~46 FLOW / 24h (normal gas spend)

No wedge events, no TTL prunes, no batch-retry exhaustion logs. Keeping the watch on for another 24h before calling it fully baked, but so far the rework is behaving exactly as intended: fast-path dominant, queue drained, drops flat.

@vishalchangrani
vishalchangrani merged commit da1e41e into main Aug 20, 2026
2 checks passed
@vishalchangrani
vishalchangrani deleted the mpeter/solidify-tx-mempool branch August 20, 2026 15:08
@github-project-automation github-project-automation Bot moved this from 👀 In Review to ✅ Done in 🌊 Flow 4D Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Burst of EVM Transactions fail with "transaction with the same nonce already submitted" error

5 participants