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
This commit is contained in:
Jordan 2026-04-14 15:19:41 +09:00
parent 01e33d14be
commit d633e1489b
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 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 // Rewind the header chain, deleting all block bodies until then
delFn := func(db ethdb.KeyValueWriter, hash common.Hash, num uint64) { 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 // Ignore the error here since light client won't hit this path
frozen, _ := bc.db.Ancients() frozen, _ := bc.db.Ancients()
if num+1 <= frozen { 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.DeleteBody(db, hash, num)
rawdb.DeleteReceipts(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 // If SetHead was only called as a chain reparation method, try to skip
// touching the header chain altogether, unless the freezer is broken // touching the header chain altogether, unless the freezer is broken
if repair { bc.txLookupLock.Lock()
if target, force := updateFn(bc.db, bc.CurrentBlock()); force { func() {
bc.hc.SetHead(target.Number.Uint64(), nil, delFn) defer bc.txLookupLock.Unlock()
}
} else { if repair {
// Rewind the chain to the requested head and keep going backwards until a if target, force := updateFn(bc.db, bc.CurrentBlock()); force {
// block with a state is found or snap sync pivot is passed bc.hc.SetHead(target.Number.Uint64(), nil, delFn)
if time > 0 { }
log.Warn("Rewinding blockchain to timestamp", "target", time)
bc.hc.SetHeadWithTimestamp(time, updateFn, delFn)
} else { } else {
log.Warn("Rewinding blockchain to block", "target", head) // Rewind the chain to the requested head and keep going backwards until a
bc.hc.SetHead(head, updateFn, delFn) // 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 // Clear out any stale content from the caches
bc.bodyCache.Purge() bc.bodyCache.Purge()
bc.bodyRLPCache.Purge() bc.bodyRLPCache.Purge()
bc.receiptsCache.Purge() bc.receiptsCache.Purge()
bc.blockCache.Purge() bc.blockCache.Purge()
bc.txLookupCache.Purge()
// Clear safe block, finalized block if needed // Clear safe block, finalized block if needed
headBlock := bc.CurrentBlock() headBlock := bc.CurrentBlock()

View file

@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "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/ethdb/pebble"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/triedb" "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 // verifyNoGaps checks that there are no gaps after the initial set of blocks in
// the database and errors if found. // the database and errors if found.
func verifyNoGaps(t *testing.T, chain *BlockChain, canonical bool, inserted types.Blocks) { 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()) 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())
}
}
})
}
}