fix(flashblocks-rpc): fix eth_getTransactionCount to expose txpool nonce awareness for pending tag#256
Conversation
…nCount
The flashblocks RPC overlay for eth_getTransactionCount("pending")
returned only the execution state nonce from the flashblock cache,
without consulting the transaction pool. This caused "replacement
transaction underpriced" errors when clients queried pending nonce
immediately after submitting transactions, as the returned nonce
was stale (only reflecting flashblock-executed txs).
Mirrors reth's LoadState::transaction_count behavior by checking
pool().get_highest_consecutive_transaction_by_sender() for the
pending tag and returning max(state_nonce, pool_nonce + 1).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@claude review this PR |
XLayer-Bot
left a comment
There was a problem hiding this comment.
The fix is correct and well-implemented.
Logic analysis
The handler for the pending tag now returns , which is the right formula. A few things I verified:
- Using the flashblock execution state nonce (not the canonical state nonce) as the argument to is correct. Pool transactions with nonce < flashblock_state_nonce are already included in the flashblock execution, so the flashblock nonce is the right floor. The pool internally guards with
on_chain_nonce.max(pool_state_nonce), so no double-counting occurs. - The
nonce.max(next_pool_tx_nonce)guard is a faithful copy of reth's own defensive pattern (labelled "guard against drifts in the pool" upstream). It's redundant in the common path but harmless and intentional. BlockId::pending()isBlockId::Number(BlockNumberOrTag::Pending). There is noBlockId::Tagvariant in alloy, and"pending"always deserialises toBlockId::Number(BlockNumberOrTag::Pending), so the equality check is exhaustive.- Skipping txpool for
None/Latest is correct — Latest is a snapshot query and txpool lookahead is only meaningful for Pending. - No stale-nonce risk:
MemoryOverlayStateProvideralways falls back to the canonical historical layer, so the flashblock state nonce is never behind canonical.
Cargo changes — reth-transaction-pool dep addition is minimal and correct.
The flashblocks RPC overlay was executing state reads (account_nonce, account_balance, account_code, storage, EVM transact) directly on the async runtime. When the MemoryOverlayStateProvider falls through to the canonical MDBX state provider for accounts not in the overlay, this becomes a blocking disk I/O read that stalls the tokio runtime. Wrap all flashblocks state API handlers in spawn_blocking_io_fut to match reth's standard EthState/EthCall patterns: - eth_call: acquire_owned_blocking_io + spawn_blocking_io_fut - eth_estimateGas: spawn_blocking_io_fut - eth_getBalance: spawn_blocking_io_fut - eth_getTransactionCount: spawn_blocking_io_fut - eth_getCode: spawn_blocking_io_fut - eth_getStorageAt: spawn_blocking_io_fut 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@claude please review again |
XLayer-Bot
left a comment
There was a problem hiding this comment.
The two fixes are both correct and well-implemented.
txpool nonce awareness ( / pending)
The logic mirrors reth's exactly: reads from the flashblock overlay state, then calls , adds 1, and returns . The is a harmless safety net; since the pool query uses the state nonce as its base, will always be ≥ . Overflow is handled with . Correct.
spawn_blocking_io_fut wrapping
All six flashblock state handlers are now off the async runtime. The acquired by in is a RAII guard: Rust evaluates the full (including the ) before dropping locals, so the semaphore slot is held for the entire duration of the spawned work. The asymmetry — takes the extra permit while the lighter read-only handlers (, , , , ) do not — is intentional and consistent with reth's pattern. is a synchronous function, so calling it without inside the blocking closure is correct.
LGTM.
XLayer-Bot
left a comment
There was a problem hiding this comment.
The two fixes are both correct and well-implemented.
txpool nonce awareness (eth_getTransactionCount / pending)
The logic mirrors reth's LoadState::transaction_count exactly: reads on_chain_account_nonce from the flashblock overlay state, then calls get_highest_consecutive_transaction_by_sender(address, on_chain_account_nonce), adds 1, and returns max(state_nonce, pool_nonce + 1). The max() is a harmless safety net — since the pool query uses the state nonce as its base, pool_nonce + 1 will always be ≥ state_nonce. Overflow is handled with checked_add(1).ok_or_else(...). Correct.
spawn_blocking_io_fut wrapping
All six flashblock state handlers are now off the async runtime. The _permit acquired by acquire_owned_blocking_io in eth_call is a RAII guard: Rust evaluates the full return expression (including the .await) before dropping locals, so the semaphore slot is held for the entire duration of the spawned work. The asymmetry — eth_call takes the extra permit while the lighter read-only handlers (balance, code, storage, transaction_count, estimate_gas) do not — is intentional and consistent with reth's EthCall pattern. estimate_gas_with is a synchronous function, so calling it without .await inside the blocking closure is correct.
LGTM.
|
🔧 CI running — workflow run |
|
✅ XLayer Reth CI passed for commit
|
Summary
eth_getTransactionCount("pending")returned only the flashblock execution state nonce without consulting the txpool. This causedreplacement transaction underpricederrors when clients queried the pending nonce right after submitting transactions.LoadState::transaction_count— for the pending tag, checkspool().get_highest_consecutive_transaction_by_sender()and returnsmax(state_nonce, pool_nonce + 1).eth_call,eth_estimateGas,eth_getBalance,eth_getTransactionCount,eth_getCode,eth_getStorageAt) were executing state reads directly on the async runtime. WhenMemoryOverlayStateProviderfalls through to MDBX for accounts not in the overlay, this is a blocking disk I/O read. Wrapped all handlers inspawn_blocking_io_futwithacquire_owned_blocking_iosemaphore foreth_call, matching reth's standard patterns.Test plan
just checkpasses (clippy + fmt + tests)make erc20benchmark — no morereplacement transaction underpriced