core: clean txlookup entries on chain rewind(#33744)

This commit is contained in:
Jordan 2026-04-14 15:19:41 +09:00
parent 01e33d14be
commit f21fd30e7e
3 changed files with 208 additions and 15 deletions

View file

@ -1068,8 +1068,19 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
}
return headHeader, wipe // Only force wipe if full synced
}
deleteTxLookupEntries := func(db ethdb.KeyValueWriter, hash common.Hash, num uint64) {
body := rawdb.ReadBody(bc.db, hash, num)
if body == nil {
return
}
for _, tx := range body.Transactions {
rawdb.DeleteTxLookupEntry(db, tx.Hash())
}
}
// Rewind the header chain, deleting all block bodies until then
delFn := func(db ethdb.KeyValueWriter, hash common.Hash, num uint64) {
deleteTxLookupEntries(db, hash, num)
// Ignore the error here since light client won't hit this path
frozen, _ := bc.db.Ancients()
if num+1 <= frozen {
@ -1086,31 +1097,36 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
rawdb.DeleteBody(db, hash, num)
rawdb.DeleteReceipts(db, hash, num)
}
// Todo(rjl493456442) txlookup, log index, etc
// Todo(rjl493456442) log index, etc
}
// If SetHead was only called as a chain reparation method, try to skip
// touching the header chain altogether, unless the freezer is broken
if repair {
if target, force := updateFn(bc.db, bc.CurrentBlock()); force {
bc.hc.SetHead(target.Number.Uint64(), nil, delFn)
}
} else {
// Rewind the chain to the requested head and keep going backwards until a
// block with a state is found or snap sync pivot is passed
if time > 0 {
log.Warn("Rewinding blockchain to timestamp", "target", time)
bc.hc.SetHeadWithTimestamp(time, updateFn, delFn)
bc.txLookupLock.Lock()
func() {
defer bc.txLookupLock.Unlock()
if repair {
if target, force := updateFn(bc.db, bc.CurrentBlock()); force {
bc.hc.SetHead(target.Number.Uint64(), nil, delFn)
}
} else {
log.Warn("Rewinding blockchain to block", "target", head)
bc.hc.SetHead(head, updateFn, delFn)
// Rewind the chain to the requested head and keep going backwards until a
// block with a state is found or snap sync pivot is passed
if time > 0 {
log.Warn("Rewinding blockchain to timestamp", "target", time)
bc.hc.SetHeadWithTimestamp(time, updateFn, delFn)
} else {
log.Warn("Rewinding blockchain to block", "target", head)
bc.hc.SetHead(head, updateFn, delFn)
}
}
}
bc.txLookupCache.Purge()
}()
// Clear out any stale content from the caches
bc.bodyCache.Purge()
bc.bodyRLPCache.Purge()
bc.receiptsCache.Purge()
bc.blockCache.Purge()
bc.txLookupCache.Purge()
// Clear safe block, finalized block if needed
headBlock := bc.CurrentBlock()

View file

@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb/pebble"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/triedb"
@ -2081,6 +2082,121 @@ func testSetHeadWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme
}
}
func TestSetHeadTxLookupCleanupWithAncients(t *testing.T) {
type freezer interface {
Freeze() error
Ancients() (uint64, error)
}
datadir := t.TempDir()
ancient := filepath.Join(datadir, "ancient")
pdb, err := pebble.New(datadir, 0, 0, "", false)
if err != nil {
t.Fatalf("failed to create persistent key-value database: %v", err)
}
db, err := rawdb.Open(pdb, rawdb.OpenOptions{Ancient: ancient})
if err != nil {
t.Fatalf("failed to create persistent freezer database: %v", err)
}
defer db.Close()
var (
testBankKey, _ = crypto.GenerateKey()
testBankAddress = crypto.PubkeyToAddress(testBankKey.PublicKey)
gspec = &Genesis{
BaseFee: big.NewInt(params.InitialBaseFee),
Config: params.AllEthashProtocolChanges,
Alloc: types.GenesisAlloc{testBankAddress: {Balance: big.NewInt(1000000000000000000)}},
}
engine = ethash.NewFullFaker()
options = &BlockChainConfig{
TrieCleanLimit: 256,
TrieDirtyLimit: 256,
TrieTimeLimit: 5 * time.Minute,
SnapshotLimit: 0,
TxLookupLimit: -1,
StateScheme: rawdb.HashScheme,
}
signer = types.HomesteadSigner{}
nonce = uint64(0)
retainedTx *types.Transaction
removedTx *types.Transaction
)
chain, err := NewBlockChain(db, gspec, engine, options)
if err != nil {
t.Fatalf("failed to create chain: %v", err)
}
defer chain.Stop()
_, blocks, _ := GenerateChainWithGenesis(gspec, engine, 24, func(i int, b *BlockGen) {
tx, err := types.SignTx(types.NewTransaction(nonce, common.HexToAddress("0xdeadbeef"), big.NewInt(1), params.TxGas, b.header.BaseFee, nil), signer, testBankKey)
if err != nil {
t.Fatalf("failed to sign transaction: %v", err)
}
b.AddTx(tx)
switch i {
case 7:
retainedTx = tx
case 15:
removedTx = tx
}
nonce++
})
if _, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("failed to insert canonical chain: %v", err)
}
if retainedTx == nil || removedTx == nil {
t.Fatal("failed to capture test transactions")
}
chain.triedb.Commit(blocks[7].Root(), false)
chain.triedb.Close()
chain.triedb = triedb.NewDatabase(chain.db, &triedb.Config{HashDB: hashdb.Defaults})
chain.SetFinalized(blocks[15].Header())
if err := db.(freezer).Freeze(); err != nil {
t.Fatalf("failed to freeze chain data: %v", err)
}
if entry := rawdb.ReadTxLookupEntry(chain.db, retainedTx.Hash()); entry == nil || *entry != 8 {
t.Fatalf("retained txlookup setup failed: have %v, want 8", entry)
}
if entry := rawdb.ReadTxLookupEntry(chain.db, removedTx.Hash()); entry == nil || *entry != 16 {
t.Fatalf("removed txlookup setup failed: have %v, want 16", entry)
}
if err := chain.SetHead(8); err != nil {
t.Fatalf("failed to rewind chain: %v", err)
}
if head := chain.CurrentBlock(); head.Number.Uint64() != 8 {
t.Fatalf("head block mismatch after rewind: have %d, want 8", head.Number.Uint64())
}
if frozen, err := db.(freezer).Ancients(); err != nil {
t.Fatalf("failed to retrieve ancient count: %v", err)
} else if frozen != 9 {
t.Fatalf("ancient count mismatch after rewind: have %d, want 9", frozen)
}
if entry := rawdb.ReadTxLookupEntry(chain.db, removedTx.Hash()); entry != nil {
t.Fatalf("removed txlookup still exists after rewind: %d", *entry)
}
if tx, _, _, _ := rawdb.ReadCanonicalTransaction(chain.db, removedTx.Hash()); tx != nil {
t.Fatalf("removed canonical transaction still exists after rewind: %s", removedTx.Hash())
}
if entry := rawdb.ReadTxLookupEntry(chain.db, retainedTx.Hash()); entry == nil || *entry != 8 {
t.Fatalf("retained txlookup mismatch after rewind: have %v, want 8", entry)
}
if tx, _, blockNumber, _ := rawdb.ReadCanonicalTransaction(chain.db, retainedTx.Hash()); tx == nil {
t.Fatal("retained canonical transaction missing after rewind")
} else if blockNumber != 8 {
t.Fatalf("retained canonical transaction number mismatch: have %d, want 8", blockNumber)
}
}
// verifyNoGaps checks that there are no gaps after the initial set of blocks in
// the database and errors if found.
func verifyNoGaps(t *testing.T, chain *BlockChain, canonical bool, inserted types.Blocks) {

View file

@ -4544,3 +4544,64 @@ func TestSetHeadBeyondRootFinalizedBug(t *testing.T) {
currentFinal.Number.Uint64())
}
}
func TestSetHeadTxLookupCleanup(t *testing.T) {
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
t.Run(scheme, func(t *testing.T) {
_, _, blockchain, err := newCanonical(ethash.NewFaker(), 100, true, scheme)
if err != nil {
t.Fatalf("failed to create pristine chain: %v", err)
}
defer blockchain.Stop()
retainedBlock := blockchain.GetBlockByNumber(50)
if retainedBlock == nil {
t.Fatal("retained block not found")
}
removedBlock := blockchain.CurrentBlock()
if removedBlock.Number.Uint64() != 100 {
t.Fatalf("setup failed: expected head 100, got %d", removedBlock.Number.Uint64())
}
retainedTx := types.NewTransaction(0, common.Address{0x01}, big.NewInt(0), params.TxGas, big.NewInt(1), nil)
removedTx := types.NewTransaction(1, common.Address{0x02}, big.NewInt(0), params.TxGas, big.NewInt(1), nil)
rawdb.WriteBody(blockchain.db, retainedBlock.Hash(), retainedBlock.NumberU64(), &types.Body{Transactions: []*types.Transaction{retainedTx}})
rawdb.WriteBody(blockchain.db, removedBlock.Hash(), removedBlock.Number.Uint64(), &types.Body{Transactions: []*types.Transaction{removedTx}})
rawdb.WriteTxLookupEntries(blockchain.db, retainedBlock.NumberU64(), []common.Hash{retainedTx.Hash()})
rawdb.WriteTxLookupEntries(blockchain.db, removedBlock.Number.Uint64(), []common.Hash{removedTx.Hash()})
if entry := rawdb.ReadTxLookupEntry(blockchain.db, retainedTx.Hash()); entry == nil || *entry != retainedBlock.NumberU64() {
t.Fatalf("retained txlookup setup failed: have %v, want %d", entry, retainedBlock.NumberU64())
}
if entry := rawdb.ReadTxLookupEntry(blockchain.db, removedTx.Hash()); entry == nil || *entry != removedBlock.Number.Uint64() {
t.Fatalf("removed txlookup setup failed: have %v, want %d", entry, removedBlock.Number.Uint64())
}
if err := blockchain.SetHead(retainedBlock.NumberU64()); err != nil {
t.Fatalf("failed to rewind chain: %v", err)
}
if entry := rawdb.ReadTxLookupEntry(blockchain.db, removedTx.Hash()); entry != nil {
t.Fatalf("removed txlookup still exists after rewind: %d", *entry)
}
if tx, _, _, _ := rawdb.ReadCanonicalTransaction(blockchain.db, removedTx.Hash()); tx != nil {
t.Fatalf("removed canonical transaction still exists after rewind: %s", removedTx.Hash())
}
if entry := rawdb.ReadTxLookupEntry(blockchain.db, retainedTx.Hash()); entry == nil || *entry != retainedBlock.NumberU64() {
t.Fatalf("retained txlookup mismatch after rewind: have %v, want %d", entry, retainedBlock.NumberU64())
}
if tx, blockHash, blockNumber, _ := rawdb.ReadCanonicalTransaction(blockchain.db, retainedTx.Hash()); tx == nil {
t.Fatal("retained canonical transaction missing after rewind")
} else {
if blockNumber != retainedBlock.NumberU64() {
t.Fatalf("retained canonical transaction number mismatch: have %d, want %d", blockNumber, retainedBlock.NumberU64())
}
if blockHash != retainedBlock.Hash() {
t.Fatalf("retained canonical transaction hash mismatch: have %s, want %s", blockHash, retainedBlock.Hash())
}
}
})
}
}