Skip to content

fix(core,eth,miner,internal/ethapi): fix the missing TIPSigning block level removal - #2591

Open
gzliudan wants to merge 4 commits into
XinFinOrg:dev-upgradefrom
gzliudan:fix-trace-replay-tipsigning
Open

gzliudan wants to merge 4 commits into
XinFinOrg:dev-upgradefrom
gzliudan:fix-trace-replay-tipsigning

Conversation

@gzliudan

@gzliudan gzliudan commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

Proposed changes

Block processing removes the legacy block signers account (common.BlockSignersBinary) before the first transaction of the block that activates TIPSigning (mainnet TIPSigningBlock 3,000,000), the same way it applies the DAO hard fork at the DAO fork block. None of the replays ran that removal: stateAtTransaction (the replay behind debug_traceTransaction and debug_traceCall), IntermediateRoots (behind debug_intermediateRoots) and the per-block state builder traceChain uses all rebuild the block from its parent state, so they keep an account canonical execution had already deleted, and the traces and every intermediate root of the activation block describe a state the chain never had.

The chain makers had the same gap, which is what makes it visible in tests rather than only on the live networks: GenerateChain mirrors the block level mutations of the DAO fork and of Prague, so a generated chain that activates TIPSigning produces a block whose root does not match what block processing computes, and such a block cannot be imported at all.

eth_simulateV1 was a fourth path: processBlock rebuilds a block from a selectable base state, applies the state overrides and the Prague parent block hash, and then executes the calls, but it ran none of the other block level state changes block processing performs before the first transaction. Simulating the activation block over its parent therefore kept the legacy account there too, and returned calls and a state root for a pre-state the chain never had.

Symptoms

  • Every trace and every intermediate root of the TIPSigning activation block (mainnet block 3,000,000) is computed on a state that still holds the legacy block signers account block processing had already removed before the first transaction of the block, so the first debug_intermediateRoots root differs.
  • A generated chain whose blocks activate TIPSigning cannot be imported: GenerateChain produces a state root block processing does not reproduce (invalid merkle root).
  • eth_simulateV1 returns calls and a state root for the activation block computed on a pre-state that still holds the legacy account, when the caller selects that block's parent as the base state.

Fix

The removal moves into core.ApplyTIPSigningHardFork, which Process, ProcessBlockNoValidator and the miner call instead of each keeping its own copy of the condition, and which the three replay paths above, GenerateChain and eth_simulateV1's processBlock call as well. The branch is four commits: one behaviour-preserving refactor that shares the removal, plus one fix for each path group (the chain makers, the tracing replays, and eth_simulateV1). The condition is unchanged — it fires on the activation block only — so block processing and the miner build the same blocks as before.

Upstream

No upstream fix to port: geth has no TIPSigning fork, so the block-level removal the replays missed has no counterpart. It has no non-EVM transaction concept either, which is why the ApplyMessage replays the other two PRs of this series replace have none.

Tests

  • eth/tracers/api_tipsigning_activation_test.goTestIntermediateRootsMatchesBlockProcessingAtTIPSigningActivation: traces a block that activates TIPSigning over a parent state that holds the legacy account, pins that premise from both sides (the account is in the parent state, and it is gone from the state the imported block carries) and compares the intermediate roots against a block processing replay pinned to block.Root(); without the call the very first root already differs. The file carries its own mirror of the block processing replay, so it does not build on the helper the intermediate-roots PR adds.
  • internal/ethapi/simulate_tipsigning_test.goTestSimulateV1AppliesTIPSigningActivation: simulates the activation block twice, over a base state that holds the legacy account and over one that does not, and requires both to land on the same state root; without the call the first one keeps the account and lands elsewhere. The account has to come from the genesis allocation, because DeleteAddress removes it from the trie only and a state override would be written back by the block finalisation.

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): on both networks this branch imported testnet and mainnet segments normally, with no bad block, no panic and no error attributable to the change.

