Skip to content

refactor(core,eth,ethstats): make fork choice independent of the TD index - #2524

Open
gzliudan wants to merge 1 commit into
XinFinOrg:dev-upgradefrom
gzliudan:remove-td-field
Open

refactor(core,eth,ethstats): make fork choice independent of the TD index#2524
gzliudan wants to merge 1 commit into
XinFinOrg:dev-upgradefrom
gzliudan:remove-td-field

Conversation

@gzliudan

@gzliudan gzliudan commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Proposed changes

The total difficulty has been removed from geth:

And we also want to resolve the nil TD issue thoroughly and gracefully, such as #2515 (comment).

Chaindata written before the TD index existed can leave the head without a total difficulty entry. Every fork-choice, sync and
stalling check assumed the value was present, so such nodes either panicked in the comparisons or stalled forever when insertion demanded a parent TD that was missing.

Rather than rebuilding the index or treating the missing value as zero everywhere, the decision layer now routes all fork-choice comparisons through forkChoiceCmp: in the v2 region, where every difficulty is 1, the comparison is exactly a height check; the v1 region keeps the heaviest-chain rule. A missing TD falls back to a height comparison, and blocks without a computable parent TD are stored without a TD entry instead of being rejected.

Wire fields and TD writes are otherwise unchanged, so mixed-version nodes stay compatible. Legacy data serves a null totalDifficulty over RPC and a zero TD in handshakes and block announcements, and ethstats reports zero instead of the nil string of a missing total difficulty, which the stats server cannot parse as a number.

Notes for reviewers and node operators:

  • NewBlock announcements no longer spawn a synchronise probe while a downloader cycle is already running; the running cycle and the periodic syncer re-evaluate peers. This applies to TD-healthy nodes as well.
  • A legacy node whose advertised peer head is already known locally and not strictly above the current head skips the sync cycle entirely, since the height-based fork choice can never prefer such a chain. Startup logs display zero rather than a nil TD.
  • The v2 boundary now lives in params.XDPoSConfig.IsV2Block, the single source of truth shared with block validation, avoiding duplicated switch-block comparisons on fork-choice hot paths.
  • InsertReceiptChain re-checks the canonical hash of the receipt batch head under chainmu before advancing the snap marker, so a head removed by a concurrent rewind cannot repoint the snap marker above the rewind point.
  • A fork-choice tie with unknown total difficulties keeps the current chain deterministically instead of the anti-selfish-mining
    coin flip, which is only applied when both TDs are known.
  • Block tracer hooks receive a zero TD instead of a nil pointer on legacy chaindata.

Types of changes

What types of changes does your code introduce to XDC network?
Put an in the boxes that apply

  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Changes that don't change source code or tests
  • docs: Documentation only changes
  • feat: A new feature
  • fix: A bug fix
  • perf: A code change that improves performance
  • refactor: A code change that neither fixes a bug nor adds a feature
  • revert: Revert something
  • style: Changes that do not affect the meaning of the code
  • test: Adding missing tests or correcting existing tests

Impacted Components

Which parts of the codebase does this PR touch?
Put an in the boxes that apply

  • Consensus
  • Account
  • Network
  • Geth
  • Smart Contract
  • External components
  • Not sure (Please specify below)

Checklist

Put an in the boxes once you have confirmed below actions (or provide reasons on not doing so) that

  • This PR has sufficient test coverage (unit/integration test) OR I have provided reason in the PR description for not having test coverage
  • Tested on a private network from the genesis block and monitored the chain operating correctly for multiple epochs.
  • Provide an end-to-end test plan in the PR description on how to manually test it on the devnet/testnet.
  • Tested the backwards compatibility.
  • Tested with XDC nodes running this version co-exist with those running the previous version.
  • Relevant documentation has been updated as part of this PR
  • N/A

@coderabbitai

coderabbitai Bot commented Aug 16, 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: Pro Plus

Run ID: df11ea67-08ce-41bc-ae8e-6cd44d57abe6

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.

Pull request overview

Refactors fork choice and synchronization to tolerate legacy chaindata with missing total-difficulty entries.

Changes:

  • Adds height-based fork-choice fallbacks when TD is unavailable.
  • Handles missing TD in sync, propagation, statistics, and logging.
  • Adds regression tests across core and downloader paths.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
core/forkchoice.go Adds centralized fork-choice comparison.
core/forkchoice_test.go Tests v1, v2, and transition behavior.
core/headerchain.go Allows headers with missing parent TD.
core/blockchain.go Applies TD fallbacks to block and receipt insertion.
core/blockchain_td_test.go Tests missing-TD blockchain operations.
eth/sync.go Adjusts synchronization thresholds for missing TD.
eth/sync_td_test.go Tests legacy-head synchronization.
eth/handler.go Handles missing TD during announcements and propagation.
eth/handler_td_test.go Tests zero-TD block propagation.
eth/downloader/downloader.go Adds height-based stalling checks.
eth/downloader/downloader_test.go Updates test-chain TD behavior.
eth/downloader/td_test.go Tests downloader behavior without TD entries.
ethstats/ethstats.go Reports zero for unavailable TD.

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

