blockchain: Optimize checkpoint handling.

This modifies the code that determines the most recently known
checkpoint to take advantage of recent changes which make the entire
block index available in memory by only storing a reference to the
specific node in the index that represents the latest known checkpoint.

Previously, the entire block was stored and new checkpoints required
loading it from the database.
This commit is contained in:
Dave Collins 2017-02-03 21:18:39 -06:00
parent 349450379f
commit 72f2a3fe49
No known key found for this signature in database
GPG key ID: B8904D9D9C93D1F2
4 changed files with 43 additions and 76 deletions

View file

@ -124,7 +124,7 @@ type BlockChain struct {
// These fields are related to checkpoint handling. They are protected
// by the chain lock.
nextCheckpoint *chaincfg.Checkpoint
checkpointBlock *btcutil.Block
checkpointNode *blockNode
// The state is used as a fairly efficient way to cache information
// about the current best chain state that is returned to callers when

View file

@ -80,87 +80,58 @@ func (b *BlockChain) verifyCheckpoint(height int32, hash *chainhash.Hash) bool {
// findPreviousCheckpoint finds the most recent checkpoint that is already
// available in the downloaded portion of the block chain and returns the
// associated block. It returns nil if a checkpoint can't be found (this should
// really only happen for blocks before the first checkpoint).
// associated block node. It returns nil if a checkpoint can't be found (this
// should really only happen for blocks before the first checkpoint).
//
// This function MUST be called with the chain lock held (for reads).
func (b *BlockChain) findPreviousCheckpoint() (*btcutil.Block, error) {
func (b *BlockChain) findPreviousCheckpoint() (*blockNode, error) {
if !b.HasCheckpoints() {
return nil, nil
}
checkpoints := b.checkpoints
numCheckpoints := len(checkpoints)
if numCheckpoints == 0 {
// No checkpoints.
return nil, nil
}
// Perform the initial search to find and cache the latest known
// checkpoint if the best chain is not known yet or we haven't already
// previously searched.
if b.checkpointBlock == nil && b.nextCheckpoint == nil {
checkpoints := b.checkpoints
numCheckpoints := len(checkpoints)
if b.checkpointNode == nil && b.nextCheckpoint == nil {
// Loop backwards through the available checkpoints to find one
// that is already available.
checkpointIndex := -1
err := b.db.View(func(dbTx database.Tx) error {
for i := numCheckpoints - 1; i >= 0; i-- {
if dbMainChainHasBlock(dbTx, checkpoints[i].Hash) {
checkpointIndex = i
break
node := b.index.LookupNode(checkpoints[i].Hash)
if node == nil || !node.inMainChain {
continue
}
// Checkpoint found. Cache it for future lookups and
// set the next expected checkpoint accordingly.
b.checkpointNode = node
if i < numCheckpoints-1 {
b.nextCheckpoint = &checkpoints[i+1]
}
return nil
})
if err != nil {
return nil, err
return b.checkpointNode, nil
}
// No known latest checkpoint. This will only happen on blocks
// before the first known checkpoint. So, set the next expected
// checkpoint to the first checkpoint and return the fact there
// is no latest known checkpoint block.
if checkpointIndex == -1 {
b.nextCheckpoint = &checkpoints[0]
return nil, nil
}
// Cache the latest known checkpoint block for future lookups.
checkpoint := checkpoints[checkpointIndex]
err = b.db.View(func(dbTx database.Tx) error {
block, err := dbFetchBlockByHash(dbTx, checkpoint.Hash)
if err != nil {
return err
}
b.checkpointBlock = block
// Set the next expected checkpoint block accordingly.
b.nextCheckpoint = nil
if checkpointIndex < numCheckpoints-1 {
b.nextCheckpoint = &checkpoints[checkpointIndex+1]
}
return nil
})
if err != nil {
return nil, err
}
return b.checkpointBlock, nil
}
// At this point we've already searched for the latest known checkpoint,
// so when there is no next checkpoint, the current checkpoint lockin
// will always be the latest known checkpoint.
if b.nextCheckpoint == nil {
return b.checkpointBlock, nil
return b.checkpointNode, nil
}
// When there is a next checkpoint and the height of the current best
// chain does not exceed it, the current checkpoint lockin is still
// the latest known checkpoint.
if b.bestNode.height < b.nextCheckpoint.Height {
return b.checkpointBlock, nil
return b.checkpointNode, nil
}
// We've reached or exceeded the next checkpoint height. Note that
@ -168,21 +139,17 @@ func (b *BlockChain) findPreviousCheckpoint() (*btcutil.Block, error) {
// any blocks before the checkpoint, so we don't have to worry about the
// checkpoint going away out from under us due to a chain reorganize.
// Cache the latest known checkpoint block for future lookups. Note
// that if this lookup fails something is very wrong since the chain
// has already passed the checkpoint which was verified as accurate
// before inserting it.
err := b.db.View(func(tx database.Tx) error {
block, err := dbFetchBlockByHash(tx, b.nextCheckpoint.Hash)
if err != nil {
return err
}
b.checkpointBlock = block
return nil
})
if err != nil {
return nil, err
// Cache the latest known checkpoint for future lookups. Note that if
// this lookup fails something is very wrong since the chain has already
// passed the checkpoint which was verified as accurate before inserting
// it.
checkpointNode := b.index.LookupNode(b.nextCheckpoint.Hash)
if checkpointNode == nil {
return nil, AssertError(fmt.Sprintf("findPreviousCheckpoint "+
"failed lookup of known good block node %s",
b.nextCheckpoint.Hash))
}
b.checkpointNode = checkpointNode
// Set the next expected checkpoint.
checkpointIndex := -1
@ -197,7 +164,7 @@ func (b *BlockChain) findPreviousCheckpoint() (*btcutil.Block, error) {
b.nextCheckpoint = &checkpoints[checkpointIndex+1]
}
return b.checkpointBlock, nil
return b.checkpointNode, nil
}
// isNonstandardTransaction determines whether a transaction contains any

View file

@ -6,6 +6,7 @@ package blockchain
import (
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/database"
@ -182,14 +183,13 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bo
// used to eat memory, and ensuring expected (versus claimed) proof of
// work requirements since the previous checkpoint are met.
blockHeader := &block.MsgBlock().Header
checkpointBlock, err := b.findPreviousCheckpoint()
checkpointNode, err := b.findPreviousCheckpoint()
if err != nil {
return false, false, err
}
if checkpointBlock != nil {
if checkpointNode != nil {
// Ensure the block timestamp is after the checkpoint timestamp.
checkpointHeader := &checkpointBlock.MsgBlock().Header
checkpointTime := checkpointHeader.Timestamp
checkpointTime := time.Unix(checkpointNode.timestamp, 0)
if blockHeader.Timestamp.Before(checkpointTime) {
str := fmt.Sprintf("block %v has timestamp %v before "+
"last checkpoint timestamp %v", blockHash,
@ -205,7 +205,7 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bo
// maximum adjustment allowed by the retarget rules.
duration := blockHeader.Timestamp.Sub(checkpointTime)
requiredTarget := CompactToBig(b.calcEasiestDifficulty(
checkpointHeader.Bits, duration))
checkpointNode.bits, duration))
currentTarget := CompactToBig(blockHeader.Bits)
if currentTarget.Cmp(requiredTarget) > 0 {
str := fmt.Sprintf("block target difficulty of %064x "+

View file

@ -692,14 +692,14 @@ 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.
checkpointBlock, err := b.findPreviousCheckpoint()
checkpointNode, err := b.findPreviousCheckpoint()
if err != nil {
return err
}
if checkpointBlock != nil && blockHeight < checkpointBlock.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, checkpointBlock.Height())
blockHeight, checkpointNode.height)
return ruleError(ErrForkTooOld, str)
}