fix(rpc): eth_call accepts any block tag; empty logsBloom is now 256 bytes#707
Conversation
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.
📝 WalkthroughWalkthroughThe PR corrects the Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 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 964-987: The eth_call handler currently ignores params[1] (block
tag); add basic validation like eth_getBlockByNumber: in the eth_call handling
function (the eth_call RPC handler that reads params[1]) parse/validate
params[1] when present — accept the canonical tags
("latest","earliest","pending") or a hex-prefixed block number (validate hex
digits and length) and reject other JSON types (objects/arrays) or malformed hex
by returning an RPC invalid-params error (-32602); do not change the existing
behavior of executing against tip when the tag is valid but historical, only
surface errors for malformed or wrong-typed block-tag inputs.
🪄 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: c2888514-ab8e-4fe6-8d51-21d227415ec3
⛔ Files ignored due to path filters (1)
CHANGELOG.mdis excluded by!**/CHANGELOG.md
📒 Files selected for processing (2)
crates/sentrix-rpc/src/jsonrpc/eth.rscrates/sentrix-rpc/src/jsonrpc/helpers.rs
| // params[1] = block tag. | ||
| // | ||
| // 2026-05-21: dropped the strict historical-state gate here for | ||
| // the same reason as eth_getTransactionCount. Off-the-shelf | ||
| // EVM agents (Hyperlane relayer, ethers, viem) pin eth_call to | ||
| // a recent past block as routine bookkeeping, even when the | ||
| // underlying view function only has meaning against tip | ||
| // (Mailbox.delivered, ERC20.totalSupply at finality, etc.). | ||
| // Returning -32004 here kills every off-the-shelf integration. | ||
| // | ||
| // The trade-off: callers asking for "balanceOf(x) at h=N" get | ||
| // current balance instead of historical. The agent ecosystem | ||
| // already accounts for this by reading from a deterministic | ||
| // current-state view of the chain. Use eth_getBalance / | ||
| // eth_getCode / eth_getStorageAt for explicit state-read | ||
| // pinning — those keep the strict gate so a wallet asking | ||
| // "what was my balance at h=N" gets an honest -32004 instead | ||
| // of a stale-passing-as-historical answer. | ||
| // | ||
| // Telemetry note: callers that pin eth_call to a non-tip block | ||
| // are visible in tracing — span enters with the requested tag, | ||
| // result reflects current state. No on-the-wire warning is | ||
| // emitted because the spec compatibility cost outweighs the | ||
| // audit-surface gain. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Consider validating the block tag parameter even when ignoring it.
The comment states params[1] = block tag (line 964), but the implementation at line 988 doesn't parse, validate, or use it. While the PR objective is to maximize compatibility with off-the-shelf agents, silently ignoring malformed block tags (e.g., "0xZZZ", invalid JSON types) means callers won't receive -32602 errors for parameter mistakes.
Compare with eth_getBlockByNumber (lines 141-146), which validates the block parameter format even when it can serve the request. Basic validation would catch caller bugs without compromising compatibility.
Optional: Add basic block tag validation
async fn eth_call(params: &Value, state: &SharedState) -> DispatchResult {
// Execute a read-only EVM call without state mutation.
// params[0] = {from, to, data, value, gas}
// params[1] = block tag.
//
// 2026-05-21: dropped the strict historical-state gate here for
// the same reason as eth_getTransactionCount. Off-the-shelf
// EVM agents (Hyperlane relayer, ethers, viem) pin eth_call to
// a recent past block as routine bookkeeping, even when the
// underlying view function only has meaning against tip
// (Mailbox.delivered, ERC20.totalSupply at finality, etc.).
// Returning -32004 here kills every off-the-shelf integration.
//
// The trade-off: callers asking for "balanceOf(x) at h=N" get
// current balance instead of historical. The agent ecosystem
// already accounts for this by reading from a deterministic
// current-state view of the chain. Use eth_getBalance /
// eth_getCode / eth_getStorageAt for explicit state-read
// pinning — those keep the strict gate so a wallet asking
// "what was my balance at h=N" gets an honest -32004 instead
// of a stale-passing-as-historical answer.
//
// Telemetry note: callers that pin eth_call to a non-tip block
// are visible in tracing — span enters with the requested tag,
// result reflects current state. No on-the-wire warning is
// emitted because the spec compatibility cost outweighs the
// audit-surface gain.
+
+ // Optional: validate block tag format even though we ignore its value
+ if let Some(tag) = params.get(1) {
+ let bc = state.read().await;
+ let _ = resolve_block_tag(Some(tag), bc.height())
+ .map_err(|e| (-32602, e.to_string()))?;
+ }
+
match run_evm_dry_run(¶ms[0], state).await {🤖 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 964 - 987, The eth_call
handler currently ignores params[1] (block tag); add basic validation like
eth_getBlockByNumber: in the eth_call handling function (the eth_call RPC
handler that reads params[1]) parse/validate params[1] when present — accept the
canonical tags ("latest","earliest","pending") or a hex-prefixed block number
(validate hex digits and length) and reject other JSON types (objects/arrays) or
malformed hex by returning an RPC invalid-params error (-32602); do not change
the existing behavior of executing against tip when the tag is valid but
historical, only surface errors for malformed or wrong-typed block-tag inputs.
PRs #705 + #707 (eth_call historical tags + logsBloom 256-byte fix) landed on main without a version bump, so post-merge main carried more RPC changes than the 2.2.14 binary already deployed to mainnet — two distinct binaries sharing one version string. Bump to 2.2.15 so the RPC-complete binary is distinguishable. This is the version to deploy on the next mainnet upgrade.
2.2.15 shipped to mainnet 2026-05-22 (34s halt window, zero cascade-jail). Documents PRs #705/#707/#708. Also scrubs all host identifiers from the file — the 2.2.14/2.2.15 entries plus six historical v2.0.0/v2.1.x entries. Topology now reads 'mainnet' / 'testnet' / 'host' generically, consistent with the file header's host-mapping-not-published note.
…counts 2.2.15 shipped to mainnet 2026-05-22 (34s halt window, zero cascade-jail). Documents PRs #705/#707/#708. Also scrubs infra detail from the file: host identifiers from the 2.2.14/2.2.15 entries + six historical v2.0.0/v2.1.x entries, and the explicit validator/host counts from the v1.0.0 entry. Topology now reads generically.
…counts (#709) 2.2.15 shipped to mainnet 2026-05-22 (34s halt window, zero cascade-jail). Documents PRs #705/#707/#708. Also scrubs infra detail from the file: host identifiers from the 2.2.14/2.2.15 entries + six historical v2.0.0/v2.1.x entries, and the explicit validator/host counts from the v1.0.0 entry. Topology now reads generically.
Follow-up to #704. Verifying the
eth_getTransactionCountfix on Sentrix Testnet surfaced two more chain-side blockers that stopped the Hyperlane relayer from auto-submittingprocess()against the Sentrix Mailbox. Both fixed here.1) eth_call rejected historical block tags
eth_callstill went through the strict historical-state gate. Hyperlane queriesMailbox.delivered(msgId)andrecipientIsm(addr)at a recent past block as routine bookkeeping — the gate returned-32004, the relayer's FallbackProvider deprioritized the host, no message ever got submitted.Fix: drop the gate for
eth_call, same rationale aseth_getTransactionCountin #704. The strict gate stays oneth_getBalance/eth_getCode/eth_getStorageAtwhere a silently-stale answer would let a caller make a wrong protocol decision on data that purports to be historical.2) EMPTY_LOGS_BLOOM was 304 bytes, not 256
The constant literal had 608 hex chars after
0x(= 304 bytes). The doc comment said "256-byte logs bloom" but the hand-typed literal was wrong. ethers' fee-oracle middleware strict-parsesBlock.logsBloomagainst the Ethereum spec — the relayer's gas-estimation path hitSerdeJson("invalid length 608, expected 256 bytes")on every attempt to estimateprocess()gas.Fix: shrink the constant to exactly 512 hex chars (256 bytes per spec).
Verified live on testnet
eth_call(Mailbox.delivered, blockN)returns current state instead of-32004Block.logsBloomlength is 514 chars (256 bytes)cast sendTest plan
cargo check -p sentrix-rpc --releasepasses with-D warningsrequire_latest_state_read_*tests still pass (gate unchanged for the three remaining methods)Summary by CodeRabbit
Bug Fixes
Improvements
eth_callRPC method now accepts historical block tags for more flexible queries.Documentation