diff --git a/core/blockchain_sethead_test.go b/core/blockchain_sethead_test.go new file mode 100644 index 000000000000..dbc266dc2db3 --- /dev/null +++ b/core/blockchain_sethead_test.go @@ -0,0 +1,315 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "fmt" + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/consensus/ethash" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/vm" + "github.com/XinFinOrg/XDPoSChain/params" +) + +// TestSetHeadCleansDanglingBlocks verifies that SetHead removes not only the +// canonical chain segment being rewound, but also any orphaned/dangling block +// data sitting above the current head (e.g. leftover from a previously +// interrupted rewind). Without this, the downloader's ancestor search can +// "jump" back onto the stale data instead of re-syncing the range. +func TestSetHeadCleansDanglingBlocks(t *testing.T) { + var ( + engine = ethash.NewFaker() + genesis = &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + db = rawdb.NewMemoryDatabase() + ) + // Archive mode so every state is persisted and rewinding to an arbitrary + // height lands exactly on that block instead of an earlier stateful one. + chain, err := NewBlockChain(db, &CacheConfig{TrieDirtyDisabled: true}, genesis, engine, vm.Config{}) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 64, nil) + + // Import blocks 1..32 as the canonical chain; head is now at 32. + if _, err := chain.InsertChain(blocks[:32]); err != nil { + t.Fatalf("failed to insert canonical chain: %v", err) + } + if head := chain.CurrentBlock().Number.Uint64(); head != 32 { + t.Fatalf("unexpected head after import: have %d want 32", head) + } + + // Simulate leftover data above the head by writing blocks 33..64 directly + // to the database without advancing any head marker, mimicking a rewind + // that was interrupted before its deletion batch was flushed. + for _, block := range blocks[32:] { + rawdb.WriteBlock(db, block) + } + for _, block := range blocks[32:] { + if !chain.HasBlock(block.Hash(), block.NumberU64()) { + t.Fatalf("setup failed: dangling block #%d not present", block.NumberU64()) + } + } + + // Rewind the chain to block 16. + if err := chain.SetHead(16); err != nil { + t.Fatalf("failed to set head: %v", err) + } + if head := chain.CurrentBlock().Number.Uint64(); head != 16 { + t.Fatalf("unexpected head after SetHead: have %d want 16", head) + } + + // Every block above the new head must be gone: both the rewound canonical + // segment (17..32) and the dangling leftover data (33..64). + for _, block := range blocks[16:] { + if chain.HasBlock(block.Hash(), block.NumberU64()) { + t.Errorf("block #%d still present after SetHead, want wiped", block.NumberU64()) + } + } +} + +// TestSetHeadCleansDanglingBlocksAcrossGap verifies that SetHead wipes orphaned +// block data above the head even when it is NOT contiguous with the head, i.e. +// there is a gap between the head and the leftover data. This mirrors the real +// world failure where a fork/rollback leaves a disconnected canonical segment +// far above the head, causing the downloader to "jump" onto it. +func TestSetHeadCleansDanglingBlocksAcrossGap(t *testing.T) { + var ( + engine = ethash.NewFaker() + genesis = &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + db = rawdb.NewMemoryDatabase() + ) + chain, err := NewBlockChain(db, &CacheConfig{TrieDirtyDisabled: true}, genesis, engine, vm.Config{}) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 64, nil) + + // Import blocks 1..32 as the canonical chain; head is now at 32. + if _, err := chain.InsertChain(blocks[:32]); err != nil { + t.Fatalf("failed to insert canonical chain: %v", err) + } + + // Write only blocks 40..64 as leftover data, leaving a GAP at heights + // 33..39. This mimics an aborted rollback that deleted a contiguous chunk + // just above the head but left a disconnected orphaned segment higher up. + for _, block := range blocks[39:] { + rawdb.WriteBlock(db, block) + } + for _, block := range blocks[39:] { + if !chain.HasBlock(block.Hash(), block.NumberU64()) { + t.Fatalf("setup failed: dangling block #%d not present", block.NumberU64()) + } + } + + // Rewind the chain to block 16. + if err := chain.SetHead(16); err != nil { + t.Fatalf("failed to set head: %v", err) + } + if head := chain.CurrentBlock().Number.Uint64(); head != 16 { + t.Fatalf("unexpected head after SetHead: have %d want 16", head) + } + + // The disconnected orphaned segment 40..64 (across the 33..39 gap) must be + // wiped, not just the contiguous range immediately above the head. + for _, block := range blocks[39:] { + if chain.HasBlock(block.Hash(), block.NumberU64()) { + t.Errorf("dangling block #%d across gap still present after SetHead, want wiped", block.NumberU64()) + } + } +} + +// TestSetHeadCleansSideForkHashes verifies that SetHead removes all hashes at a +// rewound height, including side-fork/orphan hashes that share the same block +// number as canonical blocks. +func TestSetHeadCleansSideForkHashes(t *testing.T) { + var ( + engine = ethash.NewFaker() + genesis = &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + db = rawdb.NewMemoryDatabase() + ) + chain, err := NewBlockChain(db, &CacheConfig{TrieDirtyDisabled: true}, genesis, engine, vm.Config{}) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 64, nil) + + // Import a canonical chain to height 32. + if _, err := chain.InsertChain(blocks[:32]); err != nil { + t.Fatalf("failed to insert canonical chain: %v", err) + } + + // Build an alternate fork from canonical block #16, yielding fork blocks + // #17..#20 with hashes different from canonical blocks at those heights. + forkBlocks, _ := GenerateChain(genesis.Config, blocks[15], engine, db, 4, func(i int, gen *BlockGen) { + gen.SetExtra([]byte(fmt.Sprintf("side-fork-%d", i))) + }) + for _, block := range forkBlocks { + rawdb.WriteBlock(db, block) + } + + // At fork height #17 we should now have at least two hashes: canonical + fork. + if hashes := rawdb.ReadAllHashes(db, forkBlocks[0].NumberU64()); len(hashes) < 2 { + t.Fatalf("setup failed: expected >=2 hashes at height %d, have %d", forkBlocks[0].NumberU64(), len(hashes)) + } + + // Rewind to block 16; both canonical (17..32) and side-fork (17..20) data + // must be wiped. + if err := chain.SetHead(16); err != nil { + t.Fatalf("failed to set head: %v", err) + } + if head := chain.CurrentBlock().Number.Uint64(); head != 16 { + t.Fatalf("unexpected head after SetHead: have %d want 16", head) + } + + for _, block := range blocks[16:32] { + if chain.HasBlock(block.Hash(), block.NumberU64()) { + t.Errorf("canonical block #%d still present after SetHead, want wiped", block.NumberU64()) + } + } + for _, block := range forkBlocks { + if chain.HasBlock(block.Hash(), block.NumberU64()) { + t.Errorf("side-fork block #%d still present after SetHead, want wiped", block.NumberU64()) + } + } +} + +// TestSetHeadCleansRawdbMultiHashesAtSameHeight verifies that SetHead removes +// all header-hash entries at rewound heights from rawdb, even if multiple +// orphan blocks are present at the same block number. +func TestSetHeadCleansRawdbMultiHashesAtSameHeight(t *testing.T) { + var ( + engine = ethash.NewFaker() + genesis = &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + db = rawdb.NewMemoryDatabase() + ) + chain, err := NewBlockChain(db, &CacheConfig{TrieDirtyDisabled: true}, genesis, engine, vm.Config{}) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 64, nil) + if _, err := chain.InsertChain(blocks[:32]); err != nil { + t.Fatalf("failed to insert canonical chain: %v", err) + } + + // Write two extra orphan blocks at height 17 directly into rawdb. + orphanA, _ := GenerateChain(genesis.Config, blocks[15], engine, db, 1, func(_ int, gen *BlockGen) { + gen.SetExtra([]byte("orphan-a")) + }) + orphanB, _ := GenerateChain(genesis.Config, blocks[15], engine, db, 1, func(_ int, gen *BlockGen) { + gen.SetExtra([]byte("orphan-b")) + }) + rawdb.WriteBlock(db, orphanA[0]) + rawdb.WriteBlock(db, orphanB[0]) + + if hashes := rawdb.ReadAllHashes(db, 17); len(hashes) < 3 { + t.Fatalf("setup failed: expected >=3 hashes at height 17, have %d", len(hashes)) + } + + if err := chain.SetHead(16); err != nil { + t.Fatalf("failed to set head: %v", err) + } + + if hashes := rawdb.ReadAllHashes(db, 17); len(hashes) != 0 { + t.Fatalf("height 17 still has %d hashes after SetHead, want 0", len(hashes)) + } + if chain.HasBlock(orphanA[0].Hash(), orphanA[0].NumberU64()) { + t.Fatalf("orphan-a block still present after SetHead") + } + if chain.HasBlock(orphanB[0].Hash(), orphanB[0].NumberU64()) { + t.Fatalf("orphan-b block still present after SetHead") + } +} + +// TestSetHeadCleansOnlyBadBlocksAboveHead verifies that SetHead only removes +// bad block records for blocks strictly above the target height, preserving +// debugging data for blocks at or below the head. +func TestSetHeadCleansOnlyBadBlocksAboveHead(t *testing.T) { + var ( + engine = ethash.NewFaker() + genesis = &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + db = rawdb.NewMemoryDatabase() + ) + chain, err := NewBlockChain(db, &CacheConfig{TrieDirtyDisabled: true}, genesis, engine, vm.Config{}) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + // Build a chain 0..20 + blocks, _ := GenerateChain(genesis.Config, chain.Genesis(), engine, db, 20, nil) + if _, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + // Create some bad block records + badBlockLow := blocks[5] // at height 6, below head + badBlockHigh := blocks[15] // at height 16, above rewind target + + rawdb.WriteBadBlock(db, badBlockLow) + rawdb.WriteBadBlock(db, badBlockHigh) + + // Verify both bad blocks are recorded + allBadBefore := rawdb.ReadAllBadBlocks(db) + if len(allBadBefore) != 2 { + t.Fatalf("expected 2 bad blocks before SetHead, have %d", len(allBadBefore)) + } + + // Rewind to height 10 (below the high bad block) + if err := chain.SetHead(10); err != nil { + t.Fatalf("failed to set head: %v", err) + } + + // Verify high bad block is cleaned (number 16 > 10) + allBadAfter := rawdb.ReadAllBadBlocks(db) + if len(allBadAfter) != 1 { + t.Fatalf("expected 1 bad block after SetHead(10), have %d", len(allBadAfter)) + } + + // Verify the remaining bad block is the low one (number 6 <= 10) + remaining := allBadAfter[0] + if remaining.NumberU64() != 6 { + t.Fatalf("remaining bad block has wrong number: got %d, want 6", remaining.NumberU64()) + } + if remaining.Hash() != badBlockLow.Hash() { + t.Fatalf("remaining bad block has wrong hash: got %x, want %x", remaining.Hash(), badBlockLow.Hash()) + } +} diff --git a/core/headerchain.go b/core/headerchain.go index f69f25a88a43..2fa0a4d635d4 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -453,7 +453,7 @@ func (hc *HeaderChain) SetHead(head uint64, updateFn UpdateHeadBlocksCallback, d batch = hc.chainDb.NewBatch() ) for hdr := hc.CurrentHeader(); hdr != nil && hdr.Number.Uint64() > head; hdr = hc.CurrentHeader() { - hash, num := hdr.Hash(), hdr.Number.Uint64() + num := hdr.Number.Uint64() // Rewind block chain to new head. parent := hc.GetHeader(hdr.ParentHash, num-1) @@ -481,19 +481,58 @@ func (hc *HeaderChain) SetHead(head uint64, updateFn UpdateHeadBlocksCallback, d hc.currentHeaderHash = parentHash headHeaderGauge.Update(parent.Number.Int64()) - // Remove the relative data from the database. - if delFn != nil { - delFn(batch, hash, num) + // Remove all block data at this height (canonical block plus any side + // forks) so nothing dangling is left behind on the rewound segment. + hashes := rawdb.ReadAllHashes(hc.chainDb, num) + if len(hashes) == 0 { + // As a fallback, use the rewound header hash directly. + hashes = append(hashes, hdr.Hash()) + } + for _, hash := range hashes { + if delFn != nil { + delFn(batch, hash, num) + } + rawdb.DeleteHeader(batch, hash, num) + rawdb.DeleteTd(batch, hash, num) } - // Rewind header chain to new head. - rawdb.DeleteHeader(batch, hash, num) - rawdb.DeleteTd(batch, hash, num) rawdb.DeleteCanonicalHash(batch, num) + + // Flush the batch once it grows past the ideal size to bound memory + // during very large rewinds (e.g. rolling back tens of thousands of + // heights). Note: upstream geth's setHead keeps a single batch for the + // whole rewind segment; this periodic flush is an intentional local + // enhancement. It is safe because the head marker is already lowered to + // `parent` before any data at `num` is deleted, so head never points + // above still-present data, and the post-loop flush commits the rest. + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + log.Crit("Failed to rewind block", "error", err) + } + batch.Reset() + } } - // Flush all accumulated deletions. + // Flush the rewind deletions before scanning for dangling data. Otherwise + // the dangling sweep would still observe the just-rewound segment + // (head+1..old head) that is only marked for deletion in the batch but not + // yet committed, causing duplicate deletes and redundant delFn calls (and + // extra ancient-truncation work) over the same heights during large rewinds. if err := batch.Write(); err != nil { log.Crit("Failed to rewind block", "error", err) } + // Wipe any dangling/orphaned data left ABOVE the new head. An aborted sync + // can leave non-contiguous side-fork blocks (with their canonical markers + // and, in archive mode, their state) at heights well above the head. If + // left in place, the downloader's ancestor search can latch onto them via + // HasBlock and skip re-syncing the intermediate range, jumping straight to + // a stale/bad block. The sweep iterates the header keyspace directly so gaps + // between orphaned segments are handled correctly, streaming deletions into + // batches that are flushed at ethdb.IdealBatchSize to bound memory. + if err := rawdb.DeleteDanglingHashes(hc.chainDb, head, delFn); err != nil { + log.Crit("Failed to rewind block", "error", err) + } + // Clean up bad block records for blocks above the new head. Bad blocks at or + // below the new head are kept for debugging and operator review. + rawdb.DeleteBadBlocksAbove(hc.chainDb, head) // Clear out any stale content from the caches hc.headerCache.Purge() hc.tdCache.Purge() diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index ff783c6c05f5..27c6788a1efa 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -65,6 +65,83 @@ func DeleteCanonicalHash(db ethdb.KeyValueWriter, number uint64) { } } +// ReadAllHashes retrieves all the hashes assigned to blocks at a certain height, +// both canonical and reorged forks included. +func ReadAllHashes(db ethdb.Iteratee, number uint64) []common.Hash { + prefix := headerKeyPrefix(number) + + hashes := make([]common.Hash, 0, 1) + it := db.NewIterator(prefix, nil) + defer it.Release() + + for it.Next() { + if key := it.Key(); len(key) == len(prefix)+32 { + hashes = append(hashes, common.BytesToHash(key[len(key)-32:])) + } + } + return hashes +} + +// DeleteDanglingHashes removes every header, total-difficulty and canonical-hash +// entry whose block number is strictly greater than head. It walks the header +// keyspace directly (rather than scanning contiguous heights), so orphaned +// side-fork segments left above the chain head by an aborted sync are wiped even +// when their heights are not contiguous (i.e. separated by gaps). For each +// removed hash it invokes contentFn (if non-nil) so the caller can drop any +// associated block content (bodies, receipts, ...). Deletions are streamed into +// a batch that is flushed at ethdb.IdealBatchSize, bounding memory usage during +// large sweeps. +func DeleteDanglingHashes(db ethdb.KeyValueStore, head uint64, contentFn func(ethdb.KeyValueWriter, common.Hash, uint64)) error { + // header key = headerPrefix + num (8 bytes) + hash (32 bytes) + headerKeyLen := len(headerPrefix) + 8 + common.HashLength + + // Guard against uint64 overflow when head == max. + start := head + 1 + if start == 0 { + return nil + } + + batch := db.NewBatch() + it := db.NewIterator(headerPrefix, encodeBlockNumber(start)) + defer it.Release() + + var ( + lastNum uint64 + haveNum bool + ) + for it.Next() { + key := it.Key() + // Skip canonical-hash ("n") and total-difficulty ("t") keys, which have + // different lengths than a plain header key. + if len(key) != headerKeyLen { + continue + } + number := binary.BigEndian.Uint64(key[len(headerPrefix) : len(headerPrefix)+8]) + if number <= head { + continue + } + hash := common.BytesToHash(key[len(headerPrefix)+8:]) + if contentFn != nil { + contentFn(batch, hash, number) + } + DeleteHeader(batch, hash, number) + DeleteTd(batch, hash, number) + // The iterator yields keys in ascending order, so all hashes at a given + // height are contiguous. Delete the canonical marker once per height. + if !haveNum || number != lastNum { + DeleteCanonicalHash(batch, number) + lastNum, haveNum = number, true + } + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + return err + } + batch.Reset() + } + } + return batch.Write() +} + // ReadHeaderNumber returns the header number assigned to a hash. func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) *uint64 { data, _ := db.Get(headerNumberKey(hash)) @@ -698,6 +775,50 @@ func DeleteBadBlocks(db ethdb.KeyValueWriter) { } } +// DeleteBadBlocksAbove deletes only the bad block records for blocks strictly +// above the given height, preserving records below it for debugging purposes. +func DeleteBadBlocksAbove(db ethdb.KeyValueStore, head uint64) { + blob, err := db.Get(badBlockKey) + if err != nil { + // No bad blocks to clean + return + } + var badBlocks []*badBlock + if err := rlp.DecodeBytes(blob, &badBlocks); err != nil { + log.Warn("Failed to decode bad blocks for cleanup", "error", err) + return + } + + // Filter: keep only bad blocks at or below head + filtered := make([]*badBlock, 0, len(badBlocks)) + for _, b := range badBlocks { + if b.Header.Number.Uint64() <= head { + filtered = append(filtered, b) + } + } + + // If nothing left, delete the key entirely + if len(filtered) == 0 { + if err := db.Delete(badBlockKey); err != nil { + log.Crit("Failed to delete bad blocks key", "err", err) + } + return + } + + // If some remain, re-encode and write them back + if len(filtered) != len(badBlocks) { + // Only log if we actually deleted something + log.Info("Cleaned up bad blocks above head", "head", head, "removed", len(badBlocks)-len(filtered), "remaining", len(filtered)) + data, err := rlp.EncodeToBytes(filtered) + if err != nil { + log.Crit("Failed to encode remaining bad blocks", "err", err) + } + if err := db.Put(badBlockKey, data); err != nil { + log.Crit("Failed to write cleaned bad blocks", "err", err) + } + } +} + // ReadHeadHeader returns the current canonical head header. func ReadHeadHeader(db ethdb.Reader) *types.Header { headHeaderHash := ReadHeadHeaderHash(db)