triedb/pathdb: enable trienode history (#32621)

It's the part-4 for trienode history. The trienode history persistence
has been enabled with this PR by flag `history.trienode <non-negative-number>`
This commit is contained in:
rjl493456442 2026-01-17 21:23:48 +08:00 committed by GitHub
parent 588dd94aad
commit add1890a57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 308 additions and 125 deletions

View file

@ -120,6 +120,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)

View file

@ -94,6 +94,7 @@ var (
utils.LogNoHistoryFlag,
utils.LogExportCheckpointsFlag,
utils.StateHistoryFlag,
utils.TrienodeHistoryFlag,
utils.LightKDFFlag,
utils.EthRequiredBlocksFlag,
utils.LegacyWhitelistFlag, // deprecated

View file

@ -295,6 +295,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)",
@ -1699,6 +1705,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)
}
@ -2299,15 +2308,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,

View file

@ -177,6 +177,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
@ -255,6 +260,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,

View file

@ -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)),

View file

@ -56,6 +56,7 @@ var Defaults = Config{
TransactionHistory: 2350000,
LogHistory: 2350000,
StateHistory: params.FullImmutabilityThreshold,
TrienodeHistory: -1,
DatabaseCache: 512,
TrieCleanCache: 154,
TrieDirtyCache: 256,
@ -108,6 +109,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

View file

@ -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:"-"`
SlowBlockThreshold time.Duration `toml:",omitempty"`
@ -81,6 +82,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.SlowBlockThreshold = c.SlowBlockThreshold
@ -135,6 +137,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:"-"`
SlowBlockThreshold *time.Duration `toml:",omitempty"`
@ -216,6 +219,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
}

View file

@ -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,11 +165,9 @@ 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 {
if err := freezer.SyncAncient(); err != nil {
b.flushErr = err
return
}
if err := syncHistory(freezers...); err != nil {
b.flushErr = err
return
}
nodes := b.nodes.write(batch, nodesCache)
accounts, slots := b.states.write(batch, progress, statesCache)

View file

@ -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)
}

View file

@ -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
// 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
}
freezer, err := rawdb.NewStateFreezer(ancient, db.isVerkle, db.readOnly)
if err != nil {
log.Crit("Failed to open state history freezer", "err", err)
}
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 db.stateFreezer != nil {
if db.stateIndexer != nil {
db.stateIndexer.close()
}
if frozen != 0 {
// Purge all state history indexing data first
batch := db.diskdb.NewBatch()
rawdb.DeleteStateHistoryIndexMetadata(batch)
rawdb.DeleteStateHistoryIndexes(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")
db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID(), typeStateHistory)
log.Info("Enabled state history indexing")
}
if db.trienodeFreezer != nil {
if db.trienodeIndexer != nil {
db.trienodeIndexer.close()
}
return nil
db.trienodeIndexer = newHistoryIndexer(db.diskdb, db.trienodeFreezer, db.tree.bottom().stateID(), typeTrienodeHistory)
log.Info("Enabled trienode history indexing")
}
// 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)
}
if pruned != 0 {
log.Warn("Truncated extra state histories", "number", pruned)
}
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.DeleteStateHistoryIndexes(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()
}
// Close the attached state history freezer.
if db.stateFreezer == nil {
return nil
if db.trienodeIndexer != nil {
db.trienodeIndexer.close()
}
return db.stateFreezer.Close()
// Close the attached state history freezer.
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 nil
}
// Size returns the current storage size of the memory cache in front of the

View file

@ -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"
)
@ -323,36 +324,60 @@ func (dl *diskLayer) update(root common.Hash, id uint64, block uint64, nodes *no
return newDiffLayer(dl, root, id, block, nodes, states)
}
// writeStateHistory stores the state history and indexes if indexing is
// writeHistory stores the specified history and indexes if indexing is
// permitted.
//
// What's more, this function also returns a flag indicating whether the
// buffer flushing is required, ensuring the persistent state ID is always
// greater than or equal to the first history ID.
func (dl *diskLayer) writeStateHistory(diff *diffLayer) (bool, error) {
// Short circuit if state history is not permitted
if dl.db.stateFreezer == nil {
func (dl *diskLayer) writeHistory(typ historyType, diff *diffLayer) (bool, error) {
var (
limit uint64
freezer ethdb.AncientStore
indexer *historyIndexer
writeFunc func(writer ethdb.AncientWriter, dl *diffLayer) error
)
switch typ {
case typeStateHistory:
freezer = dl.db.stateFreezer
indexer = dl.db.stateIndexer
writeFunc = writeStateHistory
limit = dl.db.config.StateHistory
case typeTrienodeHistory:
freezer = dl.db.trienodeFreezer
indexer = dl.db.trienodeIndexer
writeFunc = writeTrienodeHistory
// Skip the history commit if the trienode history is not permitted
if dl.db.config.TrienodeHistory < 0 {
return false, nil
}
limit = uint64(dl.db.config.TrienodeHistory)
default:
panic(fmt.Sprintf("unknown history type: %v", typ))
}
// Short circuit if the history freezer is nil
if freezer == nil {
return false, nil
}
// Bail out with an error if writing the state history fails.
// This can happen, for example, if the device is full.
err := writeStateHistory(dl.db.stateFreezer, diff)
err := writeFunc(freezer, diff)
if err != nil {
return false, err
}
// Notify the state history indexer for newly created history
if dl.db.stateIndexer != nil {
if err := dl.db.stateIndexer.extend(diff.stateID()); err != nil {
// Notify the history indexer for newly created history
if indexer != nil {
if err := indexer.extend(diff.stateID()); err != nil {
return false, err
}
}
// Determine if the persisted history object has exceeded the
// configured limitation.
limit := dl.db.config.StateHistory
if limit == 0 {
return false, nil
}
tail, err := dl.db.stateFreezer.Tail()
tail, err := freezer.Tail()
if err != nil {
return false, err
} // firstID = tail+1
@ -375,14 +400,14 @@ func (dl *diskLayer) writeStateHistory(diff *diffLayer) (bool, error) {
// These measures ensure the persisted state ID always remains greater
// than or equal to the first history ID.
if persistentID := rawdb.ReadPersistentStateID(dl.db.diskdb); persistentID < newFirst {
log.Debug("Skip tail truncation", "persistentID", persistentID, "tailID", tail+1, "headID", diff.stateID(), "limit", limit)
log.Debug("Skip tail truncation", "type", typ, "persistentID", persistentID, "tailID", tail+1, "headID", diff.stateID(), "limit", limit)
return true, nil
}
pruned, err := truncateFromTail(dl.db.stateFreezer, typeStateHistory, newFirst-1)
pruned, err := truncateFromTail(freezer, typ, newFirst-1)
if err != nil {
return false, err
}
log.Debug("Pruned state history", "items", pruned, "tailid", newFirst)
log.Debug("Pruned history", "type", typ, "items", pruned, "tailid", newFirst)
return false, nil
}
@ -396,10 +421,22 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
// Construct and store the state history first. If crash happens after storing
// the state history but without flushing the corresponding states(journal),
// the stored state history will be truncated from head in the next restart.
flush, err := dl.writeStateHistory(bottom)
flushA, err := dl.writeHistory(typeStateHistory, bottom)
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.
flushB, err := dl.writeHistory(typeTrienodeHistory, bottom)
if err != nil {
return nil, err
}
// Since the state history and trienode history may be configured with different
// lengths, the buffer will be flushed once either of them meets its threshold.
flush := flushA || flushB
// Mark the diskLayer as stale before applying any mutations on top.
dl.stale = true
@ -448,7 +485,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 +541,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

View file

@ -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"
)
@ -323,3 +324,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, nhead)
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
}

View file

@ -672,7 +672,6 @@ func (r *trienodeHistoryReader) read(owner common.Hash, path string) ([]byte, er
}
// writeTrienodeHistory persists the trienode history associated with the given diff layer.
// nolint:unused
func writeTrienodeHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
start := time.Now()
h := newTrienodeHistory(dl.rootHash(), dl.parent.rootHash(), dl.block, dl.nodes.nodeOrigin)

View file

@ -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 (

View file

@ -73,11 +73,8 @@ var (
stateHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/state/bytes/data", nil)
stateHistoryIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/state/bytes/index", nil)
//nolint:unused
trienodeHistoryBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/time", nil)
//nolint:unused
trienodeHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/data", nil)
//nolint:unused
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)