From cfacd19ce7640cf8404fd33ddc44d01ca6f32486 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Thu, 7 Mar 2024 14:42:32 +0800 Subject: [PATCH] triedb, core: limit the chain rewinding --- core/blockchain.go | 62 +++++++++++++++++++++++++++++---------- triedb/database.go | 9 ++++++ triedb/pathdb/database.go | 17 +++++++++++ 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 91f5182ab4..63967187de 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -653,31 +653,55 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha newHeadBlock = bc.genesisBlock } else { // Block exists. Keep rewinding until either we find one with state - // or until we exceed the optional threshold root hash - beyondRoot := (root == common.Hash{}) // Flag whether we're beyond the requested root (no root, always true) - + // or until we exceed the optional threshold root hash. The chain + // will be reset to genesis if rewinding exceeds the maximum allowed + // depth. + var ( + limit uint64 // The oldest block that will be searched for this rewinding + beyondRoot = root == common.Hash{} // Flag whether we're beyond the requested root (no root, always true) + ) + if currentBlock.Number.Uint64() > params.FullImmutabilityThreshold { + limit = currentBlock.Number.Uint64() - params.FullImmutabilityThreshold + } + if oldest, err := bc.triedb.OldestState(); err != nil && oldest < limit { + limit = oldest + } for { // If a root threshold was requested but not yet crossed, check - if root != (common.Hash{}) && !beyondRoot && newHeadBlock.Root() == root { + if !beyondRoot && newHeadBlock.Root() == root { beyondRoot, rootNumber = true, newHeadBlock.NumberU64() } + // If the associated state of new head block is not available + // and impossible to be recovered, rewind further. if !bc.HasState(newHeadBlock.Root()) && !bc.stateRecoverable(newHeadBlock.Root()) { log.Trace("Block state missing, rewinding further", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) - if pivot == nil || newHeadBlock.NumberU64() > *pivot { + + if pivot != nil && newHeadBlock.NumberU64() <= *pivot { + // Stop rewinding if the oldest state by snap sync has already + // been crossed. + log.Trace("Rewind passed pivot, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "pivot", *pivot) + newHeadBlock = bc.genesisBlock + } else { + // Either the state was synced via a full sync process, or the + // oldest state has not been reached yet, continue rewinding + // until an available state is found. parent := bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) if parent == nil { log.Error("Missing block in the middle, aiming genesis", "number", newHeadBlock.NumberU64()-1, "hash", newHeadBlock.ParentHash()) newHeadBlock = bc.genesisBlock } else { newHeadBlock = parent - if newHeadBlock.NumberU64() != 0 { + if newHeadBlock.NumberU64() != 0 && newHeadBlock.NumberU64() >= limit { continue } - log.Info("Genesis block is reached", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + if newHeadBlock.NumberU64() == 0 { + log.Info("Genesis block is reached", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + } + if newHeadBlock.NumberU64() < limit { + newHeadBlock = bc.genesisBlock + log.Info("Rewinding limit is reached, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + } } - } else { - log.Trace("Rewind passed pivot, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "pivot", *pivot) - newHeadBlock = bc.genesisBlock } } if beyondRoot || newHeadBlock.NumberU64() == 0 { @@ -704,12 +728,20 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha bc.currentBlock.Store(newHeadBlock.Header()) headBlockGauge.Update(int64(newHeadBlock.NumberU64())) - // The head state is missing, which is only possible in the path-based - // scheme. This situation occurs when the chain head is rewound below - // the pivot point. In this scenario, there is no possible recovery - // approach except for rerunning a snap sync. Do nothing here until the - // state syncer picks it up. + // The associated head state is missing, which might occur if chain head + // is rewound even below the oldest available state. The whole chain is + // reset to genesis in case. + // + // In this scenario, there is no possible recovery approach except for + // rerunning a snap sync. Do nothing here until the state syncer picks + // it up. if !bc.HasState(newHeadBlock.Root()) { + // Disable snapshot repairing explicitly, as a new snap sync + // is expected and the whole snapshot needs to be rebuilt. + rootNumber = 0 + if newHeadBlock.NumberU64() != 0 { + log.Crit("Chain is stateless at a non-genesis block") + } log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number(), "hash", newHeadBlock.Hash()) } } diff --git a/triedb/database.go b/triedb/database.go index 939a21f147..c9aec51f2b 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -332,6 +332,15 @@ func (db *Database) SetBufferSize(size int) error { return pdb.SetBufferSize(size) } +// OldestState returns the oldest state maintained by database. +func (db *Database) OldestState() (uint64, error) { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return 0, errors.New("not supported") + } + return pdb.OldestState() +} + // IsVerkle returns the indicator if the database is holding a verkle tree. func (db *Database) IsVerkle() bool { return db.config.IsVerkle diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index b1e01abac4..8e71b16636 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -474,6 +474,23 @@ func (db *Database) Scheme() string { return rawdb.PathScheme } +// OldestState returns the oldest state available in the database, no matter +// it's available or recoverable. +func (db *Database) OldestState() (uint64, error) { + if db.freezer == nil { + return 0, errors.New("state rollback is non-supported") + } + tail, err := db.freezer.Tail() + if err != nil { + return 0, err + } + h, err := readHistory(db.freezer, tail+1) + if err != nil { + return 0, err + } + return h.meta.block, nil +} + // modifyAllowed returns the indicator if mutation is allowed. This function // assumes the db.lock is already held. func (db *Database) modifyAllowed() error {