From 4a5753e63566425b232379e4bc15735a62b25f05 Mon Sep 17 00:00:00 2001
From: Harry Ngo <17699212+huyngopt1994@users.noreply.github.com>
Date: Tue, 8 Oct 2024 11:45:17 +0700
Subject: [PATCH] rawdb: implement freezer resettable & state freezer (#596)
* rawdb: implement freezer resettable
* rawdb: implement state freezer
* rawdb: update description
---
core/blockchain.go | 6 +-
core/rawdb/accessors_chain_test.go | 4 +-
core/rawdb/accessors_indexes_test.go | 4 +-
core/rawdb/accessors_state.go | 179 +++++++++++++++++++
core/rawdb/accessors_trie.go | 45 ++---
core/rawdb/ancient_scheme.go | 34 ++++
core/rawdb/chain_iterator_test.go | 8 +-
core/rawdb/database.go | 8 +-
core/rawdb/freezer.go | 32 ++--
core/rawdb/freezer_resettable.go | 240 ++++++++++++++++++++++++++
core/rawdb/freezer_resettable_test.go | 120 +++++++++++++
core/rawdb/freezer_table.go | 1 +
core/rawdb/freezer_test.go | 4 +-
core/rawdb/schema.go | 12 ++
core/rawdb/table.go | 4 +-
ethdb/database.go | 4 +-
16 files changed, 648 insertions(+), 57 deletions(-)
create mode 100644 core/rawdb/freezer_resettable.go
create mode 100644 core/rawdb/freezer_resettable_test.go
diff --git a/core/blockchain.go b/core/blockchain.go
index a41f8f4996..2bb494bc63 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -748,7 +748,7 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, root common.Hash, repair bo
if num+1 <= frozen {
// Truncate all relative data(header, total difficulty, body, receipt
// and canonical hash) from ancient store.
- if err := bc.db.TruncateHead(num); err != nil {
+ if _, err := bc.db.TruncateHead(num); err != nil {
log.Crit("Failed to truncate ancient data", "number", num, "err", err)
}
// Remove the hash <-> number mapping from the active store.
@@ -1179,7 +1179,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
// The tx index data could not be written.
// Roll back the ancient store update.
fastBlock := bc.CurrentFastBlock().NumberU64()
- if err := bc.db.TruncateHead(fastBlock + 1); err != nil {
+ if _, err := bc.db.TruncateHead(fastBlock + 1); err != nil {
log.Error("Can't truncate ancient store after failed insert", "err", err)
}
return 0, err
@@ -1195,7 +1195,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
if !updateHead(blockChain[len(blockChain)-1]) {
// We end up here if the header chain has reorg'ed, and the blocks/receipts
// don't match the canonical chain.
- if err := bc.db.TruncateHead(previousFastBlock + 1); err != nil {
+ if _, err := bc.db.TruncateHead(previousFastBlock + 1); err != nil {
log.Error("Can't truncate ancient store after failed insert", "err", err)
}
return 0, errSideChainReceipts
diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go
index 7168fa6969..fa0a0b03b0 100644
--- a/core/rawdb/accessors_chain_test.go
+++ b/core/rawdb/accessors_chain_test.go
@@ -88,7 +88,7 @@ func TestBodyStorage(t *testing.T) {
WriteBody(db, hash, 0, body)
if entry := ReadBody(db, hash, 0); entry == nil {
t.Fatalf("Stored body not found")
- } else if types.DeriveSha(types.Transactions(entry.Transactions), newHasher()) != types.DeriveSha(types.Transactions(body.Transactions), newHasher()) || types.CalcUncleHash(entry.Uncles) != types.CalcUncleHash(body.Uncles) {
+ } else if types.DeriveSha(types.Transactions(entry.Transactions), newTestHasher()) != types.DeriveSha(types.Transactions(body.Transactions), newTestHasher()) || types.CalcUncleHash(entry.Uncles) != types.CalcUncleHash(body.Uncles) {
t.Fatalf("Retrieved body mismatch: have %v, want %v", entry, body)
}
if entry := ReadBodyRLP(db, hash, 0); entry == nil {
@@ -142,7 +142,7 @@ func TestBlockStorage(t *testing.T) {
}
if entry := ReadBody(db, block.Hash(), block.NumberU64()); entry == nil {
t.Fatalf("Stored body not found")
- } else if types.DeriveSha(types.Transactions(entry.Transactions), newHasher()) != types.DeriveSha(block.Transactions(), newHasher()) || types.CalcUncleHash(entry.Uncles) != types.CalcUncleHash(block.Uncles()) {
+ } else if types.DeriveSha(types.Transactions(entry.Transactions), newTestHasher()) != types.DeriveSha(block.Transactions(), newTestHasher()) || types.CalcUncleHash(entry.Uncles) != types.CalcUncleHash(block.Uncles()) {
t.Fatalf("Retrieved body mismatch: have %v, want %v", entry, block.Body())
}
// Delete the block and verify the execution
diff --git a/core/rawdb/accessors_indexes_test.go b/core/rawdb/accessors_indexes_test.go
index 4734e986e2..dcb474c180 100644
--- a/core/rawdb/accessors_indexes_test.go
+++ b/core/rawdb/accessors_indexes_test.go
@@ -37,7 +37,7 @@ type testHasher struct {
hasher hash.Hash
}
-func newHasher() *testHasher {
+func newTestHasher() *testHasher {
return &testHasher{hasher: sha3.NewLegacyKeccak256()}
}
@@ -99,7 +99,7 @@ func TestLookupStorage(t *testing.T) {
tx3 := types.NewTransaction(3, common.BytesToAddress([]byte{0x33}), big.NewInt(333), 3333, big.NewInt(33333), []byte{0x33, 0x33, 0x33})
txs := []*types.Transaction{tx1, tx2, tx3}
- block := types.NewBlock(&types.Header{Number: big.NewInt(314)}, txs, nil, nil, newHasher())
+ block := types.NewBlock(&types.Header{Number: big.NewInt(314)}, txs, nil, nil, newTestHasher())
// Check that no transactions entries are in a pristine database
for i, tx := range txs {
diff --git a/core/rawdb/accessors_state.go b/core/rawdb/accessors_state.go
index 1438aad0ff..1990dcaf84 100644
--- a/core/rawdb/accessors_state.go
+++ b/core/rawdb/accessors_state.go
@@ -17,6 +17,8 @@
package rawdb
import (
+ "encoding/binary"
+
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
@@ -67,3 +69,180 @@ func DeleteCode(db ethdb.KeyValueWriter, hash common.Hash) {
log.Crit("Failed to delete contract code", "err", err)
}
}
+
+/* Function below support Path base state trie scheme */
+
+// ReadStateId retrieves the state id with the provided state root. (Return pointer can detect that Statid is valid or not, nil is invalid)
+func ReadStateId(db ethdb.KeyValueReader, root common.Hash) *uint64 {
+ data, err := db.Get(stateIDKey(root))
+ if err != nil || len(data) == 0 {
+ return nil
+ }
+ number := binary.BigEndian.Uint64(data)
+ return &number
+}
+
+// WriteStateID writes the provided state lookup to database.
+func WriteStateID(db ethdb.KeyValueWriter, root common.Hash, id uint64) {
+ var buff [8]byte
+ // Convert from uint64 to 8 bytes BigEndian id -> buff
+ binary.BigEndian.PutUint64(buff[:], id)
+ // Store the state id for root
+ if err := db.Put(stateIDKey(root), buff[:]); err != nil {
+ log.Crit("Failed to store state id", "err", err)
+ }
+}
+
+func DeleteStateID(db ethdb.KeyValueWriter, root common.Hash) {
+ if err := db.Delete(stateIDKey(root)); err != nil {
+ log.Crit("Failed to delete state id", "err", err)
+ }
+}
+
+// ReadPersistentStateID retrievies the id of persistent state from the database.
+func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 {
+ data, _ := db.Get(persistentStateIDKey)
+
+ if len(data) != 8 { // 8 bytes
+ return 0
+ }
+ return binary.BigEndian.Uint64(data)
+}
+
+// WritePersistentStateID writes the provided id of persistent state to the database.
+func WritePersistentStateID(db ethdb.KeyValueWriter, number uint64) {
+ if err := db.Put(persistentStateIDKey, encodeBlockNumber(number)); err != nil {
+ log.Crit("Failed to store persistent state id", "err", err)
+ }
+}
+
+// Read Trie Journal retrieves in-memory trie nodes of layers saved at
+// the last shutdown.
+func ReadTrieJournal(db ethdb.KeyValueReader) []byte {
+ data, _ := db.Get(trieJournalKey)
+ return data
+}
+
+// WriteTrieJournal stores the serialized in-memory trie nodes of layers to save at
+// shutdown.
+func WriteTrieJournal(db ethdb.KeyValueWriter, journal []byte) {
+ if err := db.Put(trieJournalKey, journal); err != nil {
+ log.Crit("Failed to store tries journal", "err", err)
+ }
+}
+
+// DeleteTrieJournal deletes the serialized in-memory trie nodes of layers saved at
+// the last shutdown.
+func DeleteTrieJournal(db ethdb.KeyValueWriter) {
+ if err := db.Delete(trieJournalKey); err != nil {
+ log.Crit("Failed to remove tries journal", "err", err)
+ }
+}
+
+/* Ancients */
+
+// ReadStateHistoryMeta retrieves the metadata corresponding to the specified
+// state history. Compute the position of state history in freezer by minus
+// one since the id of first state history starts from one(zero for initial
+// state).
+func ReadStateHistoryMeta(db ethdb.AncientReaderOp, id uint64) []byte {
+ blob, err := db.Ancient(stateHistoryMeta, id-1)
+ if err != nil {
+ return nil
+ }
+ return blob
+}
+
+// ReadStateHistoryMetaList retrieves a batch of meta objects with the specified
+// start position and count. Compute the position of state history in freezer by
+// minus one since the id of first state history starts from one(zero for initial
+// state).
+func ReadStateHistoryMetaList(db ethdb.AncientReaderOp, start uint64, count uint64) ([][]byte, error) {
+ return db.AncientRange(stateHistoryMeta, start-1, count, 0)
+}
+
+// ReadStateAccountIndex retrieves the state root corresponding to the specified
+// state history. Compute the position of state history in freezer by minus one
+// since the id of first state history starts from one(zero for initial state).
+func ReadStateAccountIndex(db ethdb.AncientReaderOp, id uint64) []byte {
+ blob, err := db.Ancient(stateHistoryAccountIndex, id-1)
+ if err != nil {
+ return nil
+ }
+ return blob
+}
+
+// ReadStateStorageIndex retrieves the state root corresponding to the specified
+// state history. Compute the position of state history in freezer by minus one
+// since the id of first state history starts from one(zero for initial state).
+func ReadStateStorageIndex(db ethdb.AncientReaderOp, id uint64) []byte {
+ blob, err := db.Ancient(stateHistoryStorageIndex, id-1)
+ if err != nil {
+ return nil
+ }
+ return blob
+}
+
+// ReadStateAccountHistory retrieves the state root corresponding to the specified
+// state history. Compute the position of state history in freezer by minus one
+// since the id of first state history starts from one(zero for initial state).
+func ReadStateAccountHistory(db ethdb.AncientReaderOp, id uint64) []byte {
+ blob, err := db.Ancient(stateHistoryAccountData, id-1)
+ if err != nil {
+ return nil
+ }
+ return blob
+}
+
+// ReadStateStorageHistory retrieves the state root corresponding to the specified
+// state history. Compute the position of state history in freezer by minus one
+// since the id of first state history starts from one(zero for initial state).
+func ReadStateStorageHistory(db ethdb.AncientReaderOp, id uint64) []byte {
+ blob, err := db.Ancient(stateHistoryStorageData, id-1)
+ if err != nil {
+ return nil
+ }
+ return blob
+}
+
+// ReadStateHistory retrieves the state history from database with provided id.
+// Compute the position of state history in freezer by minus one since the id
+// of first state history starts from one(zero for initial state).
+// Returns meta, account and storage (index, data).
+func ReadStateHistory(db ethdb.AncientReaderOp, id uint64) ([]byte, []byte, []byte, []byte, []byte, error) {
+ meta, err := db.Ancient(stateHistoryMeta, id-1)
+ if err != nil {
+ return nil, nil, nil, nil, nil, err
+ }
+ accountIndex, err := db.Ancient(stateHistoryAccountIndex, id-1)
+ if err != nil {
+ return nil, nil, nil, nil, nil, err
+ }
+ storageIndex, err := db.Ancient(stateHistoryStorageIndex, id-1)
+ if err != nil {
+ return nil, nil, nil, nil, nil, err
+ }
+ accountData, err := db.Ancient(stateHistoryAccountData, id-1)
+ if err != nil {
+ return nil, nil, nil, nil, nil, err
+ }
+ storageData, err := db.Ancient(stateHistoryStorageData, id-1)
+ if err != nil {
+ return nil, nil, nil, nil, nil, err
+ }
+ return meta, accountIndex, storageIndex, accountData, storageData, nil
+}
+
+// WriteStateHistory writes the provided state history to database. Compute the
+// position of state history in freezer by minus one since the id of first state
+// history starts from one(zero for initial state).
+func WriteStateHistory(db ethdb.AncientWriter, id uint64, meta []byte, accountIndex []byte, storageIndex []byte, accounts []byte, storages []byte) {
+ db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ op.AppendRaw(stateHistoryMeta, id-1, meta)
+ op.AppendRaw(stateHistoryAccountIndex, id-1, accountIndex)
+ op.AppendRaw(stateHistoryStorageIndex, id-1, storageIndex)
+ op.AppendRaw(stateHistoryAccountData, id-1, accounts)
+ op.AppendRaw(stateHistoryStorageData, id-1, storages)
+ return nil
+ })
+}
diff --git a/core/rawdb/accessors_trie.go b/core/rawdb/accessors_trie.go
index e240213025..611edcde5b 100644
--- a/core/rawdb/accessors_trie.go
+++ b/core/rawdb/accessors_trie.go
@@ -46,21 +46,22 @@ const HashScheme = "hashScheme"
// on extra state diffs to survive deep reorg.
const PathScheme = "pathScheme"
-// nodeHasher used to derive the hash of trie node.
-type nodeHasher struct{ sha crypto.KeccakState }
+// hasher is used to compute the sha256 hash of the provided data.
+type hasher struct{ sha crypto.KeccakState }
var hasherPool = sync.Pool{
- New: func() interface{} { return &nodeHasher{sha: sha3.NewLegacyKeccak256().(crypto.KeccakState)} },
+ New: func() interface{} { return &hasher{sha: sha3.NewLegacyKeccak256().(crypto.KeccakState)} },
}
-func newNodeHasher() *nodeHasher { return hasherPool.Get().(*nodeHasher) }
-func returnHasherToPool(h *nodeHasher) { hasherPool.Put(h) }
+func newHasher() *hasher {
+ return hasherPool.Get().(*hasher)
+}
-func (h *nodeHasher) hashData(data []byte) (n common.Hash) {
- h.sha.Reset()
- h.sha.Write(data)
- h.sha.Read(n[:])
- return n
+func (h *hasher) hash(data []byte) common.Hash {
+ return crypto.HashData(h.sha, data)
+}
+func (h *hasher) release() {
+ hasherPool.Put(h)
}
// ReadAccountTrieNode retrieves the account trie node and the associated node
@@ -70,9 +71,9 @@ func ReadAccountTrieNode(db ethdb.KeyValueReader, path []byte) ([]byte, common.H
if err != nil {
return nil, common.Hash{}
}
- hasher := newNodeHasher()
- defer returnHasherToPool(hasher)
- return data, hasher.hashData(data)
+ h := newHasher()
+ defer h.release()
+ return data, h.hash(data)
}
// HasAccountTrieNode checks the account trie node presence with the specified
@@ -82,9 +83,9 @@ func HasAccountTrieNode(db ethdb.KeyValueReader, path []byte, hash common.Hash)
if err != nil {
return false
}
- hasher := newNodeHasher()
- defer returnHasherToPool(hasher)
- return hasher.hashData(data) == hash
+ h := newHasher()
+ defer h.release()
+ return h.hash(data) == hash
}
// WriteAccountTrieNode writes the provided account trie node into database.
@@ -108,9 +109,9 @@ func ReadStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path
if err != nil {
return nil, common.Hash{}
}
- hasher := newNodeHasher()
- defer returnHasherToPool(hasher)
- return data, hasher.hashData(data)
+ h := newHasher()
+ defer h.release()
+ return data, h.hash(data)
}
// HasStorageTrieNode checks the storage trie node presence with the provided
@@ -120,9 +121,9 @@ func HasStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path [
if err != nil {
return false
}
- hasher := newNodeHasher()
- defer returnHasherToPool(hasher)
- return hasher.hashData(data) == hash
+ h := newHasher()
+ defer h.release()
+ return h.hash(data) == hash
}
// WriteStorageTrieNode writes the provided storage trie node into database.
diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go
index f621ba1a3d..b0f507cdd5 100644
--- a/core/rawdb/ancient_scheme.go
+++ b/core/rawdb/ancient_scheme.go
@@ -16,6 +16,8 @@
package rawdb
+import "path/filepath"
+
// The list of table names of chain freezer. (headers, hashes, bodies, difficulties)
const (
@@ -35,6 +37,30 @@ const (
chainFreezerDifficultyTable = "diffs"
)
+const (
+ // stateHistoryTableSize defines the maximum size of freezer data files.
+ stateHistoryTableSize = 2 * 1000 * 1000 * 1000 // 2GB
+
+ // stateHistoryAccountIndex indicates the name of the freezer state history table (Account + Storage).
+ stateHistoryMeta = "history.meta"
+ stateHistoryAccountIndex = "account.index"
+ stateHistoryStorageIndex = "storage.index"
+ stateHistoryAccountData = "account.data"
+ stateHistoryStorageData = "storage.data"
+
+ namespace = "eth/db/state"
+)
+
+// stateHistoryFreezerNoSnappy configures whether compression is disabled for the stateHistory.
+// https://github.com/golang/snappy, Reason for splititng files for looking up in archive mode easily.
+var stateHistoryFreezerNoSnappy = map[string]bool{
+ stateHistoryMeta: true,
+ stateHistoryAccountIndex: false,
+ stateHistoryStorageIndex: false,
+ stateHistoryAccountData: false,
+ stateHistoryStorageData: false,
+}
+
// chainFreezerNoSnappy configures whether compression is disabled for the ancient-tables.
// Hashes and difficulties don't compress well.
var chainFreezerNoSnappy = map[string]bool{
@@ -48,7 +74,15 @@ var chainFreezerNoSnappy = map[string]bool{
// The list of identifiers of ancient stores. It can split more in the futures.
var (
chainFreezerName = "chain" // the folder name of chain segment ancient store.
+ stateFreezerName = "state" // the folder name of reverse diff ancient store.
)
// freezers the collections of all builtin freezers.
var freezers = []string{chainFreezerName}
+
+// NewStateHistoryFreezer initializes the freezer for state history.
+func NewStateHistoryFreezer(ancientDir string, readOnly bool) (*ResettableFreezer, error) {
+ return NewResettableFreezer(
+ filepath.Join(ancientDir, stateFreezerName), namespace, readOnly,
+ stateHistoryTableSize, stateHistoryFreezerNoSnappy)
+}
diff --git a/core/rawdb/chain_iterator_test.go b/core/rawdb/chain_iterator_test.go
index 45cc6323e0..b6180f8252 100644
--- a/core/rawdb/chain_iterator_test.go
+++ b/core/rawdb/chain_iterator_test.go
@@ -34,7 +34,7 @@ func TestChainIterator(t *testing.T) {
var block *types.Block
var txs []*types.Transaction
to := common.BytesToAddress([]byte{0x11})
- block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, nil, newHasher()) // Empty genesis block
+ block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, nil, newTestHasher()) // Empty genesis block
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
for i := uint64(1); i <= 10; i++ {
@@ -60,7 +60,7 @@ func TestChainIterator(t *testing.T) {
})
}
txs = append(txs, tx)
- block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, []*types.Transaction{tx}, nil, nil, newHasher())
+ block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, []*types.Transaction{tx}, nil, nil, newTestHasher())
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
}
@@ -111,7 +111,7 @@ func TestIndexTransactions(t *testing.T) {
to := common.BytesToAddress([]byte{0x11})
// Write empty genesis block
- block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, nil, newHasher())
+ block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, nil, newTestHasher())
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
@@ -138,7 +138,7 @@ func TestIndexTransactions(t *testing.T) {
})
}
txs = append(txs, tx)
- block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, []*types.Transaction{tx}, nil, nil, newHasher())
+ block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, []*types.Transaction{tx}, nil, nil, newTestHasher())
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
}
diff --git a/core/rawdb/database.go b/core/rawdb/database.go
index 6a08932208..cf0cc15096 100644
--- a/core/rawdb/database.go
+++ b/core/rawdb/database.go
@@ -129,13 +129,13 @@ func (db *nofreezedb) Sync() error {
}
// TruncateHead returns an error as we don't have a backing chain freezer.
-func (db *nofreezedb) TruncateHead(items uint64) error {
- return errNotSupported
+func (db *nofreezedb) TruncateHead(items uint64) (uint64, error) {
+ return 0, errNotSupported
}
// TruncateTail returns an error as we don't have a backing chain freezer.
-func (db *nofreezedb) TruncateTail(items uint64) error {
- return errNotSupported
+func (db *nofreezedb) TruncateTail(items uint64) (uint64, error) {
+ return 0, errNotSupported
}
func (db *nofreezedb) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) {
diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go
index 1a96aff6d7..31f8fa80c6 100644
--- a/core/rawdb/freezer.go
+++ b/core/rawdb/freezer.go
@@ -270,46 +270,50 @@ func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize
}
// TruncateHead discards any recent data above the provided threshold number, only keep the first items ancient data.
-func (f *Freezer) TruncateHead(items uint64) error {
+// Return the old head number.
+func (f *Freezer) TruncateHead(items uint64) (uint64, error) {
if f.readonly {
- return errReadOnly
+ return 0, errReadOnly
}
f.writeLock.Lock()
defer f.writeLock.Unlock()
// If the current frozen number is less than the requested items for frozen, do nothing.
- if f.frozen.Load() <= items {
- return nil
+ previousItems := f.frozen.Load()
+ if previousItems <= items {
+ return previousItems, nil
}
for _, table := range f.tables {
if err := table.truncateHead(items); err != nil {
- return err
+ return 0, err
}
}
f.frozen.Store(items)
- return nil
+ return previousItems, nil
}
-// TruncateTail discards any recent data below the provided threshold number, only keep the last items ancient data.
-func (f *Freezer) TruncateTail(tail uint64) error {
+// TruncateTail discards any recent data below the provided threshold number, only keep the last items ancient data, return the old tail number.
+func (f *Freezer) TruncateTail(tail uint64) (uint64, error) {
if f.readonly {
- return errReadOnly
+ return 0, errReadOnly
}
f.writeLock.Lock()
defer f.writeLock.Unlock()
// If the current tail number is greater than the requested tail, seem out of range for truncating, do nothing.
- if f.tail.Load() >= tail {
- return nil
+ old := f.tail.Load()
+
+ if old >= tail {
+ return old, nil
}
for _, table := range f.tables {
if err := table.truncateTail(tail); err != nil {
- return err
+ return 0, err
}
}
f.tail.Store(tail)
- return nil
+ return old, nil
}
// Sync flushes all data tables to disk.
@@ -345,7 +349,7 @@ func (f *Freezer) repair() error {
}
}
- // Truncate all tables to the common head and tail.
+ // Truncate all tables to the common head and tail. Returns the previous head number.
for _, table := range f.tables {
if err := table.truncateHead(head); err != nil {
return err
diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go
new file mode 100644
index 0000000000..6948aee06e
--- /dev/null
+++ b/core/rawdb/freezer_resettable.go
@@ -0,0 +1,240 @@
+// Copyright 2022 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rawdb
+
+import (
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// In PBSS, this freezer is used to reverse diff
+// The idea for implementing this package is to provide a freezer which supported resettable in case we need to rollback to the genesis
+// Normally, TruncateTail is irreversible. This implementing will depend on "os.Rename" & "os.RemoveAll" to delete and recreate a new one from scratch.
+
+const tmpSuffix = ".tmp"
+
+// freezerOpenFunc is the function used to open/create a freezer.
+type freezerOpenFunc = func() (*Freezer, error)
+
+// ResettableFreezer is a wrapper of the freezer which makes the
+// freezer resettable.
+type ResettableFreezer struct {
+ freezer *Freezer
+ opener freezerOpenFunc
+ datadir string
+ lock sync.RWMutex
+}
+
+// NewResettableFreezer creates a resettable freezer, note freezer is
+// only resettable if the passed file directory is exclusively occupied
+// by the freezer. And also the user-configurable ancient root directory
+// is **not** supported for reset since it might be a mount and rename
+// will cause a copy of hundreds of gigabyte into local directory. It
+// needs some other file based solutions.
+//
+// The reset function will delete directory atomically and re-create the
+// freezer from scratch.
+// namespace is the prefix for metrics which is not stored in freezer
+func NewResettableFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]bool) (*ResettableFreezer, error) {
+ // Clean up if we figureout .tmp inside data directory
+ if err := cleanup(datadir); err != nil {
+ return nil, err
+ }
+ opener := func() (*Freezer, error) {
+ return NewFreezer(datadir, namespace, readonly, maxTableSize, tables)
+ }
+ freezer, err := opener()
+ if err != nil {
+ return nil, err
+ }
+ return &ResettableFreezer{
+ freezer: freezer,
+ opener: opener,
+ datadir: datadir,
+ }, nil
+}
+
+// Reset deletes the file directory exclusively occupied by the freezer and
+// recreate the freezer from scratch. The atomicity of directory deletion
+// is guaranteed by the rename operation,
+func (f *ResettableFreezer) Reset() error {
+ f.lock.Lock()
+ defer f.lock.Unlock()
+
+ // Close the freezer before deleting the directory
+ if err := f.freezer.Close(); err != nil {
+ return err
+ }
+
+ tmp := tmpName(f.datadir)
+ if err := os.Rename(f.datadir, tmp); err != nil {
+ return err
+ }
+
+ // the leftover directory will be cleaned up in next startup in case crash happens after rename. See in cleanup function.
+ if err := os.RemoveAll(tmp); err != nil {
+ return err
+ }
+ freezer, err := f.opener()
+ if err != nil {
+ return err
+ }
+ f.freezer = freezer
+ return nil
+}
+
+// Close terminates the chain freezer, unmapping all the data files.
+func (f *ResettableFreezer) Close() error {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.Close()
+}
+
+// HasAncient returns an indicator whether the specified ancient data exists
+// in the freezer
+func (f *ResettableFreezer) HasAncient(kind string, number uint64) (bool, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.HasAncient(kind, number)
+}
+
+// Ancient retrieves an ancient binary blob from the append-only immutable files.
+func (f *ResettableFreezer) Ancient(kind string, number uint64) ([]byte, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.Ancient(kind, number)
+}
+
+// AncientRange retrieves multiple items in sequence, starting from the index 'start'.
+// It will return
+// - at most 'max' items,
+// - at least 1 item (even if exceeding the maxByteSize), but will otherwise
+// return as many items as fit into maxByteSize
+func (f *ResettableFreezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.AncientRange(kind, start, count, maxBytes)
+}
+
+// Ancients returns the length of the frozen items.
+func (f *ResettableFreezer) Ancients() (uint64, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.Ancients()
+}
+
+// Tail returns the number of first stored item in the freezer.
+func (f *ResettableFreezer) Tail() (uint64, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.Tail()
+}
+
+// AncientSize returns the ancient size of the specified category.
+func (f *ResettableFreezer) AncientSize(kind string) (uint64, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.AncientSize(kind)
+}
+
+// ReadAncients runs the given read operation while ensuring that no writes take place
+// on the underlying freezer.
+func (f *ResettableFreezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.ReadAncients(fn)
+}
+
+// ModifyAncients runs the given write operation.
+func (f *ResettableFreezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.ModifyAncients(fn)
+}
+
+// TruncateHead discards any recent data above the provided threshold number.
+func (f *ResettableFreezer) TruncateHead(items uint64) (uint64, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.TruncateHead(items)
+}
+
+// TruncateTail discards any recent data below the provided threshold number.
+func (f *ResettableFreezer) TruncateTail(tail uint64) (uint64, error) {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.TruncateTail(tail)
+}
+
+// Sync flushes all data tables to disk.
+func (f *ResettableFreezer) Sync() error {
+ f.lock.RLock()
+ defer f.lock.RUnlock()
+
+ return f.freezer.Sync()
+}
+
+func cleanup(pathToDelete string) error {
+ parentDir := filepath.Dir(pathToDelete)
+
+ if _, err := os.Lstat(parentDir); err != nil {
+ return err
+ }
+ dir, err := os.Open(parentDir)
+ if err != nil {
+ return err
+ }
+ // Read all the names of files and directories in the parent directory with single slice.
+ names, err := dir.Readdirnames(0)
+ if err != nil {
+ return err
+ }
+ if cerr := dir.Close(); cerr != nil {
+ return cerr
+ }
+
+ for _, name := range names {
+ if name == filepath.Base(pathToDelete)+tmpSuffix {
+ // Figure out then delete the tmp directory which is renamed in Reset Method.
+ log.Info("Cleaning up the freezer Reset directory", "pathToDelete", pathToDelete, "total files inside", len(names))
+ return os.RemoveAll(filepath.Join(parentDir, name))
+ }
+ }
+ return nil
+
+}
+
+// /home/user/documents -> /home/user/documents.tmp (Directory)
+// /home/user/documents/file.txt -> /home/user/documents/file.txt.tmp (File)
+func tmpName(path string) string {
+ return filepath.Join(filepath.Dir(path), filepath.Base(path)+tmpSuffix)
+}
diff --git a/core/rawdb/freezer_resettable_test.go b/core/rawdb/freezer_resettable_test.go
new file mode 100644
index 0000000000..bd7129ae84
--- /dev/null
+++ b/core/rawdb/freezer_resettable_test.go
@@ -0,0 +1,120 @@
+// Copyright 2022 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rawdb
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/ethdb"
+)
+
+func TestResetFreezer(t *testing.T) {
+ items := []struct {
+ id uint64
+ blob []byte
+ }{
+ {0, bytes.Repeat([]byte{0}, 2048)},
+ {1, bytes.Repeat([]byte{1}, 2048)},
+ {2, bytes.Repeat([]byte{2}, 2048)},
+ {3, bytes.Repeat([]byte{3}, 2048)},
+ }
+ temp := t.TempDir()
+ f, _ := NewResettableFreezer(temp, "", false, 2048, freezerTestTableDef)
+ defer f.Close()
+
+ f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ for _, item := range items {
+ op.AppendRaw("test", item.id, item.blob)
+ }
+ return nil
+ })
+ // Expected can get
+ for _, item := range items {
+ blob, _ := f.Ancient("test", item.id)
+ if !bytes.Equal(blob, item.blob) {
+ t.Fatalf("Failed to get the correct blob")
+ }
+ }
+ if _, err := os.Lstat(temp); os.IsNotExist(err) {
+ t.Fatal("Expected datadir should exist")
+ }
+ // Reset freezer, Expect all data is removed, and the directory is still there.
+ f.Reset()
+ count, _ := f.Ancients()
+ if count != 0 {
+ t.Fatal("Failed to reset freezer")
+ }
+ for _, item := range items {
+ blob, _ := f.Ancient("test", item.id)
+ if len(blob) != 0 {
+ t.Fatal("Unexpected blob")
+ }
+ }
+ if _, err := os.Lstat(temp); os.IsNotExist(err) {
+ t.Fatal("Expected datadir should exist")
+ }
+ // Fill the freezer
+ f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ for _, item := range items {
+ op.AppendRaw("test", item.id, item.blob)
+ }
+ return nil
+ })
+ for _, item := range items {
+ blob, _ := f.Ancient("test", item.id)
+ if !bytes.Equal(blob, item.blob) {
+ t.Fatal("Unexpected blob")
+ }
+ }
+}
+
+func TestFreezerCleanUpWhenInit(t *testing.T) {
+ items := []struct {
+ id uint64
+ blob []byte
+ }{
+ {0, bytes.Repeat([]byte{0}, 2048)},
+ {1, bytes.Repeat([]byte{1}, 2048)},
+ {2, bytes.Repeat([]byte{2}, 2048)},
+ {3, bytes.Repeat([]byte{3}, 2048)},
+ }
+ // Generate a temporary directory for the freezer
+ datadir := t.TempDir()
+ // Expect nothing here.
+ f, _ := NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef)
+ // Write some data to the freezer
+ f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ for _, item := range items {
+ op.AppendRaw("test", item.id, item.blob)
+ }
+ return nil
+ })
+ f.Close()
+ fmt.Println(tmpName(datadir))
+ os.Rename(datadir, tmpName(datadir))
+ // Open the freezer again, trigger cleanup operation
+ f, _ = NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef)
+ f.Close()
+
+ // Expected datadir.tmp should be removed
+ if _, err := os.Lstat(tmpName(datadir)); !os.IsNotExist(err) {
+ t.Fatal("Failed to cleanup leftover directory")
+ }
+}
diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go
index 81e8a3155f..eee65eab1f 100644
--- a/core/rawdb/freezer_table.go
+++ b/core/rawdb/freezer_table.go
@@ -422,6 +422,7 @@ func (t *freezerTable) truncateTail(items uint64) error {
defer t.lock.Unlock()
// The truncateTarget is below the current tail, return nil, no need to truncate
+
if t.itemHidden.Load() >= items {
return nil
}
diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go
index 418e4ae5b1..9749e58616 100644
--- a/core/rawdb/freezer_test.go
+++ b/core/rawdb/freezer_test.go
@@ -196,7 +196,7 @@ func TestFreezerConcurrentModifyTruncate(t *testing.T) {
for i := 0; i < 1000; i++ {
// First reset and write 100 items.
- if err := f.TruncateHead(0); err != nil {
+ if _, err := f.TruncateHead(0); err != nil {
t.Fatal("truncate failed:", err)
}
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
@@ -231,7 +231,7 @@ func TestFreezerConcurrentModifyTruncate(t *testing.T) {
wg.Done()
}()
go func() {
- truncateErr = f.TruncateHead(10)
+ _, truncateErr = f.TruncateHead(10)
wg.Done()
}()
go func() {
diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go
index 439e47df9f..6c2afc1d1c 100644
--- a/core/rawdb/schema.go
+++ b/core/rawdb/schema.go
@@ -40,6 +40,9 @@ var (
// headFastBlockKey tracks the latest known incomplete block's hash during fast sync.
headFastBlockKey = []byte("LastFast")
+ // persistentStateIDKey tracks the id of latest stored state(for path-based only)
+ persistentStateIDKey = []byte("LastStateID")
+
// lastPivotKey tracks the last pivot block used by fast sync (to reenable on sethead).
lastPivotKey = []byte("LastPivot")
@@ -64,6 +67,9 @@ var (
// snapshotSyncStatusKey tracks the snapshot sync status across restarts.
snapshotSyncStatusKey = []byte("SnapshotSyncStatus")
+ // trieJournalKey tracks the in-memory trie node layers across restarts.
+ trieJournalKey = []byte("TrieJournal")
+
// txIndexTailKey tracks the oldest block whose transactions have been indexed.
txIndexTailKey = []byte("TransactionIndexTail")
@@ -107,6 +113,7 @@ var (
// Path-based storage scheme of merkle patricia trie.
trieNodeAccountPrefix = []byte("A") // trieNodeAccountPrefix + hexPath -> trie node
trieNodeStoragePrefix = []byte("O") // trieNodeStoragePrefix + accountHash + hexPath -> trie node
+ stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id
PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage
configPrefix = []byte("ethereum-config-") // config prefix for the db
@@ -296,3 +303,8 @@ func IsStorageTrieNode(key []byte) (bool, common.Hash, []byte) {
accountHash := common.BytesToHash(key[len(trieNodeStoragePrefix) : len(trieNodeStoragePrefix)+common.HashLength])
return true, accountHash, key[len(trieNodeStoragePrefix)+common.HashLength:]
}
+
+// stateIDKey = stateIDPrefix + root (32 bytes)
+func stateIDKey(root common.Hash) []byte {
+ return append(stateIDPrefix, root.Bytes()...)
+}
diff --git a/core/rawdb/table.go b/core/rawdb/table.go
index 2672f4ea8d..73ec8416a1 100644
--- a/core/rawdb/table.go
+++ b/core/rawdb/table.go
@@ -97,12 +97,12 @@ func (t *table) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err e
// TruncateHead is a noop passthrough that just forwards the request to the underlying
// database.
-func (t *table) TruncateHead(items uint64) error {
+func (t *table) TruncateHead(items uint64) (uint64, error) {
return t.db.TruncateHead(items)
}
// TruncateTail is a noop passthrough that just forwards the request to the underlying
-func (t *table) TruncateTail(items uint64) error {
+func (t *table) TruncateTail(items uint64) (uint64, error) {
return t.db.TruncateTail(items)
}
diff --git a/ethdb/database.go b/ethdb/database.go
index a3c5570b53..06ea193052 100644
--- a/ethdb/database.go
+++ b/ethdb/database.go
@@ -118,7 +118,7 @@ type AncientWriter interface {
// TruncateHead discards all, but keep the first n ancient data from the ancient store.
// After the truncation, the latest item can be accessed it item_ n-1 (start from 0)
// Tail 0 -> (n-1)New-headxxxxOld-head
- TruncateHead(n uint64) error
+ TruncateHead(n uint64) (uint64, error)
// TruncateTail discards the first n ancient data from the ancient store. The already
// deleted items are ignored. After the truncation, the earliest item can be accessed
@@ -126,7 +126,7 @@ type AncientWriter interface {
// immediately, but only when the accumulated deleted data reach the threshold then
// will be removed all together.
// Old-tail(0)xxxxxxxNew-tail(n)->Head
- TruncateTail(n uint64) error
+ TruncateTail(n uint64) (uint64, error)
// Sync flushes all in-memory ancient store data to disk.
Sync() error