Skip to content

fix(eth): skip the receipt-root check for exSat, which never populates it - #1357

Open
zhenek73 wants to merge 1 commit into
gluwa:usc-devfrom
zhenek73:fix/exsat-zero-receipts-root
Open

zhenek73 wants to merge 1 commit into
gluwa:usc-devfrom
zhenek73:fix/exsat-zero-receipts-root

Conversation

@zhenek73

Copy link
Copy Markdown

Reported in #1355.

What happens today

An attestor pointed at exSat's EVM layer (chain id 7200) advances through empty blocks
and then stops permanently at the first block containing a transaction, repeating
this forever:

ERROR stream_eth::roots: Eth connection error
      err=Computed transactions/receipts roots do not match block header for block 59226042
      (possible reorg between RPC calls)
WARN  stream_eth::roots: Reconnecting to Eth...

It is not a reorg, and it is not transient. It repeats on the identical block number
indefinitely, across restarts, and would recur on every subsequent transaction-bearing
block. last_attested_block never moves again.

Why

exSat's EVM layer does not maintain a receipts trie or a state trie. Every block header
it serves reports receiptsRoot and stateRoot as 32 zero bytes — on every block,
whether or not it has transactions:

$ curl -s -X POST https://evm.exsat.network -H 'Content-Type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["0x38DB18A",true]}'
# block 59801418 — txCount 1
#   transactionsRoot 0x4f8a21e5839d3c1e9249ce90a972119380cce4a477ad23027058bfb2d38ae7bf
#   receiptsRoot     0x0000000000000000000000000000000000000000000000000000000000000000
#   stateRoot        0x0000000000000000000000000000000000000000000000000000000000000000

Verified across many consecutive blocks, with and without transactions — this is
structural, not one bad RPC replica.

EthBlock::try_create (common/eth/src/lib.rs) recomputes the receipt root and compares
it to the header. For an empty block the comparison is skipped by the existing
early return, so those pass. For a block with at least one transaction the recomputed
root is a real non-zero hash which can never equal a permanent zero, so every such block
is rejected as BlockHeaderRootsMismatch — surfaced to the operator as a reorg, which
sends debugging in exactly the wrong direction.

Why skipping the check is safe here

Three things were verified independently against the live chain before proposing this:

  1. transactionsRoot is genuine and canonical on exSat. Recomputing the
    Merkle-Patricia root from each block's own transactions matches the header byte for
    byte (checked on both the block that stalls the attestor, 59226042, and a later one,
    59801416). The transaction-root check above therefore still does its job, including
    guarding against reorg-induced cross-fetch mismatches — the same reasoning the
    existing pre-Byzantium exemption already relies on.

  2. exSat's headers are internally self-consistent. The reported block hash equals
    keccak(rlp(header)) computed including the zero roots, so the header hash chain is
    sound and nothing about it is ambiguous.

  3. The receipts root is not part of anything attested or proven. In this repository
    receipts_root appears in exactly one place — the comparison this PR touches. The
    attested digest is Block::hash_payload(block_number, root, prev_digest), where root
    is the merkle root the inclusion proofs are built on. Skipping the comparison changes
    no attestation, no digest and no proof — only whether the block is accepted for
    processing at all.

The change

Extends the existing skip_receipt_root exemption — already present for pre-Byzantium
Ethereum mainnet, for an unrelated but structurally identical reason — to cover exSat, and
expands the comment to explain both cases. No behaviour changes for any other chain.

const EXSAT_CHAIN_ID: u64 = 7200;

let skip_receipt_root = (chain_id == ETHEREUM_MAINNET_CHAIN_ID
    && expected_number < ETHEREUM_BYZANTIUM_BLOCK)
    || chain_id == EXSAT_CHAIN_ID;

Testing

cargo check -p attestor passes with this change applied (clean checkout of usc-dev,
patch on top). Note that cargo check -p eth on its own fails both with and without this
patch — the crate does not build in isolation because tokio's signal feature is enabled
by a sibling crate — so the meaningful check is the one through attestor.

Alternative, if you prefer it

Hard-coding a second chain id follows the existing style, but a per-chain
skip_receipt_root_check flag in the source-chain config would generalise better — exSat
is unlikely to be the last L2 that skips the receipts trie. Happy to rework it that way;
this version was kept minimal to match what is already there.

Context

Found while building Bitcoin Witness, which
attests exSat EVM blocks in order to prove Bitcoin UTXO facts on Creditcoin. Until this is
fixed upstream, running an attestor against exSat requires a proxy that fills the zeroed
field in from the block's own receipts, which is a workaround nobody should need.

…s it

An attestor pointed at exSat's EVM layer (chain id 7200) walks through empty
blocks and then stops permanently at the first block containing a transaction,
reporting a reorg that did not happen and retrying the same block forever.

exSat does not maintain a receipts trie or a state trie. Every header it serves
reports receiptsRoot and stateRoot as 32 zero bytes, on every block, with or
without transactions -- verified across many consecutive blocks, so this is
structural rather than one bad RPC replica. Empty blocks pass because the
existing early return skips the comparison entirely; any block with at least one
transaction yields a real non-zero computed root that can never equal a
permanent zero.

Skipping the check is safe here, and was verified against the live chain before
proposing it:

- transactionsRoot is genuine and canonical on exSat -- recomputing the
  Merkle-Patricia root from each block's own transactions matches the header
  byte for byte -- so the transaction-root check above still guards against
  reorg-induced cross-fetch mismatches, which is the same reasoning the existing
  pre-Byzantium exemption relies on.
- The reported block hash equals keccak(rlp(header)) computed including those
  zeros, so exSat's header chain is internally self-consistent.
- receipts_root appears in exactly one place in this repository: the comparison
  below. The attested digest is hash_payload(block_number, root, prev_digest),
  so skipping the comparison changes no attestation, no digest and no proof --
  only whether the block is processed at all.

This extends the existing skip_receipt_root exemption rather than adding a new
mechanism, and leaves every other chain untouched. A per-chain config flag would
generalise better if preferred -- exSat is unlikely to be the last L2 that skips
the receipts trie -- but this keeps the change in the shape already there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXZBW3Mgpu6qPFYB5idfkx
@cursor

cursor Bot commented Sep 13, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Narrows validation only for a specific chain id while keeping transaction-root checks; no attestation digest or proof behavior changes per the PR rationale.

Overview
Skips receipt-root validation for exSat (chain id 7200) so attestors can process transaction-bearing blocks instead of failing forever with BlockHeaderRootsMismatch / false “reorg” errors.

exSat headers always set receiptsRoot to zero; the existing check still runs transactionsRoot verification. The PR adds EXSAT_CHAIN_ID and folds exSat into the same skip_receipt_root path already used for pre-Byzantium Ethereum mainnet, with comments and trace text updated to cover both exemptions. No change for other chains.

Reviewed by Cursor Bugbot for commit bd8b020. Bugbot is set up for automated code reviews on this repo. Configure here.

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