Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
39 changes: 31 additions & 8 deletions crates/sentrix-rpc/src/jsonrpc/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Comment on lines +964 to +987
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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(&params[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.

match run_evm_dry_run(&params[0], state).await {
Ok(receipt) => {
let output_hex = format!("0x{}", hex::encode(&receipt.output));
Expand Down
9 changes: 4 additions & 5 deletions crates/sentrix-rpc/src/jsonrpc/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@ pub(super) fn resolve_block_tag(v: Option<&Value>, latest: u64) -> Result<u64, &
}
}

/// Gate state-read methods (`eth_getBalance`, `eth_getCode`,
/// `eth_getStorageAt`, `eth_call`) against historical-specific block
/// heights. `eth_getTransactionCount` was removed from this gate
/// 2026-05-21 — see the comment in eth.rs for why nonces are different
/// from other state reads.
/// `eth_getStorageAt`) against historical-specific block heights.
/// `eth_getTransactionCount` and `eth_call` were removed from this
/// gate 2026-05-21 — see the comments in eth.rs for why nonces and
/// view-function calls are different from explicit state reads.
///
/// Sentrix doesn't yet have MDBX snapshot isolation, so account state
/// reads always serve current-tip data regardless of the block tag
Expand Down
Loading