You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(evm-rpc): validate debug traces and state diffs against receipts - #597
Debug call frames (callTracer) and state diffs carry no consensus commitment, so a trace from an execution that differs from the chain passes every existing check. Every transaction, though, leaves a footprint in data the block does commit to. This adds per-transaction checks against it:
Rule
Check
status
the receipt status agrees with the root frame's error
gas
the root frame's gasUsed equals the receipt's (regular transactions)
system-gas
a system transaction (sent by the block miner at zero gas price) whose root frame executed nothing (gasUsed ≤ intrinsic gas) while the receipt says gas was used
system-fee
the balance change of 0xff…fe in the state diff equals gasUsed × effectiveGasPrice + blobGasUsed × blobGasPrice plus the value frames with no failed ancestor moved to or from it (chains that credit fees to the system address during each transaction, e.g. BSC)
System transactions are deliberately not compared exactly: their receipt gasUsed is not an exact anchor for the execution gas (e.g. BSC slash()).
Both state-diff shapes are supported: trace_replayBlockTransactionsstateDiff and prestateTracer diff mode.
observe logs one bounded summary per block (consensus anchor violations observed in N transactions; block accepted, with rule, transaction, expected and actual values) and accepts the block
reject marks the block invalid (consensus anchor violation (<rule>) for transaction <hash>: expected …, got …) so the existing retry fetches it again
--consensus-anchor-rules <list> comma-separated subset of status,gas,system-gas,system-fee (default status,gas)
reject requires the anchors to be verified: --verify-receipts-root, the cumulative gas check (or --use-gas-used-for-receipts-root), --verify-tx-root and --verify-tx-sender. In reject mode a data request without receipts or call frames is an error, a missing call frame or root STOP is a violation of the first selected rule that applies, and so is a requested state diff that is missing or labelled with another transaction.
The checks run after receipts, call frames and state diffs are attached, and only on the debug-trace path (not with --use-trace-api). They need receipts; system-fee also needs state diffs. Full transaction objects are fetched internally when the checks are on, as for --call-frame-validation.
Tests
Real BSC mainnet data that must pass (with both state-diff shapes): a system transaction with empty calldata, system deposits, slash(), a blob transaction, direct payments to the system address, both system-transaction gas conventions, regular transactions.
Mutations that must fail: reverted root in a successful transaction, successful root in a failed one, changed root gasUsed, a system transaction stripped of its calls, a state diff whose system-address credit is not the fee or is missing.
Rpc-level tests on a whole block for off (default), observe, reject, rule selection, checks without state diffs, and configuration errors.
rushx test in evm-rpc: 431 passed, 29 skipped. Biome reports no new findings in the touched files.
Debug call frames and state diffs carry no consensus commitment, but a
transaction leaves a footprint in data the block does commit to. Check the
traces and state diffs of every transaction against it:
- status: the receipt status agrees with the root frame's error
- gas: the root frame's gasUsed equals the receipt's (regular transactions)
- system-gas: a system transaction (sent by the miner at zero gas price)
whose root frame executed nothing while the receipt says gas was used
- system-fee: the system address balance change in the state diff equals the
fee plus the value frames moved to or from it, on chains that credit fees
to the system address during each transaction (BSC)
A trace or state diff produced by an execution that differs from the chain
fails them. `--consensus-anchor-validation off|observe|reject` (default off)
and `--consensus-anchor-rules` (default status,gas) are added to evm-dump and
evm-data-service. Reject marks the block invalid so it is fetched again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- reject mode requires receipts root verification: a receipt is only an
anchor once it is checked against the root (CLI error in evm-data-service)
- reject mode fails fast when receipts or debug call frames are missing
from the data request instead of passing blocks as checked
- a missing call frame or a root STOP is a violation of the first selected
trace rule, as is a root frame without gasUsed
- a transaction without a gas price is not taken for a system transaction
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- reject mode also requires checkCumulativeGasUsed (or
useGasUsedForReceiptsRoot), verifyTxRoot and verifyTxSender: per-transaction
gasUsed is committed through cumulativeGasUsed, and the system transaction
classification reads the sender and gas price
- a state diff that was requested but is missing for a transaction is a
system-fee violation
- a missing call frame is reported under the first selected rule that applies
to the transaction
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The public RpcOptions path accepts consensusAnchorRules: [], although the CLI parser rejects an empty list. The resulting empty set makes checkConsensusAnchors return no violations for every transaction, so even consensusAnchorValidation: 'reject' silently becomes a no-op. Reject an empty array in the constructor as well.
Match prestate tracer results by transaction hash
evm/evm-rpc/src/rpc.ts:1217
When the prestate tracer returns one result per transaction, addDebugStateDiffs keeps the array positionally and does not match it by txHash. This branch then ignores the optional label, so a shuffled/mislabeled response compares each transaction with another transaction's balance delta, causing false rejection or allowing the wrong diff to satisfy system-fee. Validate the label before consuming the result (as validateDebugFrames already does for call frames).
…diffs
An empty rule list would let enabled validation accept every block, and a
prestate diff labelled with another transaction is not this transaction's
diff: the system-fee rule now treats it as missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(With Claude): The round-3 review summary (on 31b7a0c) raised two points without inline threads. Both are addressed in 096e2a5:
Empty rule sets:Rpc now refuses an enabled consensus-anchor mode with an empty rule list, so enabled validation can no longer accept every block.
Mislabelled prestate diffs: a prestate diff whose label names another transaction is treated as missing for that transaction, so system-fee fails instead of checking the wrong diff.
Both have tests in evm/evm-rpc/test/rpc.consensus-anchor.test.ts. Requesting a fresh review on the current head.
Incorrect intrinsic gas calculation for system transactions
evm/evm-rpc/src/verification.ts:1400
This is not the transaction's intrinsic gas: it charges zero calldata bytes at 16 gas and omits contract-creation, initcode, and access-list costs. Because every miner-sent zero-price transaction is classified as a system transaction, the threshold can falsely reject valid zero-heavy calls whose execution fits below this overestimate, and it can miss an empty create/access-list trace whose real intrinsic gas exceeds this estimate. Compute intrinsic gas from the full transaction under the applicable chain rules, or restrict system-gas to transaction shapes for which the threshold is valid.
…c gas
- A request without traces no longer returns before the missing-input
handling: reject refuses it and observe warns, as for any other missing
input. evm-dump and evm-data-service refuse to start with consensus anchor
validation enabled but without --with-traces and --with-receipts, or with
--use-trace-api.
- system-gas measures the root frame against the call's exact intrinsic gas
(4 gas per zero calldata byte, 16 per other, access lists, authorizations)
instead of an all-16 ceiling, which could take a short execution with
zero-heavy calldata for none. Contract creations are left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(With Claude): On the round-4 summary finding "Incorrect intrinsic gas calculation for system transactions" (verification.ts, no inline thread): agreed, fixed in 78dec18. system-gas now measures the root frame against the exact intrinsic gas of the call (intrinsicGasOfCall: 21000, 4 gas per zero calldata byte and 16 per other byte, 2400 per access-list address plus 1900 per storage key, 25000 per authorization) instead of an all-16 ceiling. Contract creations are excluded, since their intrinsic gas depends on fork-specific initcode rules and system transactions are calls. New tests: zero-heavy calldata whose short execution sits between the exact intrinsic gas and the old ceiling is no longer flagged; a creation is not checked; unit tests for intrinsicGasOfCall. The fixtures now carry each transaction's to.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Debug call frames (
callTracer) and state diffs carry no consensus commitment, so a trace from an execution that differs from the chain passes every existing check. Every transaction, though, leaves a footprint in data the block does commit to. This adds per-transaction checks against it:statuserrorgasgasUsedequals the receipt's (regular transactions)system-gassystem-fee0xff…fein the state diff equalsgasUsed × effectiveGasPrice + blobGasUsed × blobGasPriceplus the value frames with no failed ancestor moved to or from it (chains that credit fees to the system address during each transaction, e.g. BSC)System transactions are deliberately not compared exactly: their receipt
gasUsedis not an exact anchor for the execution gas (e.g. BSCslash()).Both state-diff shapes are supported:
trace_replayBlockTransactionsstateDiffandprestateTracerdiff mode.Flags (evm-dump, evm-data-service)
--consensus-anchor-validation off|observe|reject(defaultoff)observelogs one bounded summary per block (consensus anchor violations observed in N transactions; block accepted, with rule, transaction, expected and actual values) and accepts the blockrejectmarks the block invalid (consensus anchor violation (<rule>) for transaction <hash>: expected …, got …) so the existing retry fetches it again--consensus-anchor-rules <list>comma-separated subset ofstatus,gas,system-gas,system-fee(defaultstatus,gas)rejectrequires the anchors to be verified:--verify-receipts-root, the cumulative gas check (or--use-gas-used-for-receipts-root),--verify-tx-rootand--verify-tx-sender. Inrejectmode a data request without receipts or call frames is an error, a missing call frame or rootSTOPis a violation of the first selected rule that applies, and so is a requested state diff that is missing or labelled with another transaction.The checks run after receipts, call frames and state diffs are attached, and only on the debug-trace path (not with
--use-trace-api). They need receipts;system-feealso needs state diffs. Full transaction objects are fetched internally when the checks are on, as for--call-frame-validation.Tests
slash(), a blob transaction, direct payments to the system address, both system-transaction gas conventions, regular transactions.Rpc-level tests on a whole block foroff(default),observe,reject, rule selection, checks without state diffs, and configuration errors.rushx testin evm-rpc: 431 passed, 29 skipped. Biome reports no new findings in the touched files.🤖 Generated with Claude Code