blockchain: Track block validation status in block index.

Each node in the block index records some flags about its validation
state. This is just stored in memory for now, but can save effort if
attempting to reconnect a block that failed validation or was
disconnected.
This commit is contained in:
Jim Posen 2017-09-05 20:42:54 -07:00 committed by Dave Collins
parent 11d7cae82b
commit e1ef2f899b
8 changed files with 156 additions and 30 deletions

View File

@ -5,6 +5,8 @@
package blockchain package blockchain
import ( import (
"fmt"
"github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/database"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
) )
@ -22,11 +24,17 @@ import (
func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags) (bool, error) { func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags) (bool, error) {
// The height of this block is one more than the referenced previous // The height of this block is one more than the referenced previous
// block. // block.
blockHeight := int32(0) prevHash := &block.MsgBlock().Header.PrevBlock
prevNode := b.index.LookupNode(&block.MsgBlock().Header.PrevBlock) prevNode := b.index.LookupNode(prevHash)
if prevNode != nil { if prevNode == nil {
blockHeight = prevNode.height + 1 str := fmt.Sprintf("previous block %s is unknown", prevHash)
return false, ruleError(ErrPreviousBlockUnknown, str)
} else if prevNode.KnownInvalid() {
str := fmt.Sprintf("previous block %s is known to be invalid", prevHash)
return false, ruleError(ErrInvalidAncestorBlock, str)
} }
blockHeight := prevNode.height + 1
block.SetHeight(blockHeight) block.SetHeight(blockHeight)
// The block must pass all of the validation rules which depend on the // The block must pass all of the validation rules which depend on the
@ -56,6 +64,7 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags)
// block chain (could be either a side chain or the main chain). // block chain (could be either a side chain or the main chain).
blockHeader := &block.MsgBlock().Header blockHeader := &block.MsgBlock().Header
newNode := newBlockNode(blockHeader, blockHeight) newNode := newBlockNode(blockHeader, blockHeight)
newNode.status |= statusDataStored
if prevNode != nil { if prevNode != nil {
newNode.parent = prevNode newNode.parent = prevNode
newNode.height = blockHeight newNode.height = blockHeight

View File

@ -16,6 +16,29 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
) )
// blockStatus is a bit field representing the validation state of the block.
type blockStatus byte
const (
// statusDataStored indicates that the block's payload is stored on disk.
statusDataStored blockStatus = 1 << iota
// statusValid indicates that the block has been fully validated.
statusValid
// statusValidateFailed indicates that the block has failed validation.
statusValidateFailed
// statusInvalidAncestor indicates that one of the block's ancestors has
// has failed validation, thus the block is also invalid.
statusInvalidAncestor
// statusNone indicates that the block has no validation state flags set.
//
// NOTE: This must be defined last in order to avoid influencing iota.
statusNone blockStatus = 0
)
// blockNode represents a block within the block chain and is primarily used to // blockNode represents a block within the block chain and is primarily used to
// aid in selecting the best chain to be the main chain. The main chain is // aid in selecting the best chain to be the main chain. The main chain is
// stored into the block database. // stored into the block database.
@ -49,6 +72,9 @@ type blockNode struct {
nonce uint32 nonce uint32
timestamp int64 timestamp int64
merkleRoot chainhash.Hash merkleRoot chainhash.Hash
// status is a bitfield representing the validation state of the block
status blockStatus
} }
// initBlockNode initializes a block node from the given header and height. The // initBlockNode initializes a block node from the given header and height. The
@ -167,6 +193,20 @@ func (node *blockNode) CalcPastMedianTime() time.Time {
return time.Unix(medianTimestamp, 0) return time.Unix(medianTimestamp, 0)
} }
// KnownValid returns whether the block is known to be valid. This will return
// false for a valid block that has not been fully validated yet.
func (node *blockNode) KnownValid() bool {
return node.status&statusValid != 0
}
// KnownInvalid returns whether the block is known to be invalid. This may be
// because the block itself failed validation or any of its ancestors is
// invalid. This will return false for invalid blocks that have not been proven
// invalid yet.
func (node *blockNode) KnownInvalid() bool {
return node.status&(statusValidateFailed|statusInvalidAncestor) != 0
}
// blockIndex provides facilities for keeping track of an in-memory index of the // 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 // 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 // blocks, it is actually a tree-shaped structure where any node can have

View File

