Skip to content

fix(rpc): eth_getTransactionCount accepts any block tag#704

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/rpc-gettxcount-historical-tags
May 21, 2026
Merged

fix(rpc): eth_getTransactionCount accepts any block tag#704
github-actions[bot] merged 1 commit into
mainfrom
fix/rpc-gettxcount-historical-tags

Conversation

@satyakwok
Copy link
Copy Markdown
Collaborator

@satyakwok satyakwok commented May 21, 2026

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:

{"code": -32004, "message": "historical state reads not yet supported; use 'latest'"}

and the relayer's FallbackProvider eventually 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_getTransactionCount is part of routine nonce bookkeeping, not an actual historical query.

Fix

Remove the historical-state gate for eth_getTransactionCount only. Serve current nonce regardless of block tag. The pending semantics (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 --release passes with -D warnings
  • All 10 require_latest_state_read_* tests still pass (gate is unchanged)
  • Confirm Hyperlane relayer no longer spams the -32004 warn after deploy
  • Confirm the pending 0.1 USDC bridge tx auto-relays (sUSDC totalSupply 18.5M → 18.6M)

Notes

  • Bumps workspace 2.2.13 → 2.2.14.
  • Companion entry in audit findings: this is the 4th Sentrix RPC compat gap discovered. Bug-class write-up tracked separately.

Summary by CodeRabbit

  • Bug Fixes

    • eth_getTransactionCount RPC method now returns current nonce for all block tags, including historical ones, instead of error responses. This improves client compatibility.
  • Documentation

    • Updated documentation for state read gating to clarify scope and reflect transaction count behavior changes.

Review Change Stack

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.
@github-actions github-actions Bot enabled auto-merge (squash) May 21, 2026 12:49
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

This PR relaxes the historical-state read gating constraint on the eth_getTransactionCount RPC method. The implementation no longer returns errors for historical block tags (non-"pending"), instead always serving the current nonce while treating "pending" specially to include mempool counts. Supporting documentation in the helpers module is updated to clarify that eth_getTransactionCount is excluded from the historical-state read gate due to nonces having different semantics than other state reads. The workspace version is bumped to 2.2.14.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is comprehensive and well-structured, providing clear context, problem statement, fix rationale, and test plan. However, it does not follow the repository's required template structure with checkboxes for scope and checks. Reformat the description to match the repository template structure including Scope, Checks, Linked issue, and Deploy impact sections with checkboxes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: removing the historical-state gate for eth_getTransactionCount to accept any block tag, which directly addresses the bug causing the relayer failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rpc-gettxcount-historical-tags

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

Copy link
Copy Markdown

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf17d9 and a0be7a7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml
  • crates/sentrix-rpc/src/jsonrpc/eth.rs
  • crates/sentrix-rpc/src/jsonrpc/helpers.rs

Comment on lines 71 to 75
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));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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".

@github-actions github-actions Bot merged commit 7491759 into main May 21, 2026
13 checks passed
satyakwok added a commit that referenced this pull request May 21, 2026
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.
github-actions Bot pushed a commit that referenced this pull request May 21, 2026
* 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.
github-actions Bot pushed a commit that referenced this pull request May 22, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant