mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-12 09:51:36 +00:00
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:
parent
01e33d14be
commit
d633e1489b
3 changed files with 208 additions and 15 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue