diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go
index 71ff821bb9b..2e05dbf7713 100644
--- a/cmd/geth/chaincmd.go
+++ b/cmd/geth/chaincmd.go
@@ -117,6 +117,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`,
utils.LogNoHistoryFlag,
utils.LogExportCheckpointsFlag,
utils.StateHistoryFlag,
+ utils.TrienodeHistoryFlag,
}, utils.DatabaseFlags, debug.Flags),
Before: func(ctx *cli.Context) error {
flags.MigrateGlobalFlags(ctx)
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index f380c9f2d44..fb3e66cae56 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -91,6 +91,7 @@ var (
utils.LogNoHistoryFlag,
utils.LogExportCheckpointsFlag,
utils.StateHistoryFlag,
+ utils.TrienodeHistoryFlag,
utils.LightKDFFlag,
utils.EthRequiredBlocksFlag,
utils.LegacyWhitelistFlag, // deprecated
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 5e96185dbd0..c8fb345d777 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -282,6 +282,12 @@ var (
Value: ethconfig.Defaults.StateHistory,
Category: flags.StateCategory,
}
+ TrienodeHistoryFlag = &cli.Int64Flag{
+ Name: "history.trienode",
+ Usage: "Number of recent blocks to retain trienode history for, only relevant in state.scheme=path (default/negative = disabled, 0 = entire chain)",
+ Value: ethconfig.Defaults.TrienodeHistory,
+ Category: flags.StateCategory,
+ }
TransactionHistoryFlag = &cli.Uint64Flag{
Name: "history.transactions",
Usage: "Number of recent blocks to maintain transactions index for (default = about one year, 0 = entire chain)",
@@ -1663,6 +1669,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.IsSet(StateHistoryFlag.Name) {
cfg.StateHistory = ctx.Uint64(StateHistoryFlag.Name)
}
+ if ctx.IsSet(TrienodeHistoryFlag.Name) {
+ cfg.TrienodeHistory = ctx.Int64(TrienodeHistoryFlag.Name)
+ }
if ctx.IsSet(StateSchemeFlag.Name) {
cfg.StateScheme = ctx.String(StateSchemeFlag.Name)
}
@@ -2234,15 +2243,16 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh
Fatalf("%v", err)
}
options := &core.BlockChainConfig{
- TrieCleanLimit: ethconfig.Defaults.TrieCleanCache,
- NoPrefetch: ctx.Bool(CacheNoPrefetchFlag.Name),
- TrieDirtyLimit: ethconfig.Defaults.TrieDirtyCache,
- ArchiveMode: ctx.String(GCModeFlag.Name) == "archive",
- TrieTimeLimit: ethconfig.Defaults.TrieTimeout,
- SnapshotLimit: ethconfig.Defaults.SnapshotCache,
- Preimages: ctx.Bool(CachePreimagesFlag.Name),
- StateScheme: scheme,
- StateHistory: ctx.Uint64(StateHistoryFlag.Name),
+ TrieCleanLimit: ethconfig.Defaults.TrieCleanCache,
+ NoPrefetch: ctx.Bool(CacheNoPrefetchFlag.Name),
+ TrieDirtyLimit: ethconfig.Defaults.TrieDirtyCache,
+ ArchiveMode: ctx.String(GCModeFlag.Name) == "archive",
+ TrieTimeLimit: ethconfig.Defaults.TrieTimeout,
+ SnapshotLimit: ethconfig.Defaults.SnapshotCache,
+ Preimages: ctx.Bool(CachePreimagesFlag.Name),
+ StateScheme: scheme,
+ StateHistory: ctx.Uint64(StateHistoryFlag.Name),
+ TrienodeHistory: ctx.Int64(TrienodeHistoryFlag.Name),
// Disable transaction indexing/unindexing.
TxLookupLimit: -1,
diff --git a/core/blockchain.go b/core/blockchain.go
index 30f3da3004a..81a67d01804 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -175,6 +175,11 @@ type BlockChainConfig struct {
// If set to 0, all state histories across the entire chain will be retained;
StateHistory uint64
+ // Number of blocks from the chain head for which trienode histories are retained.
+ // If set to 0, all trienode histories across the entire chain will be retained;
+ // If set to -1, no trienode history will be retained;
+ TrienodeHistory int64
+
// State snapshot related options
SnapshotLimit int // Memory allowance (MB) to use for caching snapshot entries in memory
SnapshotNoBuild bool // Whether the background generation is allowed
@@ -249,6 +254,7 @@ func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config {
if cfg.StateScheme == rawdb.PathScheme {
config.PathDB = &pathdb.Config{
StateHistory: cfg.StateHistory,
+ TrienodeHistory: cfg.TrienodeHistory,
EnableStateIndexing: cfg.ArchiveMode,
TrieCleanSize: cfg.TrieCleanLimit * 1024 * 1024,
StateCleanSize: cfg.SnapshotLimit * 1024 * 1024,
diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go
index 4894523b0e5..5088918e19c 100644
--- a/core/blockchain_reader.go
+++ b/core/blockchain_reader.go
@@ -416,11 +416,11 @@ func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
return state.New(root, bc.statedb)
}
-// HistoricState returns a historic state specified by the given root.
+// HistoricalState returns a historical state specified by the given root.
// Live states are not available and won't be served, please use `State`
// or `StateAt` instead.
-func (bc *BlockChain) HistoricState(root common.Hash) (*state.StateDB, error) {
- return state.New(root, state.NewHistoricDatabase(bc.db, bc.triedb))
+func (bc *BlockChain) HistoricalState(root common.Hash) (*state.StateDB, error) {
+ return state.New(root, state.NewHistoricalDatabase(bc.db, bc.triedb))
}
// Config retrieves the chain's fork configuration.
diff --git a/core/rawdb/accessors_history.go b/core/rawdb/accessors_history.go
index cf1073f387a..95a8907edc0 100644
--- a/core/rawdb/accessors_history.go
+++ b/core/rawdb/accessors_history.go
@@ -46,6 +46,27 @@ func DeleteStateHistoryIndexMetadata(db ethdb.KeyValueWriter) {
}
}
+// ReadTrienodeHistoryIndexMetadata retrieves the metadata of trienode history index.
+func ReadTrienodeHistoryIndexMetadata(db ethdb.KeyValueReader) []byte {
+ data, _ := db.Get(headTrienodeHistoryIndexKey)
+ return data
+}
+
+// WriteTrienodeHistoryIndexMetadata stores the metadata of trienode history index
+// into database.
+func WriteTrienodeHistoryIndexMetadata(db ethdb.KeyValueWriter, blob []byte) {
+ if err := db.Put(headTrienodeHistoryIndexKey, blob); err != nil {
+ log.Crit("Failed to store the metadata of trienode history index", "err", err)
+ }
+}
+
+// DeleteTrienodeHistoryIndexMetadata removes the metadata of trienode history index.
+func DeleteTrienodeHistoryIndexMetadata(db ethdb.KeyValueWriter) {
+ if err := db.Delete(headTrienodeHistoryIndexKey); err != nil {
+ log.Crit("Failed to delete the metadata of trienode history index", "err", err)
+ }
+}
+
// ReadAccountHistoryIndex retrieves the account history index with the provided
// account address.
func ReadAccountHistoryIndex(db ethdb.KeyValueReader, addressHash common.Hash) []byte {
@@ -95,6 +116,30 @@ func DeleteStorageHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash,
}
}
+// ReadTrienodeHistoryIndex retrieves the trienode history index with the provided
+// account address and storage key hash.
+func ReadTrienodeHistoryIndex(db ethdb.KeyValueReader, addressHash common.Hash, path []byte) []byte {
+ data, err := db.Get(trienodeHistoryIndexKey(addressHash, path))
+ if err != nil || len(data) == 0 {
+ return nil
+ }
+ return data
+}
+
+// WriteTrienodeHistoryIndex writes the provided trienode history index into database.
+func WriteTrienodeHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte, data []byte) {
+ if err := db.Put(trienodeHistoryIndexKey(addressHash, path), data); err != nil {
+ log.Crit("Failed to store trienode history index", "err", err)
+ }
+}
+
+// DeleteTrienodeHistoryIndex deletes the specified trienode index from the database.
+func DeleteTrienodeHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte) {
+ if err := db.Delete(trienodeHistoryIndexKey(addressHash, path)); err != nil {
+ log.Crit("Failed to delete trienode history index", "err", err)
+ }
+}
+
// ReadAccountHistoryIndexBlock retrieves the index block with the provided
// account address along with the block id.
func ReadAccountHistoryIndexBlock(db ethdb.KeyValueReader, addressHash common.Hash, blockID uint32) []byte {
@@ -143,6 +188,30 @@ func DeleteStorageHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.
}
}
+// ReadTrienodeHistoryIndexBlock retrieves the index block with the provided state
+// identifier along with the block id.
+func ReadTrienodeHistoryIndexBlock(db ethdb.KeyValueReader, addressHash common.Hash, path []byte, blockID uint32) []byte {
+ data, err := db.Get(trienodeHistoryIndexBlockKey(addressHash, path, blockID))
+ if err != nil || len(data) == 0 {
+ return nil
+ }
+ return data
+}
+
+// WriteTrienodeHistoryIndexBlock writes the provided index block into database.
+func WriteTrienodeHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte, id uint32, data []byte) {
+ if err := db.Put(trienodeHistoryIndexBlockKey(addressHash, path, id), data); err != nil {
+ log.Crit("Failed to store trienode index block", "err", err)
+ }
+}
+
+// DeleteTrienodeHistoryIndexBlock deletes the specified index block from the database.
+func DeleteTrienodeHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte, id uint32) {
+ if err := db.Delete(trienodeHistoryIndexBlockKey(addressHash, path, id)); err != nil {
+ log.Crit("Failed to delete trienode index block", "err", err)
+ }
+}
+
// increaseKey increase the input key by one bit. Return nil if the entire
// addition operation overflows.
func increaseKey(key []byte) []byte {
@@ -155,14 +224,26 @@ func increaseKey(key []byte) []byte {
return nil
}
-// DeleteStateHistoryIndex completely removes all history indexing data, including
+// DeleteStateHistoryIndexes completely removes all history indexing data, including
// indexes for accounts and storages.
-//
-// Note, this method assumes the storage space with prefix `StateHistoryIndexPrefix`
-// is exclusively occupied by the history indexing data!
-func DeleteStateHistoryIndex(db ethdb.KeyValueRangeDeleter) {
- start := StateHistoryIndexPrefix
- limit := increaseKey(bytes.Clone(StateHistoryIndexPrefix))
+func DeleteStateHistoryIndexes(db ethdb.KeyValueRangeDeleter) {
+ DeleteHistoryByRange(db, StateHistoryAccountMetadataPrefix)
+ DeleteHistoryByRange(db, StateHistoryStorageMetadataPrefix)
+ DeleteHistoryByRange(db, StateHistoryAccountBlockPrefix)
+ DeleteHistoryByRange(db, StateHistoryStorageBlockPrefix)
+}
+
+// DeleteTrienodeHistoryIndexes completely removes all trienode history indexing data.
+func DeleteTrienodeHistoryIndexes(db ethdb.KeyValueRangeDeleter) {
+ DeleteHistoryByRange(db, TrienodeHistoryMetadataPrefix)
+ DeleteHistoryByRange(db, TrienodeHistoryBlockPrefix)
+}
+
+// DeleteHistoryByRange completely removes all database entries with the specific prefix.
+// Note, this method assumes the space with the given prefix is exclusively occupied!
+func DeleteHistoryByRange(db ethdb.KeyValueRangeDeleter, prefix []byte) {
+ start := prefix
+ limit := increaseKey(bytes.Clone(prefix))
// Try to remove the data in the range by a loop, as the leveldb
// doesn't support the native range deletion.
diff --git a/core/rawdb/accessors_state.go b/core/rawdb/accessors_state.go
index 2359fb18f1b..c03c769225b 100644
--- a/core/rawdb/accessors_state.go
+++ b/core/rawdb/accessors_state.go
@@ -292,3 +292,76 @@ func WriteStateHistory(db ethdb.AncientWriter, id uint64, meta []byte, accountIn
})
return err
}
+
+// ReadTrienodeHistory retrieves the trienode history corresponding to the specified id.
+// Compute the position of trienode history in freezer by minus one since the id of first
+// trienode history starts from one(zero for initial state).
+func ReadTrienodeHistory(db ethdb.AncientReaderOp, id uint64) ([]byte, []byte, []byte, error) {
+ header, err := db.Ancient(trienodeHistoryHeaderTable, id-1)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ keySection, err := db.Ancient(trienodeHistoryKeySectionTable, id-1)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ valueSection, err := db.Ancient(trienodeHistoryValueSectionTable, id-1)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ return header, keySection, valueSection, nil
+}
+
+// ReadTrienodeHistoryHeader retrieves the header section of trienode history.
+func ReadTrienodeHistoryHeader(db ethdb.AncientReaderOp, id uint64) ([]byte, error) {
+ return db.Ancient(trienodeHistoryHeaderTable, id-1)
+}
+
+// ReadTrienodeHistoryKeySection retrieves the key section of trienode history.
+func ReadTrienodeHistoryKeySection(db ethdb.AncientReaderOp, id uint64) ([]byte, error) {
+ return db.Ancient(trienodeHistoryKeySectionTable, id-1)
+}
+
+// ReadTrienodeHistoryValueSection retrieves the value section of trienode history.
+func ReadTrienodeHistoryValueSection(db ethdb.AncientReaderOp, id uint64) ([]byte, error) {
+ return db.Ancient(trienodeHistoryValueSectionTable, id-1)
+}
+
+// ReadTrienodeHistoryList retrieves the a list of trienode history corresponding
+// to the specified range.
+// Compute the position of trienode history in freezer by minus one since the id
+// of first trienode history starts from one(zero for initial state).
+func ReadTrienodeHistoryList(db ethdb.AncientReaderOp, start uint64, count uint64) ([][]byte, [][]byte, [][]byte, error) {
+ header, err := db.AncientRange(trienodeHistoryHeaderTable, start-1, count, 0)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ keySection, err := db.AncientRange(trienodeHistoryKeySectionTable, start-1, count, 0)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ valueSection, err := db.AncientRange(trienodeHistoryValueSectionTable, start-1, count, 0)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ if len(header) != len(keySection) || len(header) != len(valueSection) {
+ return nil, nil, nil, errors.New("trienode history is corrupted")
+ }
+ return header, keySection, valueSection, nil
+}
+
+// WriteTrienodeHistory writes the provided trienode history to database.
+// Compute the position of trienode history in freezer by minus one since
+// the id of first state history starts from one(zero for initial state).
+func WriteTrienodeHistory(db ethdb.AncientWriter, id uint64, header []byte, keySection []byte, valueSection []byte) error {
+ _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ if err := op.AppendRaw(trienodeHistoryHeaderTable, id-1, header); err != nil {
+ return err
+ }
+ if err := op.AppendRaw(trienodeHistoryKeySectionTable, id-1, keySection); err != nil {
+ return err
+ }
+ return op.AppendRaw(trienodeHistoryValueSectionTable, id-1, valueSection)
+ })
+ return err
+}
diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go
index 1ffebed3e7c..afec7848c88 100644
--- a/core/rawdb/ancient_scheme.go
+++ b/core/rawdb/ancient_scheme.go
@@ -75,15 +75,38 @@ var stateFreezerTableConfigs = map[string]freezerTableConfig{
stateHistoryStorageData: {noSnappy: false, prunable: true},
}
+const (
+ trienodeHistoryHeaderTable = "trienode.header"
+ trienodeHistoryKeySectionTable = "trienode.key"
+ trienodeHistoryValueSectionTable = "trienode.value"
+)
+
+// trienodeFreezerTableConfigs configures the settings for tables in the trienode freezer.
+var trienodeFreezerTableConfigs = map[string]freezerTableConfig{
+ trienodeHistoryHeaderTable: {noSnappy: false, prunable: true},
+
+ // Disable snappy compression to allow efficient partial read.
+ trienodeHistoryKeySectionTable: {noSnappy: true, prunable: true},
+
+ // Disable snappy compression to allow efficient partial read.
+ trienodeHistoryValueSectionTable: {noSnappy: true, prunable: true},
+}
+
// The list of identifiers of ancient stores.
var (
- ChainFreezerName = "chain" // the folder name of chain segment ancient store.
- MerkleStateFreezerName = "state" // the folder name of state history ancient store.
- VerkleStateFreezerName = "state_verkle" // the folder name of state history ancient store.
+ ChainFreezerName = "chain" // the folder name of chain segment ancient store.
+ MerkleStateFreezerName = "state" // the folder name of state history ancient store.
+ VerkleStateFreezerName = "state_verkle" // the folder name of state history ancient store.
+ MerkleTrienodeFreezerName = "trienode" // the folder name of trienode history ancient store.
+ VerkleTrienodeFreezerName = "trienode_verkle" // the folder name of trienode history ancient store.
)
// freezers the collections of all builtin freezers.
-var freezers = []string{ChainFreezerName, MerkleStateFreezerName, VerkleStateFreezerName}
+var freezers = []string{
+ ChainFreezerName,
+ MerkleStateFreezerName, VerkleStateFreezerName,
+ MerkleTrienodeFreezerName, VerkleTrienodeFreezerName,
+}
// NewStateFreezer initializes the ancient store for state history.
//
@@ -103,3 +126,22 @@ func NewStateFreezer(ancientDir string, verkle bool, readOnly bool) (ethdb.Reset
}
return newResettableFreezer(name, "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerTableConfigs)
}
+
+// NewTrienodeFreezer initializes the ancient store for trienode history.
+//
+// - if the empty directory is given, initializes the pure in-memory
+// trienode freezer (e.g. dev mode).
+// - if non-empty directory is given, initializes the regular file-based
+// trienode freezer.
+func NewTrienodeFreezer(ancientDir string, verkle bool, readOnly bool) (ethdb.ResettableAncientStore, error) {
+ if ancientDir == "" {
+ return NewMemoryFreezer(readOnly, trienodeFreezerTableConfigs), nil
+ }
+ var name string
+ if verkle {
+ name = filepath.Join(ancientDir, VerkleTrienodeFreezerName)
+ } else {
+ name = filepath.Join(ancientDir, MerkleTrienodeFreezerName)
+ }
+ return newResettableFreezer(name, "eth/db/trienode", readOnly, stateHistoryTableSize, trienodeFreezerTableConfigs)
+}
diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go
index f4909d86e73..b940d910402 100644
--- a/core/rawdb/ancient_utils.go
+++ b/core/rawdb/ancient_utils.go
@@ -105,6 +105,23 @@ func inspectFreezers(db ethdb.Database) ([]freezerInfo, error) {
}
infos = append(infos, info)
+ case MerkleTrienodeFreezerName, VerkleTrienodeFreezerName:
+ datadir, err := db.AncientDatadir()
+ if err != nil {
+ return nil, err
+ }
+ f, err := NewTrienodeFreezer(datadir, freezer == VerkleTrienodeFreezerName, true)
+ if err != nil {
+ continue // might be possible the trienode freezer is not existent
+ }
+ defer f.Close()
+
+ info, err := inspect(freezer, trienodeFreezerTableConfigs, f)
+ if err != nil {
+ return nil, err
+ }
+ infos = append(infos, info)
+
default:
return nil, fmt.Errorf("unknown freezer, supported ones: %v", freezers)
}
diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go
index 9a17e1c1738..ed7922e5639 100644
--- a/core/rawdb/schema.go
+++ b/core/rawdb/schema.go
@@ -80,6 +80,10 @@ var (
// been indexed.
headStateHistoryIndexKey = []byte("LastStateHistoryIndex")
+ // headTrienodeHistoryIndexKey tracks the ID of the latest state history that has
+ // been indexed.
+ headTrienodeHistoryIndexKey = []byte("LastTrienodeHistoryIndex")
+
// txIndexTailKey tracks the oldest block whose transactions have been indexed.
txIndexTailKey = []byte("TransactionIndexTail")
@@ -125,8 +129,10 @@ var (
StateHistoryIndexPrefix = []byte("m") // The global prefix of state history index data
StateHistoryAccountMetadataPrefix = []byte("ma") // StateHistoryAccountMetadataPrefix + account address hash => account metadata
StateHistoryStorageMetadataPrefix = []byte("ms") // StateHistoryStorageMetadataPrefix + account address hash + storage slot hash => slot metadata
+ TrienodeHistoryMetadataPrefix = []byte("mt") // TrienodeHistoryMetadataPrefix + account address hash + trienode path => trienode metadata
StateHistoryAccountBlockPrefix = []byte("mba") // StateHistoryAccountBlockPrefix + account address hash + blockID => account block
StateHistoryStorageBlockPrefix = []byte("mbs") // StateHistoryStorageBlockPrefix + account address hash + storage slot hash + blockID => slot block
+ TrienodeHistoryBlockPrefix = []byte("mbt") // TrienodeHistoryBlockPrefix + account address hash + trienode path + blockID => trienode block
// VerklePrefix is the database prefix for Verkle trie data, which includes:
// (a) Trie nodes
@@ -395,6 +401,19 @@ func storageHistoryIndexKey(addressHash common.Hash, storageHash common.Hash) []
return out
}
+// trienodeHistoryIndexKey = TrienodeHistoryMetadataPrefix + addressHash + trienode path
+func trienodeHistoryIndexKey(addressHash common.Hash, path []byte) []byte {
+ totalLen := len(TrienodeHistoryMetadataPrefix) + common.HashLength + len(path)
+ out := make([]byte, totalLen)
+
+ off := 0
+ off += copy(out[off:], TrienodeHistoryMetadataPrefix)
+ off += copy(out[off:], addressHash.Bytes())
+ copy(out[off:], path)
+
+ return out
+}
+
// accountHistoryIndexBlockKey = StateHistoryAccountBlockPrefix + addressHash + blockID
func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte {
var buf4 [4]byte
@@ -428,6 +447,23 @@ func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Has
return out
}
+// trienodeHistoryIndexBlockKey = TrienodeHistoryBlockPrefix + addressHash + trienode path + blockID
+func trienodeHistoryIndexBlockKey(addressHash common.Hash, path []byte, blockID uint32) []byte {
+ var buf4 [4]byte
+ binary.BigEndian.PutUint32(buf4[:], blockID)
+
+ totalLen := len(TrienodeHistoryBlockPrefix) + common.HashLength + len(path) + 4
+ out := make([]byte, totalLen)
+
+ off := 0
+ off += copy(out[off:], TrienodeHistoryBlockPrefix)
+ off += copy(out[off:], addressHash.Bytes())
+ off += copy(out[off:], path)
+ copy(out[off:], buf4[:])
+
+ return out
+}
+
// transitionStateKey = transitionStatusKey + hash
func transitionStateKey(hash common.Hash) []byte {
return append(VerkleTransitionStatePrefix, hash.Bytes()...)
diff --git a/core/state/database_history.go b/core/state/database_history.go
index 314c56c4708..7bf4347004d 100644
--- a/core/state/database_history.go
+++ b/core/state/database_history.go
@@ -17,32 +17,34 @@
package state
import (
- "errors"
+ "fmt"
+ "sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/utils"
"github.com/ethereum/go-ethereum/triedb"
+ "github.com/ethereum/go-ethereum/triedb/database"
"github.com/ethereum/go-ethereum/triedb/pathdb"
)
-// historicReader wraps a historical state reader defined in path database,
+// historicalStateReader wraps a historical state reader defined in path database,
// providing historic state serving over the path scheme.
-//
-// TODO(rjl493456442): historicReader is not thread-safe and does not fully
-// comply with the StateReader interface requirements, needs to be fixed.
-// Currently, it is only used in a non-concurrent context, so it is safe for now.
-type historicReader struct {
+type historicalStateReader struct {
reader *pathdb.HistoricalStateReader
+ lock sync.Mutex // Lock for protecting concurrent read
+
}
-// newHistoricReader constructs a reader for historic state serving.
-func newHistoricReader(r *pathdb.HistoricalStateReader) *historicReader {
- return &historicReader{reader: r}
+// newHistoricalStateReader constructs a reader for historical state serving.
+func newHistoricalStateReader(r *pathdb.HistoricalStateReader) *historicalStateReader {
+ return &historicalStateReader{reader: r}
}
// Account implements StateReader, retrieving the account specified by the address.
@@ -51,7 +53,10 @@ func newHistoricReader(r *pathdb.HistoricalStateReader) *historicReader {
// the requested account is not yet covered by the snapshot.
//
// The returned account might be nil if it's not existent.
-func (r *historicReader) Account(addr common.Address) (*types.StateAccount, error) {
+func (r *historicalStateReader) Account(addr common.Address) (*types.StateAccount, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
account, err := r.reader.Account(addr)
if err != nil {
return nil, err
@@ -81,7 +86,10 @@ func (r *historicReader) Account(addr common.Address) (*types.StateAccount, erro
// the requested storage slot is not yet covered by the snapshot.
//
// The returned storage slot might be empty if it's not existent.
-func (r *historicReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) {
+func (r *historicalStateReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
blob, err := r.reader.Storage(addr, key)
if err != nil {
return common.Hash{}, err
@@ -98,9 +106,128 @@ func (r *historicReader) Storage(addr common.Address, key common.Hash) (common.H
return slot, nil
}
-// HistoricDB is the implementation of Database interface, with the ability to
+// historicalTrieOpener is a wrapper of pathdb.HistoricalNodeReader, implementing
+// the database.NodeDatabase by adding NodeReader function.
+type historicalTrieOpener struct {
+ root common.Hash
+ reader *pathdb.HistoricalNodeReader
+}
+
+// newHistoricalTrieOpener constructs the historical trie opener.
+func newHistoricalTrieOpener(root common.Hash, reader *pathdb.HistoricalNodeReader) *historicalTrieOpener {
+ return &historicalTrieOpener{
+ root: root,
+ reader: reader,
+ }
+}
+
+// NodeReader implements database.NodeDatabase, returning a node reader of a
+// specified state.
+func (o *historicalTrieOpener) NodeReader(root common.Hash) (database.NodeReader, error) {
+ if root != o.root {
+ return nil, fmt.Errorf("state %x is not available", root)
+ }
+ return o.reader, nil
+}
+
+// historicalTrieReader wraps a historical node reader defined in path database,
+// providing historical node serving over the path scheme.
+type historicalTrieReader struct {
+ root common.Hash
+ opener *historicalTrieOpener
+ tr Trie
+
+ subRoots map[common.Address]common.Hash // Set of storage roots, cached when the account is resolved
+ subTries map[common.Address]Trie // Group of storage tries, cached when it's resolved
+ lock sync.Mutex // Lock for protecting concurrent read
+}
+
+// newHistoricalTrieReader constructs a reader for historical trie node serving.
+func newHistoricalTrieReader(root common.Hash, r *pathdb.HistoricalNodeReader) (*historicalTrieReader, error) {
+ opener := newHistoricalTrieOpener(root, r)
+ tr, err := trie.NewStateTrie(trie.StateTrieID(root), opener)
+ if err != nil {
+ return nil, err
+ }
+ return &historicalTrieReader{
+ root: root,
+ opener: opener,
+ tr: tr,
+ subRoots: make(map[common.Address]common.Hash),
+ subTries: make(map[common.Address]Trie),
+ }, nil
+}
+
+// account is the inner version of Account and assumes the r.lock is already held.
+func (r *historicalTrieReader) account(addr common.Address) (*types.StateAccount, error) {
+ account, err := r.tr.GetAccount(addr)
+ if err != nil {
+ return nil, err
+ }
+ if account == nil {
+ r.subRoots[addr] = types.EmptyRootHash
+ } else {
+ r.subRoots[addr] = account.Root
+ }
+ return account, nil
+}
+
+// Account implements StateReader, retrieving the account specified by the address.
+//
+// An error will be returned if the associated snapshot is already stale or
+// the requested account is not yet covered by the snapshot.
+//
+// The returned account might be nil if it's not existent.
+func (r *historicalTrieReader) Account(addr common.Address) (*types.StateAccount, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ return r.account(addr)
+}
+
+// Storage implements StateReader, retrieving the storage slot specified by the
+// address and slot key.
+//
+// An error will be returned if the associated snapshot is already stale or
+// the requested storage slot is not yet covered by the snapshot.
+//
+// The returned storage slot might be empty if it's not existent.
+func (r *historicalTrieReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ tr, found := r.subTries[addr]
+ if !found {
+ root, ok := r.subRoots[addr]
+
+ // The storage slot is accessed without account caching. It's unexpected
+ // behavior but try to resolve the account first anyway.
+ if !ok {
+ _, err := r.account(addr)
+ if err != nil {
+ return common.Hash{}, err
+ }
+ root = r.subRoots[addr]
+ }
+ var err error
+ tr, err = trie.NewStateTrie(trie.StorageTrieID(r.root, crypto.Keccak256Hash(addr.Bytes()), root), r.opener)
+ if err != nil {
+ return common.Hash{}, err
+ }
+ r.subTries[addr] = tr
+ }
+ ret, err := tr.GetStorage(addr, key.Bytes())
+ if err != nil {
+ return common.Hash{}, err
+ }
+ var value common.Hash
+ value.SetBytes(ret)
+ return value, nil
+}
+
+// HistoricalStateDB is the implementation of Database interface, with the ability to
// access historical state.
-type HistoricDB struct {
+type HistoricalStateDB struct {
disk ethdb.KeyValueStore
triedb *triedb.Database
codeCache *lru.SizeConstrainedCache[common.Hash, []byte]
@@ -108,9 +235,9 @@ type HistoricDB struct {
pointCache *utils.PointCache
}
-// NewHistoricDatabase creates a historic state database.
-func NewHistoricDatabase(disk ethdb.KeyValueStore, triedb *triedb.Database) *HistoricDB {
- return &HistoricDB{
+// NewHistoricalDatabase creates a historical state database.
+func NewHistoricalDatabase(disk ethdb.KeyValueStore, triedb *triedb.Database) *HistoricalStateDB {
+ return &HistoricalStateDB{
disk: disk,
triedb: triedb,
codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize),
@@ -120,36 +247,69 @@ func NewHistoricDatabase(disk ethdb.KeyValueStore, triedb *triedb.Database) *His
}
// Reader implements Database interface, returning a reader of the specific state.
-func (db *HistoricDB) Reader(stateRoot common.Hash) (Reader, error) {
- hr, err := db.triedb.HistoricReader(stateRoot)
+func (db *HistoricalStateDB) Reader(stateRoot common.Hash) (Reader, error) {
+ var readers []StateReader
+ sr, err := db.triedb.HistoricalStateReader(stateRoot)
+ if err == nil {
+ readers = append(readers, newHistoricalStateReader(sr))
+ }
+ nr, err := db.triedb.HistoricalNodeReader(stateRoot)
+ if err == nil {
+ tr, err := newHistoricalTrieReader(stateRoot, nr)
+ if err == nil {
+ readers = append(readers, tr)
+ }
+ }
+ if len(readers) == 0 {
+ return nil, fmt.Errorf("historical state %x is not available", stateRoot)
+ }
+ combined, err := newMultiStateReader(readers...)
if err != nil {
return nil, err
}
- return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newHistoricReader(hr)), nil
+ return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), combined), nil
}
// OpenTrie opens the main account trie. It's not supported by historic database.
-func (db *HistoricDB) OpenTrie(root common.Hash) (Trie, error) {
- return nil, errors.New("not implemented")
+func (db *HistoricalStateDB) OpenTrie(root common.Hash) (Trie, error) {
+ // TODO(rjl493456442) reuse the same historical node reader
+ nr, err := db.triedb.HistoricalNodeReader(root)
+ if err != nil {
+ return nil, err
+ }
+ tr, err := trie.NewStateTrie(trie.StateTrieID(root), newHistoricalTrieOpener(root, nr))
+ if err != nil {
+ return nil, err
+ }
+ return tr, nil
}
// OpenStorageTrie opens the storage trie of an account. It's not supported by
// historic database.
-func (db *HistoricDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, trie Trie) (Trie, error) {
- return nil, errors.New("not implemented")
+func (db *HistoricalStateDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, _ Trie) (Trie, error) {
+ // TODO(rjl493456442) reuse the same historical node reader
+ nr, err := db.triedb.HistoricalNodeReader(stateRoot)
+ if err != nil {
+ return nil, err
+ }
+ tr, err := trie.NewStateTrie(trie.StorageTrieID(stateRoot, crypto.Keccak256Hash(address.Bytes()), root), newHistoricalTrieOpener(stateRoot, nr))
+ if err != nil {
+ return nil, err
+ }
+ return tr, nil
}
// PointCache returns the cache holding points used in verkle tree key computation
-func (db *HistoricDB) PointCache() *utils.PointCache {
+func (db *HistoricalStateDB) PointCache() *utils.PointCache {
return db.pointCache
}
// TrieDB returns the underlying trie database for managing trie nodes.
-func (db *HistoricDB) TrieDB() *triedb.Database {
+func (db *HistoricalStateDB) TrieDB() *triedb.Database {
return db.triedb
}
// Snapshot returns the underlying state snapshot.
-func (db *HistoricDB) Snapshot() *snapshot.Tree {
+func (db *HistoricalStateDB) Snapshot() *snapshot.Tree {
return nil
}
diff --git a/eth/api_backend.go b/eth/api_backend.go
index 3ae73e78af1..f7f077816a5 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -238,7 +238,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B
}
stateDb, err := b.eth.BlockChain().StateAt(header.Root)
if err != nil {
- stateDb, err = b.eth.BlockChain().HistoricState(header.Root)
+ stateDb, err = b.eth.BlockChain().HistoricalState(header.Root)
if err != nil {
return nil, nil, err
}
@@ -263,7 +263,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN
}
stateDb, err := b.eth.BlockChain().StateAt(header.Root)
if err != nil {
- stateDb, err = b.eth.BlockChain().HistoricState(header.Root)
+ stateDb, err = b.eth.BlockChain().HistoricalState(header.Root)
if err != nil {
return nil, nil, err
}
diff --git a/eth/backend.go b/eth/backend.go
index 3bfe0765f4c..dbb61c80dea 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -230,6 +230,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
SnapshotLimit: config.SnapshotCache,
Preimages: config.Preimages,
StateHistory: config.StateHistory,
+ TrienodeHistory: config.TrienodeHistory,
StateScheme: scheme,
ChainHistoryMode: config.HistoryMode,
TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)),
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index 4fe24c0fe07..f82f03493fa 100644
--- a/eth/ethconfig/config.go
+++ b/eth/ethconfig/config.go
@@ -56,6 +56,7 @@ var Defaults = Config{
TransactionHistory: 2350000,
LogHistory: 2350000,
StateHistory: params.FullImmutabilityThreshold,
+ TrienodeHistory: -1,
DatabaseCache: 512,
TrieCleanCache: 154,
TrieDirtyCache: 256,
@@ -105,6 +106,7 @@ type Config struct {
LogNoHistory bool `toml:",omitempty"` // No log search index is maintained.
LogExportCheckpoints string // export log index checkpoints to file
StateHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head whose state histories are reserved.
+ TrienodeHistory int64 `toml:",omitempty"` // Number of blocks from the chain head for which trienode histories are retained
// State scheme represents the scheme used to store ethereum states and trie
// nodes on top. It can be 'hash', 'path', or none which means use the scheme
diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go
index 50eb5c4161f..530ef71997f 100644
--- a/eth/ethconfig/gen_config.go
+++ b/eth/ethconfig/gen_config.go
@@ -31,6 +31,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
LogNoHistory bool `toml:",omitempty"`
LogExportCheckpoints string
StateHistory uint64 `toml:",omitempty"`
+ TrienodeHistory int64 `toml:",omitempty"`
StateScheme string `toml:",omitempty"`
RequiredBlocks map[uint64]common.Hash `toml:"-"`
SkipBcVersionCheck bool `toml:"-"`
@@ -76,6 +77,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
enc.LogNoHistory = c.LogNoHistory
enc.LogExportCheckpoints = c.LogExportCheckpoints
enc.StateHistory = c.StateHistory
+ enc.TrienodeHistory = c.TrienodeHistory
enc.StateScheme = c.StateScheme
enc.RequiredBlocks = c.RequiredBlocks
enc.SkipBcVersionCheck = c.SkipBcVersionCheck
@@ -125,6 +127,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
LogNoHistory *bool `toml:",omitempty"`
LogExportCheckpoints *string
StateHistory *uint64 `toml:",omitempty"`
+ TrienodeHistory *int64 `toml:",omitempty"`
StateScheme *string `toml:",omitempty"`
RequiredBlocks map[uint64]common.Hash `toml:"-"`
SkipBcVersionCheck *bool `toml:"-"`
@@ -201,6 +204,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
if dec.StateHistory != nil {
c.StateHistory = *dec.StateHistory
}
+ if dec.TrienodeHistory != nil {
+ c.TrienodeHistory = *dec.TrienodeHistory
+ }
if dec.StateScheme != nil {
c.StateScheme = *dec.StateScheme
}
diff --git a/eth/state_accessor.go b/eth/state_accessor.go
index 79c91043a3c..8fead857d3d 100644
--- a/eth/state_accessor.go
+++ b/eth/state_accessor.go
@@ -182,7 +182,7 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro
if err == nil {
return statedb, noopReleaser, nil
}
- statedb, err = eth.blockchain.HistoricState(block.Root())
+ statedb, err = eth.blockchain.HistoricalState(block.Root())
if err == nil {
return statedb, noopReleaser, nil
}
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index ebb8ece7301..f5c5af08b87 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -47,7 +47,6 @@ import (
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
- "github.com/ethereum/go-ethereum/trie"
)
// estimateGasErrorRatio is the amount of overestimation eth_estimateGas is
@@ -388,8 +387,7 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
if len(keys) > 0 {
var storageTrie state.Trie
if storageRoot != types.EmptyRootHash && storageRoot != (common.Hash{}) {
- id := trie.StorageTrieID(header.Root, crypto.Keccak256Hash(address.Bytes()), storageRoot)
- st, err := trie.NewStateTrie(id, statedb.Database().TrieDB())
+ st, err := statedb.Database().OpenStorageTrie(header.Root, address, storageRoot, nil)
if err != nil {
return nil, err
}
@@ -420,7 +418,7 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
}
}
// Create the accountProof.
- tr, err := trie.NewStateTrie(trie.StateTrieID(header.Root), statedb.Database().TrieDB())
+ tr, err := statedb.Database().OpenTrie(header.Root)
if err != nil {
return nil, err
}
diff --git a/triedb/database.go b/triedb/database.go
index d2637bd909a..18e40754e25 100644
--- a/triedb/database.go
+++ b/triedb/database.go
@@ -129,13 +129,22 @@ func (db *Database) StateReader(blockRoot common.Hash) (database.StateReader, er
return db.backend.StateReader(blockRoot)
}
-// HistoricReader constructs a reader for accessing the requested historic state.
-func (db *Database) HistoricReader(root common.Hash) (*pathdb.HistoricalStateReader, error) {
+// HistoricalStateReader constructs a reader for accessing the historical state.
+func (db *Database) HistoricalStateReader(root common.Hash) (*pathdb.HistoricalStateReader, error) {
pdb, ok := db.backend.(*pathdb.Database)
if !ok {
return nil, errors.New("not supported")
}
- return pdb.HistoricReader(root)
+ return pdb.HistoricalStateReader(root)
+}
+
+// HistoricalNodeReader constructs a reader for accessing the historical trie node.
+func (db *Database) HistoricalNodeReader(root common.Hash) (*pathdb.HistoricalNodeReader, error) {
+ pdb, ok := db.backend.(*pathdb.Database)
+ if !ok {
+ return nil, errors.New("not supported")
+ }
+ return pdb.HistoricalNodeReader(root)
}
// Update performs a state transition by committing dirty nodes contained in the
diff --git a/triedb/pathdb/buffer.go b/triedb/pathdb/buffer.go
index 138962110f0..3f5ebebf912 100644
--- a/triedb/pathdb/buffer.go
+++ b/triedb/pathdb/buffer.go
@@ -132,7 +132,7 @@ func (b *buffer) size() uint64 {
// flush persists the in-memory dirty trie node into the disk if the configured
// memory threshold is reached. Note, all data must be written atomically.
-func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
+func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezers []ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
if b.done != nil {
panic("duplicated flush operation")
}
@@ -165,7 +165,10 @@ func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.A
//
// This step is crucial to guarantee that the corresponding state history remains
// available for state rollback.
- if freezer != nil {
+ for _, freezer := range freezers {
+ if freezer == nil {
+ continue
+ }
if err := freezer.SyncAncient(); err != nil {
b.flushErr = err
return
diff --git a/triedb/pathdb/config.go b/triedb/pathdb/config.go
index 3745a63eddd..0da8604b6cf 100644
--- a/triedb/pathdb/config.go
+++ b/triedb/pathdb/config.go
@@ -53,6 +53,7 @@ var (
// Defaults contains default settings for Ethereum mainnet.
var Defaults = &Config{
StateHistory: params.FullImmutabilityThreshold,
+ TrienodeHistory: -1,
EnableStateIndexing: false,
TrieCleanSize: defaultTrieCleanSize,
StateCleanSize: defaultStateCleanSize,
@@ -61,14 +62,16 @@ var Defaults = &Config{
// ReadOnly is the config in order to open database in read only mode.
var ReadOnly = &Config{
- ReadOnly: true,
- TrieCleanSize: defaultTrieCleanSize,
- StateCleanSize: defaultStateCleanSize,
+ ReadOnly: true,
+ TrienodeHistory: -1,
+ TrieCleanSize: defaultTrieCleanSize,
+ StateCleanSize: defaultStateCleanSize,
}
// Config contains the settings for database.
type Config struct {
StateHistory uint64 // Number of recent blocks to maintain state history for, 0: full chain
+ TrienodeHistory int64 // Number of recent blocks to maintain trienode history for, 0: full chain, negative: disable
EnableStateIndexing bool // Whether to enable state history indexing for external state access
TrieCleanSize int // Maximum memory allowance (in bytes) for caching clean trie data
StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data
@@ -108,6 +111,13 @@ func (c *Config) fields() []interface{} {
} else {
list = append(list, "state-history", fmt.Sprintf("last %d blocks", c.StateHistory))
}
+ if c.TrienodeHistory >= 0 {
+ if c.TrienodeHistory == 0 {
+ list = append(list, "trie-history", "entire chain")
+ } else {
+ list = append(list, "trie-history", fmt.Sprintf("last %d blocks", c.TrienodeHistory))
+ }
+ }
if c.EnableStateIndexing {
list = append(list, "index-history", true)
}
diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go
index 546d2e0301f..5c122133106 100644
--- a/triedb/pathdb/database.go
+++ b/triedb/pathdb/database.go
@@ -137,6 +137,9 @@ type Database struct {
stateFreezer ethdb.ResettableAncientStore // Freezer for storing state histories, nil possible in tests
stateIndexer *historyIndexer // History indexer historical state data, nil possible
+ trienodeFreezer ethdb.ResettableAncientStore // Freezer for storing trienode histories, nil possible in tests
+ trienodeIndexer *historyIndexer // History indexer for historical trienode data
+
lock sync.RWMutex // Lock to prevent mutations from happening at the same time
}
@@ -169,11 +172,14 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
// and in-memory layer journal.
db.tree = newLayerTree(db.loadLayers())
- // Repair the state history, which might not be aligned with the state
- // in the key-value store due to an unclean shutdown.
- if err := db.repairHistory(); err != nil {
- log.Crit("Failed to repair state history", "err", err)
+ // Repair the history, which might not be aligned with the persistent
+ // state in the key-value store due to an unclean shutdown.
+ states, trienodes, err := repairHistory(db.diskdb, isVerkle, db.config.ReadOnly, db.tree.bottom().stateID(), db.config.TrienodeHistory >= 0)
+ if err != nil {
+ log.Crit("Failed to repair history", "err", err)
}
+ db.stateFreezer, db.trienodeFreezer = states, trienodes
+
// Disable database in case node is still in the initial state sync stage.
if rawdb.ReadSnapSyncStatusFlag(diskdb) == rawdb.StateSyncRunning && !db.readOnly {
if err := db.Disable(); err != nil {
@@ -187,11 +193,8 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
if err := db.setStateGenerator(); err != nil {
log.Crit("Failed to setup the generator", "err", err)
}
- // TODO (rjl493456442) disable the background indexing in read-only mode
- if db.stateFreezer != nil && db.config.EnableStateIndexing {
- db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID(), typeStateHistory)
- log.Info("Enabled state history indexing")
- }
+ db.setHistoryIndexer()
+
fields := config.fields()
if db.isVerkle {
fields = append(fields, "verkle", true)
@@ -200,59 +203,28 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
return db
}
-// repairHistory truncates leftover state history objects, which may occur due
-// to an unclean shutdown or other unexpected reasons.
-func (db *Database) repairHistory() error {
- // Open the freezer for state history. This mechanism ensures that
- // only one database instance can be opened at a time to prevent
- // accidental mutation.
- ancient, err := db.diskdb.AncientDatadir()
- if err != nil {
- // TODO error out if ancient store is disabled. A tons of unit tests
- // disable the ancient store thus the error here will immediately fail
- // all of them. Fix the tests first.
- return nil
- }
- freezer, err := rawdb.NewStateFreezer(ancient, db.isVerkle, db.readOnly)
- if err != nil {
- log.Crit("Failed to open state history freezer", "err", err)
+// setHistoryIndexer initializes the indexers for both state history and
+// trienode history if available. Note that this function may be called while
+// existing indexers are still running, so they must be closed beforehand.
+func (db *Database) setHistoryIndexer() {
+ // TODO (rjl493456442) disable the background indexing in read-only mode
+ if !db.config.EnableStateIndexing {
+ return
}
- db.stateFreezer = freezer
-
- // Reset the entire state histories if the trie database is not initialized
- // yet. This action is necessary because these state histories are not
- // expected to exist without an initialized trie database.
- id := db.tree.bottom().stateID()
- if id == 0 {
- frozen, err := db.stateFreezer.Ancients()
- if err != nil {
- log.Crit("Failed to retrieve head of state history", "err", err)
- }
- if frozen != 0 {
- // Purge all state history indexing data first
- batch := db.diskdb.NewBatch()
- rawdb.DeleteStateHistoryIndexMetadata(batch)
- rawdb.DeleteStateHistoryIndex(batch)
- if err := batch.Write(); err != nil {
- log.Crit("Failed to purge state history index", "err", err)
- }
- if err := db.stateFreezer.Reset(); err != nil {
- log.Crit("Failed to reset state histories", "err", err)
- }
- log.Info("Truncated extraneous state history")
+ if db.stateFreezer != nil {
+ if db.stateIndexer != nil {
+ db.stateIndexer.close()
}
- return nil
- }
- // Truncate the extra state histories above in freezer in case it's not
- // aligned with the disk layer. It might happen after a unclean shutdown.
- pruned, err := truncateFromHead(db.stateFreezer, typeStateHistory, id)
- if err != nil {
- log.Crit("Failed to truncate extra state histories", "err", err)
+ db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID(), typeStateHistory)
+ log.Info("Enabled state history indexing")
}
- if pruned != 0 {
- log.Warn("Truncated extra state histories", "number", pruned)
+ if db.trienodeFreezer != nil {
+ if db.trienodeIndexer != nil {
+ db.trienodeIndexer.close()
+ }
+ db.trienodeIndexer = newHistoryIndexer(db.diskdb, db.trienodeFreezer, db.tree.bottom().stateID(), typeTrienodeHistory)
+ log.Info("Enabled trienode history indexing")
}
- return nil
}
// setStateGenerator loads the state generation progress marker and potentially
@@ -333,8 +305,13 @@ func (db *Database) Update(root common.Hash, parentRoot common.Hash, block uint6
if err := db.modifyAllowed(); err != nil {
return err
}
- // TODO(rjl493456442) tracking the origins in the following PRs.
- if err := db.tree.add(root, parentRoot, block, NewNodeSetWithOrigin(nodes.Nodes(), nil), states); err != nil {
+ var nodesWithOrigins *nodeSetWithOrigin
+ if db.config.TrienodeHistory >= 0 {
+ nodesWithOrigins = NewNodeSetWithOrigin(nodes.NodeAndOrigins())
+ } else {
+ nodesWithOrigins = NewNodeSetWithOrigin(nodes.Nodes(), nil)
+ }
+ if err := db.tree.add(root, parentRoot, block, nodesWithOrigins, states); err != nil {
return err
}
// Keep 128 diff layers in the memory, persistent layer is 129th.
@@ -422,18 +399,9 @@ func (db *Database) Enable(root common.Hash) error {
// all root->id mappings should be removed as well. Since
// mappings can be huge and might take a while to clear
// them, just leave them in disk and wait for overwriting.
- if db.stateFreezer != nil {
- // Purge all state history indexing data first
- batch.Reset()
- rawdb.DeleteStateHistoryIndexMetadata(batch)
- rawdb.DeleteStateHistoryIndex(batch)
- if err := batch.Write(); err != nil {
- return err
- }
- if err := db.stateFreezer.Reset(); err != nil {
- return err
- }
- }
+ purgeHistory(db.stateFreezer, db.diskdb, typeStateHistory)
+ purgeHistory(db.trienodeFreezer, db.diskdb, typeTrienodeHistory)
+
// Re-enable the database as the final step.
db.waitSync = false
rawdb.WriteSnapSyncStatusFlag(db.diskdb, rawdb.StateSyncFinished)
@@ -446,11 +414,8 @@ func (db *Database) Enable(root common.Hash) error {
// To ensure the history indexer always matches the current state, we must:
// 1. Close any existing indexer
// 2. Re-initialize the indexer so it starts indexing from the new state root.
- if db.stateIndexer != nil && db.stateFreezer != nil && db.config.EnableStateIndexing {
- db.stateIndexer.close()
- db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID(), typeStateHistory)
- log.Info("Re-enabled state history indexing")
- }
+ db.setHistoryIndexer()
+
log.Info("Rebuilt trie database", "root", root)
return nil
}
@@ -506,6 +471,12 @@ func (db *Database) Recover(root common.Hash) error {
if err != nil {
return err
}
+ if db.trienodeFreezer != nil {
+ _, err = truncateFromHead(db.trienodeFreezer, typeTrienodeHistory, dl.stateID())
+ if err != nil {
+ return err
+ }
+ }
log.Debug("Recovered state", "root", root, "elapsed", common.PrettyDuration(time.Since(start)))
return nil
}
@@ -566,11 +537,21 @@ func (db *Database) Close() error {
if db.stateIndexer != nil {
db.stateIndexer.close()
}
+ if db.trienodeIndexer != nil {
+ db.trienodeIndexer.close()
+ }
// Close the attached state history freezer.
- if db.stateFreezer == nil {
- return nil
+ if db.stateFreezer != nil {
+ if err := db.stateFreezer.Close(); err != nil {
+ return err
+ }
+ }
+ if db.trienodeFreezer != nil {
+ if err := db.trienodeFreezer.Close(); err != nil {
+ return err
+ }
}
- return db.stateFreezer.Close()
+ return nil
}
// Size returns the current storage size of the memory cache in front of the
diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go
index 76f3f5a46ea..2244d7f8d7c 100644
--- a/triedb/pathdb/disklayer.go
+++ b/triedb/pathdb/disklayer.go
@@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
@@ -386,6 +387,41 @@ func (dl *diskLayer) writeStateHistory(diff *diffLayer) (bool, error) {
return false, nil
}
+// writeTrienodeHistory stores the trienode history and indexes if indexing is
+// permitted.
+func (dl *diskLayer) writeTrienodeHistory(diff *diffLayer) error {
+ // Short circuit if trienode history is not permitted
+ if dl.db.trienodeFreezer == nil {
+ return nil
+ }
+ // Bail out with an error if writing the trienode history fails.
+ // This can happen, for example, if the device is full.
+ err := writeTrienodeHistory(dl.db.trienodeFreezer, diff)
+ if err != nil {
+ return err
+ }
+ // Notify the trienode history indexer for newly created history
+ if dl.db.trienodeIndexer != nil {
+ if err := dl.db.trienodeIndexer.extend(diff.stateID()); err != nil {
+ return err
+ }
+ }
+ // Determine if the persisted history object has exceeded the
+ // configured limitation.
+ limit := dl.db.config.TrienodeHistory
+ if limit == 0 {
+ return nil
+ }
+ newFirst := diff.stateID() - uint64(limit) + 1 // the id of first history **after truncation**
+
+ pruned, err := truncateFromTail(dl.db.trienodeFreezer, typeTrienodeHistory, newFirst-1)
+ if err != nil {
+ return err
+ }
+ log.Debug("Pruned trienode history", "items", pruned, "tailid", newFirst)
+ return nil
+}
+
// commit merges the given bottom-most diff layer into the node buffer
// and returns a newly constructed disk layer. Note the current disk
// layer must be tagged as stale first to prevent re-access.
@@ -400,6 +436,13 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
if err != nil {
return nil, err
}
+ // Construct and store the trienode history first. If crash happens after
+ // storing the trienode history but without flushing the corresponding
+ // states(journal), the stored trienode history will be truncated from head
+ // in the next restart.
+ if err := dl.writeTrienodeHistory(bottom); err != nil {
+ return nil, err
+ }
// Mark the diskLayer as stale before applying any mutations on top.
dl.stale = true
@@ -448,7 +491,7 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
// Freeze the live buffer and schedule background flushing
dl.frozen = combined
- dl.frozen.flush(bottom.root, dl.db.diskdb, dl.db.stateFreezer, progress, dl.nodes, dl.states, bottom.stateID(), func() {
+ dl.frozen.flush(bottom.root, dl.db.diskdb, []ethdb.AncientWriter{dl.db.stateFreezer, dl.db.trienodeFreezer}, progress, dl.nodes, dl.states, bottom.stateID(), func() {
// Resume the background generation if it's not completed yet.
// The generator is assumed to be available if the progress is
// not nil.
@@ -504,12 +547,17 @@ func (dl *diskLayer) revert(h *stateHistory) (*diskLayer, error) {
dl.stale = true
- // Unindex the corresponding state history
+ // Unindex the corresponding history
if dl.db.stateIndexer != nil {
if err := dl.db.stateIndexer.shorten(dl.id); err != nil {
return nil, err
}
}
+ if dl.db.trienodeIndexer != nil {
+ if err := dl.db.trienodeIndexer.shorten(dl.id); err != nil {
+ return nil, err
+ }
+ }
// State change may be applied to node buffer, or the persistent
// state, depends on if node buffer is empty or not. If the node
// buffer is not empty, it means that the state transition that
diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go
index ae022236fef..d11d89666b3 100644
--- a/triedb/pathdb/history.go
+++ b/triedb/pathdb/history.go
@@ -22,6 +22,7 @@ import (
"iter"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
@@ -32,6 +33,9 @@ type historyType uint8
const (
// typeStateHistory indicates history data related to account or storage changes.
typeStateHistory historyType = 0
+
+ // typeTrienodeHistory indicates history data related to trie node changes.
+ typeTrienodeHistory historyType = 1
)
// String returns the string format representation.
@@ -39,6 +43,8 @@ func (h historyType) String() string {
switch h {
case typeStateHistory:
return "state"
+ case typeTrienodeHistory:
+ return "trienode"
default:
return fmt.Sprintf("unknown type: %d", h)
}
@@ -48,8 +54,9 @@ func (h historyType) String() string {
type elementType uint8
const (
- typeAccount elementType = 0 // represents the account data
- typeStorage elementType = 1 // represents the storage slot data
+ typeAccount elementType = 0 // represents the account data
+ typeStorage elementType = 1 // represents the storage slot data
+ typeTrienode elementType = 2 // represents the trie node data
)
// String returns the string format representation.
@@ -59,6 +66,8 @@ func (e elementType) String() string {
return "account"
case typeStorage:
return "storage"
+ case typeTrienode:
+ return "trienode"
default:
return fmt.Sprintf("unknown element type: %d", e)
}
@@ -69,11 +78,14 @@ func toHistoryType(typ elementType) historyType {
if typ == typeAccount || typ == typeStorage {
return typeStateHistory
}
+ if typ == typeTrienode {
+ return typeTrienodeHistory
+ }
panic(fmt.Sprintf("unknown element type %v", typ))
}
// stateIdent represents the identifier of a state element, which can be
-// an account or a storage slot.
+// an account, a storage slot or a trienode.
type stateIdent struct {
typ elementType
@@ -91,6 +103,12 @@ type stateIdent struct {
//
// This field is null if the identifier refers to an account or a trie node.
storageHash common.Hash
+
+ // The trie node path within the trie.
+ //
+ // This field is null if the identifier refers to an account or a storage slot.
+ // String type is chosen to make stateIdent comparable.
+ path string
}
// String returns the string format state identifier.
@@ -98,7 +116,10 @@ func (ident stateIdent) String() string {
if ident.typ == typeAccount {
return ident.addressHash.Hex()
}
- return ident.addressHash.Hex() + ident.storageHash.Hex()
+ if ident.typ == typeStorage {
+ return ident.addressHash.Hex() + ident.storageHash.Hex()
+ }
+ return ident.addressHash.Hex() + ident.path
}
// newAccountIdent constructs a state identifier for an account.
@@ -120,8 +141,18 @@ func newStorageIdent(addressHash common.Hash, storageHash common.Hash) stateIden
}
}
-// stateIdentQuery is the extension of stateIdent by adding the account address
-// and raw storage key.
+// newTrienodeIdent constructs a state identifier for a trie node.
+// The address denotes the address hash of the associated account;
+// the path denotes the path of the node within the trie;
+func newTrienodeIdent(addressHash common.Hash, path string) stateIdent {
+ return stateIdent{
+ typ: typeTrienode,
+ addressHash: addressHash,
+ path: path,
+ }
+}
+
+// stateIdentQuery is the extension of stateIdent by adding the raw storage key.
type stateIdentQuery struct {
stateIdent
@@ -150,8 +181,19 @@ func newStorageIdentQuery(address common.Address, addressHash common.Hash, stora
}
}
-// history defines the interface of historical data, implemented by stateHistory
-// and trienodeHistory (in the near future).
+// newTrienodeIdentQuery constructs a state identifier for a trie node.
+// the addressHash denotes the address hash of the associated account;
+// the path denotes the path of the node within the trie;
+//
+// nolint:unused
+func newTrienodeIdentQuery(addrHash common.Hash, path []byte) stateIdentQuery {
+ return stateIdentQuery{
+ stateIdent: newTrienodeIdent(addrHash, string(path)),
+ }
+}
+
+// history defines the interface of historical data, shared by stateHistory
+// and trienodeHistory.
type history interface {
// typ returns the historical data type held in the history.
typ() historyType
@@ -221,3 +263,133 @@ func truncateFromTail(store ethdb.AncientStore, typ historyType, ntail uint64) (
// Associated root->id mappings are left in the database.
return int(ntail - otail), nil
}
+
+// purgeHistory resets the history and also purges the associated index data.
+func purgeHistory(store ethdb.ResettableAncientStore, disk ethdb.KeyValueStore, typ historyType) {
+ if store == nil {
+ return
+ }
+ frozen, err := store.Ancients()
+ if err != nil {
+ log.Crit("Failed to retrieve head of history", "type", typ, "err", err)
+ }
+ if frozen == 0 {
+ return
+ }
+ // Purge all state history indexing data first
+ batch := disk.NewBatch()
+ if typ == typeStateHistory {
+ rawdb.DeleteStateHistoryIndexMetadata(batch)
+ rawdb.DeleteStateHistoryIndexes(batch)
+ } else {
+ rawdb.DeleteTrienodeHistoryIndexMetadata(batch)
+ rawdb.DeleteTrienodeHistoryIndexes(batch)
+ }
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed to purge history index", "type", typ, "err", err)
+ }
+ if err := store.Reset(); err != nil {
+ log.Crit("Failed to reset history", "type", typ, "err", err)
+ }
+ log.Info("Truncated extraneous history", "type", typ)
+}
+
+// syncHistory explicitly sync the provided history stores.
+func syncHistory(stores ...ethdb.AncientWriter) error {
+ for _, store := range stores {
+ if store == nil {
+ continue
+ }
+ if err := store.SyncAncient(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// repairHistory truncates any leftover history objects in either the state
+// history or the trienode history, which may occur due to an unclean shutdown
+// or other unexpected events.
+//
+// Additionally, this mechanism ensures that the state history and trienode
+// history remain aligned. Since the trienode history is optional and not
+// required by regular users, a gap between the trienode history and the
+// persistent state may appear if the trienode history was disabled during the
+// previous run. This process detects and resolves such gaps, preventing
+// unexpected panics.
+func repairHistory(db ethdb.Database, isVerkle bool, readOnly bool, stateID uint64, enableTrienode bool) (ethdb.ResettableAncientStore, ethdb.ResettableAncientStore, error) {
+ ancient, err := db.AncientDatadir()
+ if err != nil {
+ // TODO error out if ancient store is disabled. A tons of unit tests
+ // disable the ancient store thus the error here will immediately fail
+ // all of them. Fix the tests first.
+ return nil, nil, nil
+ }
+ // State history is mandatory as it is the key component that ensures
+ // resilience to deep reorgs.
+ states, err := rawdb.NewStateFreezer(ancient, isVerkle, readOnly)
+ if err != nil {
+ log.Crit("Failed to open state history freezer", "err", err)
+ }
+
+ // Trienode history is optional and only required for building archive
+ // node with state proofs.
+ var trienodes ethdb.ResettableAncientStore
+ if enableTrienode {
+ trienodes, err = rawdb.NewTrienodeFreezer(ancient, isVerkle, readOnly)
+ if err != nil {
+ log.Crit("Failed to open trienode history freezer", "err", err)
+ }
+ }
+
+ // Reset the both histories if the trie database is not initialized yet.
+ // This action is necessary because these histories are not expected
+ // to exist without an initialized trie database.
+ if stateID == 0 {
+ purgeHistory(states, db, typeStateHistory)
+ purgeHistory(trienodes, db, typeTrienodeHistory)
+ return states, trienodes, nil
+ }
+ // Truncate excessive history entries in either the state history or
+ // the trienode history, ensuring both histories remain aligned with
+ // the state.
+ head, err := states.Ancients()
+ if err != nil {
+ return nil, nil, err
+ }
+ if stateID > head {
+ return nil, nil, fmt.Errorf("gap between state [#%d] and state history [#%d]", stateID, head)
+ }
+ if trienodes != nil {
+ th, err := trienodes.Ancients()
+ if err != nil {
+ return nil, nil, err
+ }
+ if stateID > th {
+ return nil, nil, fmt.Errorf("gap between state [#%d] and trienode history [#%d]", stateID, th)
+ }
+ if th != head {
+ log.Info("Histories are not aligned with each other", "state", head, "trienode", th)
+ head = min(head, th)
+ }
+ }
+ head = min(head, stateID)
+
+ // Truncate the extra history elements above in freezer in case it's not
+ // aligned with the state. It might happen after an unclean shutdown.
+ truncate := func(store ethdb.AncientStore, typ historyType, nhead uint64) {
+ if store == nil {
+ return
+ }
+ pruned, err := truncateFromHead(store, typ, head)
+ if err != nil {
+ log.Crit("Failed to truncate extra histories", "typ", typ, "err", err)
+ }
+ if pruned != 0 {
+ log.Warn("Truncated extra histories", "typ", typ, "number", pruned)
+ }
+ }
+ truncate(states, typeStateHistory, head)
+ truncate(trienodes, typeTrienodeHistory, head)
+ return states, trienodes, nil
+}
diff --git a/triedb/pathdb/history_index.go b/triedb/pathdb/history_index.go
index 47cee9820dd..5b4c91d7e61 100644
--- a/triedb/pathdb/history_index.go
+++ b/triedb/pathdb/history_index.go
@@ -376,6 +376,8 @@ func readStateIndex(ident stateIdent, db ethdb.KeyValueReader) []byte {
return rawdb.ReadAccountHistoryIndex(db, ident.addressHash)
case typeStorage:
return rawdb.ReadStorageHistoryIndex(db, ident.addressHash, ident.storageHash)
+ case typeTrienode:
+ return rawdb.ReadTrienodeHistoryIndex(db, ident.addressHash, []byte(ident.path))
default:
panic(fmt.Errorf("unknown type: %v", ident.typ))
}
@@ -389,6 +391,8 @@ func writeStateIndex(ident stateIdent, db ethdb.KeyValueWriter, data []byte) {
rawdb.WriteAccountHistoryIndex(db, ident.addressHash, data)
case typeStorage:
rawdb.WriteStorageHistoryIndex(db, ident.addressHash, ident.storageHash, data)
+ case typeTrienode:
+ rawdb.WriteTrienodeHistoryIndex(db, ident.addressHash, []byte(ident.path), data)
default:
panic(fmt.Errorf("unknown type: %v", ident.typ))
}
@@ -402,6 +406,8 @@ func deleteStateIndex(ident stateIdent, db ethdb.KeyValueWriter) {
rawdb.DeleteAccountHistoryIndex(db, ident.addressHash)
case typeStorage:
rawdb.DeleteStorageHistoryIndex(db, ident.addressHash, ident.storageHash)
+ case typeTrienode:
+ rawdb.DeleteTrienodeHistoryIndex(db, ident.addressHash, []byte(ident.path))
default:
panic(fmt.Errorf("unknown type: %v", ident.typ))
}
@@ -415,6 +421,8 @@ func readStateIndexBlock(ident stateIdent, db ethdb.KeyValueReader, id uint32) [
return rawdb.ReadAccountHistoryIndexBlock(db, ident.addressHash, id)
case typeStorage:
return rawdb.ReadStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id)
+ case typeTrienode:
+ return rawdb.ReadTrienodeHistoryIndexBlock(db, ident.addressHash, []byte(ident.path), id)
default:
panic(fmt.Errorf("unknown type: %v", ident.typ))
}
@@ -428,6 +436,8 @@ func writeStateIndexBlock(ident stateIdent, db ethdb.KeyValueWriter, id uint32,
rawdb.WriteAccountHistoryIndexBlock(db, ident.addressHash, id, data)
case typeStorage:
rawdb.WriteStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id, data)
+ case typeTrienode:
+ rawdb.WriteTrienodeHistoryIndexBlock(db, ident.addressHash, []byte(ident.path), id, data)
default:
panic(fmt.Errorf("unknown type: %v", ident.typ))
}
@@ -441,6 +451,8 @@ func deleteStateIndexBlock(ident stateIdent, db ethdb.KeyValueWriter, id uint32)
rawdb.DeleteAccountHistoryIndexBlock(db, ident.addressHash, id)
case typeStorage:
rawdb.DeleteStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id)
+ case typeTrienode:
+ rawdb.DeleteTrienodeHistoryIndexBlock(db, ident.addressHash, []byte(ident.path), id)
default:
panic(fmt.Errorf("unknown type: %v", ident.typ))
}
diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go
index d6185859291..368ff78d415 100644
--- a/triedb/pathdb/history_indexer.go
+++ b/triedb/pathdb/history_indexer.go
@@ -36,8 +36,10 @@ const (
// The batch size for reading state histories
historyReadBatch = 1000
- stateIndexV0 = uint8(0) // initial version of state index structure
- stateIndexVersion = stateIndexV0 // the current state index version
+ stateHistoryIndexV0 = uint8(0) // initial version of state index structure
+ stateHistoryIndexVersion = stateHistoryIndexV0 // the current state index version
+ trienodeHistoryIndexV0 = uint8(0) // initial version of trienode index structure
+ trienodeHistoryIndexVersion = trienodeHistoryIndexV0 // the current trienode index version
)
// indexVersion returns the latest index version for the given history type.
@@ -45,7 +47,9 @@ const (
func indexVersion(typ historyType) uint8 {
switch typ {
case typeStateHistory:
- return stateIndexVersion
+ return stateHistoryIndexVersion
+ case typeTrienodeHistory:
+ return trienodeHistoryIndexVersion
default:
panic(fmt.Errorf("unknown history type: %d", typ))
}
@@ -63,6 +67,8 @@ func loadIndexMetadata(db ethdb.KeyValueReader, typ historyType) *indexMetadata
switch typ {
case typeStateHistory:
blob = rawdb.ReadStateHistoryIndexMetadata(db)
+ case typeTrienodeHistory:
+ blob = rawdb.ReadTrienodeHistoryIndexMetadata(db)
default:
panic(fmt.Errorf("unknown history type %d", typ))
}
@@ -90,6 +96,8 @@ func storeIndexMetadata(db ethdb.KeyValueWriter, typ historyType, last uint64) {
switch typ {
case typeStateHistory:
rawdb.WriteStateHistoryIndexMetadata(db, blob)
+ case typeTrienodeHistory:
+ rawdb.WriteTrienodeHistoryIndexMetadata(db, blob)
default:
panic(fmt.Errorf("unknown history type %d", typ))
}
@@ -101,6 +109,8 @@ func deleteIndexMetadata(db ethdb.KeyValueWriter, typ historyType) {
switch typ {
case typeStateHistory:
rawdb.DeleteStateHistoryIndexMetadata(db)
+ case typeTrienodeHistory:
+ rawdb.DeleteTrienodeHistoryIndexMetadata(db)
default:
panic(fmt.Errorf("unknown history type %d", typ))
}
@@ -215,7 +225,11 @@ func (b *batchIndexer) finish(force bool) error {
func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader, typ historyType) error {
start := time.Now()
defer func() {
- indexHistoryTimer.UpdateSince(start)
+ if typ == typeStateHistory {
+ stateIndexHistoryTimer.UpdateSince(start)
+ } else if typ == typeTrienodeHistory {
+ trienodeIndexHistoryTimer.UpdateSince(start)
+ }
}()
metadata := loadIndexMetadata(db, typ)
@@ -234,7 +248,7 @@ func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancient
if typ == typeStateHistory {
h, err = readStateHistory(freezer, historyID)
} else {
- // h, err = readTrienodeHistory(freezer, historyID)
+ h, err = readTrienodeHistory(freezer, historyID)
}
if err != nil {
return err
@@ -253,7 +267,11 @@ func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancient
func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader, typ historyType) error {
start := time.Now()
defer func() {
- unindexHistoryTimer.UpdateSince(start)
+ if typ == typeStateHistory {
+ stateUnindexHistoryTimer.UpdateSince(start)
+ } else if typ == typeTrienodeHistory {
+ trienodeUnindexHistoryTimer.UpdateSince(start)
+ }
}()
metadata := loadIndexMetadata(db, typ)
@@ -272,7 +290,7 @@ func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancie
if typ == typeStateHistory {
h, err = readStateHistory(freezer, historyID)
} else {
- // h, err = readTrienodeHistory(freezer, historyID)
+ h, err = readTrienodeHistory(freezer, historyID)
}
if err != nil {
return err
@@ -546,13 +564,13 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
return
}
} else {
- // histories, err = readTrienodeHistories(i.freezer, current, count)
- // if err != nil {
- // // The history read might fall if the history is truncated from
- // // head due to revert operation.
- // i.log.Error("Failed to read history for indexing", "current", current, "count", count, "err", err)
- // return
- // }
+ histories, err = readTrienodeHistories(i.freezer, current, count)
+ if err != nil {
+ // The history read might fall if the history is truncated from
+ // head due to revert operation.
+ i.log.Error("Failed to read history for indexing", "current", current, "count", count, "err", err)
+ return
+ }
}
for _, h := range histories {
if err := batch.process(h, current); err != nil {
@@ -570,7 +588,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
done = current - beginID
)
eta := common.CalculateETA(done, left, time.Since(start))
- i.log.Info("Indexing state history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta))
+ i.log.Info("Indexing history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta))
}
}
i.indexed.Store(current - 1) // update indexing progress
@@ -657,6 +675,8 @@ func checkVersion(disk ethdb.KeyValueStore, typ historyType) {
var blob []byte
if typ == typeStateHistory {
blob = rawdb.ReadStateHistoryIndexMetadata(disk)
+ } else if typ == typeTrienodeHistory {
+ blob = rawdb.ReadTrienodeHistoryIndexMetadata(disk)
} else {
panic(fmt.Errorf("unknown history type: %v", typ))
}
@@ -666,24 +686,32 @@ func checkVersion(disk ethdb.KeyValueStore, typ historyType) {
return
}
// Short circuit if the metadata is found and the version is matched
+ ver := stateHistoryIndexVersion
+ if typ == typeTrienodeHistory {
+ ver = trienodeHistoryIndexVersion
+ }
var m indexMetadata
err := rlp.DecodeBytes(blob, &m)
- if err == nil && m.Version == stateIndexVersion {
+ if err == nil && m.Version == ver {
return
}
// Version is not matched, prune the existing data and re-index from scratch
+ batch := disk.NewBatch()
+ if typ == typeStateHistory {
+ rawdb.DeleteStateHistoryIndexMetadata(batch)
+ rawdb.DeleteStateHistoryIndexes(batch)
+ } else {
+ rawdb.DeleteTrienodeHistoryIndexMetadata(batch)
+ rawdb.DeleteTrienodeHistoryIndexes(batch)
+ }
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed to purge history index", "type", typ, "err", err)
+ }
version := "unknown"
if err == nil {
version = fmt.Sprintf("%d", m.Version)
}
-
- batch := disk.NewBatch()
- rawdb.DeleteStateHistoryIndexMetadata(batch)
- rawdb.DeleteStateHistoryIndex(batch)
- if err := batch.Write(); err != nil {
- log.Crit("Failed to purge state history index", "err", err)
- }
- log.Info("Cleaned up obsolete state history index", "version", version, "want", stateIndexVersion)
+ log.Info("Cleaned up obsolete history index", "type", typ, "version", version, "want", version)
}
// newHistoryIndexer constructs the history indexer and launches the background
diff --git a/triedb/pathdb/history_reader.go b/triedb/pathdb/history_reader.go
index ce6aa693d11..27fb90058ab 100644
--- a/triedb/pathdb/history_reader.go
+++ b/triedb/pathdb/history_reader.go
@@ -230,6 +230,15 @@ func (r *historyReader) readStorage(address common.Address, storageKey common.Ha
return data[offset : offset+length], nil
}
+// readTrienode retrieves the trienode data from the specified trienode history.
+func (r *historyReader) readTrienode(addrHash common.Hash, path string, historyID uint64) ([]byte, error) {
+ tr, err := newTrienodeHistoryReader(historyID, r.freezer)
+ if err != nil {
+ return nil, err
+ }
+ return tr.read(addrHash, path)
+}
+
// read retrieves the state element data associated with the stateID.
// stateID: represents the ID of the state of the specified version;
// lastID: represents the ID of the latest/newest state history;
@@ -285,5 +294,8 @@ func (r *historyReader) read(state stateIdentQuery, stateID uint64, lastID uint6
if state.typ == typeAccount {
return r.readAccount(state.address, historyID)
}
- return r.readStorage(state.address, state.storageKey, state.storageHash, historyID)
+ if state.typ == typeStorage {
+ return r.readStorage(state.address, state.storageKey, state.storageHash, historyID)
+ }
+ return r.readTrienode(state.addressHash, state.path, historyID)
}
diff --git a/triedb/pathdb/history_reader_test.go b/triedb/pathdb/history_reader_test.go
index 3e1a545ff32..9af8cc9af57 100644
--- a/triedb/pathdb/history_reader_test.go
+++ b/triedb/pathdb/history_reader_test.go
@@ -202,7 +202,7 @@ func TestHistoricalStateReader(t *testing.T) {
fakeRoot := testrand.Hash()
rawdb.WriteStateID(env.db.diskdb, fakeRoot, 10)
- _, err := env.db.HistoricReader(fakeRoot)
+ _, err := env.db.HistoricalStateReader(fakeRoot)
if err == nil {
t.Fatal("expected error")
}
@@ -210,7 +210,7 @@ func TestHistoricalStateReader(t *testing.T) {
// canonical state
realRoot := env.roots[9]
- _, err = env.db.HistoricReader(realRoot)
+ _, err = env.db.HistoricalStateReader(realRoot)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
diff --git a/triedb/pathdb/history_state.go b/triedb/pathdb/history_state.go
index 9d1e4dfb099..bc21915dbaa 100644
--- a/triedb/pathdb/history_state.go
+++ b/triedb/pathdb/history_state.go
@@ -605,9 +605,9 @@ func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
if err := rawdb.WriteStateHistory(writer, dl.stateID(), history.meta.encode(), accountIndex, storageIndex, accountData, storageData); err != nil {
return err
}
- historyDataBytesMeter.Mark(int64(dataSize))
- historyIndexBytesMeter.Mark(int64(indexSize))
- historyBuildTimeMeter.UpdateSince(start)
+ stateHistoryDataBytesMeter.Mark(int64(dataSize))
+ stateHistoryIndexBytesMeter.Mark(int64(indexSize))
+ stateHistoryBuildTimeMeter.UpdateSince(start)
log.Debug("Stored state history", "id", dl.stateID(), "block", dl.block, "data", dataSize, "index", indexSize, "elapsed", common.PrettyDuration(time.Since(start)))
return nil
diff --git a/triedb/pathdb/history_state_test.go b/triedb/pathdb/history_state_test.go
index 5718081566c..4046fb96400 100644
--- a/triedb/pathdb/history_state_test.go
+++ b/triedb/pathdb/history_state_test.go
@@ -98,13 +98,13 @@ func testEncodeDecodeStateHistory(t *testing.T, rawStorageKey bool) {
if !compareSet(dec.accounts, obj.accounts) {
t.Fatal("account data is mismatched")
}
- if !compareStorages(dec.storages, obj.storages) {
+ if !compareMapSet(dec.storages, obj.storages) {
t.Fatal("storage data is mismatched")
}
if !compareList(dec.accountList, obj.accountList) {
t.Fatal("account list is mismatched")
}
- if !compareStorageList(dec.storageList, obj.storageList) {
+ if !compareMapList(dec.storageList, obj.storageList) {
t.Fatal("storage list is mismatched")
}
}
@@ -292,32 +292,32 @@ func compareList[k comparable](a, b []k) bool {
return true
}
-func compareStorages(a, b map[common.Address]map[common.Hash][]byte) bool {
+func compareMapSet[K1 comparable, K2 comparable](a, b map[K1]map[K2][]byte) bool {
if len(a) != len(b) {
return false
}
- for h, subA := range a {
- subB, ok := b[h]
+ for key, subsetA := range a {
+ subsetB, ok := b[key]
if !ok {
return false
}
- if !compareSet(subA, subB) {
+ if !compareSet(subsetA, subsetB) {
return false
}
}
return true
}
-func compareStorageList(a, b map[common.Address][]common.Hash) bool {
+func compareMapList[K comparable, V comparable](a, b map[K][]V) bool {
if len(a) != len(b) {
return false
}
- for h, la := range a {
- lb, ok := b[h]
+ for key, listA := range a {
+ listB, ok := b[key]
if !ok {
return false
}
- if !compareList(la, lb) {
+ if !compareList(listA, listB) {
return false
}
}
diff --git a/triedb/pathdb/history_trienode.go b/triedb/pathdb/history_trienode.go
new file mode 100644
index 00000000000..5b13e2bc703
--- /dev/null
+++ b/triedb/pathdb/history_trienode.go
@@ -0,0 +1,731 @@
+// Copyright 2025 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 pathdb
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "iter"
+ "maps"
+ "math"
+ "slices"
+ "sort"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// Each trie node history entry consists of three parts (stored in three freezer
+// tables according):
+//
+// # Header
+// The header records metadata, including:
+//
+// - the history version (1 byte)
+// - the parent state root (32 bytes)
+// - the current state root (32 bytes)
+// - block number (8 bytes)
+//
+// - a lexicographically sorted list of trie IDs
+// - the corresponding offsets into the key and value sections for each trie data chunk
+//
+// Although some fields (e.g., parent state root, block number) are duplicated
+// between the state history and the trienode history, these two histories
+// operate independently. To ensure each remains self-contained and self-descriptive,
+// we have chosen to maintain these duplicate fields.
+//
+// # Key section
+// The key section stores trie node keys (paths) in a compressed format.
+// It also contains relative offsets into the value section for resolving
+// the corresponding trie node data. Note that these offsets are relative
+// to the data chunk for the trie; the chunk offset must be added to obtain
+// the absolute position.
+//
+// # Value section
+// The value section is a concatenated byte stream of all trie node data.
+// Each trie node can be retrieved using the offset and length specified
+// by its index entry.
+//
+// The header and key sections are sufficient for locating a trie node,
+// while a partial read of the value section is enough to retrieve its data.
+
+// Header section:
+//
+// +----------+------------------+---------------------+---------------------+-------+------------------+---------------------+---------------------|
+// | metadata | TrieID(32 bytes) | key offset(4 bytes) | val offset(4 bytes) | ... | TrieID(32 bytes) | key offset(4 bytes) | val offset(4 bytes) |
+// +----------+------------------+---------------------+---------------------+-------+------------------+---------------------+---------------------|
+//
+//
+// Key section:
+//
+// + restart point + restart point (depends on restart interval)
+// / /
+// +---------------+---------------+---------------+---------------+---------+
+// | node entry 1 | node entry 2 | ... | node entry n | trailer |
+// +---------------+---------------+---------------+---------------+---------+
+// \ /
+// +---- restart block ------+
+//
+// node entry:
+//
+// +---- key len ----+
+// / \
+// +-------+---------+-----------+---------+-----------------------+-----------------+
+// | shared (varint) | not shared (varint) | value length (varlen) | key (varlen) |
+// +-----------------+---------------------+-----------------------+-----------------+
+//
+// trailer:
+//
+// +---- 4-bytes ----+ +---- 4-bytes ----+
+// / \ / \
+// +----------------------+------------------------+-----+--------------------------+
+// | restart_1 key offset | restart_1 value offset | ... | restart number (4-bytes) |
+// +----------------------+------------------------+-----+--------------------------+
+//
+// Note: Both the key offset and the value offset are relative to the start of
+// the trie data chunk. To obtain the absolute offset, add the offset of the
+// trie data chunk itself.
+//
+// Value section:
+//
+// +--------------+--------------+-------+---------------+
+// | node data 1 | node data 2 | ... | node data n |
+// +--------------+--------------+-------+---------------+
+//
+// NOTE: All fixed-length integer are big-endian.
+
+const (
+ trienodeHistoryV0 = uint8(0) // initial version of node history structure
+ trienodeHistoryVersion = trienodeHistoryV0 // the default node history version
+ trienodeMetadataSize = 1 + 2*common.HashLength + 8 // the size of metadata in the history
+ trienodeTrieHeaderSize = 8 + common.HashLength // the size of a single trie header in history
+ trienodeDataBlockRestartLen = 16 // The restart interval length of trie node block
+)
+
+// trienodeMetadata describes the meta data of trienode history.
+type trienodeMetadata struct {
+ version uint8 // version tag of history object
+ parent common.Hash // prev-state root before the state transition
+ root common.Hash // post-state root after the state transition
+ block uint64 // associated block number
+}
+
+// trienodeHistory represents a set of trie node changes resulting from a state
+// transition across the main account trie and all associated storage tries.
+type trienodeHistory struct {
+ meta *trienodeMetadata // Metadata of the history
+ owners []common.Hash // List of trie identifier sorted lexicographically
+ nodeList map[common.Hash][]string // Set of node paths sorted lexicographically
+ nodes map[common.Hash]map[string][]byte // Set of original value of trie nodes before state transition
+}
+
+// newTrienodeHistory constructs a trienode history with the provided trie nodes.
+func newTrienodeHistory(root common.Hash, parent common.Hash, block uint64, nodes map[common.Hash]map[string][]byte) *trienodeHistory {
+ nodeList := make(map[common.Hash][]string)
+ for owner, subset := range nodes {
+ keys := sort.StringSlice(slices.Collect(maps.Keys(subset)))
+ keys.Sort()
+ nodeList[owner] = keys
+ }
+ return &trienodeHistory{
+ meta: &trienodeMetadata{
+ version: trienodeHistoryVersion,
+ parent: parent,
+ root: root,
+ block: block,
+ },
+ owners: slices.SortedFunc(maps.Keys(nodes), common.Hash.Cmp),
+ nodeList: nodeList,
+ nodes: nodes,
+ }
+}
+
+// sharedLen returns the length of the common prefix shared by a and b.
+func sharedLen(a, b []byte) int {
+ n := len(a)
+ if len(b) < n {
+ n = len(b)
+ }
+ for i := 0; i < n; i++ {
+ if a[i] != b[i] {
+ return i
+ }
+ }
+ return n
+}
+
+// typ implements the history interface, returning the historical data type held.
+func (h *trienodeHistory) typ() historyType {
+ return typeTrienodeHistory
+}
+
+// forEach implements the history interface, returning an iterator to traverse the
+// state entries in the history.
+func (h *trienodeHistory) forEach() iter.Seq[stateIdent] {
+ return func(yield func(stateIdent) bool) {
+ for _, owner := range h.owners {
+ for _, path := range h.nodeList[owner] {
+ if !yield(newTrienodeIdent(owner, path)) {
+ return
+ }
+ }
+ }
+ }
+}
+
+// encode serializes the contained trie nodes into bytes.
+func (h *trienodeHistory) encode() ([]byte, []byte, []byte, error) {
+ var (
+ buf = make([]byte, 64)
+ headerSection bytes.Buffer
+ keySection bytes.Buffer
+ valueSection bytes.Buffer
+ )
+ binary.Write(&headerSection, binary.BigEndian, h.meta.version) // 1 byte
+ headerSection.Write(h.meta.parent.Bytes()) // 32 bytes
+ headerSection.Write(h.meta.root.Bytes()) // 32 bytes
+ binary.Write(&headerSection, binary.BigEndian, h.meta.block) // 8 byte
+
+ for _, owner := range h.owners {
+ // Fill the header section with offsets at key and value section
+ headerSection.Write(owner.Bytes()) // 32 bytes
+ binary.Write(&headerSection, binary.BigEndian, uint32(keySection.Len())) // 4 bytes
+
+ // The offset to the value section is theoretically unnecessary, since the
+ // individual value offset is already tracked in the key section. However,
+ // we still keep it here for two reasons:
+ // - It's cheap to store (only 4 bytes for each trie).
+ // - It can be useful for decoding the trie data when key is not required (e.g., in hash mode).
+ binary.Write(&headerSection, binary.BigEndian, uint32(valueSection.Len())) // 4 bytes
+
+ // Fill the key section with node index
+ var (
+ prevKey []byte
+ restarts []uint32
+ prefixLen int
+
+ internalKeyOffset uint32 // key offset for the trie internally
+ internalValOffset uint32 // value offset for the trie internally
+ )
+ for i, path := range h.nodeList[owner] {
+ key := []byte(path)
+ if i%trienodeDataBlockRestartLen == 0 {
+ restarts = append(restarts, internalKeyOffset)
+ restarts = append(restarts, internalValOffset)
+ prefixLen = 0
+ } else {
+ prefixLen = sharedLen(prevKey, key)
+ }
+ value := h.nodes[owner][path]
+
+ // key section
+ n := binary.PutUvarint(buf[0:], uint64(prefixLen)) // key length shared (varint)
+ n += binary.PutUvarint(buf[n:], uint64(len(key)-prefixLen)) // key length not shared (varint)
+ n += binary.PutUvarint(buf[n:], uint64(len(value))) // value length (varint)
+
+ if _, err := keySection.Write(buf[:n]); err != nil {
+ return nil, nil, nil, err
+ }
+ // unshared key
+ if _, err := keySection.Write(key[prefixLen:]); err != nil {
+ return nil, nil, nil, err
+ }
+ n += len(key) - prefixLen
+ prevKey = key
+
+ // value section
+ if _, err := valueSection.Write(value); err != nil {
+ return nil, nil, nil, err
+ }
+ internalKeyOffset += uint32(n)
+ internalValOffset += uint32(len(value))
+ }
+
+ // Encode trailer
+ var trailer []byte
+ for _, number := range append(restarts, uint32(len(restarts))/2) {
+ binary.BigEndian.PutUint32(buf[:4], number)
+ trailer = append(trailer, buf[:4]...)
+ }
+ if _, err := keySection.Write(trailer); err != nil {
+ return nil, nil, nil, err
+ }
+ }
+ return headerSection.Bytes(), keySection.Bytes(), valueSection.Bytes(), nil
+}
+
+// decodeHeader resolves the metadata from the header section. An error
+// should be returned if the header section is corrupted.
+func decodeHeader(data []byte) (*trienodeMetadata, []common.Hash, []uint32, []uint32, error) {
+ if len(data) < trienodeMetadataSize {
+ return nil, nil, nil, nil, fmt.Errorf("trienode history is too small, index size: %d", len(data))
+ }
+ version := data[0]
+ if version != trienodeHistoryVersion {
+ return nil, nil, nil, nil, fmt.Errorf("unregonized trienode history version: %d", version)
+ }
+ parent := common.BytesToHash(data[1 : common.HashLength+1]) // 32 bytes
+ root := common.BytesToHash(data[common.HashLength+1 : common.HashLength*2+1]) // 32 bytes
+ block := binary.BigEndian.Uint64(data[common.HashLength*2+1 : trienodeMetadataSize]) // 8 bytes
+
+ size := len(data) - trienodeMetadataSize
+ if size%trienodeTrieHeaderSize != 0 {
+ return nil, nil, nil, nil, fmt.Errorf("truncated trienode history data, size %d", len(data))
+ }
+ count := size / trienodeTrieHeaderSize
+
+ var (
+ owners = make([]common.Hash, 0, count)
+ keyOffsets = make([]uint32, 0, count)
+ valOffsets = make([]uint32, 0, count)
+ )
+ for i := 0; i < count; i++ {
+ n := trienodeMetadataSize + trienodeTrieHeaderSize*i
+ owner := common.BytesToHash(data[n : n+common.HashLength])
+ if i != 0 && bytes.Compare(owner.Bytes(), owners[i-1].Bytes()) <= 0 {
+ return nil, nil, nil, nil, fmt.Errorf("trienode owners are out of order, prev: %v, cur: %v", owners[i-1], owner)
+ }
+ owners = append(owners, owner)
+
+ // Decode the offset to the key section
+ keyOffset := binary.BigEndian.Uint32(data[n+common.HashLength : n+common.HashLength+4])
+ if i != 0 && keyOffset <= keyOffsets[i-1] {
+ return nil, nil, nil, nil, fmt.Errorf("key offset is out of order, prev: %v, cur: %v", keyOffsets[i-1], keyOffset)
+ }
+ keyOffsets = append(keyOffsets, keyOffset)
+
+ // Decode the offset into the value section. Note that identical value offsets
+ // are valid if the node values in the last trie chunk are all zero (e.g., after
+ // a trie deletion).
+ valOffset := binary.BigEndian.Uint32(data[n+common.HashLength+4 : n+common.HashLength+8])
+ if i != 0 && valOffset < valOffsets[i-1] {
+ return nil, nil, nil, nil, fmt.Errorf("value offset is out of order, prev: %v, cur: %v", valOffsets[i-1], valOffset)
+ }
+ valOffsets = append(valOffsets, valOffset)
+ }
+ return &trienodeMetadata{
+ version: version,
+ parent: parent,
+ root: root,
+ block: block,
+ }, owners, keyOffsets, valOffsets, nil
+}
+
+func decodeSingle(keySection []byte, onValue func([]byte, int, int) error) ([]string, error) {
+ var (
+ prevKey []byte
+ items int
+ keyOffsets []uint32
+ valOffsets []uint32
+
+ keyOff int // the key offset within the single trie data
+ valOff int // the value offset within the single trie data
+
+ keys []string
+ )
+ // Decode restarts
+ if len(keySection) < 4 {
+ return nil, fmt.Errorf("key section too short, size: %d", len(keySection))
+ }
+ nRestarts := binary.BigEndian.Uint32(keySection[len(keySection)-4:])
+
+ if len(keySection) < int(8*nRestarts)+4 {
+ return nil, fmt.Errorf("key section too short, restarts: %d, size: %d", nRestarts, len(keySection))
+ }
+ for i := 0; i < int(nRestarts); i++ {
+ o := len(keySection) - 4 - (int(nRestarts)-i)*8
+ keyOffset := binary.BigEndian.Uint32(keySection[o : o+4])
+ if i != 0 && keyOffset <= keyOffsets[i-1] {
+ return nil, fmt.Errorf("key offset is out of order, prev: %v, cur: %v", keyOffsets[i-1], keyOffset)
+ }
+ keyOffsets = append(keyOffsets, keyOffset)
+
+ // Same value offset is allowed just in case all the trie nodes in the last
+ // section have zero-size value.
+ valOffset := binary.BigEndian.Uint32(keySection[o+4 : o+8])
+ if i != 0 && valOffset < valOffsets[i-1] {
+ return nil, fmt.Errorf("value offset is out of order, prev: %v, cur: %v", valOffsets[i-1], valOffset)
+ }
+ valOffsets = append(valOffsets, valOffset)
+ }
+ keyLimit := len(keySection) - 4 - int(nRestarts)*8
+
+ // Decode data
+ for keyOff < keyLimit {
+ // Validate the key and value offsets within the single trie data chunk
+ if items%trienodeDataBlockRestartLen == 0 {
+ if keyOff != int(keyOffsets[items/trienodeDataBlockRestartLen]) {
+ return nil, fmt.Errorf("key offset is not matched, recorded: %d, want: %d", keyOffsets[items/trienodeDataBlockRestartLen], keyOff)
+ }
+ if valOff != int(valOffsets[items/trienodeDataBlockRestartLen]) {
+ return nil, fmt.Errorf("value offset is not matched, recorded: %d, want: %d", valOffsets[items/trienodeDataBlockRestartLen], valOff)
+ }
+ }
+ // Resolve the entry from key section
+ nShared, nn := binary.Uvarint(keySection[keyOff:]) // key length shared (varint)
+ keyOff += nn
+ nUnshared, nn := binary.Uvarint(keySection[keyOff:]) // key length not shared (varint)
+ keyOff += nn
+ nValue, nn := binary.Uvarint(keySection[keyOff:]) // value length (varint)
+ keyOff += nn
+
+ // Resolve unshared key
+ if keyOff+int(nUnshared) > len(keySection) {
+ return nil, fmt.Errorf("key length too long, unshared key length: %d, off: %d, section size: %d", nUnshared, keyOff, len(keySection))
+ }
+ unsharedKey := keySection[keyOff : keyOff+int(nUnshared)]
+ keyOff += int(nUnshared)
+
+ // Assemble the full key
+ var key []byte
+ if items%trienodeDataBlockRestartLen == 0 {
+ if nShared != 0 {
+ return nil, fmt.Errorf("unexpected non-zero shared key prefix: %d", nShared)
+ }
+ key = unsharedKey
+ } else {
+ if int(nShared) > len(prevKey) {
+ return nil, fmt.Errorf("unexpected shared key prefix: %d, prefix key length: %d", nShared, len(prevKey))
+ }
+ key = append([]byte{}, prevKey[:nShared]...)
+ key = append(key, unsharedKey...)
+ }
+ if items != 0 && bytes.Compare(prevKey, key) >= 0 {
+ return nil, fmt.Errorf("trienode paths are out of order, prev: %v, cur: %v", prevKey, key)
+ }
+ prevKey = key
+
+ // Resolve value
+ if onValue != nil {
+ if err := onValue(key, valOff, valOff+int(nValue)); err != nil {
+ return nil, err
+ }
+ }
+ valOff += int(nValue)
+
+ items++
+ keys = append(keys, string(key))
+ }
+ if keyOff != keyLimit {
+ return nil, fmt.Errorf("excessive key data after decoding, offset: %d, size: %d", keyOff, keyLimit)
+ }
+ return keys, nil
+}
+
+func decodeSingleWithValue(keySection []byte, valueSection []byte) ([]string, map[string][]byte, error) {
+ var (
+ offset int
+ nodes = make(map[string][]byte)
+ )
+ paths, err := decodeSingle(keySection, func(key []byte, start int, limit int) error {
+ if start != offset {
+ return fmt.Errorf("gapped value section offset: %d, want: %d", start, offset)
+ }
+ // start == limit is allowed for zero-value trie node (e.g., non-existent node)
+ if start > limit {
+ return fmt.Errorf("invalid value offsets, start: %d, limit: %d", start, limit)
+ }
+ if start > len(valueSection) || limit > len(valueSection) {
+ return fmt.Errorf("value section out of range: start: %d, limit: %d, size: %d", start, limit, len(valueSection))
+ }
+ nodes[string(key)] = valueSection[start:limit]
+
+ offset = limit
+ return nil
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+ if offset != len(valueSection) {
+ return nil, nil, fmt.Errorf("excessive value data after decoding, offset: %d, size: %d", offset, len(valueSection))
+ }
+ return paths, nodes, nil
+}
+
+// decode deserializes the contained trie nodes from the provided bytes.
+func (h *trienodeHistory) decode(header []byte, keySection []byte, valueSection []byte) error {
+ metadata, owners, keyOffsets, valueOffsets, err := decodeHeader(header)
+ if err != nil {
+ return err
+ }
+ h.meta = metadata
+ h.owners = owners
+ h.nodeList = make(map[common.Hash][]string)
+ h.nodes = make(map[common.Hash]map[string][]byte)
+
+ for i := 0; i < len(owners); i++ {
+ // Resolve the boundary of key section
+ keyStart := keyOffsets[i]
+ keyLimit := len(keySection)
+ if i != len(owners)-1 {
+ keyLimit = int(keyOffsets[i+1])
+ }
+ if int(keyStart) > len(keySection) || keyLimit > len(keySection) {
+ return fmt.Errorf("invalid key offsets: keyStart: %d, keyLimit: %d, size: %d", keyStart, keyLimit, len(keySection))
+ }
+
+ // Resolve the boundary of value section
+ valStart := valueOffsets[i]
+ valLimit := len(valueSection)
+ if i != len(owners)-1 {
+ valLimit = int(valueOffsets[i+1])
+ }
+ if int(valStart) > len(valueSection) || valLimit > len(valueSection) {
+ return fmt.Errorf("invalid value offsets: valueStart: %d, valueLimit: %d, size: %d", valStart, valLimit, len(valueSection))
+ }
+
+ // Decode the key and values for this specific trie
+ paths, nodes, err := decodeSingleWithValue(keySection[keyStart:keyLimit], valueSection[valStart:valLimit])
+ if err != nil {
+ return err
+ }
+ h.nodeList[owners[i]] = paths
+ h.nodes[owners[i]] = nodes
+ }
+ return nil
+}
+
+type iRange struct {
+ start uint32
+ limit uint32
+}
+
+// singleTrienodeHistoryReader provides read access to a single trie within the
+// trienode history. It stores an offset to the trie's position in the history,
+// along with a set of per-node offsets that can be resolved on demand.
+type singleTrienodeHistoryReader struct {
+ id uint64
+ reader ethdb.AncientReader
+ valueRange iRange // value range within the total value section
+ valueInternalOffsets map[string]iRange // value offset within the single trie data
+}
+
+func newSingleTrienodeHistoryReader(id uint64, reader ethdb.AncientReader, keyRange iRange, valueRange iRange) (*singleTrienodeHistoryReader, error) {
+ // TODO(rjl493456442) partial freezer read should be supported
+ keyData, err := rawdb.ReadTrienodeHistoryKeySection(reader, id)
+ if err != nil {
+ return nil, err
+ }
+ keyStart := int(keyRange.start)
+ keyLimit := int(keyRange.limit)
+ if keyLimit == math.MaxUint32 {
+ keyLimit = len(keyData)
+ }
+ if len(keyData) < keyStart || len(keyData) < keyLimit {
+ return nil, fmt.Errorf("key section too short, start: %d, limit: %d, size: %d", keyStart, keyLimit, len(keyData))
+ }
+
+ valueOffsets := make(map[string]iRange)
+ _, err = decodeSingle(keyData[keyStart:keyLimit], func(key []byte, start int, limit int) error {
+ valueOffsets[string(key)] = iRange{
+ start: uint32(start),
+ limit: uint32(limit),
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &singleTrienodeHistoryReader{
+ id: id,
+ reader: reader,
+ valueRange: valueRange,
+ valueInternalOffsets: valueOffsets,
+ }, nil
+}
+
+// read retrieves the trie node data with the provided node path.
+func (sr *singleTrienodeHistoryReader) read(path string) ([]byte, error) {
+ offset, exists := sr.valueInternalOffsets[path]
+ if !exists {
+ return nil, fmt.Errorf("trienode %v not found", []byte(path))
+ }
+ // TODO(rjl493456442) partial freezer read should be supported
+ valueData, err := rawdb.ReadTrienodeHistoryValueSection(sr.reader, sr.id)
+ if err != nil {
+ return nil, err
+ }
+ if len(valueData) < int(sr.valueRange.start) {
+ return nil, fmt.Errorf("value section too short, start: %d, size: %d", sr.valueRange.start, len(valueData))
+ }
+ entryStart := sr.valueRange.start + offset.start
+ entryLimit := sr.valueRange.start + offset.limit
+ if len(valueData) < int(entryStart) || len(valueData) < int(entryLimit) {
+ return nil, fmt.Errorf("value section too short, start: %d, limit: %d, size: %d", entryStart, entryLimit, len(valueData))
+ }
+ return valueData[int(entryStart):int(entryLimit)], nil
+}
+
+// trienodeHistoryReader provides read access to node data in the trie node history.
+// It resolves data from the underlying ancient store only when needed, minimizing
+// I/O overhead.
+type trienodeHistoryReader struct {
+ id uint64 // ID of the associated trienode history
+ reader ethdb.AncientReader // Database reader of ancient store
+ keyRanges map[common.Hash]iRange // Key ranges identifying trie chunks
+ valRanges map[common.Hash]iRange // Value ranges identifying trie chunks
+ iReaders map[common.Hash]*singleTrienodeHistoryReader // readers for each individual trie chunk
+}
+
+// newTrienodeHistoryReader constructs the reader for specific trienode history.
+func newTrienodeHistoryReader(id uint64, reader ethdb.AncientReader) (*trienodeHistoryReader, error) {
+ r := &trienodeHistoryReader{
+ id: id,
+ reader: reader,
+ keyRanges: make(map[common.Hash]iRange),
+ valRanges: make(map[common.Hash]iRange),
+ iReaders: make(map[common.Hash]*singleTrienodeHistoryReader),
+ }
+ if err := r.decodeHeader(); err != nil {
+ return nil, err
+ }
+ return r, nil
+}
+
+// decodeHeader decodes the header section of trienode history.
+func (r *trienodeHistoryReader) decodeHeader() error {
+ header, err := rawdb.ReadTrienodeHistoryHeader(r.reader, r.id)
+ if err != nil {
+ return err
+ }
+ _, owners, keyOffsets, valOffsets, err := decodeHeader(header)
+ if err != nil {
+ return err
+ }
+ for i, owner := range owners {
+ // Decode the key range for this trie chunk
+ var keyLimit uint32
+ if i == len(owners)-1 {
+ keyLimit = math.MaxUint32
+ } else {
+ keyLimit = keyOffsets[i+1]
+ }
+ r.keyRanges[owner] = iRange{
+ start: keyOffsets[i],
+ limit: keyLimit,
+ }
+
+ // Decode the value range for this trie chunk
+ var valLimit uint32
+ if i == len(owners)-1 {
+ valLimit = math.MaxUint32
+ } else {
+ valLimit = valOffsets[i+1]
+ }
+ r.valRanges[owner] = iRange{
+ start: valOffsets[i],
+ limit: valLimit,
+ }
+ }
+ return nil
+}
+
+// read retrieves the trie node data with the provided TrieID and node path.
+func (r *trienodeHistoryReader) read(owner common.Hash, path string) ([]byte, error) {
+ ir, ok := r.iReaders[owner]
+ if !ok {
+ keyRange, exists := r.keyRanges[owner]
+ if !exists {
+ return nil, fmt.Errorf("trie %x is unknown", owner)
+ }
+ valRange, exists := r.valRanges[owner]
+ if !exists {
+ return nil, fmt.Errorf("trie %x is unknown", owner)
+ }
+ var err error
+ ir, err = newSingleTrienodeHistoryReader(r.id, r.reader, keyRange, valRange)
+ if err != nil {
+ return nil, err
+ }
+ r.iReaders[owner] = ir
+ }
+ return ir.read(path)
+}
+
+// writeTrienodeHistory persists the trienode history associated with the given diff layer.
+func writeTrienodeHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
+ start := time.Now()
+ h := newTrienodeHistory(dl.rootHash(), dl.parent.rootHash(), dl.block, dl.nodes.nodeOrigin)
+ header, keySection, valueSection, err := h.encode()
+ if err != nil {
+ return err
+ }
+ // Write history data into five freezer table respectively.
+ if err := rawdb.WriteTrienodeHistory(writer, dl.stateID(), header, keySection, valueSection); err != nil {
+ return err
+ }
+ trienodeHistoryDataBytesMeter.Mark(int64(len(valueSection)))
+ trienodeHistoryIndexBytesMeter.Mark(int64(len(header) + len(keySection)))
+ trienodeHistoryBuildTimeMeter.UpdateSince(start)
+
+ log.Debug(
+ "Stored trienode history", "id", dl.stateID(), "block", dl.block,
+ "header", common.StorageSize(len(header)),
+ "keySection", common.StorageSize(len(keySection)),
+ "valueSection", common.StorageSize(len(valueSection)),
+ "elapsed", common.PrettyDuration(time.Since(start)),
+ )
+ return nil
+}
+
+// readTrienodeMetadata resolves the metadata of the specified trienode history.
+// nolint:unused
+func readTrienodeMetadata(reader ethdb.AncientReader, id uint64) (*trienodeMetadata, error) {
+ header, err := rawdb.ReadTrienodeHistoryHeader(reader, id)
+ if err != nil {
+ return nil, err
+ }
+ metadata, _, _, _, err := decodeHeader(header)
+ if err != nil {
+ return nil, err
+ }
+ return metadata, nil
+}
+
+// readTrienodeHistory resolves a single trienode history object with specific id.
+func readTrienodeHistory(reader ethdb.AncientReader, id uint64) (*trienodeHistory, error) {
+ header, keySection, valueSection, err := rawdb.ReadTrienodeHistory(reader, id)
+ if err != nil {
+ return nil, err
+ }
+ var h trienodeHistory
+ if err := h.decode(header, keySection, valueSection); err != nil {
+ return nil, err
+ }
+ return &h, nil
+}
+
+// readTrienodeHistories resolves a list of trienode histories with the specific range.
+func readTrienodeHistories(reader ethdb.AncientReader, start uint64, count uint64) ([]history, error) {
+ headers, keySections, valueSections, err := rawdb.ReadTrienodeHistoryList(reader, start, count)
+ if err != nil {
+ return nil, err
+ }
+ var res []history
+ for i, header := range headers {
+ var h trienodeHistory
+ if err := h.decode(header, keySections[i], valueSections[i]); err != nil {
+ return nil, err
+ }
+ res = append(res, &h)
+ }
+ return res, nil
+}
diff --git a/triedb/pathdb/history_trienode_test.go b/triedb/pathdb/history_trienode_test.go
new file mode 100644
index 00000000000..b02c0d5380a
--- /dev/null
+++ b/triedb/pathdb/history_trienode_test.go
@@ -0,0 +1,723 @@
+// Copyright 2025 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 pathdb
+
+import (
+ "bytes"
+ "encoding/binary"
+ "math/rand"
+ "reflect"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/internal/testrand"
+)
+
+// randomTrienodes generates a random trienode set.
+func randomTrienodes(n int) (map[common.Hash]map[string][]byte, common.Hash) {
+ var (
+ root common.Hash
+ nodes = make(map[common.Hash]map[string][]byte)
+ )
+ for i := 0; i < n; i++ {
+ owner := testrand.Hash()
+ if i == 0 {
+ owner = common.Hash{}
+ }
+ nodes[owner] = make(map[string][]byte)
+
+ for j := 0; j < 10; j++ {
+ pathLen := rand.Intn(10)
+ path := testrand.Bytes(pathLen)
+ for z := 0; z < len(path); z++ {
+ valLen := rand.Intn(128)
+ nodes[owner][string(path[:z])] = testrand.Bytes(valLen)
+ }
+ }
+ // zero-size trie node, representing it was non-existent before
+ for j := 0; j < 10; j++ {
+ path := testrand.Bytes(32)
+ nodes[owner][string(path)] = nil
+ }
+ // root node with zero-size path
+ rnode := testrand.Bytes(256)
+ nodes[owner][""] = rnode
+ if owner == (common.Hash{}) {
+ root = crypto.Keccak256Hash(rnode)
+ }
+ }
+ return nodes, root
+}
+
+func makeTrinodeHistory() *trienodeHistory {
+ nodes, root := randomTrienodes(10)
+ return newTrienodeHistory(root, common.Hash{}, 1, nodes)
+}
+
+func makeTrienodeHistories(n int) []*trienodeHistory {
+ var (
+ parent common.Hash
+ result []*trienodeHistory
+ )
+ for i := 0; i < n; i++ {
+ nodes, root := randomTrienodes(10)
+ result = append(result, newTrienodeHistory(root, parent, uint64(i+1), nodes))
+ parent = root
+ }
+ return result
+}
+
+func TestEncodeDecodeTrienodeHistory(t *testing.T) {
+ var (
+ dec trienodeHistory
+ obj = makeTrinodeHistory()
+ )
+ header, keySection, valueSection, err := obj.encode()
+ if err != nil {
+ t.Fatalf("Failed to encode trienode history: %v", err)
+ }
+ if err := dec.decode(header, keySection, valueSection); err != nil {
+ t.Fatalf("Failed to decode trienode history: %v", err)
+ }
+
+ if !reflect.DeepEqual(obj.meta, dec.meta) {
+ t.Fatal("trienode metadata is mismatched")
+ }
+ if !compareList(dec.owners, obj.owners) {
+ t.Fatal("trie owner list is mismatched")
+ }
+ if !compareMapList(dec.nodeList, obj.nodeList) {
+ t.Fatal("trienode list is mismatched")
+ }
+ if !compareMapSet(dec.nodes, obj.nodes) {
+ t.Fatal("trienode content is mismatched")
+ }
+}
+
+func TestTrienodeHistoryReader(t *testing.T) {
+ var (
+ hs = makeTrienodeHistories(10)
+ freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
+ )
+ defer freezer.Close()
+
+ for i, h := range hs {
+ header, keySection, valueSection, _ := h.encode()
+ if err := rawdb.WriteTrienodeHistory(freezer, uint64(i+1), header, keySection, valueSection); err != nil {
+ t.Fatalf("Failed to write trienode history: %v", err)
+ }
+ }
+ for i, h := range hs {
+ tr, err := newTrienodeHistoryReader(uint64(i+1), freezer)
+ if err != nil {
+ t.Fatalf("Failed to construct the history reader: %v", err)
+ }
+ for _, owner := range h.owners {
+ nodes := h.nodes[owner]
+ for key, value := range nodes {
+ blob, err := tr.read(owner, key)
+ if err != nil {
+ t.Fatalf("Failed to read trienode history: %v", err)
+ }
+ if !bytes.Equal(blob, value) {
+ t.Fatalf("Unexpected trie node data, want: %v, got: %v", value, blob)
+ }
+ }
+ }
+ }
+ for i, h := range hs {
+ metadata, err := readTrienodeMetadata(freezer, uint64(i+1))
+ if err != nil {
+ t.Fatalf("Failed to read trienode history metadata: %v", err)
+ }
+ if !reflect.DeepEqual(h.meta, metadata) {
+ t.Fatalf("Unexpected trienode metadata, want: %v, got: %v", h.meta, metadata)
+ }
+ }
+}
+
+// TestEmptyTrienodeHistory tests encoding/decoding of empty trienode history
+func TestEmptyTrienodeHistory(t *testing.T) {
+ h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, make(map[common.Hash]map[string][]byte))
+
+ // Test encoding empty history
+ header, keySection, valueSection, err := h.encode()
+ if err != nil {
+ t.Fatalf("Failed to encode empty trienode history: %v", err)
+ }
+
+ // Verify sections are minimal but valid
+ if len(header) == 0 {
+ t.Fatal("Header should not be empty")
+ }
+ if len(keySection) != 0 {
+ t.Fatal("Key section should be empty for empty history")
+ }
+ if len(valueSection) != 0 {
+ t.Fatal("Value section should be empty for empty history")
+ }
+
+ // Test decoding empty history
+ var decoded trienodeHistory
+ if err := decoded.decode(header, keySection, valueSection); err != nil {
+ t.Fatalf("Failed to decode empty trienode history: %v", err)
+ }
+
+ if len(decoded.owners) != 0 {
+ t.Fatal("Decoded history should have no owners")
+ }
+ if len(decoded.nodeList) != 0 {
+ t.Fatal("Decoded history should have no node lists")
+ }
+ if len(decoded.nodes) != 0 {
+ t.Fatal("Decoded history should have no nodes")
+ }
+}
+
+// TestSingleTrieHistory tests encoding/decoding of history with single trie
+func TestSingleTrieHistory(t *testing.T) {
+ nodes := make(map[common.Hash]map[string][]byte)
+ owner := testrand.Hash()
+ nodes[owner] = make(map[string][]byte)
+
+ // Add some nodes with various sizes
+ nodes[owner][""] = testrand.Bytes(32) // empty key
+ nodes[owner]["a"] = testrand.Bytes(1) // small value
+ nodes[owner]["bb"] = testrand.Bytes(100) // medium value
+ nodes[owner]["ccc"] = testrand.Bytes(1000) // large value
+ nodes[owner]["dddd"] = testrand.Bytes(0) // empty value
+
+ h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
+ testEncodeDecode(t, h)
+}
+
+// TestMultipleTries tests multiple tries with different node counts
+func TestMultipleTries(t *testing.T) {
+ nodes := make(map[common.Hash]map[string][]byte)
+
+ // First trie with many small nodes
+ owner1 := testrand.Hash()
+ nodes[owner1] = make(map[string][]byte)
+ for i := 0; i < 100; i++ {
+ key := string(testrand.Bytes(rand.Intn(10)))
+ nodes[owner1][key] = testrand.Bytes(rand.Intn(50))
+ }
+
+ // Second trie with few large nodes
+ owner2 := testrand.Hash()
+ nodes[owner2] = make(map[string][]byte)
+ for i := 0; i < 5; i++ {
+ key := string(testrand.Bytes(rand.Intn(20)))
+ nodes[owner2][key] = testrand.Bytes(1000 + rand.Intn(1000))
+ }
+
+ // Third trie with nil values (zero-size nodes)
+ owner3 := testrand.Hash()
+ nodes[owner3] = make(map[string][]byte)
+ for i := 0; i < 10; i++ {
+ key := string(testrand.Bytes(rand.Intn(15)))
+ nodes[owner3][key] = nil
+ }
+
+ h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
+ testEncodeDecode(t, h)
+}
+
+// TestLargeNodeValues tests encoding/decoding with very large node values
+func TestLargeNodeValues(t *testing.T) {
+ nodes := make(map[common.Hash]map[string][]byte)
+ owner := testrand.Hash()
+ nodes[owner] = make(map[string][]byte)
+
+ // Test with progressively larger values
+ sizes := []int{1024, 10 * 1024, 100 * 1024, 1024 * 1024} // 1KB, 10KB, 100KB, 1MB
+ for _, size := range sizes {
+ key := string(testrand.Bytes(10))
+ nodes[owner][key] = testrand.Bytes(size)
+
+ h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
+ testEncodeDecode(t, h)
+ t.Logf("Successfully tested encoding/decoding with %dKB value", size/1024)
+ }
+}
+
+// TestNilNodeValues tests encoding/decoding with nil (zero-length) node values
+func TestNilNodeValues(t *testing.T) {
+ nodes := make(map[common.Hash]map[string][]byte)
+ owner := testrand.Hash()
+ nodes[owner] = make(map[string][]byte)
+
+ // Mix of nil and non-nil values
+ nodes[owner]["nil"] = nil
+ nodes[owner]["data1"] = []byte("some data")
+ nodes[owner]["data2"] = []byte("more data")
+
+ h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
+ testEncodeDecode(t, h)
+
+ // Verify nil values are preserved
+ _, ok := h.nodes[owner]["nil"]
+ if !ok {
+ t.Fatal("Nil value should be preserved")
+ }
+}
+
+// TestCorruptedHeader tests error handling for corrupted header data
+func TestCorruptedHeader(t *testing.T) {
+ h := makeTrinodeHistory()
+ header, keySection, valueSection, _ := h.encode()
+
+ // Test corrupted version
+ corruptedHeader := make([]byte, len(header))
+ copy(corruptedHeader, header)
+ corruptedHeader[0] = 0xFF // Invalid version
+
+ var decoded trienodeHistory
+ if err := decoded.decode(corruptedHeader, keySection, valueSection); err == nil {
+ t.Fatal("Expected error for corrupted version")
+ }
+
+ // Test truncated header
+ truncatedHeader := header[:len(header)-5]
+ if err := decoded.decode(truncatedHeader, keySection, valueSection); err == nil {
+ t.Fatal("Expected error for truncated header")
+ }
+
+ // Test header with invalid trie header size
+ invalidHeader := make([]byte, len(header))
+ copy(invalidHeader, header)
+ invalidHeader = invalidHeader[:trienodeMetadataSize+5] // Not divisible by trie header size
+
+ if err := decoded.decode(invalidHeader, keySection, valueSection); err == nil {
+ t.Fatal("Expected error for invalid header size")
+ }
+}
+
+// TestCorruptedKeySection tests error handling for corrupted key section data
+func TestCorruptedKeySection(t *testing.T) {
+ h := makeTrinodeHistory()
+ header, keySection, valueSection, _ := h.encode()
+
+ // Test empty key section when header indicates data
+ if len(keySection) > 0 {
+ var decoded trienodeHistory
+ if err := decoded.decode(header, []byte{}, valueSection); err == nil {
+ t.Fatal("Expected error for empty key section with non-empty header")
+ }
+ }
+
+ // Test truncated key section
+ if len(keySection) > 10 {
+ truncatedKeySection := keySection[:len(keySection)-10]
+ var decoded trienodeHistory
+ if err := decoded.decode(header, truncatedKeySection, valueSection); err == nil {
+ t.Fatal("Expected error for truncated key section")
+ }
+ }
+
+ // Test corrupted key section with invalid varint
+ corruptedKeySection := make([]byte, len(keySection))
+ copy(corruptedKeySection, keySection)
+ if len(corruptedKeySection) > 5 {
+ corruptedKeySection[5] = 0xFF // Corrupt varint encoding
+ var decoded trienodeHistory
+ if err := decoded.decode(header, corruptedKeySection, valueSection); err == nil {
+ t.Fatal("Expected error for corrupted varint in key section")
+ }
+ }
+}
+
+// TestCorruptedValueSection tests error handling for corrupted value section data
+func TestCorruptedValueSection(t *testing.T) {
+ h := makeTrinodeHistory()
+ header, keySection, valueSection, _ := h.encode()
+
+ // Test truncated value section
+ if len(valueSection) > 10 {
+ truncatedValueSection := valueSection[:len(valueSection)-10]
+ var decoded trienodeHistory
+ if err := decoded.decode(header, keySection, truncatedValueSection); err == nil {
+ t.Fatal("Expected error for truncated value section")
+ }
+ }
+
+ // Test empty value section when key section indicates data exists
+ if len(valueSection) > 0 {
+ var decoded trienodeHistory
+ if err := decoded.decode(header, keySection, []byte{}); err == nil {
+ t.Fatal("Expected error for empty value section with non-empty key section")
+ }
+ }
+}
+
+// TestInvalidOffsets tests error handling for invalid offsets in encoded data
+func TestInvalidOffsets(t *testing.T) {
+ h := makeTrinodeHistory()
+ header, keySection, valueSection, _ := h.encode()
+
+ // Corrupt key offset in header (make it larger than key section)
+ corruptedHeader := make([]byte, len(header))
+ copy(corruptedHeader, header)
+ corruptedHeader[trienodeMetadataSize+common.HashLength] = 0xff
+
+ var dec1 trienodeHistory
+ if err := dec1.decode(corruptedHeader, keySection, valueSection); err == nil {
+ t.Fatal("Expected error for invalid key offset")
+ }
+
+ // Corrupt value offset in header (make it larger than value section)
+ corruptedHeader = make([]byte, len(header))
+ copy(corruptedHeader, header)
+ corruptedHeader[trienodeMetadataSize+common.HashLength+4] = 0xff
+
+ var dec2 trienodeHistory
+ if err := dec2.decode(corruptedHeader, keySection, valueSection); err == nil {
+ t.Fatal("Expected error for invalid value offset")
+ }
+}
+
+// TestTrienodeHistoryReaderNonExistentPath tests reading non-existent paths
+func TestTrienodeHistoryReaderNonExistentPath(t *testing.T) {
+ var (
+ h = makeTrinodeHistory()
+ freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
+ )
+ defer freezer.Close()
+
+ header, keySection, valueSection, _ := h.encode()
+ if err := rawdb.WriteTrienodeHistory(freezer, 1, header, keySection, valueSection); err != nil {
+ t.Fatalf("Failed to write trienode history: %v", err)
+ }
+
+ tr, err := newTrienodeHistoryReader(1, freezer)
+ if err != nil {
+ t.Fatalf("Failed to construct history reader: %v", err)
+ }
+
+ // Try to read a non-existent path
+ _, err = tr.read(testrand.Hash(), "nonexistent")
+ if err == nil {
+ t.Fatal("Expected error for non-existent trie owner")
+ }
+
+ // Try to read from existing owner but non-existent path
+ owner := h.owners[0]
+ _, err = tr.read(owner, "nonexistent-path")
+ if err == nil {
+ t.Fatal("Expected error for non-existent path")
+ }
+}
+
+// TestTrienodeHistoryReaderNilValues tests reading nil (zero-length) values
+func TestTrienodeHistoryReaderNilValues(t *testing.T) {
+ nodes := make(map[common.Hash]map[string][]byte)
+ owner := testrand.Hash()
+ nodes[owner] = make(map[string][]byte)
+
+ // Add some nil values
+ nodes[owner]["nil1"] = nil
+ nodes[owner]["nil2"] = nil
+ nodes[owner]["data1"] = []byte("some data")
+
+ h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
+
+ var freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
+ defer freezer.Close()
+
+ header, keySection, valueSection, _ := h.encode()
+ if err := rawdb.WriteTrienodeHistory(freezer, 1, header, keySection, valueSection); err != nil {
+ t.Fatalf("Failed to write trienode history: %v", err)
+ }
+
+ tr, err := newTrienodeHistoryReader(1, freezer)
+ if err != nil {
+ t.Fatalf("Failed to construct history reader: %v", err)
+ }
+
+ // Test reading nil values
+ data1, err := tr.read(owner, "nil1")
+ if err != nil {
+ t.Fatalf("Failed to read nil value: %v", err)
+ }
+ if len(data1) != 0 {
+ t.Fatal("Expected nil data for nil value")
+ }
+
+ data2, err := tr.read(owner, "nil2")
+ if err != nil {
+ t.Fatalf("Failed to read nil value: %v", err)
+ }
+ if len(data2) != 0 {
+ t.Fatal("Expected nil data for nil value")
+ }
+
+ // Test reading non-nil value
+ data3, err := tr.read(owner, "data1")
+ if err != nil {
+ t.Fatalf("Failed to read non-nil value: %v", err)
+ }
+ if !bytes.Equal(data3, []byte("some data")) {
+ t.Fatal("Data mismatch for non-nil value")
+ }
+}
+
+// TestTrienodeHistoryReaderNilKey tests reading nil (zero-length) key
+func TestTrienodeHistoryReaderNilKey(t *testing.T) {
+ nodes := make(map[common.Hash]map[string][]byte)
+ owner := testrand.Hash()
+ nodes[owner] = make(map[string][]byte)
+
+ // Add some nil values
+ nodes[owner][""] = []byte("some data")
+ nodes[owner]["data1"] = []byte("some data")
+
+ h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
+
+ var freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
+ defer freezer.Close()
+
+ header, keySection, valueSection, _ := h.encode()
+ if err := rawdb.WriteTrienodeHistory(freezer, 1, header, keySection, valueSection); err != nil {
+ t.Fatalf("Failed to write trienode history: %v", err)
+ }
+
+ tr, err := newTrienodeHistoryReader(1, freezer)
+ if err != nil {
+ t.Fatalf("Failed to construct history reader: %v", err)
+ }
+
+ // Test reading nil values
+ data1, err := tr.read(owner, "")
+ if err != nil {
+ t.Fatalf("Failed to read nil value: %v", err)
+ }
+ if !bytes.Equal(data1, []byte("some data")) {
+ t.Fatal("Data mismatch for nil key")
+ }
+
+ // Test reading non-nil value
+ data2, err := tr.read(owner, "data1")
+ if err != nil {
+ t.Fatalf("Failed to read non-nil value: %v", err)
+ }
+ if !bytes.Equal(data2, []byte("some data")) {
+ t.Fatal("Data mismatch for non-nil key")
+ }
+}
+
+// TestTrienodeHistoryReaderIterator tests the iterator functionality
+func TestTrienodeHistoryReaderIterator(t *testing.T) {
+ h := makeTrinodeHistory()
+
+ // Count expected entries
+ expectedCount := 0
+ expectedNodes := make(map[stateIdent]bool)
+ for owner, nodeList := range h.nodeList {
+ expectedCount += len(nodeList)
+ for _, node := range nodeList {
+ expectedNodes[stateIdent{
+ typ: typeTrienode,
+ addressHash: owner,
+ path: node,
+ }] = true
+ }
+ }
+
+ // Test the iterator
+ actualCount := 0
+ for x := range h.forEach() {
+ _ = x
+ actualCount++
+ }
+ if actualCount != expectedCount {
+ t.Fatalf("Iterator count mismatch: expected %d, got %d", expectedCount, actualCount)
+ }
+
+ // Test that iterator yields expected state identifiers
+ seen := make(map[stateIdent]bool)
+ for ident := range h.forEach() {
+ if ident.typ != typeTrienode {
+ t.Fatal("Iterator should only yield trienode history identifiers")
+ }
+ key := stateIdent{typ: ident.typ, addressHash: ident.addressHash, path: ident.path}
+ if seen[key] {
+ t.Fatal("Iterator yielded duplicate identifier")
+ }
+ seen[key] = true
+
+ if !expectedNodes[key] {
+ t.Fatalf("Unexpected yielded identifier %v", key)
+ }
+ }
+}
+
+// TestSharedLen tests the sharedLen helper function
+func TestSharedLen(t *testing.T) {
+ tests := []struct {
+ a, b []byte
+ expected int
+ }{
+ // Empty strings
+ {[]byte(""), []byte(""), 0},
+ // One empty string
+ {[]byte(""), []byte("abc"), 0},
+ {[]byte("abc"), []byte(""), 0},
+ // No common prefix
+ {[]byte("abc"), []byte("def"), 0},
+ // Partial common prefix
+ {[]byte("abc"), []byte("abx"), 2},
+ {[]byte("prefix"), []byte("pref"), 4},
+ // Complete common prefix (shorter first)
+ {[]byte("ab"), []byte("abcd"), 2},
+ // Complete common prefix (longer first)
+ {[]byte("abcd"), []byte("ab"), 2},
+ // Identical strings
+ {[]byte("identical"), []byte("identical"), 9},
+ // Binary data
+ {[]byte{0x00, 0x01, 0x02}, []byte{0x00, 0x01, 0x03}, 2},
+ // Large strings
+ {bytes.Repeat([]byte("a"), 1000), bytes.Repeat([]byte("a"), 1000), 1000},
+ {bytes.Repeat([]byte("a"), 1000), append(bytes.Repeat([]byte("a"), 999), []byte("b")...), 999},
+ }
+
+ for i, test := range tests {
+ result := sharedLen(test.a, test.b)
+ if result != test.expected {
+ t.Errorf("Test %d: sharedLen(%q, %q) = %d, expected %d",
+ i, test.a, test.b, result, test.expected)
+ }
+ // Test commutativity
+ resultReverse := sharedLen(test.b, test.a)
+ if result != resultReverse {
+ t.Errorf("Test %d: sharedLen is not commutative: sharedLen(a,b)=%d, sharedLen(b,a)=%d",
+ i, result, resultReverse)
+ }
+ }
+}
+
+// TestDecodeHeaderCorruptedData tests decodeHeader with corrupted data
+func TestDecodeHeaderCorruptedData(t *testing.T) {
+ // Create valid header data first
+ h := makeTrinodeHistory()
+ header, _, _, _ := h.encode()
+
+ // Test with empty header
+ _, _, _, _, err := decodeHeader([]byte{})
+ if err == nil {
+ t.Fatal("Expected error for empty header")
+ }
+
+ // Test with invalid version
+ corruptedVersion := make([]byte, len(header))
+ copy(corruptedVersion, header)
+ corruptedVersion[0] = 0xFF
+ _, _, _, _, err = decodeHeader(corruptedVersion)
+ if err == nil {
+ t.Fatal("Expected error for invalid version")
+ }
+
+ // Test with truncated header (not divisible by trie header size)
+ truncated := header[:trienodeMetadataSize+5]
+ _, _, _, _, err = decodeHeader(truncated)
+ if err == nil {
+ t.Fatal("Expected error for truncated header")
+ }
+
+ // Test with unordered trie owners
+ unordered := make([]byte, len(header))
+ copy(unordered, header)
+
+ // Swap two owner hashes to make them unordered
+ hash1Start := trienodeMetadataSize
+ hash2Start := trienodeMetadataSize + trienodeTrieHeaderSize
+ hash1 := unordered[hash1Start : hash1Start+common.HashLength]
+ hash2 := unordered[hash2Start : hash2Start+common.HashLength]
+
+ // Only swap if they would be out of order
+ copy(unordered[hash1Start:hash1Start+common.HashLength], hash2)
+ copy(unordered[hash2Start:hash2Start+common.HashLength], hash1)
+
+ _, _, _, _, err = decodeHeader(unordered)
+ if err == nil {
+ t.Fatal("Expected error for unordered trie owners")
+ }
+}
+
+// TestDecodeSingleCorruptedData tests decodeSingle with corrupted data
+func TestDecodeSingleCorruptedData(t *testing.T) {
+ h := makeTrinodeHistory()
+ _, keySection, _, _ := h.encode()
+
+ // Test with empty key section
+ _, err := decodeSingle([]byte{}, nil)
+ if err == nil {
+ t.Fatal("Expected error for empty key section")
+ }
+
+ // Test with key section too small for trailer
+ if len(keySection) > 0 {
+ _, err := decodeSingle(keySection[:3], nil) // Less than 4 bytes for trailer
+ if err == nil {
+ t.Fatal("Expected error for key section too small for trailer")
+ }
+ }
+
+ // Test with corrupted varint in key section
+ corrupted := make([]byte, len(keySection))
+ copy(corrupted, keySection)
+ corrupted[5] = 0xFF // Corrupt varint
+ _, err = decodeSingle(corrupted, nil)
+ if err == nil {
+ t.Fatal("Expected error for corrupted varint")
+ }
+
+ // Test with corrupted trailer (invalid restart count)
+ corrupted = make([]byte, len(keySection))
+ copy(corrupted, keySection)
+ // Set restart count to something too large
+ binary.BigEndian.PutUint32(corrupted[len(corrupted)-4:], 10000)
+ _, err = decodeSingle(corrupted, nil)
+ if err == nil {
+ t.Fatal("Expected error for invalid restart count")
+ }
+}
+
+// Helper function to test encode/decode cycle
+func testEncodeDecode(t *testing.T, h *trienodeHistory) {
+ header, keySection, valueSection, err := h.encode()
+ if err != nil {
+ t.Fatalf("Failed to encode trienode history: %v", err)
+ }
+
+ var decoded trienodeHistory
+ if err := decoded.decode(header, keySection, valueSection); err != nil {
+ t.Fatalf("Failed to decode trienode history: %v", err)
+ }
+
+ // Compare the decoded history with original
+ if !compareList(decoded.owners, h.owners) {
+ t.Fatal("Trie owner list mismatch")
+ }
+ if !compareMapList(decoded.nodeList, h.nodeList) {
+ t.Fatal("Trienode list mismatch")
+ }
+ if !compareMapSet(decoded.nodes, h.nodes) {
+ t.Fatal("Trienode content mismatch")
+ }
+}
diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go
index 02bdef5d34f..efcc3f25497 100644
--- a/triedb/pathdb/journal.go
+++ b/triedb/pathdb/journal.go
@@ -338,10 +338,8 @@ func (db *Database) Journal(root common.Hash) error {
// but the ancient store is not properly closed, resulting in recent writes
// being lost. After a restart, the ancient store would then be misaligned
// with the disk layer, causing data corruption.
- if db.stateFreezer != nil {
- if err := db.stateFreezer.SyncAncient(); err != nil {
- return err
- }
+ if err := syncHistory(db.stateFreezer, db.trienodeFreezer); err != nil {
+ return err
}
// Store the journal into the database and return
var (
diff --git a/triedb/pathdb/metrics.go b/triedb/pathdb/metrics.go
index 779f9d813ff..a0a626f9b51 100644
--- a/triedb/pathdb/metrics.go
+++ b/triedb/pathdb/metrics.go
@@ -69,18 +69,25 @@ var (
gcStorageMeter = metrics.NewRegisteredMeter("pathdb/gc/storage/count", nil)
gcStorageBytesMeter = metrics.NewRegisteredMeter("pathdb/gc/storage/bytes", nil)
- historyBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/time", nil)
- historyDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/data", nil)
- historyIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/index", nil)
+ stateHistoryBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/state/time", nil)
+ stateHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/state/bytes/data", nil)
+ stateHistoryIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/state/bytes/index", nil)
- indexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/index/time", nil)
- unindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/unindex/time", nil)
+ trienodeHistoryBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/time", nil)
+ trienodeHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/data", nil)
+ trienodeHistoryIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/index", nil)
+
+ stateIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/index/time", nil)
+ stateUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/unindex/time", nil)
+ trienodeIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/index/time", nil)
+ trienodeUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/unindex/time", nil)
lookupAddLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/add/time", nil)
lookupRemoveLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/remove/time", nil)
- historicalAccountReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/account/reads", nil)
- historicalStorageReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/storage/reads", nil)
+ historicalAccountReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/account/reads", nil)
+ historicalStorageReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/storage/reads", nil)
+ historicalTrienodeReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/reads", nil)
)
// Metrics in generation
diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go
index 842ac0972e3..514c02435fb 100644
--- a/triedb/pathdb/reader.go
+++ b/triedb/pathdb/reader.go
@@ -204,8 +204,8 @@ type HistoricalStateReader struct {
id uint64
}
-// HistoricReader constructs a reader for accessing the requested historic state.
-func (db *Database) HistoricReader(root common.Hash) (*HistoricalStateReader, error) {
+// HistoricalStateReader constructs a reader for accessing the requested historic state.
+func (db *Database) HistoricalStateReader(root common.Hash) (*HistoricalStateReader, error) {
// Bail out if the state history hasn't been fully indexed
if db.stateIndexer == nil || db.stateFreezer == nil {
return nil, fmt.Errorf("historical state %x is not available", root)
@@ -217,7 +217,7 @@ func (db *Database) HistoricReader(root common.Hash) (*HistoricalStateReader, er
// via `db.StateReader`.
//
// - States older than the current disk layer (including the disk layer
- // itself) are available via `db.HistoricReader`.
+ // itself) are available via `db.HistoricalStateReader`.
id := rawdb.ReadStateID(db.diskdb, root)
if id == nil {
return nil, fmt.Errorf("state %#x is not available", root)
@@ -318,3 +318,91 @@ func (r *HistoricalStateReader) Storage(address common.Address, key common.Hash)
}
return r.reader.read(newStorageIdentQuery(address, addrHash, key, keyHash), r.id, dl.stateID(), latest)
}
+
+// HistoricalNodeReader is a wrapper over history reader, providing access to
+// historical trie node data.
+type HistoricalNodeReader struct {
+ db *Database
+ reader *historyReader
+ id uint64
+}
+
+// HistoricalNodeReader constructs a reader for accessing the requested historic state.
+func (db *Database) HistoricalNodeReader(root common.Hash) (*HistoricalNodeReader, error) {
+ // Bail out if the state history hasn't been fully indexed
+ if db.trienodeIndexer == nil || db.trienodeFreezer == nil {
+ return nil, fmt.Errorf("historical trienode %x is not available", root)
+ }
+ if !db.trienodeIndexer.inited() {
+ return nil, errors.New("trienode histories haven't been fully indexed yet")
+ }
+ // - States at the current disk layer or above are directly accessible
+ // via `db.NodeReader`.
+ //
+ // - States older than the current disk layer (including the disk layer
+ // itself) are available via `db.HistoricalNodeReader`.
+ id := rawdb.ReadStateID(db.diskdb, root)
+ if id == nil {
+ return nil, fmt.Errorf("state %#x is not available", root)
+ }
+ // Ensure the requested trienode history is canonical, states on side chain
+ // are not accessible.
+ // TODO(rjl493456442)
+ meta, err := readTrienodeMetadata(db.trienodeFreezer, *id+1)
+ if err != nil {
+ return nil, err // e.g., the referred trienode history has been pruned
+ }
+ if meta.parent != root {
+ return nil, fmt.Errorf("state %#x is not canonincal", root)
+ }
+ return &HistoricalNodeReader{
+ id: *id,
+ db: db,
+ reader: newHistoryReader(db.diskdb, db.trienodeFreezer),
+ }, nil
+}
+
+// Node directly retrieves the trie node data associated with a particular path,
+// within a particular account. An error will be returned if the read operation
+// exits abnormally. Specifically, if the layer is already stale.
+//
+// Note:
+// - the returned trie node data is not a copy, please don't modify it.
+// - an error will be returned if the requested trie node is not found in database.
+func (r *HistoricalNodeReader) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) {
+ defer func(start time.Time) {
+ historicalTrienodeReadTimer.UpdateSince(start)
+ }(time.Now())
+
+ // TODO(rjl493456442): Theoretically, the obtained disk layer could become stale
+ // within a very short time window.
+ //
+ // While reading the account data while holding `db.tree.lock` can resolve
+ // this issue, but it will introduce a heavy contention over the lock.
+ //
+ // Let's optimistically assume the situation is very unlikely to happen,
+ // and try to define a low granularity lock if the current approach doesn't
+ // work later.
+ dl := r.db.tree.bottom()
+ latest, h, _, err := dl.node(owner, path, 0)
+ if err != nil {
+ return nil, err
+ }
+ if h == hash {
+ return latest, nil
+ }
+ blob, err := r.reader.read(newTrienodeIdentQuery(owner, path), r.id, dl.stateID(), latest)
+ if err != nil {
+ return nil, err
+ }
+ // Error out if the local one is inconsistent with the target.
+ if crypto.Keccak256Hash(blob) != hash {
+ blobHex := "nil"
+ if len(blob) > 0 {
+ blobHex = hexutil.Encode(blob)
+ }
+ log.Error("Unexpected trie node", "owner", owner.Hex(), "path", path, "blob", blobHex)
+ return nil, fmt.Errorf("unexpected node: (%x %v), blob: %s", owner, path, blobHex)
+ }
+ return blob, nil
+}