Skip to content

Commit

Permalink
core: improve chain rewinding mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
rjl493456442 committed Mar 11, 2024
1 parent aadcb88 commit 2f0f190
Showing 1 changed file with 147 additions and 55 deletions.
202 changes: 147 additions & 55 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,136 @@ func (bc *BlockChain) SetSafe(header *types.Header) {
}
}

// rewindPathHead implements the logic of rewindHead in the context of hash scheme.
func (bc *BlockChain) rewindHashHead(root common.Hash) (*types.Header, uint64) {
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)
pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot point state
headBlock = bc.CurrentBlock() // Head block of the chain
rootNumber uint64 // Associated block number of requested root
)
// The oldest block to be searched is determined by the pivot block or a soft
// finality threshold. The rationale behind this is as follows:
//
// - Snap sync is selected if the pivot block is available. The earliest available
// state is the pivot block itself, so there is no sense in going further back.
//
// - Full sync is selected if the pivot block does not exist. The hash database
// periodically flushes the state to disk, and the soft finality threshold is
// considered sufficient to find a persistent state, even for the testnet. It
// might be not enough for a chain that is nearly empty. In the worst case,
// the entire chain is reset to genesis, and snap sync is re-enabled on top,
// which is still acceptable.
if pivot != nil {
limit = *pivot
} else if headBlock.Number.Uint64() > params.FullImmutabilityThreshold {
limit = headBlock.Number.Uint64() - params.FullImmutabilityThreshold
}
for {
// If a root threshold was requested but not yet crossed, check
if !beyondRoot && headBlock.Root == root {
beyondRoot, rootNumber = true, headBlock.Number.Uint64()
}
// If the associated state is not reachable, continue searching
// backwards until an available state is found.
if !bc.HasState(headBlock.Root) {
log.Trace("Block state missing, rewinding further", "number", headBlock.Number, "hash", headBlock.Hash())

// If search limit is reached, return the genesis block as
// the new chain head.
if headBlock.Number.Uint64() < limit {
log.Info("Rewinding limit is reached, aiming genesis", "number", headBlock.Number, "hash", headBlock.Hash(), "limit", limit)
return bc.genesisBlock.Header(), rootNumber
}
// If the chain is gapped in the middle, return the genesis
// block as the new chain head.
parent := bc.GetHeader(headBlock.ParentHash, headBlock.Number.Uint64()-1)
if parent == nil {
log.Error("Missing block in the middle, aiming genesis", "number", headBlock.Number.Uint64()-1, "hash", headBlock.ParentHash)
return bc.genesisBlock.Header(), rootNumber
}
headBlock = parent

// If the genesis block is reached, stop searching.
if headBlock.Number.Uint64() == 0 {
log.Info("Genesis block is reached", "number", headBlock.Number, "hash", headBlock.Hash())
return headBlock, rootNumber
}
continue // keep rewinding
}
// Once the available state is found, ensure that the requested root has already
// been crossed. If not, continue rewinding.
if beyondRoot || headBlock.Number.Uint64() == 0 {
log.Debug("Rewound to block with state", "number", headBlock.Number, "hash", headBlock.Hash())
return headBlock, rootNumber
}
log.Debug("Skipping block with threshold state", "number", headBlock.Number, "hash", headBlock.Hash(), "root", headBlock.Root)
headBlock = bc.GetHeader(headBlock.ParentHash, headBlock.Number.Uint64()-1) // Keep rewinding
}
}

// rewindPathHead implements the logic of rewindHead in the context of path scheme.
func (bc *BlockChain) rewindPathHead(root common.Hash) (*types.Header, uint64) {
var (
beyondRoot = root == common.Hash{} // Flag whether we're beyond the requested root (no root, always true)
headBlock = bc.CurrentBlock() // Head block of the chain
pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot point state
rootNumber uint64 // Associated block number of requested root
)
// Rewind the head block tag until an available state is found.
for !bc.HasState(headBlock.Root) {
// If a root threshold was requested but not yet crossed, check
if !beyondRoot && headBlock.Root == root {
beyondRoot, rootNumber = true, headBlock.Number.Uint64()
}
// If pivot block is reached, return the genesis block as the
// new chain head. Theoretically there must be a persistent
// state before or at the pivot block, prevent endless rewinding
// towards the genesis just in case.
if pivot != nil && *pivot >= headBlock.Number.Uint64() {
log.Info("Pivot block is reached, aiming genesis", "number", headBlock.Number, "hash", headBlock.Hash())
return bc.genesisBlock.Header(), rootNumber
}
headBlock = bc.GetHeader(headBlock.ParentHash, headBlock.Number.Uint64()-1) // Keep rewinding
}
// The requested root has already been crossed. Return the block
// corresponding to the available state as the new chain head.
if beyondRoot {
return headBlock, rootNumber
}
// If the requested root has not yet been crossed, prevent further
// backward rewinding if reaching the root is deemed impossible.
if !bc.HasState(root) && !bc.stateRecoverable(root) {
return headBlock, 0
}
// Rewind the head block tag until the requested root is reached.
for !beyondRoot {
headBlock = bc.GetHeader(headBlock.ParentHash, headBlock.Number.Uint64()-1) // Keep rewinding
beyondRoot = headBlock.Root == root
}
// Recover if the target state is not available.
if !bc.HasState(root) {
if err := bc.triedb.Recover(root); err != nil {
log.Crit("Failed to rollback state", "err", err) // Shouldn't happen
}
}
return headBlock, rootNumber
}

// rewindHead searches the available states in the database and returns the associated
// block as the new head block.
//
// If the given root is not empty, then the rewind should attempt to pass the specified
// state root and return the associated block number as well. If the root is deemed
// impossible to pass, 0 is returned.
func (bc *BlockChain) rewindHead(root common.Hash) (*types.Header, uint64) {
if bc.triedb.Scheme() == rawdb.PathScheme {
return bc.rewindPathHead(root)
}
return bc.rewindHashHead(root)
}

// setHeadBeyondRoot rewinds the local chain to a new head with the extra condition
// that the rewind must pass the specified state root. This method is meant to be
// used when rewinding with snapshots enabled to ensure that we go back further than
Expand All @@ -634,79 +764,40 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
}
defer bc.chainmu.Unlock()

// Track the block number of the requested root hash
var rootNumber uint64 // (no root == always 0)

// Retrieve the last pivot block to short circuit rollbacks beyond it and the
// current freezer limit to start nuking id underflown
pivot := rawdb.ReadLastPivotNumber(bc.db)
frozen, _ := bc.db.Ancients()
var (
// Track the block number of the requested root hash
rootNumber uint64 // (no root == always 0)

// Retrieve the last pivot block to short circuit rollbacks beyond it
// and the current freezer limit to start nuking it's underflown.
pivot = rawdb.ReadLastPivotNumber(bc.db)
)
updateFn := func(db ethdb.KeyValueWriter, header *types.Header) (*types.Header, bool) {
// Rewind the blockchain, ensuring we don't end up with a stateless head
// block. Note, depth equality is permitted to allow using SetHead as a
// chain reparation mechanism without deleting any data!
if currentBlock := bc.CurrentBlock(); currentBlock != nil && header.Number.Uint64() <= currentBlock.Number.Uint64() {
newHeadBlock := bc.GetBlock(header.Hash(), header.Number.Uint64())
if newHeadBlock == nil {
log.Error("Gap in the chain, rewinding to genesis", "number", header.Number, "hash", header.Hash())
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)

for {
// If a root threshold was requested but not yet crossed, check
if root != (common.Hash{}) && !beyondRoot && newHeadBlock.Root() == root {
beyondRoot, rootNumber = true, newHeadBlock.NumberU64()
}
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 {
parent := bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1)
if parent != nil {
newHeadBlock = parent
continue
}
log.Error("Missing block in the middle, aiming genesis", "number", newHeadBlock.NumberU64()-1, "hash", newHeadBlock.ParentHash())
newHeadBlock = bc.genesisBlock
} else {
log.Trace("Rewind passed pivot, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "pivot", *pivot)
newHeadBlock = bc.genesisBlock
}
}
if beyondRoot || newHeadBlock.NumberU64() == 0 {
if !bc.HasState(newHeadBlock.Root()) && bc.stateRecoverable(newHeadBlock.Root()) {
// Rewind to a block with recoverable state. If the state is
// missing, run the state recovery here.
if err := bc.triedb.Recover(newHeadBlock.Root()); err != nil {
log.Crit("Failed to rollback state", "err", err) // Shouldn't happen
}
log.Debug("Rewound to block with state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash())
}
break
}
log.Debug("Skipping block with threshold state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "root", newHeadBlock.Root())
newHeadBlock = bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) // Keep rewinding
}
}
var newHeadBlock *types.Header
newHeadBlock, rootNumber = bc.rewindHead(root)
rawdb.WriteHeadBlockHash(db, newHeadBlock.Hash())

// Degrade the chain markers if they are explicitly reverted.
// In theory we should update all in-memory markers in the
// last step, however the direction of SetHead is from high
// to low, so it's safe to update in-memory markers directly.
bc.currentBlock.Store(newHeadBlock.Header())
headBlockGauge.Update(int64(newHeadBlock.NumberU64()))
bc.currentBlock.Store(newHeadBlock)
headBlockGauge.Update(int64(newHeadBlock.Number.Uint64()))

// 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.
if !bc.HasState(newHeadBlock.Root()) {
log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number(), "hash", newHeadBlock.Hash())
if !bc.HasState(newHeadBlock.Root) {
if newHeadBlock.Number.Uint64() != 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())
}
}
// Rewind the snap block in a simpleton way to the target head
Expand All @@ -733,6 +824,7 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
// intent afterwards is full block importing, delete the chain segment
// between the stateful-block and the sethead target.
var wipe bool
frozen, _ := bc.db.Ancients()
if headNumber+1 < frozen {
wipe = pivot == nil || headNumber >= *pivot
}
Expand Down

0 comments on commit 2f0f190

Please sign in to comment.