From d633e1489b35bcda045df27cb5e538161bb490a5 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 Delete txlookup entries during SetHead rewinds so transaction hash lookups do not survive after their blocks are removed. Add regression tests for both the active-store rewind path and rewinds that cross the ancient boundary. Refs: #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()) + } + } + }) + } +}