Skip to content

Commit d3a74f2

Browse files
committed
refactor(core,eth,ethstats,params): make fork choice independent of the 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 an invalid JSON "<nil>" value. 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.
1 parent f13dc61 commit d3a74f2

16 files changed

Lines changed: 1683 additions & 58 deletions

core/blockchain.go

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -736,12 +736,23 @@ func (bc *BlockChain) loadLastState() error {
736736
headerTd = bc.GetTd(headHeader.Hash(), headHeader.Number.Uint64())
737737
blockTd = bc.GetTd(headBlock.Hash(), headBlock.NumberU64())
738738
)
739+
// Legacy chaindata can leave the head without a TD entry: display a zero
740+
// value rather than the "<nil>" of a nil pointer.
741+
if headerTd == nil {
742+
headerTd = common.Big0
743+
}
744+
if blockTd == nil {
745+
blockTd = common.Big0
746+
}
739747
if headHeader.Hash() != headBlock.Hash() {
740748
log.Info("Loaded most recent local header", "number", headHeader.Number, "hash", headHeader.Hash(), "td", headerTd, "age", common.PrettyAge(time.Unix(int64(headHeader.Time), 0)))
741749
}
742750
log.Info("Loaded most recent local block", "number", headBlock.Number(), "hash", headBlock.Hash(), "td", blockTd, "age", common.PrettyAge(time.Unix(int64(headBlock.Time()), 0)))
743751
if headBlock.Hash() != currentSnapBlock.Hash() {
744752
fastTd := bc.GetTd(currentSnapBlock.Hash(), currentSnapBlock.Number.Uint64())
753+
if fastTd == nil {
754+
fastTd = common.Big0
755+
}
745756
log.Info("Loaded most recent local snap block", "number", currentSnapBlock.Number, "hash", currentSnapBlock.Hash(), "td", fastTd, "age", common.PrettyAge(time.Unix(int64(currentSnapBlock.Time), 0)))
746757
}
747758

@@ -1439,13 +1450,19 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
14391450
return 0, errChainStopped
14401451
}
14411452
head := blockChain[len(blockChain)-1]
1442-
if td := bc.GetTd(head.Hash(), head.NumberU64()); td != nil { // Rewind may have occurred, skip in that case
1443-
currentSnapBlock := bc.CurrentSnapBlock()
1444-
if bc.GetTd(currentSnapBlock.Hash(), currentSnapBlock.Number.Uint64()).Cmp(td) < 0 {
1445-
rawdb.WriteHeadFastBlockHash(bc.db, head.Hash())
1446-
bc.currentSnapBlock.Store(head.Header())
1447-
headFastBlockGauge.Update(int64(head.NumberU64()))
1448-
}
1453+
currentSnapBlock := bc.CurrentSnapBlock()
1454+
// The receipt data above is written without chainmu, so a concurrent
1455+
// rewind (SetHead) may have deleted the batch head's header, TD and
1456+
// canonical-hash markers before the lock was acquired. Such a stale head
1457+
// has a nil TD and would win the height fallback below, repointing the
1458+
// snap marker above the rewind. Require the head to still be the
1459+
// canonical header at its height: a legacy canonical head with a missing
1460+
// TD index passes this check and can still advance the snap block.
1461+
canonical := bc.GetHeaderByNumber(head.NumberU64())
1462+
if canonical != nil && canonical.Hash() == head.Hash() && forkChoiceCmp(bc.chainConfig, head.NumberU64(), bc.GetTd(head.Hash(), head.NumberU64()), currentSnapBlock.Number.Uint64(), bc.GetTd(currentSnapBlock.Hash(), currentSnapBlock.Number.Uint64())) > 0 {
1463+
rawdb.WriteHeadFastBlockHash(bc.db, head.Hash())
1464+
bc.currentSnapBlock.Store(head.Header())
1465+
headFastBlockGauge.Update(int64(head.NumberU64()))
14491466
}
14501467
bc.chainmu.Unlock()
14511468

@@ -1473,7 +1490,9 @@ func (bc *BlockChain) writeBlockWithoutState(block *types.Block, td *big.Int) (e
14731490
}
14741491

14751492
batch := bc.db.NewBatch()
1476-
rawdb.WriteTd(batch, block.Hash(), block.NumberU64(), td)
1493+
if td != nil {
1494+
rawdb.WriteTd(batch, block.Hash(), block.NumberU64(), td)
1495+
}
14771496
rawdb.WriteBlock(batch, block)
14781497
if err := batch.Write(); err != nil {
14791498
log.Crit("Failed to write block into disk", "err", err)
@@ -1497,22 +1516,31 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
14971516
return NonStatTy, errInsertionInterrupted
14981517
}
14991518

1500-
// Calculate the total difficulty of the block
1501-
ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1)
1502-
if ptd == nil {
1519+
// Calculate the total difficulty of the block. The parent header must
1520+
// exist for the chain to be importable, but its total difficulty may be
1521+
// unknown on legacy XDPoS chaindata that predates the TD index. The block
1522+
// is then stored without a TD entry and the fork choice falls back to a
1523+
// height comparison.
1524+
if bc.GetHeader(block.ParentHash(), block.NumberU64()-1) == nil {
15031525
return NonStatTy, consensus.ErrUnknownAncestor
15041526
}
1527+
ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1)
15051528
// Make sure no inconsistent state is leaked during insertion
15061529
currentBlock := bc.CurrentBlock()
15071530
localTd := bc.GetTd(currentBlock.Hash(), currentBlock.Number.Uint64())
1508-
externTd := new(big.Int).Add(block.Difficulty(), ptd)
1531+
var externTd *big.Int
1532+
if ptd != nil {
1533+
externTd = new(big.Int).Add(block.Difficulty(), ptd)
1534+
}
15091535

