Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 113 additions & 1 deletion core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1441,7 +1441,12 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
head := blockChain[len(blockChain)-1]
if td := bc.GetTd(head.Hash(), head.NumberU64()); td != nil { // Rewind may have occurred, skip in that case
currentSnapBlock := bc.CurrentSnapBlock()
if bc.GetTd(currentSnapBlock.Hash(), currentSnapBlock.Number.Uint64()).Cmp(td) < 0 {
snapTd := bc.GetTd(currentSnapBlock.Hash(), currentSnapBlock.Number.Uint64())
if snapTd == nil {
// See writeBlockWithState: a missing local TD is treated as zero.
snapTd = new(big.Int)
}
if snapTd.Cmp(td) < 0 {
rawdb.WriteHeadFastBlockHash(bc.db, head.Hash())
bc.currentSnapBlock.Store(head.Header())
headFastBlockGauge.Update(int64(head.NumberU64()))
Expand Down Expand Up @@ -1481,6 +1486,98 @@ func (bc *BlockChain) writeBlockWithoutState(block *types.Block, td *big.Int) (e
return nil
}

// RepairMissingTd reconstructs and persists the total difficulty entries for
// the canonical chain segment leading to the given head, which may be missing
// on legacy XDPoS chaindata that predates the TD index. It walks the canonical
// parents from the head down to the nearest ancestor with a known TD entry
// (or genesis, which always has one) and writes back the accumulated values.
//
// Total difficulty values are immutable chain facts, so the repair does not
// need to hold the chain mutex: concurrent inserts can only ever write the
// same value for the same block, and the head is snapshotted at entry. The
// method returns the head's total difficulty, or nil if the head is unknown
// or the node is shutting down.
func (bc *BlockChain) RepairMissingTd(headHash common.Hash, headNumber uint64) *big.Int {
start := time.Now()

// Fast path: nothing to repair.
if td := bc.GetTd(headHash, headNumber); td != nil {
return td
}
// Walk the canonical parent chain collecting the segment with missing TDs,
// stopping at the first ancestor with a known entry.
type missingTd struct {
hash common.Hash
number uint64
difficulty *big.Int
}
var (
segment []missingTd
base *big.Int
visited int
)
for hash, number := headHash, headNumber; ; {
header := bc.GetHeader(hash, number)
if header == nil {
return nil
}
if td := bc.GetTd(hash, number); td != nil {
base = td
break
}
segment = append(segment, missingTd{hash: hash, number: number, difficulty: header.Difficulty})
visited++
if visited%100000 == 0 {
log.Info("Repairing missing total difficulties", "blocks", visited, "number", number, "head", headNumber, "elapsed", common.PrettyDuration(time.Since(start)))
}
if number == 0 {
// Genesis reached without any known TD: anchor at zero so the
// genesis entry becomes its own difficulty.
base = new(big.Int)
break
}
hash, number = header.ParentHash, number-1
}
// Reconstruct the TDs from the anchor back towards the head, persisting
// them in chunks to bound memory usage.
const tdWriteChunk = 50000
var (
batch = bc.db.NewBatch()
written int
acc = new(big.Int).Set(base)
)
flush := func() {
if written == 0 {
return
}
if err := batch.Write(); err != nil {
log.Crit("Failed to write repaired total difficulties", "err", err)
}
batch = bc.db.NewBatch()
written = 0
}
for i := len(segment) - 1; i >= 0; i-- {
if bc.insertStopped() {
// Shutting down: persist the partial result (entries are immutable
// facts) and report the repair as incomplete.
flush()
log.Warn("Total difficulty repair interrupted", "blocks", len(segment)-i-1, "head", headNumber)
return nil
}
entry := segment[i]
acc.Add(acc, entry.difficulty)
rawdb.WriteTd(batch, entry.hash, entry.number, new(big.Int).Set(acc))
bc.hc.tdCache.Add(entry.hash, new(big.Int).Set(acc))
written++
if written >= tdWriteChunk {
flush()
}
}
flush()
log.Info("Repaired missing total difficulties", "blocks", len(segment), "head", headNumber, "elapsed", common.PrettyDuration(time.Since(start)))
return acc
}

// WriteBlockWithState writes the block and all associated state to the database.
func (bc *BlockChain) WriteBlockWithState(block *types.Block, receipts []*types.Receipt, state *state.StateDB, tradingState *tradingstate.TradingStateDB, lendingState *lendingstate.LendingStateDB) (status WriteStatus, err error) {
if !bc.chainmu.TryLock() {
Expand All @@ -1505,6 +1602,13 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
// Make sure no inconsistent state is leaked during insertion
currentBlock := bc.CurrentBlock()
localTd := bc.GetTd(currentBlock.Hash(), currentBlock.Number.Uint64())
if localTd == nil {
// Legacy XDPoS chaindata may predate the TD index, leaving the
// current head without a TD entry. The sync layer repairs the entry
// before downloading, but a still-missing value is treated as zero
// (i.e. lower than any real TD) to avoid a nil comparison panic.
localTd = new(big.Int)
}
externTd := new(big.Int).Add(block.Difficulty(), ptd)

// Irrelevant of the canonical status, write the block itself to the database.
Expand Down Expand Up @@ -2106,6 +2210,10 @@ func (bc *BlockChain) insertSidechain(block *types.Block, it *insertIterator) (i
// If the externTd was larger than our local TD, we now need to reimport the previous
// blocks to regenerate the required state
localTd := bc.GetTd(bc.CurrentBlock().Hash(), current)
if localTd == nil {
// See writeBlockWithState: a missing local TD is treated as zero.
localTd = new(big.Int)
}
if localTd.Cmp(externTd) > 0 {
log.Info("Sidechain written to disk", "start", it.first().NumberU64(), "end", it.previous().Number, "sidetd", externTd, "localtd", localTd)
return it.index, nil, nil, err
Expand Down Expand Up @@ -2236,6 +2344,10 @@ func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*Resu
// until the competitor TD goes above the canonical TD
currentBlock := bc.CurrentBlock()
localTd := bc.GetTd(currentBlock.Hash(), currentBlock.Number.Uint64())
if localTd == nil {
// See writeBlockWithState: a missing local TD is treated as zero.
localTd = new(big.Int)
}
externTd := new(big.Int).Add(bc.GetTd(block.ParentHash(), block.NumberU64()-1), block.Difficulty())
if localTd.Cmp(externTd) > 0 {
return nil, err
Expand Down
140 changes: 140 additions & 0 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2890,3 +2890,143 @@ func TestDeleteCreateRevert(t *testing.T) {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
}

// TestWriteHeaderMissingLocalTd exercises the defensive nil handling in
// HeaderChain.WriteHeader: a fork rooted below the head must be importable
// when the current head's total difficulty entry is missing (legacy chaindata
// predating the TD index), without panicking in the fork-choice comparison.
func TestWriteHeaderMissingLocalTd(t *testing.T) {
engine := ethash.NewFaker()
db, _, blockchain, err := newCanonical(engine, 5, false)
if err != nil {
t.Fatalf("failed to create canonical header chain: %v", err)
}
defer blockchain.Stop()

// Build a side fork rooted at block 3, so the fork parent has a TD entry
// while the canonical head's entry has been removed below.
fork := makeHeaderChain(blockchain.chainConfig, blockchain.GetHeaderByNumber(3), 2, engine, db, forkSeed)

head := blockchain.CurrentHeader()
rawdb.DeleteTd(blockchain.ChainDb(), head.Hash(), head.Number.Uint64())
blockchain.hc.tdCache.Purge()

if _, err := blockchain.InsertHeaderChain(fork, 1); err != nil {
t.Fatalf("failed to insert fork with missing local TD: %v", err)
}
if got := blockchain.CurrentHeader().Hash(); got != fork[len(fork)-1].Hash() {
t.Errorf("canonical head mismatch: have %v, want %v", got, fork[len(fork)-1].Hash())
}
}

// TestWriteBlockWithStateMissingLocalTd exercises the defensive nil handling
// in writeBlockWithState: importing a fork rooted below the head must not
// panic when the current head's TD entry is missing (legacy chaindata
// predating the TD index).
func TestWriteBlockWithStateMissingLocalTd(t *testing.T) {
engine := ethash.NewFaker()
db, _, blockchain, err := newCanonical(engine, 5, true)
if err != nil {
t.Fatalf("failed to create canonical block chain: %v", err)
}
defer blockchain.Stop()

fork := makeBlockChain(blockchain.chainConfig, blockchain.GetBlockByNumber(3), 2, engine, db, forkSeed)

head := blockchain.CurrentBlock()
rawdb.DeleteTd(blockchain.ChainDb(), head.Hash(), head.Number.Uint64())
blockchain.hc.tdCache.Purge()

if _, err := blockchain.InsertChain(fork); err != nil {
t.Fatalf("failed to insert fork with missing local TD: %v", err)
}
if got := blockchain.CurrentBlock().Hash(); got != fork[len(fork)-1].Hash() {
t.Errorf("canonical head mismatch: have %v, want %v", got, fork[len(fork)-1].Hash())
}
}

// TestRepairMissingTd verifies that RepairMissingTd rebuilds the missing TD
// entries along the canonical chain leading to the head, stopping at the
// nearest ancestor with a known TD, and that the operation is idempotent.
func TestRepairMissingTd(t *testing.T) {
engine := ethash.NewFaker()
_, _, blockchain, err := newCanonical(engine, 8, true)
if err != nil {
t.Fatalf("failed to create canonical block chain: %v", err)
}
defer blockchain.Stop()

head := blockchain.CurrentBlock()
want := blockchain.GetTd(head.Hash(), head.Number.Uint64())
if want == nil {
t.Fatalf("expected a TD entry before deletion")
}
// Simulate legacy chaindata: drop the TD entries of the last three blocks.
for num := head.Number.Uint64(); num > head.Number.Uint64()-3; num-- {
block := blockchain.GetBlockByNumber(num)
rawdb.DeleteTd(blockchain.ChainDb(), block.Hash(), num)
}
blockchain.hc.tdCache.Purge()

got := blockchain.RepairMissingTd(head.Hash(), head.Number.Uint64())
if got == nil || got.Cmp(want) != 0 {
t.Fatalf("repaired head TD mismatch: have %v, want %v", got, want)
}
// The repaired head TD must be persisted.
if td := blockchain.GetTd(head.Hash(), head.Number.Uint64()); td == nil || td.Cmp(want) != 0 {
t.Fatalf("head TD not persisted after repair: have %v, want %v", td, want)
}
// The intermediate ancestors must have been repaired as well.
for num := head.Number.Uint64() - 2; num <= head.Number.Uint64(); num++ {
block := blockchain.GetBlockByNumber(num)
if td := blockchain.GetTd(block.Hash(), num); td == nil {
t.Errorf("ancestor %d TD not repaired", num)
}
}
// A second call is a no-op returning the same value.
if again := blockchain.RepairMissingTd(head.Hash(), head.Number.Uint64()); again == nil || again.Cmp(want) != 0 {
t.Fatalf("idempotent repair mismatch: have %v, want %v", again, want)
}
}

// TestRepairMissingTdHeaderChain verifies repair on a header-only chain, the
// shape used by the snap-sync header phase.
func TestRepairMissingTdHeaderChain(t *testing.T) {
engine := ethash.NewFaker()
_, _, blockchain, err := newCanonical(engine, 8, false)
if err != nil {
t.Fatalf("failed to create canonical header chain: %v", err)
}
defer blockchain.Stop()

head := blockchain.CurrentHeader()
want := blockchain.GetTd(head.Hash(), head.Number.Uint64())
if want == nil {
t.Fatalf("expected a TD entry before deletion")
}
for num := head.Number.Uint64(); num > head.Number.Uint64()-3; num-- {
header := blockchain.GetHeaderByNumber(num)
rawdb.DeleteTd(blockchain.ChainDb(), header.Hash(), num)
}
blockchain.hc.tdCache.Purge()

got := blockchain.RepairMissingTd(head.Hash(), head.Number.Uint64())
if got == nil || got.Cmp(want) != 0 {
t.Fatalf("repaired head TD mismatch: have %v, want %v", got, want)
}
}

// TestRepairMissingTdUnknownHead verifies that RepairMissingTd reports failure
// for an unknown head instead of panicking or corrupting the database.
func TestRepairMissingTdUnknownHead(t *testing.T) {
engine := ethash.NewFaker()
_, _, blockchain, err := newCanonical(engine, 2, true)
if err != nil {
t.Fatalf("failed to create canonical block chain: %v", err)
}
defer blockchain.Stop()

if td := blockchain.RepairMissingTd(common.Hash{1}, 12345); td != nil {
t.Fatalf("expected nil for unknown head, got %v", td)
}
}
7 changes: 7 additions & 0 deletions core/headerchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ func (hc *HeaderChain) WriteHeader(header *types.Header) (status WriteStatus, er
return NonStatTy, consensus.ErrUnknownAncestor
}
localTd := hc.GetTd(hc.currentHeaderHash, hc.CurrentHeader().Number.Uint64())
if localTd == nil {
// Legacy XDPoS chaindata may predate the TD index, leaving the
// current head without a TD entry. The sync layer repairs the entry
// before downloading, but a still-missing value is treated as zero
// (i.e. lower than any real TD) to avoid a nil comparison panic.
localTd = new(big.Int)
}
externTd := new(big.Int).Add(header.Difficulty, ptd)

// Irrelevant of the canonical status, write the td and header to the database
Expand Down
61 changes: 57 additions & 4 deletions eth/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ type Downloader struct {
synchronising int32
notified int32
committed int32
// missingTdWarned is set after the missing-TD warning was logged once and is
// deliberately never reset, so the warning fires at most once per downloader
// lifetime. missingTdCounter keeps counting every occurrence, leaving
// operators an ongoing signal for a persistent condition.
missingTdWarned uint32

// Pivot block configuration (set before sync starts)
pivotNumber uint64 // Fixed pivot block number (0 = use default calculation)
Expand Down Expand Up @@ -1352,6 +1357,41 @@ func (d *Downloader) fetchParts(deliveryCh chan dataPack, deliver func(dataPack)
}
}

// warnMissingTd reports a missing local total difficulty entry that made the
// stalling-peer check unverifiable, causing the peer to be treated as
// stalling. Header insertion writes TDs atomically with the headers, so this
// only happens when the peer delivered nothing and the local head predates
// the TD index (legacy XDPoS chaindata). The occurrence is counted on every
// cycle so operators can still observe a persistent condition, but the
// warning is logged only once per downloader lifetime to avoid spamming each
// sync cycle.
func (d *Downloader) warnMissingTd(number uint64, hash common.Hash) {
missingTdCounter.Inc(1)
if atomic.CompareAndSwapUint32(&d.missingTdWarned, 0, 1) {
log.Warn("Treating peer as stalling, local TD missing", "number", number, "hash", hash)
} else {
log.Debug("Treating peer as stalling, local TD missing", "number", number, "hash", hash)
}
}

// checkStallingPeer returns errStallingPeer if the promised total difficulty
// exceeds the local TD of the given head, i.e. the peer bailed out of
// delivering the chain it promised. A missing local TD entry (legacy XDPoS
// chaindata predating the TD index) makes the promise unverifiable; the
// condition is reported via warnMissingTd and conservatively treated as
// stalling, since returning success would silently accept an unverifiable TD
// promise (and td.Cmp(nil) would panic).
func (d *Downloader) checkStallingPeer(td, localTD *big.Int, head *types.Header) error {
if localTD == nil {
d.warnMissingTd(head.Number.Uint64(), head.Hash())
return errStallingPeer
}
if td.Cmp(localTD) > 0 {
return errStallingPeer
}
return nil
}

// processHeaders takes batches of retrieved headers from an input channel and
// keeps processing and scheduling them into the header chain and downloader's
// queue until the stream ends or a failure occurs.
Expand Down Expand Up @@ -1425,8 +1465,15 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er
// R: Nothing to give
if mode != LightSync {
head := d.blockchain.CurrentBlock()
if !gotHeaders && td.Cmp(d.blockchain.GetTd(head.Hash(), head.Number.Uint64())) > 0 {
return errStallingPeer
if !gotHeaders {
// A missing TD is only possible on legacy XDPoS chaindata that
// predates the TD index; upstream geth assumes it always exists.
// This branch only runs when the peer delivered nothing, so
// treating the unverifiable promise as stalling cannot penalise
// a peer that fed headers.
if err := d.checkStallingPeer(td, d.blockchain.GetTd(head.Hash(), head.Number.Uint64()), head); err != nil {
return err
}
}
}
// If fast or light syncing, ensure promised headers are indeed delivered. This is
Expand All @@ -1441,8 +1488,14 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er
if lastInserted != nil && lastInserted.Number.Uint64() > head.Number.Uint64() {
head = lastInserted
}
if td.Cmp(d.lightchain.GetTd(head.Hash(), head.Number.Uint64())) > 0 {
return errStallingPeer
// Header insertion writes the TD atomically with the header, so a
// missing TD only triggers when no headers were delivered this
// cycle and the current head predates the TD index (legacy XDPoS
// chaindata); the peer is then treated as stalling. The post-pivot
// attack detection is unaffected whenever the peer delivered
// anything.
if err := d.checkStallingPeer(td, d.lightchain.GetTd(head.Hash(), head.Number.Uint64()), head); err != nil {
return err
}
}
// Disable any rollback and return
Expand Down
Loading