From caf4f39d83938e395e3161364310a960e30167a4 Mon Sep 17 00:00:00 2001 From: satyakwok <119509589+satyakwok@users.noreply.github.com> Date: Thu, 21 May 2026 14:48:49 +0200 Subject: [PATCH 1/2] fix(rpc): eth_getTransactionCount + eth_call + logsBloom length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/sentrix-rpc/src/jsonrpc/eth.rs | 39 ++++++++++++++++++----- crates/sentrix-rpc/src/jsonrpc/helpers.rs | 9 +++--- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/sentrix-rpc/src/jsonrpc/eth.rs b/crates/sentrix-rpc/src/jsonrpc/eth.rs index ee4e48a..5087e6e 100644 --- a/crates/sentrix-rpc/src/jsonrpc/eth.rs +++ b/crates/sentrix-rpc/src/jsonrpc/eth.rs @@ -176,8 +176,13 @@ const ZERO_HASH_HEX: &str = "0x0000000000000000000000000000000000000000000000000 // chain emits. const EMPTY_SHA3_UNCLES: &str = "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"; -// Empty 256-byte logs bloom (2 hex chars per byte → 512 zeros after 0x). -const EMPTY_LOGS_BLOOM: &str = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; +// Empty 256-byte logs bloom (Ethereum spec: 2048-bit / 256-byte field, +// rendered as 512 hex chars after the `0x` prefix). The constant pre-2026-05-21 +// was accidentally 608 hex chars (304 bytes), which broke ethers / viem fee +// oracle middlewares that strict-parse Block.logsBloom — the Hyperlane +// relayer fee-estimation path SerdeJson'd on this with "invalid length 608, +// expected 256 bytes" before we could submit any process() tx. +const EMPTY_LOGS_BLOOM: &str = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; fn build_block_json(block: &sentrix_primitives::Block) -> Value { let state_root = match block.state_root { @@ -956,12 +961,30 @@ async fn run_evm_dry_run( 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 (latest by default; specific historical - // heights gated until snapshot isolation lands) - { - let bc = state.read().await; - require_latest_state_read(params.get(1), bc.height())?; - } + // 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. match run_evm_dry_run(¶ms[0], state).await { Ok(receipt) => { let output_hex = format!("0x{}", hex::encode(&receipt.output)); diff --git a/crates/sentrix-rpc/src/jsonrpc/helpers.rs b/crates/sentrix-rpc/src/jsonrpc/helpers.rs index efb4c5f..1fb41dd 100644 --- a/crates/sentrix-rpc/src/jsonrpc/helpers.rs +++ b/crates/sentrix-rpc/src/jsonrpc/helpers.rs @@ -32,11 +32,10 @@ pub(super) fn resolve_block_tag(v: Option<&Value>, latest: u64) -> Result Date: Thu, 21 May 2026 19:20:13 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(changelog):=20add=202.2.14=20entry=20?= =?UTF-8?q?=E2=80=94=20five=20RPC=20compat=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b3faf..88cd62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,18 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.2.14] — 2026-05-21 — Five RPC compat fixes; off-the-shelf EVM tooling unblocked + +**Production binary on mainnet (vps3 + vps6) + testnet (vps4) since 2026-05-21.** Mainnet halt window 21s. Chain progression resumed 1.36 s/blk. Zero cascade-jail. + +- **`eth_getTransactionByHash` returns EVM-standard JSON shape** (PR #692). Pre-fix the response carried the chain-native shape (`block_hash` / `transaction.{amount,chain_id,data:"EVM:..."}`) which crashed ethers / viem / alloy / Hyperlane CLI on every tx fetch. The new path converts to the canonical EVM tx response (`from`, `to`, `value`, `gas`, `gasPrice`, `nonce`, `v`, `r`, `s`, `hash`, `input`, `transactionIndex`, `blockNumber`, `blockHash`). Live-verified against a recent mainnet tx — all 14 keys present. +- **B3 trie reconcile fail-soft on missing nodes** (PR #696). The boot-time integrity check used to halt-crash if a single trie node was missing in the snapshot. Now it logs + skips so the chain can self-heal via consensus replay instead of needing a manual operator rsync from a clean peer. +- **RPC pending fields + coinbase intermediate fix** (PR #702). Pending tx response shape now matches spec; `miner` / `coinbase` block fields return canonical hex strings. +- **`null` v/r/s for undecodable EVM tx sigs** (PR #703). When `extract_vrs_from_rlp` fails we now return `null` for the three signature fields instead of `"0x0"`. EIP-1474 clients already treat null sig fields as "not recoverable"; the previous all-zero strings parsed as a valid ECDSA point pair and `ecrecover` returned junk that callers could trust by accident. +- **`eth_getTransactionCount` accepts any block tag** (PR #704). The 2026-05-06 strict gate (`-32004 historical state reads not yet supported`) broke Hyperlane relayer / ethers / viem nonce bookkeeping. A stale nonce is self-correcting (chain rejects wrong-nonce tx, caller retries) so this method now serves current nonce regardless of block tag. The strict gate stays on `eth_getBalance` / `eth_getCode` / `eth_getStorageAt` / `eth_call` — those keep returning `-32004` for any non-`latest` tag because silently serving current state for an explicit "balance at h=N" query is the original silent-lie risk the 2026-05-05 audit caught. + +**Sibling work, not in this binary**: `eth_call` historical-tag loosening + empty `logsBloom` constant 304B → 256B (Ethereum spec). Both live on testnet via branch `fix/rpc-eth-call-and-logsbloom`. Mainnet pickup planned for 2.2.15. + ## [2.2.11] — 2026-05-13 — EVM value-transfer + gas-fix forks activated; Blockchain refactor pass **Production binary on mainnet + testnet since 2026-05-13.**