15101536
// Irrelevant of the canonical status, write the block itself to the database.
15111537
//
15121538
// Note all the components of block(td, hash->number map, header, body, receipts)
15131539
// should be written atomically. BlockBatch is used for containing all components.
15141540
blockBatch := bc.db.NewBatch()
1515-
rawdb.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd)
1541+
if externTd != nil {
1542+
rawdb.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd)
1543+
}
15161544
rawdb.WriteBlock(blockBatch, block)
15171545
rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts)
15181546
rawdb.WritePreimages(blockBatch, state.Preimages())
@@ -1664,13 +1692,14 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
16641692
}
16651693
}
16661694

1667-
// If the total difficulty is higher than our known, add it to the canonical chain
1695+
// If the fork-choice weight is higher than our known, add it to the canonical chain
16681696
// Second clause in the if statement reduces the vulnerability to selfish mining.
16691697
// Please refer to http://www.cs.cornell.edu/~ie53/publications/btcProcFC.pdf
1670-
reorg := externTd.Cmp(localTd) > 0
1698+
cmp := forkChoiceCmp(bc.chainConfig, block.NumberU64(), externTd, currentBlock.Number.Uint64(), localTd)
1699+
reorg := cmp > 0
16711700
currentBlock = bc.CurrentBlock()
1672-
if !reorg && externTd.Cmp(localTd) == 0 {
1673-
// Split same-difficulty blocks by number
1701+
if !reorg && cmp == 0 {
1702+
// Split equal-weight blocks by number
16741703
reorg = block.NumberU64() > currentBlock.Number.Uint64()
16751704
}
16761705
if reorg {
@@ -1976,6 +2005,12 @@ func (bc *BlockChain) processBlock(block *types.Block, parent *types.Header, sta
19762005
// TODO(daniel): implement CurrentFinalBlock() and CurrentSafeBlock(), ref PR #29189
19772006
if bc.logger != nil && bc.logger.OnBlockStart != nil {
19782007
td := bc.GetTd(block.ParentHash(), block.NumberU64()-1)
2008+
// Legacy chaindata predating the TD index can leave the parent TD
2009+
// missing: report a zero value to block tracer hooks, whose
2010+
// implementations may dereference the difficulty.
2011+
if td == nil {
2012+
td = common.Big0
2013+
}
19792014
bc.logger.OnBlockStart(tracing.BlockEvent{
19802015
Block: block,
19812016
TD: td,
@@ -2084,9 +2119,13 @@ func (bc *BlockChain) insertSidechain(block *types.Block, it *insertIterator) (i
20842119
}
20852120
}
20862121
if externTd == nil {
2087-
externTd = bc.GetTd(block.ParentHash(), block.NumberU64()-1)
2122+
if ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1); ptd != nil {
2123+
externTd = ptd
2124+
}
2125+
}
2126+
if externTd != nil {
2127+
externTd = new(big.Int).Add(externTd, block.Difficulty())
20882128
}
2089-
externTd = new(big.Int).Add(externTd, block.Difficulty())
20902129

20912130
if !bc.HasBlock(block.Hash(), block.NumberU64()) {
20922131
start := time.Now()
@@ -2106,7 +2145,7 @@ func (bc *BlockChain) insertSidechain(block *types.Block, it *insertIterator) (i
21062145
// If the externTd was larger than our local TD, we now need to reimport the previous
21072146
// blocks to regenerate the required state
21082147
localTd := bc.GetTd(bc.CurrentBlock().Hash(), current)
2109-
if localTd.Cmp(externTd) > 0 {
2148+
if forkChoiceCmp(bc.chainConfig, it.previous().Number.Uint64(), externTd, current, localTd) < 0 {
21102149
log.Info("Sidechain written to disk", "start", it.first().NumberU64(), "end", it.previous().Number, "sidetd", externTd, "localtd", localTd)
21112150
return it.index, nil, nil, err
21122151
}
@@ -2233,11 +2272,14 @@ func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*Resu
22332272
}
22342273
case err == consensus.ErrPrunedAncestor:
22352274
// Block competing with the canonical chain, store in the db, but don't process
2236-
// until the competitor TD goes above the canonical TD
2275+
// until the competitor fork-choice weight goes above the canonical one
22372276
currentBlock := bc.CurrentBlock()
22382277
localTd := bc.GetTd(currentBlock.Hash(), currentBlock.Number.Uint64())
2239-
externTd := new(big.Int).Add(bc.GetTd(block.ParentHash(), block.NumberU64()-1), block.Difficulty())
2240-
if localTd.Cmp(externTd) > 0 {
2278+
var externTd *big.Int
2279+
if ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1); ptd != nil {
2280+
externTd = new(big.Int).Add(ptd, block.Difficulty())
2281+
}
2282+
if forkChoiceCmp(bc.chainConfig, block.NumberU64(), externTd, currentBlock.Number.Uint64(), localTd) < 0 {
22412283
return nil, err
22422284
}
22432285
// Competitor chain beat canonical, gather all blocks from the common ancestor

0 commit comments

Comments
 (0)