fix(rpc): eth_getTransactionCount accepts any block tag#704
Conversation
Hyperlane relayer + every off-the-shelf EVM tool queries `eth_getTransactionCount(addr, blockN)` with a recent past block as part of normal nonce bookkeeping. Since the 2026-05-06 strict gate, we returned -32004 for those calls, which broke the agent's submit loop (logged repeatedly as "historical state reads not yet supported; use 'latest'" until the host was deprioritized in its FallbackProvider). Unlike eth_getBalance / eth_getCode / eth_getStorageAt / eth_call where a stale-vs-current answer can drive wrong protocol decisions, a stale nonce is self-correcting: the chain rejects a tx with a wrong nonce, the caller retries, no decision is made on stale data. So this method serves current nonce regardless of block tag and trusts the caller to handle the retry loop. The strict gate stays on the other four state-read methods. Bumps workspace 2.2.13 -> 2.2.14.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
📝 WalkthroughWalkthroughThis PR relaxes the historical-state read gating constraint on the Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@crates/sentrix-rpc/src/jsonrpc/eth.rs`:
- Around line 71-75: The code currently coerces params.get(1) to "latest" when
it's not a string, which hides malformed requests; change the params[1] handling
so that if params.get(1) exists and is not a string or null, the method returns
a JSON-RPC invalid params error (-32602); preserve existing behavior for string
tags (including historical hex strings) and for missing/null tags by treating
them as "latest"/relaxed; update the logic around params.get(1) / block_tag in
the eth RPC handler (the block_tag variable and the branch that checks "pending"
and uses bc.accounts.get_nonce and bc.mempool_pending_count) to perform this
type check and return the proper error instead of silently defaulting to
"latest".
🪄 Autofix (Beta)
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 53e19f43-830d-44a5-b0a7-67a1ec653b49
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/*.lock
📒 Files selected for processing (3)
Cargo.tomlcrates/sentrix-rpc/src/jsonrpc/eth.rscrates/sentrix-rpc/src/jsonrpc/helpers.rs
| let block_tag = params.get(1).and_then(|v| v.as_str()).unwrap_or("latest"); | ||
| let bc = state.read().await; | ||
| if block_tag != "pending" { | ||
| require_latest_state_read(params.get(1), bc.height())?; | ||
| } | ||
| let mut nonce = bc.accounts.get_nonce(&address); | ||
| if block_tag == "pending" { | ||
| nonce = nonce.saturating_add(bc.mempool_pending_count(&address)); |
There was a problem hiding this comment.
Validate non-string block_tag instead of silently treating it as latest.
params[1] values with invalid types (number/object/array) are currently coerced to "latest". This hides malformed requests and can return unintended nonce values. Return -32602 for non-string/non-null tags, while keeping relaxed behavior for string tags (including historical hex strings).
Suggested patch
- let block_tag = params.get(1).and_then(|v| v.as_str()).unwrap_or("latest");
+ let block_tag = match params.get(1) {
+ None | Some(Value::Null) => "latest",
+ Some(Value::String(s)) => s.as_str(),
+ Some(other) => {
+ return Err((-32602, format!("invalid block tag: {other}")));
+ }
+ };🤖 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 `@crates/sentrix-rpc/src/jsonrpc/eth.rs` around lines 71 - 75, The code
currently coerces params.get(1) to "latest" when it's not a string, which hides
malformed requests; change the params[1] handling so that if params.get(1)
exists and is not a string or null, the method returns a JSON-RPC invalid params
error (-32602); preserve existing behavior for string tags (including historical
hex strings) and for missing/null tags by treating them as "latest"/relaxed;
update the logic around params.get(1) / block_tag in the eth RPC handler (the
block_tag variable and the branch that checks "pending" and uses
bc.accounts.get_nonce and bc.mempool_pending_count) to perform this type check
and return the proper error instead of silently defaulting to "latest".
* fix(rpc): eth_getTransactionCount + eth_call + logsBloom length Three discrete chain-RPC fixes bundled because they share one cause (strict-but-non-spec behavior breaking off-the-shelf EVM agents). All discovered together standing up the Hyperlane relayer for the Base Sepolia ↔ Sentrix Testnet bridge (audit fix H-4). 1) eth_getTransactionCount accepts any block tag. Relayer + ethers + viem all pin nonce queries to a recent past block. A stale nonce is self-correcting (chain rejects wrong-nonce tx, caller retries) so this method serves current nonce regardless of block tag. 2) eth_call accepts any block tag. Same pattern: Hyperlane queries Mailbox.delivered(msgId) and recipientIsm(addr) at past blocks. Returns current-state. The strict gate stays on eth_getBalance / eth_getCode / eth_getStorageAt where wrong = wrong protocol decision. 3) Empty logsBloom is now actually 256 bytes (Ethereum spec). The EMPTY_LOGS_BLOOM const was 304 bytes (608 hex chars) because of an off-by-one in the original hand-typed string. The doc comment said 256 but the literal was 304. ethers' fee-oracle middleware strict-parses Block.logsBloom — Hyperlane gas estimation SerdeJson'd on this with "invalid length 608, expected 256 bytes" before any process() tx could be submitted. Bumps workspace 2.2.13 -> 2.2.14. * docs(changelog): add 2.2.14 entry — five RPC compat fixes 2.2.14 went live on mainnet 2026-05-21 17:02 UTC carrying PRs #692/#696/#702/#703/#704. Document the five fixes + flag that the eth_call + logsBloom fixes on this branch are scheduled for 2.2.15.
…bytes (#707) * fix(rpc): eth_getTransactionCount + eth_call + logsBloom length Three discrete chain-RPC fixes bundled because they share one cause (strict-but-non-spec behavior breaking off-the-shelf EVM agents). All discovered together standing up the Hyperlane relayer for the Base Sepolia ↔ Sentrix Testnet bridge (audit fix H-4). 1) eth_getTransactionCount accepts any block tag. Relayer + ethers + viem all pin nonce queries to a recent past block. A stale nonce is self-correcting (chain rejects wrong-nonce tx, caller retries) so this method serves current nonce regardless of block tag. 2) eth_call accepts any block tag. Same pattern: Hyperlane queries Mailbox.delivered(msgId) and recipientIsm(addr) at past blocks. Returns current-state. The strict gate stays on eth_getBalance / eth_getCode / eth_getStorageAt where wrong = wrong protocol decision. 3) Empty logsBloom is now actually 256 bytes (Ethereum spec). The EMPTY_LOGS_BLOOM const was 304 bytes (608 hex chars) because of an off-by-one in the original hand-typed string. The doc comment said 256 but the literal was 304. ethers' fee-oracle middleware strict-parses Block.logsBloom — Hyperlane gas estimation SerdeJson'd on this with "invalid length 608, expected 256 bytes" before any process() tx could be submitted. Bumps workspace 2.2.13 -> 2.2.14. * docs(changelog): add 2.2.14 entry — five RPC compat fixes 2.2.14 went live on mainnet 2026-05-21 17:02 UTC carrying PRs #692/#696/#702/#703/#704. Document the five fixes + flag that the eth_call + logsBloom fixes on this branch are scheduled for 2.2.15.
Discovered 2026-05-21 while standing up the Hyperlane relayer agent for the Base Sepolia ↔ Sentrix Testnet bridge (audit fix H-4). The relayer queries
eth_getTransactionCount(signer, blockN)against a recent past block in a tight loop. With the strict historical-state gate from 2026-05-06 active, every call returned:and the relayer's
FallbackProvidereventually deprioritized the Sentrix RPC host entirely (reason: "Too many errors").Hyperlane, ethers, viem, and most off-the-shelf EVM tooling all do this — passing a block tag to
eth_getTransactionCountis part of routine nonce bookkeeping, not an actual historical query.Fix
Remove the historical-state gate for
eth_getTransactionCountonly. Serve current nonce regardless of block tag. Thependingsemantics (mempool-aware path) is preserved.The gate stays on
eth_getBalance/eth_getCode/eth_getStorageAt/eth_call— those four are the methods where a silently-stale answer (instead of-32004) would let a caller make a wrong protocol decision on data that purports to be historical.A stale nonce is different: the chain rejects any tx that arrives with the wrong nonce, the caller retries, no protocol decision is built on the stale value. So the trade-off is "agent works" vs "honesty about nonce being current-only," and current-only is the right answer because relayers + faucets + dapps already assume it.
Test plan
cargo check -p sentrix-rpc --releasepasses with-D warningsrequire_latest_state_read_*tests still pass (gate is unchanged)-32004warn after deployNotes
Summary by CodeRabbit
Bug Fixes
eth_getTransactionCountRPC method now returns current nonce for all block tags, including historical ones, instead of error responses. This improves client compatibility.Documentation