@ -497,10 +497,14 @@ func LockTimeToSequence(isSeconds bool, locktime uint32) uint32 {
// //
// This function MUST be called with the chain state lock held (for reads). // This function MUST be called with the chain state lock held (for reads).
func (b *BlockChain) getReorganizeNodes(node *blockNode) (*list.List, *list.List) { func (b *BlockChain) getReorganizeNodes(node *blockNode) (*list.List, *list.List) {
// Nothing to detach or attach if there is no node.
attachNodes := list.New() attachNodes := list.New()
detachNodes := list.New() detachNodes := list.New()
if node == nil {
// Do not reorganize to a known invalid chain. Ancestors deeper than the
// direct parent are checked below but this is a quick check before doing
// more unnecessary work.
if node.parent.KnownInvalid() {
node.status |= statusInvalidAncestor
return detachNodes, attachNodes return detachNodes, attachNodes
} }
@ -509,10 +513,27 @@ func (b *BlockChain) getReorganizeNodes(node *blockNode) (*list.List, *list.List
// so they are attached in the appropriate order when iterating the list // so they are attached in the appropriate order when iterating the list
// later. // later.
forkNode := b.bestChain.FindFork(node) forkNode := b.bestChain.FindFork(node)
invalidChain := false
for n := node; n != nil && n != forkNode; n = n.parent { for n := node; n != nil && n != forkNode; n = n.parent {
if n.KnownInvalid() {
invalidChain = true
break
}
attachNodes.PushFront(n) attachNodes.PushFront(n)
} }
// If any of the node's ancestors are invalid, unwind attachNodes, marking
// each one as invalid for future reference.
if invalidChain {
var next *list.Element
for e := attachNodes.Front(); e != nil; e = next {
next = e.Next()
n := attachNodes.Remove(e).(*blockNode)
n.status |= statusInvalidAncestor
}
return detachNodes, attachNodes
}
// Start from the end of the main chain and work backwards until the // Start from the end of the main chain and work backwards until the
// common ancestor adding each block to the list of nodes to detach from // common ancestor adding each block to the list of nodes to detach from
// the main chain. // the main chain.
@ -854,8 +875,17 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
// at least a couple of ways accomplish that rollback, but both involve // at least a couple of ways accomplish that rollback, but both involve
// tweaking the chain and/or database. This approach catches these // tweaking the chain and/or database. This approach catches these
// issues before ever modifying the chain. // issues before ever modifying the chain.
var validationError error
for e := attachNodes.Front(); e != nil; e = e.Next() { for e := attachNodes.Front(); e != nil; e = e.Next() {
n := e.Value.(*blockNode) n := e.Value.(*blockNode)
// If any previous nodes in attachNodes failed validation,
// mark this one as having an invalid ancestor.
if validationError != nil {
n.status |= statusInvalidAncestor
continue
}
var block *btcutil.Block var block *btcutil.Block
err := b.db.View(func(dbTx database.Tx) error { err := b.db.View(func(dbTx database.Tx) error {
var err error var err error
@ -869,14 +899,42 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
// Store the loaded block for later. // Store the loaded block for later.
attachBlocks = append(attachBlocks, block) attachBlocks = append(attachBlocks, block)
// Skip checks if node has already been fully validated. Although
// checkConnectBlock gets skipped, we still need to update the UTXO
// view.
if n.KnownValid() {
err = view.fetchInputUtxos(b.db, block)
if err != nil {
return err
}
err = view.connectTransactions(block, nil)
if err != nil {
return err
}
continue
}
// Notice the spent txout details are not requested here and // Notice the spent txout details are not requested here and
// thus will not be generated. This is done because the state // thus will not be generated. This is done because the state
// is not being immediately written to the database, so it is // is not being immediately written to the database, so it is
// not needed. // not needed.
err = b.checkConnectBlock(n, block, view, nil) err = b.checkConnectBlock(n, block, view, nil)
if err != nil { if err != nil {
// If the block failed validation mark it as invalid, then
// continue to loop through remaining nodes, marking them as
// having an invalid ancestor.
if _, ok := err.(RuleError); ok {
n.status |= statusValidateFailed
validationError = err
continue
}
return err return err
} }
n.status |= statusValid
}
if validationError != nil {
return validationError
} }
// Reset the view for the actual connection code below. This is // Reset the view for the actual connection code below. This is
@ -975,6 +1033,9 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
// most common case. // most common case.
parentHash := &block.MsgBlock().Header.PrevBlock parentHash := &block.MsgBlock().Header.PrevBlock
if parentHash.IsEqual(&b.bestChain.Tip().hash) { if parentHash.IsEqual(&b.bestChain.Tip().hash) {
// Skip checks if node has already been fully validated.
fastAdd = fastAdd || node.KnownValid()
// Perform several checks to verify the block can be connected // Perform several checks to verify the block can be connected
// to the main chain without violating any rules and without // to the main chain without violating any rules and without
// actually connecting the block. // actually connecting the block.
@ -984,8 +1045,12 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
if !fastAdd { if !fastAdd {
err := b.checkConnectBlock(node, block, view, &stxos) err := b.checkConnectBlock(node, block, view, &stxos)
if err != nil { if err != nil {
if _, ok := err.(RuleError); ok {
node.status |= statusValidateFailed
}
return false, err return false, err
} }
node.status |= statusValid
} }
// In the fast add case the code to check the block connection // In the fast add case the code to check the block connection

View File

@ -1060,6 +1060,7 @@ func (b *BlockChain) createChainState() error {
genesisBlock := btcutil.NewBlock(b.chainParams.GenesisBlock) genesisBlock := btcutil.NewBlock(b.chainParams.GenesisBlock)
header := &genesisBlock.MsgBlock().Header header := &genesisBlock.MsgBlock().Header
node := newBlockNode(header, 0) node := newBlockNode(header, 0)
node.status |= statusDataStored | statusValid
b.bestChain.SetTip(node) b.bestChain.SetTip(node)
// Add the new node to the index which is used for faster lookups. // Add the new node to the index which is used for faster lookups.
@ -1164,6 +1165,7 @@ func (b *BlockChain) initChainState() error {
// and add it to the block index. // and add it to the block index.
node := &blockNodes[height] node := &blockNodes[height]
initBlockNode(node, header, height) initBlockNode(node, header, height)
node.status |= statusDataStored | statusValid
if tip != nil { if tip != nil {
node.parent = tip node.parent = tip
node.workSum = node.workSum.Add(tip.workSum, node.workSum = node.workSum.Add(tip.workSum,

View File

@ -99,10 +99,6 @@ const (
// transaction. // transaction.
ErrNoTransactions ErrNoTransactions
// ErrTooManyTransactions indicates the block has more transactions than
// are allowed.
ErrTooManyTransactions
// ErrNoTxInputs indicates a transaction does not have any inputs. A // ErrNoTxInputs indicates a transaction does not have any inputs. A
// valid transaction must have at least one input. // valid transaction must have at least one input.
ErrNoTxInputs ErrNoTxInputs
@ -213,6 +209,13 @@ const (
// manually computed witness commitment. // manually computed witness commitment.
ErrWitnessCommitmentMismatch ErrWitnessCommitmentMismatch
// ErrPreviousBlockUnknown indicates that the previous block is not known.
ErrPreviousBlockUnknown
// ErrInvalidAncestorBlock indicates that an ancestor of this block has
// already failed validation.
ErrInvalidAncestorBlock
// ErrPrevBlockNotBest indicates that the block's previous block is not the // ErrPrevBlockNotBest indicates that the block's previous block is not the
// current chain tip. This is not a block validation rule, but is required // current chain tip. This is not a block validation rule, but is required
// for block proposals submitted via getblocktemplate RPC. // for block proposals submitted via getblocktemplate RPC.
@ -236,7 +239,6 @@ var errorCodeStrings = map[ErrorCode]string{
ErrForkTooOld: "ErrForkTooOld", ErrForkTooOld: "ErrForkTooOld",
ErrCheckpointTimeTooOld: "ErrCheckpointTimeTooOld", ErrCheckpointTimeTooOld: "ErrCheckpointTimeTooOld",
ErrNoTransactions: "ErrNoTransactions", ErrNoTransactions: "ErrNoTransactions",
ErrTooManyTransactions: "ErrTooManyTransactions",
ErrNoTxInputs: "ErrNoTxInputs", ErrNoTxInputs: "ErrNoTxInputs",
ErrNoTxOutputs: "ErrNoTxOutputs", ErrNoTxOutputs: "ErrNoTxOutputs",
ErrTxTooBig: "ErrTxTooBig", ErrTxTooBig: "ErrTxTooBig",
@ -262,6 +264,8 @@ var errorCodeStrings = map[ErrorCode]string{
ErrUnexpectedWitness: "ErrUnexpectedWitness", ErrUnexpectedWitness: "ErrUnexpectedWitness",
ErrInvalidWitnessCommitment: "ErrInvalidWitnessCommitment", ErrInvalidWitnessCommitment: "ErrInvalidWitnessCommitment",
ErrWitnessCommitmentMismatch: "ErrWitnessCommitmentMismatch", ErrWitnessCommitmentMismatch: "ErrWitnessCommitmentMismatch",
ErrPreviousBlockUnknown: "ErrPreviousBlockUnknown",
ErrInvalidAncestorBlock: "ErrInvalidAncestorBlock",
ErrPrevBlockNotBest: "ErrPrevBlockNotBest", ErrPrevBlockNotBest: "ErrPrevBlockNotBest",
} }

View File

@ -16,6 +16,7 @@ func TestErrorCodeStringer(t *testing.T) {
}{ }{
{ErrDuplicateBlock, "ErrDuplicateBlock"}, {ErrDuplicateBlock, "ErrDuplicateBlock"},
{ErrBlockTooBig, "ErrBlockTooBig"}, {ErrBlockTooBig, "ErrBlockTooBig"},
{ErrBlockWeightTooHigh, "ErrBlockWeightTooHigh"},
{ErrBlockVersionTooOld, "ErrBlockVersionTooOld"}, {ErrBlockVersionTooOld, "ErrBlockVersionTooOld"},
{ErrInvalidTime, "ErrInvalidTime"}, {ErrInvalidTime, "ErrInvalidTime"},
{ErrTimeTooOld, "ErrTimeTooOld"}, {ErrTimeTooOld, "ErrTimeTooOld"},
@ -28,7 +29,6 @@ func TestErrorCodeStringer(t *testing.T) {
{ErrForkTooOld, "ErrForkTooOld"}, {ErrForkTooOld, "ErrForkTooOld"},
{ErrCheckpointTimeTooOld, "ErrCheckpointTimeTooOld"}, {ErrCheckpointTimeTooOld, "ErrCheckpointTimeTooOld"},
{ErrNoTransactions, "ErrNoTransactions"}, {ErrNoTransactions, "ErrNoTransactions"},
{ErrTooManyTransactions, "ErrTooManyTransactions"},
{ErrNoTxInputs, "ErrNoTxInputs"}, {ErrNoTxInputs, "ErrNoTxInputs"},
{ErrNoTxOutputs, "ErrNoTxOutputs"}, {ErrNoTxOutputs, "ErrNoTxOutputs"},
{ErrTxTooBig, "ErrTxTooBig"}, {ErrTxTooBig, "ErrTxTooBig"},
@ -52,6 +52,11 @@ func TestErrorCodeStringer(t *testing.T) {
{ErrBadCoinbaseHeight, "ErrBadCoinbaseHeight"}, {ErrBadCoinbaseHeight, "ErrBadCoinbaseHeight"},
{ErrScriptMalformed, "ErrScriptMalformed"}, {ErrScriptMalformed, "ErrScriptMalformed"},
{ErrScriptValidation, "ErrScriptValidation"}, {ErrScriptValidation, "ErrScriptValidation"},
{ErrUnexpectedWitness, "ErrUnexpectedWitness"},
{ErrInvalidWitnessCommitment, "ErrInvalidWitnessCommitment"},
{ErrWitnessCommitmentMismatch, "ErrWitnessCommitmentMismatch"},
{ErrPreviousBlockUnknown, "ErrPreviousBlockUnknown"},
{ErrInvalidAncestorBlock, "ErrInvalidAncestorBlock"},
{ErrPrevBlockNotBest, "ErrPrevBlockNotBest"}, {ErrPrevBlockNotBest, "ErrPrevBlockNotBest"},
{0xffff, "Unknown ErrorCode (65535)"}, {0xffff, "Unknown ErrorCode (65535)"},
} }

View File

@ -482,11 +482,12 @@ func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, timeSource Median
"any transactions") "any transactions")
} }
// A block must not have more transactions than the max block payload. // A block must not have more transactions than the max block payload or
if numTx > wire.MaxBlockPayload { // else it is certainly over the weight limit.
if numTx > MaxBlockBaseSize {
str := fmt.Sprintf("block contains too many transactions - "+ str := fmt.Sprintf("block contains too many transactions - "+
"got %d, max %d", numTx, wire.MaxBlockPayload) "got %d, max %d", numTx, MaxBlockBaseSize)
return ruleError(ErrTooManyTransactions, str) return ruleError(ErrBlockTooBig, str)
} }
// A block must not exceed the maximum allowed block payload when // A block must not exceed the maximum allowed block payload when
@ -644,11 +645,6 @@ func checkSerializedHeight(coinbaseTx *btcutil.Tx, wantHeight int32) error {
// //
// This function MUST be called with the chain state lock held (for writes). // 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 { func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode *blockNode, flags BehaviorFlags) error {
// The genesis block is valid by definition.
if prevNode == nil {
return nil
}
fastAdd := flags&BFFastAdd == BFFastAdd fastAdd := flags&BFFastAdd == BFFastAdd
if !fastAdd { if !fastAdd {
// Ensure the difficulty specified in the block header matches // Ensure the difficulty specified in the block header matches
@ -731,11 +727,6 @@ func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode
// //
// This function MUST be called with the chain state lock held (for writes). // This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) checkBlockContext(block *btcutil.Block, prevNode *blockNode, flags BehaviorFlags) error { func (b *BlockChain) checkBlockContext(block *btcutil.Block, prevNode *blockNode, flags BehaviorFlags) error {
// The genesis block is valid by definition.
if prevNode == nil {
return nil
}
// Perform all block header related validation checks. // Perform all block header related validation checks.
header := &block.MsgBlock().Header header := &block.MsgBlock().Header
err := b.checkBlockHeaderContext(header, prevNode, flags) err := b.checkBlockHeaderContext(header, prevNode, flags)
@ -822,7 +813,7 @@ func (b *BlockChain) checkBlockContext(block *btcutil.Block, prevNode *blockNode
str := fmt.Sprintf("block's weight metric is "+ str := fmt.Sprintf("block's weight metric is "+
"too high - got %v, max %v", "too high - got %v, max %v",
blockWeight, MaxBlockWeight) blockWeight, MaxBlockWeight)
return ruleError(ErrBlockVersionTooOld, str) return ruleError(ErrBlockWeightTooHigh, str)
} }
} }
} }

View File

@ -1969,7 +1969,9 @@ func chainErrToGBTErrString(err error) string {
case blockchain.ErrDuplicateBlock: case blockchain.ErrDuplicateBlock:
return "duplicate" return "duplicate"
case blockchain.ErrBlockTooBig: case blockchain.ErrBlockTooBig:
return "bad-block-size" return "bad-blk-length"
case blockchain.ErrBlockWeightTooHigh:
return "bad-blk-weight"
case blockchain.ErrBlockVersionTooOld: case blockchain.ErrBlockVersionTooOld:
return "bad-version" return "bad-version"
case blockchain.ErrInvalidTime: case blockchain.ErrInvalidTime:
@ -1994,8 +1996,6 @@ func chainErrToGBTErrString(err error) string {
return "checkpoint-time-too-old" return "checkpoint-time-too-old"
case blockchain.ErrNoTransactions: case blockchain.ErrNoTransactions:
return "bad-txns-none" return "bad-txns-none"
case blockchain.ErrTooManyTransactions:
return "bad-txns-toomany"
case blockchain.ErrNoTxInputs: case blockchain.ErrNoTxInputs:
return "bad-txns-noinputs" return "bad-txns-noinputs"
case blockchain.ErrNoTxOutputs: case blockchain.ErrNoTxOutputs:
@ -2040,6 +2040,16 @@ func chainErrToGBTErrString(err error) string {
return "bad-script-malformed" return "bad-script-malformed"
case blockchain.ErrScriptValidation: case blockchain.ErrScriptValidation:
return "bad-script-validate" return "bad-script-validate"
case blockchain.ErrUnexpectedWitness:
return "unexpected-witness"
case blockchain.ErrInvalidWitnessCommitment:
return "bad-witness-nonce-size"
case blockchain.ErrWitnessCommitmentMismatch:
return "bad-witness-merkle-match"
case blockchain.ErrPreviousBlockUnknown:
return "prev-blk-not-found"
case blockchain.ErrInvalidAncestorBlock:
return "bad-prevblk"
case blockchain.ErrPrevBlockNotBest: case blockchain.ErrPrevBlockNotBest:
return "inconclusive-not-best-prvblk" return "inconclusive-not-best-prvblk"
} }