Skip to content

Commit c170fa2

Browse files
authored
core: improve chain rewinding mechanism (#29196)
* core: improve chain rewinding mechanism * core: address comment * core: periodically print progress log * core: address comments * core: fix comment * core: fix rewinding in path * core: fix beyondRoot condition * core: polish code * core: polish code * core: extend code comment * core: stop rewinding if chain is gapped or genesis is reached * core: fix broken tests
1 parent b80643b commit c170fa2

File tree

1 file changed

+183
-55
lines changed

1 file changed

+183
-55
lines changed

core/blockchain.go

Lines changed: 183 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,172 @@ func (bc *BlockChain) SetSafe(header *types.Header) {
616616
}
617617
}
618618

619+
// rewindPathHead implements the logic of rewindHead in the context of hash scheme.
620+
func (bc *BlockChain) rewindHashHead(head *types.Header, root common.Hash) (*types.Header, uint64) {
621+
var (
622+
limit uint64 // The oldest block that will be searched for this rewinding
623+
beyondRoot = root == common.Hash{} // Flag whether we're beyond the requested root (no root, always true)
624+
pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot point state
625+
rootNumber uint64 // Associated block number of requested root
626+
627+
start = time.Now() // Timestamp the rewinding is restarted
628+
logged = time.Now() // Timestamp last progress log was printed
629+
)
630+
// The oldest block to be searched is determined by the pivot block or a constant
631+
// searching threshold. The rationale behind this is as follows:
632+
//
633+
// - Snap sync is selected if the pivot block is available. The earliest available
634+
// state is the pivot block itself, so there is no sense in going further back.
635+
//
636+
// - Full sync is selected if the pivot block does not exist. The hash database
637+
// periodically flushes the state to disk, and the used searching threshold is
638+
// considered sufficient to find a persistent state, even for the testnet. It
639+
// might be not enough for a chain that is nearly empty. In the worst case,
640+
// the entire chain is reset to genesis, and snap sync is re-enabled on top,
641+
// which is still acceptable.
642+
if pivot != nil {
643+
limit = *pivot
644+
} else if head.Number.Uint64() > params.FullImmutabilityThreshold {
645+
limit = head.Number.Uint64() - params.FullImmutabilityThreshold
646+
}
647+
for {
648+
logger := log.Trace
649+
if time.Since(logged) > time.Second*8 {
650+
logged = time.Now()
651+
logger = log.Info
652+
}
653+
logger("Block state missing, rewinding further", "number", head.Number, "hash", head.Hash(), "elapsed", common.PrettyDuration(time.Since(start)))
654+
655+
// If a root threshold was requested but not yet crossed, check
656+
if !beyondRoot && head.Root == root {
657+
beyondRoot, rootNumber = true, head.Number.Uint64()
658+
}
659+
// If search limit is reached, return the genesis block as the
660+
// new chain head.
661+
if head.Number.Uint64() < limit {
662+
log.Info("Rewinding limit reached, resetting to genesis", "number", head.Number, "hash", head.Hash(), "limit", limit)
663+
return bc.genesisBlock.Header(), rootNumber
664+
}
665+
// If the associated state is not reachable, continue searching
666+
// backwards until an available state is found.
667+
if !bc.HasState(head.Root) {
668+
// If the chain is gapped in the middle, return the genesis
669+
// block as the new chain head.
670+
parent := bc.GetHeader(head.ParentHash, head.Number.Uint64()-1)
671+
if parent == nil {
672+
log.Error("Missing block in the middle, resetting to genesis", "number", head.Number.Uint64()-1, "hash", head.ParentHash)
673+
return bc.genesisBlock.Header(), rootNumber
674+
}
675+
head = parent
676+
677+
// If the genesis block is reached, stop searching.
678+
if head.Number.Uint64() == 0 {
679+
log.Info("Genesis block reached", "number", head.Number, "hash", head.Hash())
680+
return head, rootNumber
681+
}
682+
continue // keep rewinding
683+
}
684+
// Once the available state is found, ensure that the requested root
685+
// has already been crossed. If not, continue rewinding.
686+
if beyondRoot || head.Number.Uint64() == 0 {
687+
log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash())
688+
return head, rootNumber
689+
}
690+
log.Debug("Skipping block with threshold state", "number", head.Number, "hash", head.Hash(), "root", head.Root)
691+
head = bc.GetHeader(head.ParentHash, head.Number.Uint64()-1) // Keep rewinding
692+
}
693+
}
694+
695+
// rewindPathHead implements the logic of rewindHead in the context of path scheme.
696+
func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*types.Header, uint64) {
697+
var (
698+
pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot block
699+
rootNumber uint64 // Associated block number of requested root
700+
701+
// BeyondRoot represents whether the requested root is already
702+
// crossed. The flag value is set to true if the root is empty.
703+
beyondRoot = root == common.Hash{}
704+
705+
// noState represents if the target state requested for search
706+
// is unavailable and impossible to be recovered.
707+
noState = !bc.HasState(root) && !bc.stateRecoverable(root)
708+
709+
start = time.Now() // Timestamp the rewinding is restarted
710+
logged = time.Now() // Timestamp last progress log was printed
711+
)
712+
// Rewind the head block tag until an available state is found.
713+
for {
714+
logger := log.Trace
715+
if time.Since(logged) > time.Second*8 {
716+
logged = time.Now()
717+
logger = log.Info
718+
}
719+
logger("Block state missing, rewinding further", "number", head.Number, "hash", head.Hash(), "elapsed", common.PrettyDuration(time.Since(start)))
720+
721+
// If a root threshold was requested but not yet crossed, check
722+
if !beyondRoot && head.Root == root {
723+
beyondRoot, rootNumber = true, head.Number.Uint64()
724+
}
725+
// If the root threshold hasn't been crossed but the available
726+
// state is reached, quickly determine if the target state is
727+
// possible to be reached or not.
728+
if !beyondRoot && noState && bc.HasState(head.Root) {
729+
beyondRoot = true
730+
log.Info("Disable the search for unattainable state", "root", root)
731+
}
732+
// Check if the associated state is available or recoverable if
733+
// the requested root has already been crossed.
734+
if beyondRoot && (bc.HasState(head.Root) || bc.stateRecoverable(head.Root)) {
735+
break
736+
}
737+
// If pivot block is reached, return the genesis block as the
738+
// new chain head. Theoretically there must be a persistent
739+
// state before or at the pivot block, prevent endless rewinding
740+
// towards the genesis just in case.
741+
if pivot != nil && *pivot >= head.Number.Uint64() {
742+
log.Info("Pivot block reached, resetting to genesis", "number", head.Number, "hash", head.Hash())
743+
return bc.genesisBlock.Header(), rootNumber
744+
}
745+
// If the chain is gapped in the middle, return the genesis
746+
// block as the new chain head
747+
parent := bc.GetHeader(head.ParentHash, head.Number.Uint64()-1) // Keep rewinding
748+
if parent == nil {
749+
log.Error("Missing block in the middle, resetting to genesis", "number", head.Number.Uint64()-1, "hash", head.ParentHash)
750+
return bc.genesisBlock.Header(), rootNumber
751+
}
752+
head = parent
753+
754+
// If the genesis block is reached, stop searching.
755+
if head.Number.Uint64() == 0 {
756+
log.Info("Genesis block reached", "number", head.Number, "hash", head.Hash())
757+
return head, rootNumber
758+
}
759+
}
760+
// Recover if the target state if it's not available yet.
761+
if !bc.HasState(head.Root) {
762+
if err := bc.triedb.Recover(head.Root); err != nil {
763+
log.Crit("Failed to rollback state", "err", err)
764+
}
765+
}
766+
log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash())
767+
return head, rootNumber
768+
}
769+
770+
// rewindHead searches the available states in the database and returns the associated
771+
// block as the new head block.
772+
//
773+
// If the given root is not empty, then the rewind should attempt to pass the specified
774+
// state root and return the associated block number as well. If the root, typically
775+
// representing the state corresponding to snapshot disk layer, is deemed impassable,
776+
// then block number zero is returned, indicating that snapshot recovery is disabled
777+
// and the whole snapshot should be auto-generated in case of head mismatch.
778+
func (bc *BlockChain) rewindHead(head *types.Header, root common.Hash) (*types.Header, uint64) {
779+
if bc.triedb.Scheme() == rawdb.PathScheme {
780+
return bc.rewindPathHead(head, root)
781+
}
782+
return bc.rewindHashHead(head, root)
783+
}
784+
619785
// setHeadBeyondRoot rewinds the local chain to a new head with the extra condition
620786
// that the rewind must pass the specified state root. This method is meant to be
621787
// used when rewinding with snapshots enabled to ensure that we go back further than
@@ -634,79 +800,40 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
634800
}
635801
defer bc.chainmu.Unlock()
636802

