Conversation
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
|
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 |
This was referenced Sep 19, 2026
Closed
This was referenced Sep 19, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Proposed changes
Debug tracing rebuilds the pre-state of a transaction by re-executing the transactions that come before it.
stateAtTransaction, the replay behinddebug_traceTransactionanddebug_traceCall, did that withcore.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 same trailing
ApplyMessageand the same unconditional skip sit in the block-level APIs,debug_intermediateRootsandtraceBlock; those two are fixed separately, because they are different defects with their own probes.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).Fix
Two commits.
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. The log a non-EVM handler records moves intoaddNonEVMTxLogfor the same reason: it is not a receipt artefact,StateDB.AddLogadvances the block-wide log count every later log takes its index from, and a copy that dropped it would make the following transactions report indexes the chain never had. The commit also addscore.ApplyTransactionForReplay, the entry point a replay should use instead ofcore.ApplyMessage: it shares the routing, 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 (it never firesOnTxStart/OnTxEnd) or a state that is not a*state.StateDB, with sentinel errors the caller can tell apart from a transaction failure. No behaviour change.fix(eth):stateAtTransactiongoes throughcore.ApplyTransactionForReplayinstead ofcore.ApplyMessage. A replay that was handed an EVM it cannot use is reported as it is instead of being blamed on the transaction. The tracers test backend mirrors the same change so it keeps modelling the production behaviour.Upstream
No upstream fix to port: geth has no non-EVM transaction concept, so neither the routing 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.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.Mainnet 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 lowManual test plan: start a node on that data directory with
--rpcapi debugand calldebug_traceTransactionon0x2545d30and0x2e69c13; the baseline binary fails both 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
This is the first of three PRs that split a single debug tracing series, so every defect is reviewable on its own. It carries the shared routing and the replay entry point the other two build on.
The second PR (#2588) fixes
debug_intermediateRoots— it replayed throughApplyMessageand dropped every transaction sent to the system addresses, so it returned one root fewer than the block has transactions and its roots did not match block processing. The third fixes the traces and intermediate roots of the block that activates TIPSigning, which did not remove the legacy block signers account block processing removes before the first transaction.#2581 fixes the unconditional skip in
traceBlockand in the state feeder of the JS tracer path — thenonce too highand thenullhole indebug_traceBlock*. It carries its own copy of theskipNonceForkCases/newSkipNonceBackendfixtures this PR introduces, because its block-level tests build 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.