Conversation
…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
PR SummaryLow Risk Overview exSat headers always set Reviewed by Cursor Bugbot for commit bd8b020. Bugbot is set up for automated code reviews on this repo. Configure here. |
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:
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_blocknever moves again.Why
exSat's EVM layer does not maintain a receipts trie or a state trie. Every block header
it serves reports
receiptsRootandstateRootas 32 zero bytes — on every block,whether or not it has transactions:
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 comparesit 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, whichsends 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:
transactionsRootis genuine and canonical on exSat. Recomputing theMerkle-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.
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 issound and nothing about it is ambiguous.
The receipts root is not part of anything attested or proven. In this repository
receipts_rootappears in exactly one place — the comparison this PR touches. Theattested digest is
Block::hash_payload(block_number, root, prev_digest), whererootis 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_rootexemption — already present for pre-ByzantiumEthereum 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.
Testing
cargo check -p attestorpasses with this change applied (clean checkout ofusc-dev,patch on top). Note that
cargo check -p ethon its own fails both with and without thispatch — the crate does not build in isolation because
tokio'ssignalfeature is enabledby 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_checkflag in the source-chain config would generalise better — exSatis 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.