637-
// Track the block number of the requested root hash
638-
var rootNumber uint64 // (no root == always 0)
639-
640-
// Retrieve the last pivot block to short circuit rollbacks beyond it and the
641-
// current freezer limit to start nuking id underflown
642-
pivot := rawdb.ReadLastPivotNumber(bc.db)
643-
frozen, _ := bc.db.Ancients()
803+
var (
804+
// Track the block number of the requested root hash
805+
rootNumber uint64 // (no root == always 0)
644806

807+
// Retrieve the last pivot block to short circuit rollbacks beyond it
808+
// and the current freezer limit to start nuking it's underflown.
809+
pivot = rawdb.ReadLastPivotNumber(bc.db)
810+
)
645811
updateFn := func(db ethdb.KeyValueWriter, header *types.Header) (*types.Header, bool) {
646812
// Rewind the blockchain, ensuring we don't end up with a stateless head
647813
// block. Note, depth equality is permitted to allow using SetHead as a
648814
// chain reparation mechanism without deleting any data!
649815
if currentBlock := bc.CurrentBlock(); currentBlock != nil && header.Number.Uint64() <= currentBlock.Number.Uint64() {
650-
newHeadBlock := bc.GetBlock(header.Hash(), header.Number.Uint64())
651-
if newHeadBlock == nil {
652-
log.Error("Gap in the chain, rewinding to genesis", "number", header.Number, "hash", header.Hash())
653-
newHeadBlock = bc.genesisBlock
654-
} else {
655-
// Block exists. Keep rewinding until either we find one with state
656-
// or until we exceed the optional threshold root hash
657-
beyondRoot := (root == common.Hash{}) // Flag whether we're beyond the requested root (no root, always true)
658-
659-
for {
660-
// If a root threshold was requested but not yet crossed, check
661-
if root != (common.Hash{}) && !beyondRoot && newHeadBlock.Root() == root {
662-
beyondRoot, rootNumber = true, newHeadBlock.NumberU64()
663-
}
664-
if !bc.HasState(newHeadBlock.Root()) && !bc.stateRecoverable(newHeadBlock.Root()) {
665-
log.Trace("Block state missing, rewinding further", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash())
666-
if pivot == nil || newHeadBlock.NumberU64() > *pivot {
667-
parent := bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1)
668-
if parent != nil {
669-
newHeadBlock = parent
670-
continue
671-
}
672-
log.Error("Missing block in the middle, aiming genesis", "number", newHeadBlock.NumberU64()-1, "hash", newHeadBlock.ParentHash())
673-
newHeadBlock = bc.genesisBlock
674-
} else {
675-
log.Trace("Rewind passed pivot, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "pivot", *pivot)
676-
newHeadBlock = bc.genesisBlock
677-
}
678-
}
679-
if beyondRoot || newHeadBlock.NumberU64() == 0 {
680-
if !bc.HasState(newHeadBlock.Root()) && bc.stateRecoverable(newHeadBlock.Root()) {
681-
// Rewind to a block with recoverable state. If the state is
682-
// missing, run the state recovery here.
683-
if err := bc.triedb.Recover(newHeadBlock.Root()); err != nil {
684-
log.Crit("Failed to rollback state", "err", err) // Shouldn't happen
685-
}
686-
log.Debug("Rewound to block with state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash())
687-
}
688-
break
689-
}
690-
log.Debug("Skipping block with threshold state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "root", newHeadBlock.Root())
691-
newHeadBlock = bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) // Keep rewinding
692-
}
693-
}
816+
var newHeadBlock *types.Header
817+
newHeadBlock, rootNumber = bc.rewindHead(header, root)
694818
rawdb.WriteHeadBlockHash(db, newHeadBlock.Hash())
695819

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

703827
// The head state is missing, which is only possible in the path-based
704828
// scheme. This situation occurs when the chain head is rewound below
705829
// the pivot point. In this scenario, there is no possible recovery
706830
// approach except for rerunning a snap sync. Do nothing here until the
707831
// state syncer picks it up.
708-
if !bc.HasState(newHeadBlock.Root()) {
709-
log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number(), "hash", newHeadBlock.Hash())
832+
if !bc.HasState(newHeadBlock.Root) {
833+
if newHeadBlock.Number.Uint64() != 0 {
834+
log.Crit("Chain is stateless at a non-genesis block")
835+
}
836+
log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number, "hash", newHeadBlock.Hash())
710837
}
711838
}
712839
// Rewind the snap block in a simpleton way to the target head
@@ -733,6 +860,7 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
733860
// intent afterwards is full block importing, delete the chain segment
734861
// between the stateful-block and the sethead target.
735862
var wipe bool
863+
frozen, _ := bc.db.Ancients()
736864
if headNumber+1 < frozen {
737865
wipe = pivot == nil || headNumber >= *pivot
738866
}

0 commit comments

Comments
 (0)