From f21fd30e7ef18ed9867f34eb26b36618acaa3250 Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 14 Apr 2026 15:19:41 +0900 Subject: [PATCH] core: clean txlookup entries on chain rewind(#33744) --- core/blockchain.go | 46 ++++++++----- core/blockchain_sethead_test.go | 116 ++++++++++++++++++++++++++++++++ core/blockchain_test.go | 61 +++++++++++++++++ 3 files changed, 208 insertions(+), 15 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 35b2d35dc7..b23312f064 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -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() diff --git a/core/blockchain_sethead_test.go b/core/blockchain_sethead_test.go index f2fbc003f1..36cc29590e 100644 --- a/core/blockchain_sethead_test.go +++ b/core/blockchain_sethead_test.go @@ -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) { diff --git a/core/blockchain_test.go b/core/blockchain_test.go index d3ca21b2b3..7a0158f3ec 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -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()) + } + } + }) + } +}