Merge pull request #1931 from Crypt-iQ/export_header_funcs

blockchain: refactor and export header validation checks
This commit is contained in:
Olaoluwa Osuntokun 2023-06-29 15:01:41 -04:00 committed by GitHub
commit f5eeb10d03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 270 additions and 76 deletions

View File

@ -169,6 +169,60 @@ func (node *blockNode) Ancestor(height int32) *blockNode {
return n
}
// Height returns the blockNode's height in the chain.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Height() int32 {
return node.height
}
// Bits returns the blockNode's nBits.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Bits() uint32 {
return node.bits
}
// Timestamp returns the blockNode's timestamp.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Timestamp() int64 {
return node.timestamp
}
// Parent returns the blockNode's parent.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) Parent() HeaderCtx {
if node.parent == nil {
// This is required since node.parent is a *blockNode and if we
// do not explicitly return nil here, the caller may fail when
// nil-checking this.
return nil
}
return node.parent
}
// RelativeAncestorCtx returns the blockNode's ancestor that is distance blocks
// before it in the chain. This is equivalent to the RelativeAncestor function
// below except that the return type is different.
//
// This function is safe for concurrent access.
//
// NOTE: Part of the HeaderCtx interface.
func (node *blockNode) RelativeAncestorCtx(distance int32) HeaderCtx {
ancestor := node.RelativeAncestor(distance)
if ancestor == nil {
// This is required since RelativeAncestor returns a *blockNode
// and if we do not explicitly return nil here, the caller may
// fail when nil-checking this.
return nil
}
return ancestor
}
// RelativeAncestor returns the ancestor block node a relative 'distance' blocks
// before this node. This is equivalent to calling Ancestor with the node's
// height minus provided distance.
@ -182,17 +236,17 @@ func (node *blockNode) RelativeAncestor(distance int32) *blockNode {
// prior to, and including, the block node.
//
// This function is safe for concurrent access.
func (node *blockNode) CalcPastMedianTime() time.Time {
func CalcPastMedianTime(node HeaderCtx) time.Time {
// Create a slice of the previous few block timestamps used to calculate
// the median per the number defined by the constant medianTimeBlocks.
timestamps := make([]int64, medianTimeBlocks)
numNodes := 0
iterNode := node
for i := 0; i < medianTimeBlocks && iterNode != nil; i++ {
timestamps[i] = iterNode.timestamp
timestamps[i] = iterNode.Timestamp()
numNodes++
iterNode = iterNode.parent
iterNode = iterNode.Parent()
}
// Prune the slice to the actual number of available timestamps which
@ -217,6 +271,10 @@ func (node *blockNode) CalcPastMedianTime() time.Time {
return time.Unix(medianTimestamp, 0)
}
// A compile-time assertion to ensure blockNode implements the HeaderCtx
// interface.
var _ HeaderCtx = (*blockNode)(nil)
// blockIndex provides facilities for keeping track of an in-memory index of the
// block chain. Although the name block chain suggests a single chain of
// blocks, it is actually a tree-shaped structure where any node can have

View File

@ -437,7 +437,7 @@ func (b *BlockChain) calcSequenceLock(node *blockNode, tx *btcutil.Tx, utxoView
prevInputHeight = 0
}
blockNode := node.Ancestor(prevInputHeight)
medianTime := blockNode.CalcPastMedianTime()
medianTime := CalcPastMedianTime(blockNode)
// Time based relative time-locks as defined by BIP 68
// have a time granularity of RelativeLockSeconds, so
@ -595,7 +595,8 @@ func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block,
blockSize := uint64(block.MsgBlock().SerializeSize())
blockWeight := uint64(GetBlockWeight(block))
state := newBestState(node, blockSize, blockWeight, numTxns,
curTotalTxns+numTxns, node.CalcPastMedianTime())
curTotalTxns+numTxns, CalcPastMedianTime(node),
)
// Atomically insert info into the database.
err = b.db.Update(func(dbTx database.Tx) error {
@ -708,7 +709,7 @@ func (b *BlockChain) disconnectBlock(node *blockNode, block *btcutil.Block, view
blockWeight := uint64(GetBlockWeight(prevBlock))
newTotalTxns := curTotalTxns - uint64(len(block.MsgBlock().Transactions))
state := newBestState(prevNode, blockSize, blockWeight, numTxns,
newTotalTxns, prevNode.CalcPastMedianTime())
newTotalTxns, CalcPastMedianTime(prevNode))
err = b.db.Update(func(dbTx database.Tx) error {
// Update best block state.

View File

@ -163,13 +163,13 @@ func TestCalcSequenceLock(t *testing.T) {
// Obtain the median time past from the PoV of the input created above.
// The MTP for the input is the MTP from the PoV of the block *prior*
// to the one that included it.
medianTime := node.RelativeAncestor(5).CalcPastMedianTime().Unix()
medianTime := CalcPastMedianTime(node.RelativeAncestor(5)).Unix()
// The median time calculated from the PoV of the best block in the
// test chain. For unconfirmed inputs, this value will be used since
// the MTP will be calculated from the PoV of the yet-to-be-mined
// block.
nextMedianTime := node.CalcPastMedianTime().Unix()
nextMedianTime := CalcPastMedianTime(node).Unix()
nextBlockHeight := int32(numBlocksToActivate) + 1
// Add an additional transaction which will serve as our unconfirmed

View File

@ -1236,7 +1236,7 @@ func (b *BlockChain) initChainState() error {
blockWeight := uint64(GetBlockWeight(btcutil.NewBlock(&block)))
numTxns := uint64(len(block.Transactions))
b.stateSnapshot = newBestState(tip, blockSize, blockWeight,
numTxns, state.totalTxns, tip.CalcPastMedianTime())
numTxns, state.totalTxns, CalcPastMedianTime(tip))
return nil
})

View File

@ -193,88 +193,87 @@ func (b *BlockChain) calcEasiestDifficulty(bits uint32, duration time.Duration)
// findPrevTestNetDifficulty returns the difficulty of the previous block which
// did not have the special testnet minimum difficulty rule applied.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) findPrevTestNetDifficulty(startNode *blockNode) uint32 {
func findPrevTestNetDifficulty(startNode HeaderCtx, c ChainCtx) uint32 {
// Search backwards through the chain for the last block without
// the special rule applied.
iterNode := startNode
for iterNode != nil && iterNode.height%b.blocksPerRetarget != 0 &&
iterNode.bits == b.chainParams.PowLimitBits {
for iterNode != nil && iterNode.Height()%c.BlocksPerRetarget() != 0 &&
iterNode.Bits() == c.ChainParams().PowLimitBits {
iterNode = iterNode.parent
iterNode = iterNode.Parent()
}
// Return the found difficulty or the minimum difficulty if no
// appropriate block was found.
lastBits := b.chainParams.PowLimitBits
lastBits := c.ChainParams().PowLimitBits
if iterNode != nil {
lastBits = iterNode.bits
lastBits = iterNode.Bits()
}
return lastBits
}
// calcNextRequiredDifficulty calculates the required difficulty for the block
// after the passed previous block node based on the difficulty retarget rules.
// after the passed previous HeaderCtx based on the difficulty retarget rules.
// This function differs from the exported CalcNextRequiredDifficulty in that
// the exported version uses the current best chain as the previous block node
// while this function accepts any block node.
func (b *BlockChain) calcNextRequiredDifficulty(lastNode *blockNode,
newBlockTime time.Time) (uint32, error) {
// the exported version uses the current best chain as the previous HeaderCtx
// while this function accepts any block node. This function accepts a ChainCtx
// parameter that gives the necessary difficulty context variables.
func calcNextRequiredDifficulty(lastNode HeaderCtx, newBlockTime time.Time,
c ChainCtx) (uint32, error) {
// Emulate the same behavior as Bitcoin Core that for regtest there is
// no difficulty retargeting.
if b.chainParams.PoWNoRetargeting {
return b.chainParams.PowLimitBits, nil
if c.ChainParams().PoWNoRetargeting {
return c.ChainParams().PowLimitBits, nil
}
// Genesis block.
if lastNode == nil {
return b.chainParams.PowLimitBits, nil
return c.ChainParams().PowLimitBits, nil
}
// Return the previous block's difficulty requirements if this block
// is not at a difficulty retarget interval.
if (lastNode.height+1)%b.blocksPerRetarget != 0 {
if (lastNode.Height()+1)%c.BlocksPerRetarget() != 0 {
// For networks that support it, allow special reduction of the
// required difficulty once too much time has elapsed without
// mining a block.
if b.chainParams.ReduceMinDifficulty {
if c.ChainParams().ReduceMinDifficulty {
// Return minimum difficulty when more than the desired
// amount of time has elapsed without mining a block.
reductionTime := int64(b.chainParams.MinDiffReductionTime /
reductionTime := int64(c.ChainParams().MinDiffReductionTime /
time.Second)
allowMinTime := lastNode.timestamp + reductionTime
allowMinTime := lastNode.Timestamp() + reductionTime
if newBlockTime.Unix() > allowMinTime {
return b.chainParams.PowLimitBits, nil
return c.ChainParams().PowLimitBits, nil
}
// The block was mined within the desired timeframe, so
// return the difficulty for the last block which did
// not have the special minimum difficulty rule applied.
return b.findPrevTestNetDifficulty(lastNode), nil
return findPrevTestNetDifficulty(lastNode, c), nil
}
// For the main network (or any unrecognized networks), simply
// return the previous block's difficulty requirements.
return lastNode.bits, nil
return lastNode.Bits(), nil
}
// Get the block node at the previous retarget (targetTimespan days
// worth of blocks).
firstNode := lastNode.RelativeAncestor(b.blocksPerRetarget - 1)
firstNode := lastNode.RelativeAncestorCtx(c.BlocksPerRetarget() - 1)
if firstNode == nil {
return 0, AssertError("unable to obtain previous retarget block")
}
// Limit the amount of adjustment that can occur to the previous
// difficulty.
actualTimespan := lastNode.timestamp - firstNode.timestamp
actualTimespan := lastNode.Timestamp() - firstNode.Timestamp()
adjustedTimespan := actualTimespan
if actualTimespan < b.minRetargetTimespan {
adjustedTimespan = b.minRetargetTimespan
} else if actualTimespan > b.maxRetargetTimespan {
adjustedTimespan = b.maxRetargetTimespan
if actualTimespan < c.MinRetargetTimespan() {
adjustedTimespan = c.MinRetargetTimespan()
} else if actualTimespan > c.MaxRetargetTimespan() {
adjustedTimespan = c.MaxRetargetTimespan()
}
// Calculate new target difficulty as:
@ -282,14 +281,14 @@ func (b *BlockChain) calcNextRequiredDifficulty(lastNode *blockNode,
// The result uses integer division which means it will be slightly
// rounded down. Bitcoind also uses integer division to calculate this
// result.
oldTarget := CompactToBig(lastNode.bits)
oldTarget := CompactToBig(lastNode.Bits())
newTarget := new(big.Int).Mul(oldTarget, big.NewInt(adjustedTimespan))
targetTimeSpan := int64(b.chainParams.TargetTimespan / time.Second)
targetTimeSpan := int64(c.ChainParams().TargetTimespan / time.Second)
newTarget.Div(newTarget, big.NewInt(targetTimeSpan))
// Limit new value to the proof of work limit.
if newTarget.Cmp(b.chainParams.PowLimit) > 0 {
newTarget.Set(b.chainParams.PowLimit)
if newTarget.Cmp(c.ChainParams().PowLimit) > 0 {
newTarget.Set(c.ChainParams().PowLimit)
}
// Log new target difficulty and return it. The new target logging is
@ -297,13 +296,13 @@ func (b *BlockChain) calcNextRequiredDifficulty(lastNode *blockNode,
// newTarget since conversion to the compact representation loses
// precision.
newTargetBits := BigToCompact(newTarget)
log.Debugf("Difficulty retarget at block height %d", lastNode.height+1)
log.Debugf("Old target %08x (%064x)", lastNode.bits, oldTarget)
log.Debugf("Difficulty retarget at block height %d", lastNode.Height()+1)
log.Debugf("Old target %08x (%064x)", lastNode.Bits(), oldTarget)
log.Debugf("New target %08x (%064x)", newTargetBits, CompactToBig(newTargetBits))
log.Debugf("Actual timespan %v, adjusted timespan %v, target timespan %v",
time.Duration(actualTimespan)*time.Second,
time.Duration(adjustedTimespan)*time.Second,
b.chainParams.TargetTimespan)
c.ChainParams().TargetTimespan)
return newTargetBits, nil
}
@ -315,7 +314,7 @@ func (b *BlockChain) calcNextRequiredDifficulty(lastNode *blockNode,
// This function is safe for concurrent access.
func (b *BlockChain) CalcNextRequiredDifficulty(timestamp time.Time) (uint32, error) {
b.chainLock.Lock()
difficulty, err := b.calcNextRequiredDifficulty(b.bestChain.Tip(), timestamp)
difficulty, err := calcNextRequiredDifficulty(b.bestChain.Tip(), timestamp, b)
b.chainLock.Unlock()
return difficulty, err
}

55
blockchain/interfaces.go Normal file
View File

@ -0,0 +1,55 @@
package blockchain
import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)
// ChainCtx is an interface that abstracts away blockchain parameters.
type ChainCtx interface {
// ChainParams returns the chain's configured chaincfg.Params.
ChainParams() *chaincfg.Params
// BlocksPerRetarget returns the number of blocks before retargeting
// occurs.
BlocksPerRetarget() int32
// MinRetargetTimespan returns the minimum amount of time to use in the
// difficulty calculation.
MinRetargetTimespan() int64
// MaxRetargetTimespan returns the maximum amount of time to use in the
// difficulty calculation.
MaxRetargetTimespan() int64
// VerifyCheckpoint returns whether the passed height and hash match
// the checkpoint data. Not all instances of VerifyCheckpoint will use
// this function for validation.
VerifyCheckpoint(height int32, hash *chainhash.Hash) bool
// FindPreviousCheckpoint returns the most recent checkpoint that we
// have validated. Not all instances of FindPreviousCheckpoint will use
// this function for validation.
FindPreviousCheckpoint() (HeaderCtx, error)
}
// HeaderCtx is an interface that describes information about a block. This is
// used so that external libraries can provide their own context (the header's
// parent, bits, etc.) when attempting to contextually validate a header.
type HeaderCtx interface {
// Height returns the header's height.
Height() int32
// Bits returns the header's bits.
Bits() uint32
// Timestamp returns the header's timestamp.
Timestamp() int64
// Parent returns the header's parent.
Parent() HeaderCtx
// RelativeAncestorCtx returns the header's ancestor that is distance
// blocks before it in the chain.
RelativeAncestorCtx(distance int32) HeaderCtx
}

View File

@ -153,7 +153,7 @@ func (b *BlockChain) PastMedianTime(blockHeader *wire.BlockHeader) (time.Time, e
blockNode := newBlockNode(blockHeader, prevNode)
return blockNode.CalcPastMedianTime(), nil
return CalcPastMedianTime(blockNode), nil
}
// thresholdStateTransition given a state, a previous node, and a toeholds

View File

@ -421,13 +421,15 @@ func CountP2SHSigOps(tx *btcutil.Tx, isCoinBaseTx bool, utxoView *UtxoViewpoint)
return totalSigOps, nil
}
// checkBlockHeaderSanity performs some preliminary checks on a block header to
// CheckBlockHeaderSanity performs some preliminary checks on a block header to
// ensure it is sane before continuing with processing. These checks are
// context free.
//
// The flags do not modify the behavior of this function directly, however they
// are needed to pass along to checkProofOfWork.
func checkBlockHeaderSanity(header *wire.BlockHeader, powLimit *big.Int, timeSource MedianTimeSource, flags BehaviorFlags) error {
func CheckBlockHeaderSanity(header *wire.BlockHeader, powLimit *big.Int,
timeSource MedianTimeSource, flags BehaviorFlags) error {
// Ensure the proof of work bits in the block header is in min/max range
// and the block hash is less than the target value described by the
// bits.
@ -467,7 +469,7 @@ func checkBlockHeaderSanity(header *wire.BlockHeader, powLimit *big.Int, timeSou
func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, timeSource MedianTimeSource, flags BehaviorFlags) error {
msgBlock := block.MsgBlock()
header := &msgBlock.Header
err := checkBlockHeaderSanity(header, powLimit, timeSource, flags)
err := CheckBlockHeaderSanity(header, powLimit, timeSource, flags)
if err != nil {
return err
}
@ -633,22 +635,30 @@ func checkSerializedHeight(coinbaseTx *btcutil.Tx, wantHeight int32) error {
return nil
}
// checkBlockHeaderContext performs several validation checks on the block header
// CheckBlockHeaderContext performs several validation checks on the block header
// which depend on its position within the block chain.
//
// The flags modify the behavior of this function as follows:
// - BFFastAdd: All checks except those involving comparing the header against
// the checkpoints are not performed.
//
// The skipCheckpoint boolean is used so that libraries can skip the checkpoint
// sanity checks.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode *blockNode, flags BehaviorFlags) error {
// NOTE: Ignore the above lock requirement if this function is not passed a
// *Blockchain instance as the ChainCtx argument.
func CheckBlockHeaderContext(header *wire.BlockHeader, prevNode HeaderCtx,
flags BehaviorFlags, c ChainCtx, skipCheckpoint bool) error {
fastAdd := flags&BFFastAdd == BFFastAdd
if !fastAdd {
// Ensure the difficulty specified in the block header matches
// the calculated difficulty based on the previous block and
// difficulty retarget rules.
expectedDifficulty, err := b.calcNextRequiredDifficulty(prevNode,
header.Timestamp)
expectedDifficulty, err := calcNextRequiredDifficulty(
prevNode, header.Timestamp, c,
)
if err != nil {
return err
}
@ -661,7 +671,7 @@ func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode
// Ensure the timestamp for the block header is after the
// median time of the last several blocks (medianTimeBlocks).
medianTime := prevNode.CalcPastMedianTime()
medianTime := CalcPastMedianTime(prevNode)
if !header.Timestamp.After(medianTime) {
str := "block timestamp of %v is not after expected %v"
str = fmt.Sprintf(str, header.Timestamp, medianTime)
@ -671,11 +681,30 @@ func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode
// The height of this block is one more than the referenced previous
// block.
blockHeight := prevNode.height + 1
blockHeight := prevNode.Height() + 1
// Reject outdated block versions once a majority of the network
// has upgraded. These were originally voted on by BIP0034,
// BIP0065, and BIP0066.
params := c.ChainParams()
if header.Version < 2 && blockHeight >= params.BIP0034Height ||
header.Version < 3 && blockHeight >= params.BIP0066Height ||
header.Version < 4 && blockHeight >= params.BIP0065Height {
str := "new blocks with version %d are no longer valid"
str = fmt.Sprintf(str, header.Version)
return ruleError(ErrBlockVersionTooOld, str)
}
if skipCheckpoint {
// If the caller wants us to skip the checkpoint checks, we'll
// return early.
return nil
}
// Ensure chain matches up to predetermined checkpoints.
blockHash := header.BlockHash()
if !b.verifyCheckpoint(blockHeight, &blockHash) {
if !c.VerifyCheckpoint(blockHeight, &blockHash) {
str := fmt.Sprintf("block at height %d does not match "+
"checkpoint hash", blockHeight)
return ruleError(ErrBadCheckpoint, str)
@ -685,30 +714,17 @@ func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode
// chain before it. This prevents storage of new, otherwise valid,
// blocks which build off of old blocks that are likely at a much easier
// difficulty and therefore could be used to waste cache and disk space.
checkpointNode, err := b.findPreviousCheckpoint()
checkpointNode, err := c.FindPreviousCheckpoint()
if err != nil {
return err
}
if checkpointNode != nil && blockHeight < checkpointNode.height {
if checkpointNode != nil && blockHeight < checkpointNode.Height() {
str := fmt.Sprintf("block at height %d forks the main chain "+
"before the previous checkpoint at height %d",
blockHeight, checkpointNode.height)
blockHeight, checkpointNode.Height())
return ruleError(ErrForkTooOld, str)
}
// Reject outdated block versions once a majority of the network
// has upgraded. These were originally voted on by BIP0034,
// BIP0065, and BIP0066.
params := b.chainParams
if header.Version < 2 && blockHeight >= params.BIP0034Height ||
header.Version < 3 && blockHeight >= params.BIP0066Height ||
header.Version < 4 && blockHeight >= params.BIP0065Height {
str := "new blocks with version %d are no longer valid"
str = fmt.Sprintf(str, header.Version)
return ruleError(ErrBlockVersionTooOld, str)
}
return nil
}
@ -726,7 +742,7 @@ func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode
func (b *BlockChain) checkBlockContext(block *btcutil.Block, prevNode *blockNode, flags BehaviorFlags) error {
// Perform all block header related validation checks.
header := &block.MsgBlock().Header
err := b.checkBlockHeaderContext(header, prevNode, flags)
err := CheckBlockHeaderContext(header, prevNode, flags, b, false)
if err != nil {
return err
}
@ -746,7 +762,7 @@ func (b *BlockChain) checkBlockContext(block *btcutil.Block, prevNode *blockNode
// timestamps for all lock-time based checks.
blockTime := header.Timestamp
if csvState == ThresholdActive {
blockTime = prevNode.CalcPastMedianTime()
blockTime = CalcPastMedianTime(prevNode)
}
// The height of this block is one more than the referenced
@ -1186,7 +1202,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block *btcutil.Block, vi
// We obtain the MTP of the *previous* block in order to
// determine if transactions in the current block are final.
medianTime := node.parent.CalcPastMedianTime()
medianTime := CalcPastMedianTime(node.parent)
// Additionally, if the CSV soft-fork package is now active,
// then we also enforce the relative sequence number based
@ -1288,3 +1304,68 @@ func (b *BlockChain) CheckConnectBlockTemplate(block *btcutil.Block) error {
newNode := newBlockNode(&header, tip)
return b.checkConnectBlock(newNode, block, view, nil)
}
// ChainParams returns the Blockchain's configured chaincfg.Params.
//
// NOTE: Part of the ChainCtx interface.
func (b *BlockChain) ChainParams() *chaincfg.Params {
return b.chainParams
}
// BlocksPerRetarget returns the number of blocks before retargeting occurs.
//
// NOTE: Part of the ChainCtx interface.
func (b *BlockChain) BlocksPerRetarget() int32 {
return b.blocksPerRetarget
}
// MinRetargetTimespan returns the minimum amount of time to use in the
// difficulty calculation.
//
// NOTE: Part of the ChainCtx interface.
func (b *BlockChain) MinRetargetTimespan() int64 {
return b.minRetargetTimespan
}
// MaxRetargetTimespan returns the maximum amount of time to use in the
// difficulty calculation.
//
// NOTE: Part of the ChainCtx interface.
func (b *BlockChain) MaxRetargetTimespan() int64 {
return b.maxRetargetTimespan
}
// VerifyCheckpoint checks that the height and hash match the stored
// checkpoints.
//
// NOTE: Part of the ChainCtx interface.
func (b *BlockChain) VerifyCheckpoint(height int32,
hash *chainhash.Hash) bool {
return b.verifyCheckpoint(height, hash)
}
// FindPreviousCheckpoint finds the checkpoint we've encountered during
// validation.
//
// NOTE: Part of the ChainCtx interface.
func (b *BlockChain) FindPreviousCheckpoint() (HeaderCtx, error) {
checkpoint, err := b.findPreviousCheckpoint()
if err != nil {
return nil, err
}
if checkpoint == nil {
// This check is necessary because if we just return the nil
// blockNode as a HeaderCtx, a caller performing a nil-check
// will fail. This is a quirk of go where a nil value stored in
// an interface is different from the actual nil interface.
return nil, nil
}
return checkpoint, err
}
// A compile-time assertion to ensure BlockChain implements the ChainCtx
// interface.
var _ ChainCtx = (*BlockChain)(nil)