Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
d462c3e to
5a7f0c7
Compare
5a7f0c7 to
cb9c2b9
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Replay still diverges from canonical processing for synthetic log indexes and sequential TRC21 fee-capacity accounting.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes debug tracing replay so transaction routing and trace frames match canonical block processing.
Changes:
- Adds a shared routing-aware replay path.
- Corrects synthetic versus real call frames.
- Adds regression coverage for nonce, root, and tracer behavior.
File summaries
| File | Description |
|---|---|
| eth/tracers/native/call.go | Selects real or synthetic call frames correctly. |
| eth/tracers/native/call_test.go | Tests real-frame preservation. |
| eth/tracers/native/call_flat.go | Supports synthetic non-EVM frames. |
| eth/tracers/native/call_flat_test.go | Tests flat tracer frame handling. |
| eth/tracers/internal/tracetest/non_evm_trace_test.go | Tests routing through production processing. |
| eth/tracers/api.go | Replays block state through shared routing. |
| eth/tracers/api_test.go | Adds block, transaction, and root regressions. |
| eth/state_accessor.go | Uses routing-aware transaction replay. |
| eth/state_accessor_test.go | Tests nonce preservation during replay. |
| core/state_processor.go | Centralizes routing and adds replay processing. |
Review details
Suppressed comments (2)
eth/tracers/internal/tracetest/non_evm_trace_test.go:74
- The test only sends to
TradingStateAddrBinaryand toggles the receiver fork, so it never exercisesrouteSign/ApplySignTransaction. Add a case usingBlockSignersBinarywithTIPSigningBlockactive; this is especially important because the PR's reportedflatCallTracerfailure involved a sign transaction and the new replay branch has distinct nonce and log behavior.
key, _ := crypto.GenerateKey()
from := crypto.PubkeyToAddress(key.PublicKey)
to := common.TradingStateAddrBinary
blockNumber := common.Big1
eth/state_accessor.go:266
- These replay-error returns discard the
releasecallback obtained fromStateAtBlock(..., readOnly=true), so repeated failed traces can retain live trie references. This leak predates this replay change and is already addressed by the linked #2578; ensure that dependency lands before this branch is merged, or include the same deferred release guard here.
if err := core.ApplyTransactionForReplay(msg, new(core.GasPool).AddGas(tx.Gas()), block.Number(), tx, evm, balance); err != nil {
// An EVM this replay cannot use is a problem of this caller, not of the
// transaction: report it as it is instead of blaming the transaction.
if errors.Is(err, core.ErrReplayTracingEVM) || errors.Is(err, core.ErrReplayStateType) {
return nil, vm.BlockContext{}, nil, nil, err
}
return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err)
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
eae1995 to
96fd012
Compare
96fd012 to
411dc8f
Compare
411dc8f to
bb21871
Compare
ApplyTransactionWithEVM decided inline which handler a transaction goes through, so any caller that has to replay a transaction had to re-derive that decision on its own — and every copy drifts from block processing the moment the routing changes. Pull the decision into routeTransaction, the per-transaction finalisation into finaliseTxState, and the sender nonce handling of ApplySignTransaction into applySignTransactionNonce, so a replay can follow exactly the same path. The log both non-EVM handlers record moves into addNonEVMTxLog for the same reason: it is not a receipt artefact, StateDB.AddLog advances the block wide log count that every later log takes its index from, so a copy that skipped it would make the following transactions report indexes the chain never had. Also add core.ApplyTransactionForReplay, the entry point a replay should use instead of core.ApplyMessage: it shares the routing above, records the same log through the same helper, and skips the receipt and its bloom, which a replay does not need. It refuses an EVM that carries a tracer, as it never fires the OnTxStart/OnTxEnd hooks, and it takes the state to replay on from evm.StateDB, so the finalisation, the nonce handling and the TRC21 fee handling cannot land on a state other than the one the execution wrote to. Both refusals are sentinel errors, so a caller can tell them apart from a transaction failure. The replay paths converted in the commits that follow go through it. No behaviour change: the conditions, the finalisation, the nonce handling and the log are the ones ApplyTransactionWithEVM already used. finaliseTxState takes the state the EVM executes against rather than the plain *state.StateDB, because a hooked state reports the balance burnt by self-destructed accounts to the tracer on Finalise (core/state/statedb_hooked.go); IntermediateRoot forwards unchanged either way. TestApplyTransactionForReplayKeepsTheNonEVMTxLog drives the replay and block processing of a transaction to a system address and compares the logs they record.
stateAtTransaction rebuilds the pre-state of a transaction by replaying the transactions before it, and replayed them with core.ApplyMessage. That diverges from block processing for transactions sent to the XDCX system addresses 0x91/0x92/0x93/0x94: while the XDCX receiver fork is active those are handled by ApplyEmptyTransaction, which does not execute the EVM and leaves the sender nonce untouched, whereas the replay executed them as ordinary EVM transactions and bumped the nonce. The block fee was credited to the zero address instead of the coinbase owner, and the historical balance bypass was not applied either. Every following transaction of the same sender reuses that nonce, so debug_traceTransaction failed for it and for everything after it that needed the replay to get past it: on Apothem block 0x2e69c13 indices 5, 6 and 7 all failed with nonce too low. Replay through core.ApplyTransactionForReplay instead, the entry point block processing and the other replay paths share: it routes those addresses exactly like block processing, and skips the receipt, its logs and the bloom, which a replay does not need. A replay that was handed an EVM it cannot use is reported as it is instead of being blamed on the transaction. Mirror the same change in the tracers test backend so it keeps modelling the production behaviour. Add TestStateAtTransactionReplayKeepsNonceLessSenderNonce, which fails on the old replay with sender nonce after replay = 1 want 0, and TestTraceTransactionSkipNonceTransactions for the debug_traceTransaction path, with the skipNonceForkCases and newSkipNonceBackend fixtures it drives both receiver fork settings with. Refs: gzliudan/XDPoSChain#256
bb21871 to
09cb6e8
Compare
09cb6e8 to
a5b9cbb
Compare
debug_intermediateRoots rebuilt the pre-state of a block with core.ApplyMessage, which knows nothing about the routing block processing applies, and skipped every transaction sent to the XDCX system addresses outright. While the receiver fork is active those transactions are handled by ApplyEmptyTransaction, which neither checks nor increments the sender nonce and leaves the state untouched, whereas the replay executed them as ordinary EVM transactions and bumped the nonce; the block fee was credited to the zero address instead of the coinbase owner, and the historical balance bypass was not applied either. It therefore returned one root fewer than the block has transactions, and the roots it did return were not the ones block processing produces. Replay through core.ApplyTransactionForReplay, the entry point that shares the routing with ApplyTransactionWithEVM but skips the receipt, its logs and the bloom, and stop skipping those transactions: every transaction then gets an intermediate root, and a nonce-less one leaves the state untouched, so its root is the previous one. Add TestIntermediateRootsMatchesBlockProcessing, which compares the roots against a replay through core.ApplyTransactionWithEVM, pins the last one to the block root after the block finalisation and fails when the coinbase owner fee is dropped; the base has no intermediate-roots test, so this one is also the first to pin the replay instead of only counting its roots. No corresponding fix exists upstream: geth has no non-EVM transaction concept.
a5b9cbb to
0201b80
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Pre-Byzantium replay redundantly hashes trie state for every transaction, adding avoidable cost to historical tracing.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Balanced
Proposed changes
Debug tracing rebuilds the pre-state of a transaction, and of a whole block, by re-executing the transactions that come before it. Those replays called
core.ApplyMessage, which knows nothing about the routing block processing applies. While the XDCX receiver fork is active (mainnetTIPXDCXBlock38,383,838…TIPXDCXReceiverDisableBlock80,370,900), transactions to the system addresses0x91/0x92/0x93/0x94are handled byApplyEmptyTransaction: no EVM execution, no nonce check, no nonce increment and no state change. A plainApplyMessagereplay executes them as ordinary EVM transactions and bumps the sender nonce, so every following transaction of the same sender is replayed against a nonce the chain never had. That replay also credited the block fee to the zero address instead of the coinbase owner and skipped the historical balance bypass.The block-level APIs made a second, related mistake.
IntermediateRootsskipped those transactions withtx.IsSkipNonceTransaction(), a condition that does not consult the fork at all. Outside the fork window block processing does execute them and does bump the nonce, so dropping them loses exactly the nonce increment the following transaction relies on;IntermediateRootsthen hits the error and silently truncates, returning the roots collected so far. Inside the window the skip keeps the state right but leaves one root missing. The same unconditional skip intraceBlockand in the state feeder of the JS tracer path — the one that makesdebug_traceBlock*fail withnonce too highoutside the window and leave anullhole inside it — is a separate change, submitted as #2581.Symptoms
nonce too low—debug_traceTransactionon any transaction that follows a system-address transaction of the same sender, while the fork is active.stateAtTransactionnever skipped, so it bumped a nonce block processing leaves alone. Apothem block 48,667,667 (0x2e69c13, transactions 5–7), mainnet block 39,083,312 (0x2545d30, transaction 5).debug_intermediateRoots, and onenullhole next to it indebug_traceBlockByNumber/ByHash/traceBlock(that side is fix(eth/tracers): fix nonce too high and null entries in block traces #2581), whenever the fork is active.debug_intermediateRootsroot diverges from the block state root whenever the block charges a non-zero fee, because the replay credited it to the zero address instead of the coinbase owner (mainnet block 107,084,658).Fix
Three commits: one per defect, plus the shared routing and the replay entry point they need.
refactor(core):ApplyTransactionWithEVMdecided the routing inline, so every replay caller had to re-derive it and each copy drifted. The decision moves intorouteTransaction, the per-transaction finalisation intofinaliseTxStateand the sign-transaction nonce handling intoapplySignTransactionNonce, so a replay can follow exactly the same path. No behaviour change.fix(eth):stateAtTransaction, the replay behinddebug_traceTransactionanddebug_traceCall, goes through the shared entry point instead ofApplyMessage.fix(eth/tracers):IntermediateRootsreplays through the same entry point and stops skipping the transactions sent to the system addresses, so every transaction gets a root and the roots match block processing.core.ApplyTransactionForReplayfollows exactly the routing ofApplyTransactionWithEVM, records the same log through the sameaddNonEVMTxLoghelper for a transaction the EVM never executes, and skips the receipt and its bloom, which a replay does not need. That log is not a receipt artefact:StateDB.AddLogadvances the block-wide log count every later log takes its index from, so a replay that dropped it would makecallTracerreport indexes the chain never had. It refuses an EVM that carries a tracer (it never firesOnTxStart/OnTxEnd) or a state that is not a*state.StateDB, with sentinel errors the callers can tell apart from a transaction failure;IntermediateRootsreports them instead of returning the roots it has collected so far.Upstream
No upstream fix to port: geth has no non-EVM transaction concept, so neither the routing, the unconditional skips nor the trailing
ApplyMessagehave a geth counterpart.Tests
core/state_processor_test.go—TestApplyTransactionForReplayKeepsTheNonEVMTxLog: drives the replay and block processing of a transaction to a system address (the trading state address with the receiver fork active, and the block signers address) and compares the logs they record, so the replay cannot lose the log that advances the block-wide log count the following transactions index their logs with; without it the replay records no log where block processing records one.eth/state_accessor_test.go—TestStateAtTransactionReplayKeepsNonceLessSenderNonce: replays the block behind the issue (first transaction to the trading state address, receiver fork active from genesis) and asserts the sender nonce after the replay is still 0; the old replay left it at 1, which is what made the following transaction fail withnonce too low.eth/tracers/api_test.go—TestTraceTransactionSkipNonceTransactions: drivesdebug_traceTransactionfor the skip-nonce transaction itself and for the follower, over both receiver fork settings.eth/tracers/api_test.go—TestIntermediateRootsMatchesBlockProcessing: compares the roots against a replay throughcore.ApplyTransactionWithEVM, pins the last root to the block root after block finalisation and fails when the coinbase owner fee is dropped; the base has no intermediate-roots test at all.End-to-end verification
The branch binary was run against archive nodes of both live networks, on the same data directory and the same node as the baseline binary (
dev-upgrade@cdce8fc5c), and the tracing RPCs were compared on the same blocks.The probes below were run before the two
eth/tracerscommits moved to #2584. They exercise the replay and the intermediate roots, which this branch still carries unchanged.Mainnet block 37,849,457 (
0x2418971, 204 transactions, outside the receiver fork window, contains sign transactions):debug_intermediateRootsstateRootMainnet block 39,083,312 (
0x2545d30, 14 transactions, inside the window; tx 0 goes to0x92, tx 5 to0x90from the same sender with the same nonce) and Apothem block 48,667,667 (0x2e69c13, 8 transactions; tx 0 to0x92, tx 5 to0x89from the same sender with the same nonce):debug_traceTransaction(the follower)tracing failed: nonce too lowdebug_intermediateRootsstateRootMainnet block 107,084,658 (
0x661FB72, 6 transactions, after the receiver-disable fork, no system-address transaction,baseFeePerGas12.5 Gwei): onlydebug_intermediateRootsdiffers — the baseline last root is not the blockstateRoot, this branch's is.Manual test plan: start a node on that data directory with
--rpcapi debugand calldebug_traceTransactionon0x2545d30and0x2e69c13, anddebug_intermediateRootson0x2418971,0x2545d30and0x2e69c13; the baseline binary fails every one of them and this branch passes them.Regression: on both networks this branch imported testnet and mainnet segments normally, with no bad block, no panic and no error attributable to the change.
Types of changes
Impacted Components
core/state_processor.go)Checklist
Relation to other work
#2584 carries the two
eth/tracerscommits this one used to contain —callTracershadowing the real top-level frame of a system-address transaction, andflatCallTracerfailing the whole call withinvalid number of callson a block that carries a non-EVM transaction — so the two defects of the debug tracing surface are reviewable on their own. TheflatCallTracerrow that used to be in the table above moved there with its fix. The two branches touch disjoint files.#2581 fixes the unconditional skip in
traceBlockand in the state feeder of the JS tracer path — thenonce too highand thenullhole indebug_traceBlock*— and carries theskipNonceForkCases/newSkipNonceBackendfixtures and the two block-level tests. This PR carries its own copy of those fixtures, becauseTestTraceTransactionSkipNonceTransactionsbuilds on them; the second of the two PRs to merge drops that copy, which is the only textual overlap between them.#2578 fixes the give-up paths of the same
stateAtTransactionfunction whose replay this PR rewrites. Both branches toucheth/state_accessor.go; merging #2578 first keeps this one a trivial rebase.