Comment thread core/blockchain.go
Comment thread eth/sync.go Outdated
Comment thread eth/handler.go
@gzliudan
gzliudan force-pushed the remove-td-field branch 3 times, most recently from ef854b3 to d3a74f2 Compare August 16, 2026 09:18
…he TD index

Chaindata written before the TD index existed can leave the head
without a total difficulty entry. Every fork-choice, sync and
stalling check assumed the value was present, so such nodes either
panicked in the comparisons or stalled forever when insertion
demanded a parent TD that was missing.

Rather than rebuilding the index or treating the missing value as
zero everywhere, the decision layer now routes all fork-choice
comparisons through forkChoiceCmp: in the v2 region, where every
difficulty is 1, the comparison is exactly a height check; the v1
region keeps the heaviest-chain rule. A missing TD falls back to a
height comparison, and blocks without a computable parent TD are
stored without a TD entry instead of being rejected.

Wire fields and TD writes are otherwise unchanged, so mixed-version
nodes stay compatible. Legacy data serves a null totalDifficulty
over RPC and a zero TD in handshakes and block announcements, and
ethstats reports zero instead of the "<nil>" string of a missing
total difficulty, which the stats server cannot parse as a number.

Notes for reviewers and node operators:

- NewBlock announcements no longer spawn a synchronise probe while a
  downloader cycle is already running; the running cycle and the
  periodic syncer re-evaluate peers. This applies to TD-healthy nodes
  as well.
- A legacy node whose advertised peer head is already known locally
  and not strictly above the current head skips the sync cycle
  entirely, since the height-based fork choice can never prefer such
  a chain. Startup logs display zero rather than a "<nil>" TD.
- The v2 boundary now lives in params.XDPoSConfig.IsV2Block, the
  single source of truth shared with block validation, avoiding
  duplicated switch-block comparisons on fork-choice hot paths.
- InsertReceiptChain re-checks the canonical hash of the receipt
  batch head under chainmu before advancing the snap marker, so a
  head removed by a concurrent rewind cannot repoint the snap marker
  above the rewind point.
- A fork-choice tie with unknown total difficulties keeps the
  current chain deterministically instead of the anti-selfish-mining
  coin flip, which is only applied when both TDs are known.
- Block tracer hooks receive a zero TD instead of a nil pointer on
  legacy chaindata.

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.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (4)

eth/downloader/downloader.go:1455

  • This fast/light-sync terminator has the same zero-TD gap: when the delivered header has a TD but the peer's promised TD is the zero sentinel, the TD branch accepts an incomplete delivery and never checks the promised height. Fall back to height when td.Sign() == 0, otherwise a zero-TD peer can make an incomplete fast sync appear successful.
					if deliveredTd := d.lightchain.GetTd(head.Hash(), head.Number.Uint64()); deliveredTd != nil {

eth/handler.go:793

  • A positive-TD head update leaves headNum stale. If this connection later sends a zero-TD announcement whose parent is above that stale counter but below the positive-TD head, the branch below overwrites the newer head with the older one, defeating the monotonicity guarantee. Record the announced parent height for meaningful-TD updates too, ideally through one atomic compare-and-set API shared by both branches.
		if _, td := p.Head(); trueTD.Sign() > 0 && trueTD.Cmp(td) > 0 {
			p.SetHead(trueHead, trueTD)

eth/downloader/downloader.go:1438

  • The zero-TD sentinel is still treated as a verifiable promise whenever the local TD exists. For a zero-TD peer that advertises a higher head and then returns no headers, 0.Cmp(localTd) is never positive, so the downloader reports success instead of errStallingPeer; the protocol manager can then enable transactions and disable snap sync without making progress. Use the height check when either TD is unavailable, including td.Sign() == 0.

This issue also appears on line 1455 of the same file.

						if localTd := d.blockchain.GetTd(head.Hash(), head.Number.Uint64()); localTd != nil {

eth/peer.go:104

  • Tracking headNum does not make periodic recovery height-aware: peerSet.BestPeer at eth/peer.go:1027-1040 still compares only TD, so every zero-TD peer ties and map iteration chooses one arbitrarily. If a higher zero-TD peer's announcement arrives while another sync is active, the direct probe is suppressed and the periodic syncer can repeatedly select a stale peer instead. Include the tracked height when ranking unknown-TD peers (with an explicit ordering against known-TD peers).
	// headNum is the height of the advertised head, tracked only for peers
	// whose TD is unknown (the zero wire sentinel of legacy nodes). The
	// handshake assigns head and td directly, so headNum starts at zero for
	// every peer and is advanced monotonically by SetHeadByNumber from block
	// announcements. SetHead deliberately does not reset it: a peer uses a
	// single announcement format in practice, and resetting would let a stale
	// zero-TD announcement overwrite a real head in the mixed-format case.
	headNum uint64

@gzliudan
gzliudan requested a review from wgr523 August 16, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants