dcrd/blockchain/checkpoints.go
Ryan Staudt f41f2798ef blockchain: Set BFFastAdd based on assume valid.
This updates BlockChain to set the BFFastAdd flag if a block is both an
ancestor of the assumed valid block and an ancestor of the best header.

A helper function isAssumeValidAncestor is introduced that determines
whether the provided node is both an ancestor of the assumed valid node
and an ancestor of the best header.  False is returned when the assume
valid node has not been discovered or when assume valid is disabled.

The assumed valid node is clamped back as needed to ensure that it is at
least 2 weeks worth of blocks behind the best header.  This protects
against malicious actors trying to play games by tricking users into
using an assumed valid hash near the tip.
2021-12-11 01:07:58 -06:00

174 lines
5.6 KiB
Go

// Copyright (c) 2013-2016 The btcsuite developers
// Copyright (c) 2015-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package blockchain
import (
"fmt"
"time"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/txscript/v4/stdscript"
)
// CheckpointConfirmations is the number of blocks before the end of the current
// best block chain that a good checkpoint candidate must be.
const CheckpointConfirmations = 4096
// LatestCheckpoint returns the most recent checkpoint (regardless of whether it
// is already known). When checkpoints are disabled or there are no checkpoints
// for the active network, it will return nil.
//
// This function is safe for concurrent access.
func (b *BlockChain) LatestCheckpoint() *chaincfg.Checkpoint {
return b.latestCheckpoint
}
// verifyCheckpoint returns whether the passed block height and hash combination
// match the hard-coded checkpoint data. It also returns true if there is no
// checkpoint data for the passed block height.
//
// This function MUST be called with the chain lock held (for reads).
func (b *BlockChain) verifyCheckpoint(height int64, hash *chainhash.Hash) bool {
if b.latestCheckpoint == nil {
return true
}
// Nothing to check if there is no checkpoint data for the block height.
if b.latestCheckpoint.Height != height {
return true
}
if *b.latestCheckpoint.Hash != *hash {
return false
}
log.Debugf("Verified checkpoint at height %d/block %s",
b.latestCheckpoint.Height, b.latestCheckpoint.Hash)
return true
}
// maybeUpdateMostRecentCheckpoint potentially updates the most recently known
// checkpoint to the provided block node.
//
// This function MUST be called with the chain lock held (for writes).
func (b *BlockChain) maybeUpdateMostRecentCheckpoint(node *blockNode) {
if b.latestCheckpoint == nil {
return
}
// Nothing to update if there is no checkpoint data for the block height or
// the checkpoint hash does not match.
if node.height != b.latestCheckpoint.Height ||
node.hash != *b.latestCheckpoint.Hash {
return
}
// Update the previous checkpoint to current block so long as it is more
// recent than the existing previous checkpoint.
if b.checkpointNode == nil ||
b.checkpointNode.height < b.latestCheckpoint.Height {
log.Debugf("Most recent checkpoint updated to %s (height %d)",
node.hash, node.height)
b.checkpointNode = node
}
}
// isNonstandardTransaction determines whether a transaction contains any
// scripts which are not one of the standard types.
func isNonstandardTransaction(tx *dcrutil.Tx) bool {
// Check all of the output public key scripts for non-standard scripts.
for _, txOut := range tx.MsgTx().TxOut {
typ := stdscript.DetermineScriptType(txOut.Version, txOut.PkScript)
if typ == stdscript.STNonStandard {
return true
}
}
return false
}
// IsCheckpointCandidate returns whether or not the passed block is a good
// checkpoint candidate.
//
// The factors used to determine a good checkpoint are:
// - The block must be in the main chain
// - The block must be at least 'CheckpointConfirmations' blocks prior to the
// current end of the main chain
// - The timestamps for the blocks before and after the checkpoint must have
// timestamps which are also before and after the checkpoint, respectively
// (due to the median time allowance this is not always the case)
// - The block must not contain any strange transaction such as those with
// nonstandard scripts
//
// The intent is that candidates are reviewed by a developer to make the final
// decision and then manually added to the list of checkpoints for a network.
//
// This function is safe for concurrent access.
func (b *BlockChain) IsCheckpointCandidate(block *dcrutil.Block) (bool, error) {
b.chainLock.RLock()
defer b.chainLock.RUnlock()
// A checkpoint must be in the main chain.
node := b.index.LookupNode(block.Hash())
if node == nil || !b.bestChain.Contains(node) {
return false, nil
}
// Ensure the height of the passed block and the entry for the block in
// the main chain match. This should always be the case unless the
// caller provided an invalid block.
if node.height != block.Height() {
return false, fmt.Errorf("passed block height of %d does not "+
"match the main chain height of %d", block.Height(),
node.height)
}
// A checkpoint must be at least CheckpointConfirmations blocks before
// the end of the main chain.
if node.height > (b.bestChain.Tip().height - CheckpointConfirmations) {
return false, nil
}
// A checkpoint must be have at least one block after it.
//
// This should always succeed since the check above already made sure it
// is CheckpointConfirmations back, but be safe in case the constant
// changes.
nextNode := b.bestChain.Next(node)
if nextNode == nil {
return false, nil
}
// A checkpoint must be have at least one block before it.
if node.parent == nil {
return false, nil
}
// A checkpoint must have timestamps for the block and the blocks on
// either side of it in order (due to the median time allowance this is
// not always the case).
prevTime := time.Unix(node.parent.timestamp, 0)
curTime := block.MsgBlock().Header.Timestamp
nextTime := time.Unix(nextNode.timestamp, 0)
if prevTime.After(curTime) || nextTime.Before(curTime) {
return false, nil
}
// A checkpoint must have transactions that only contain standard
// scripts.
for _, tx := range block.Transactions() {
if isNonstandardTransaction(tx) {
return false, nil
}
}
// All of the checks passed, so the block is a candidate.
return true, nil
}