The activation block itself was not probed on the live networks, because it is far outside the segments the archive nodes re-import. Its coverage is the unit test above, which pins its own premise from both sides and checks the comparison path against block.Root().

Manual test plan: start a node on mainnet archive data with --rpcapi debug and call debug_intermediateRoots and debug_traceTransaction on mainnet block 3,000,000; the baseline binary replays that block without the removal, so its first intermediate root already differs from the one block processing produces.

Types of changes

  • fix: A bug fix
  • refactor: A code change that neither fixes a bug nor adds a feature
  • test: Adding missing tests or correcting existing tests
  • build / ci / chore / docs / feat / perf / revert / style

Impacted Components

  • Geth
  • Not sure (the changes are the debug JSON-RPC surface and eth_simulateV1, plus the TIPSigning removal they now share with the miner and the chain makers)
  • Consensus
  • Account
  • Network
  • Smart Contract
  • External components

Checklist

  • This PR has sufficient test coverage (unit/integration test)
  • Provide an end-to-end test plan in the PR description on how to manually test it on the devnet/testnet (see "End-to-end verification")
  • Tested the backwards compatibility — the change only affects replays and the chain makers; block processing and the miner keep the same condition, finalisation and nonce handling, and no API, RPC method or state format changes
  • Tested on a private network from the genesis block and monitored the chain operating correctly for multiple epochs — not done for this branch. It was run against live testnet and mainnet archive nodes instead (segments imported, no bad block, no panic, no new error).
  • Tested with XDC nodes running this version co-exist with those running the previous version — the branch ran on the live networks next to the rest of the network without being dropped
  • Relevant documentation has been updated as part of this PR

Relation to other work

This is the third of three PRs that split a single debug tracing series, so every defect is reviewable on its own.

#2589 carries the shared block processing routing and the replay entry point, and rewrites the stateAtTransaction replay — the nonce too low of debug_traceTransaction. #2590 fixes the missing intermediate roots of a block. Neither is a prerequisite for this one: the removal introduced here is independent of the routing they share, and the call sites it adds are all in code the base already has.

Its scope is the missing block level TIPSigning removal on every path that rebuilds a block's pre-state: the three tracing replay paths, the chain makers and eth_simulateV1, which is why the title covers more than tracing.

This PR carries its own mirror of the block processing replay in its test, because the shared helper comes with #2590. When the other two land, that copy is dropped in favour of the shared helper, which is the only change this branch needs on rebase.

#2584 carries the two eth/tracers fixes this series used to contain — callTracer shadowing the real top-level frame of a system-address transaction, and flatCallTracer failing the whole call with invalid number of calls on a block that carries a non-EVM transaction.

#2581 fixes the unconditional skip in traceBlock and in the state feeder of the JS tracer path — the nonce too high and the null hole in debug_traceBlock*.

#2578 fixes the give-up paths of the same stateAtTransaction function #2589 rewrites.

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 8ca87bfd-e5a3-435d-8e13-aca052f05f78

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

eth_simulateV1 still omits the TIPSigning activation transition and can simulate against a noncanonical pre-state.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Centralizes the TIPSigning activation state transition so block generation and debug tracing reproduce canonical execution.

Changes:

  • Adds ApplyTIPSigningHardFork and integrates it into processing, mining, generation, and tracing paths.
  • Adds an activation-block intermediate-root regression test.
  • Updates tracing test backends to apply the transition.
File Description
core/​state_processor.go Centralizes the TIPSigning state transition.
core/​chain_makers.go Applies the transition during chain generation.
miner/​worker.go Uses the shared helper while building blocks.
eth/​state_accessor.go Corrects transaction replay pre-state.
eth/​tracers/​api.go Corrects block and intermediate-root replay state.
eth/​tracers/​api_test.go Updates tracing test backends.
eth/​tracers/​api_tipsigning_activation_test.go Tests activation-block roots and import consistency.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread core/state_processor.go
@gzliudan gzliudan changed the title fix(core,eth,miner): fix traces and intermediate roots of the TIPSigning activation block fix(core,eth,miner,internal/ethapi): fix the missing TIPSigning block level removal in block replays and eth_simulateV1 Sep 19, 2026
@gzliudan gzliudan changed the title fix(core,eth,miner,internal/ethapi): fix the missing TIPSigning block level removal in block replays and eth_simulateV1 fix(core,eth,miner,internal/ethapi): fix the missing TIPSigning block level removal Sep 19, 2026
@gzliudan
gzliudan force-pushed the fix-trace-replay-tipsigning branch from 09b1eb4 to 8cd497f Compare September 19, 2026 10:58
Block processing removes the legacy block signers account before the first
transaction of the block that activates TIPSigning, and Process,
ProcessBlockNoValidator and the miner each carried their own copy of that
condition and of the deletion.

Pull them into core.ApplyTIPSigningHardFork, so that a path which has to rebuild a
block can run the same removal, and replace the three copies with a call to it.
Two of the copies guarded on blockNumber.Sign() > 0 and the miner copy did not;
the shared condition keeps that guard, which holds for every block the miner
builds, so block processing and the miner produce the same state as before.

No behaviour change.
…Signing

GenerateChain mirrors the block level mutations of the DAO fork and of Prague, so
a generated chain is expected to carry blocks whose root matches what block
processing computes. It never ran the TIPSigning one, so a chain that activates
the fork inside it is generated with a root block processing does not reproduce,
and such a block cannot be imported at all (invalid merkle root).

Run the shared removal there too.

This is a pre-existing gap: the base commit carries the same GenerateChain
without it. geth has no TIPSigning fork, so there is no upstream fix to port.
…ivation block

debug_traceTransaction and debug_traceCall (stateAtTransaction),
debug_intermediateRoots (IntermediateRoots), the blocks debug_traceChain walks and
the parallel feeder of traceBlock all rebuild the pre-state of a block from its
parent state, and none of them ran the block level removal the activation block
performs before its first transaction. The transaction traces and every
intermediate root of that block therefore describe a state the chain never had,
one that still holds an account canonical execution had already removed.

Call core.ApplyTIPSigningHardFork in all four paths.

Add TestIntermediateRootsMatchesBlockProcessingAtTIPSigningActivation, which
traces a block that activates TIPSigning over a parent state holding the legacy
account, pins that premise from both sides (the account is in the parent state and
gone from the state the imported block carries) and compares the intermediate
roots against a block processing replay pinned to block.Root(); without the call
the very first root already differs. The test carries its own mirror of the block
processing replay instead of building on the helper of the intermediate-roots
fix, so this commit can be reviewed and imported on its own.

This is a pre-existing gap: the base commit replays those blocks through
ApplyMessage and without the removal as well. geth has no TIPSigning fork, so
there is no upstream fix to port.
…_simulateV1

eth_simulateV1 rebuilds a block from a selectable base state: processBlock applies
the state overrides and the Prague parent block hash, then executes the calls. It ran
none of the other block level state changes block processing performs before the first
transaction, so simulating the block that activates TIPSigning over its parent kept the
legacy block signers account canonical execution had already removed, and returned
calls and a state root for a pre-state the chain never had.

Call core.ApplyTIPSigningHardFork before the first simulated call, the same helper the
replay paths use.

Add TestSimulateV1AppliesTIPSigningActivation, which simulates that block twice, over a
base state that holds the legacy account and over one that does not, and fails when the
two do not land on the same state root; without the call the first one keeps the account
and lands elsewhere. The account has to come from the genesis allocation, because
DeleteAddress removes it from the trie only and a state override would be written back by
the block finalisation.

This is a pre-existing gap: the base commit carries the same processBlock and the same
missing removal. geth has no TIPSigning fork, so there is no upstream fix to port.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The shared state transition preserves canonical processing behavior and is consistently integrated with focused regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants