refactor(core,eth,ethstats): make fork choice independent of the TD index - #2524
refactor(core,eth,ethstats): make fork choice independent of the TD index#2524gzliudan wants to merge 1 commit into
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: Pro Plus 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 |
There was a problem hiding this comment.
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.
ef854b3 to
d3a74f2
Compare
…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.
d3a74f2 to
9bd6229
Compare
There was a problem hiding this comment.
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
headNumstale. 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 oferrStallingPeer; the protocol manager can then enable transactions and disable snap sync without making progress. Use the height check when either TD is unavailable, includingtd.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
headNumdoes not make periodic recovery height-aware:peerSet.BestPeerateth/peer.go:1027-1040still 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
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:
coin flip, which is only applied when both TDs are known.
Types of changes
What types of changes does your code introduce to XDC network?
Put an
✅in the boxes that applyImpacted Components
Which parts of the codebase does this PR touch?
Put an
✅in the boxes that applyChecklist
Put an
✅in the boxes once you have confirmed below actions (or provide reasons on not doing so) that