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:
parent
11d7cae82b
commit
e1ef2f899b
8 changed files with 156 additions and 30 deletions
|
@ -5,6 +5,8 @@
|
|||
package blockchain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/database"
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
@ -22,11 +24,17 @@ import (
|
|||
func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags) (bool, error) {
|
||||
// The height of this block is one more than the referenced previous
|
||||
// block.
|
||||
blockHeight := int32(0)
|
||||
prevNode := b.index.LookupNode(&block.MsgBlock().Header.PrevBlock)
|
||||
if prevNode != nil {
|
||||
blockHeight = prevNode.height + 1
|
||||
prevHash := &block.MsgBlock().Header.PrevBlock
|
||||
prevNode := b.index.LookupNode(prevHash)
|
||||
if prevNode == nil {
|
||||
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)
|
||||
|
||||
// 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).
|
||||
blockHeader := &block.MsgBlock().Header
|
||||
newNode := newBlockNode(blockHeader, blockHeight)
|
||||
newNode.status |= statusDataStored
|
||||
if prevNode != nil {
|
||||
newNode.parent = prevNode
|
||||
newNode.height = blockHeight
|
||||
|
|
|
@ -16,6 +16,29 @@ import (
|
|||
"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
|
||||
// aid in selecting the best chain to be the main chain. The main chain is
|
||||
// stored into the block database.
|
||||
|
@ -49,6 +72,9 @@ type blockNode struct {
|
|||
nonce uint32
|
||||
timestamp int64
|
||||
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
|
||||
|
@ -167,6 +193,20 @@ func (node *blockNode) CalcPastMedianTime() time.Time {
|
|||
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
|
||||
// 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
|
||||
|
|
|
@ -497,10 +497,14 @@ func LockTimeToSequence(isSeconds bool, locktime uint32) uint32 {
|
|||
//
|
||||
// This function MUST be called with the chain state lock held (for reads).
|
||||
func (b *BlockChain) getReorganizeNodes(node *blockNode) (*list.List, *list.List) {
|
||||
// Nothing to detach or attach if there is no node.
|
||||
attachNodes := 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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
// later.
|
||||
forkNode := b.bestChain.FindFork(node)
|
||||
invalidChain := false
|
||||
for n := node; n != nil && n != forkNode; n = n.parent {
|
||||
if n.KnownInvalid() {
|
||||
invalidChain = true
|
||||
break
|
||||
}
|
||||
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
|
||||
// common ancestor adding each block to the list of nodes to detach from
|
||||
// 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
|
||||
// tweaking the chain and/or database. This approach catches these
|
||||
// issues before ever modifying the chain.
|
||||
var validationError error
|
||||
for e := attachNodes.Front(); e != nil; e = e.Next() {
|
||||
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
|
||||
err := b.db.View(func(dbTx database.Tx) error {
|
||||
var err error
|
||||
|
@ -869,14 +899,42 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
|
|||
// Store the loaded block for later.
|
||||
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
|
||||
// thus will not be generated. This is done because the state
|
||||
// is not being immediately written to the database, so it is
|
||||
// not needed.
|
||||
err = b.checkConnectBlock(n, block, view, 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
|
||||
}
|
||||
n.status |= statusValid
|
||||
}
|
||||
|
||||
if validationError != nil {
|
||||
return validationError
|
||||
}
|
||||
|
||||
// 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.
|
||||
parentHash := &block.MsgBlock().Header.PrevBlock
|
||||
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
|
||||
// to the main chain without violating any rules and without
|
||||
// actually connecting the block.
|
||||
|
@ -984,8 +1045,12 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
|
|||
if !fastAdd {
|
||||
err := b.checkConnectBlock(node, block, view, &stxos)
|
||||
if err != nil {
|
||||
if _, ok := err.(RuleError); ok {
|
||||
node.status |= statusValidateFailed
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
node.status |= statusValid
|
||||
}
|
||||
|
||||
// In the fast add case the code to check the block connection
|
||||
|
|
|
@ -1060,6 +1060,7 @@ func (b *BlockChain) createChainState() error {
|
|||
genesisBlock := btcutil.NewBlock(b.chainParams.GenesisBlock)
|
||||
header := &genesisBlock.MsgBlock().Header
|
||||
node := newBlockNode(header, 0)
|
||||
node.status |= statusDataStored | statusValid
|
||||
b.bestChain.SetTip(node)
|
||||
|
||||
// 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.
|
||||
node := &blockNodes[height]
|
||||
initBlockNode(node, header, height)
|
||||
node.status |= statusDataStored | statusValid
|
||||
if tip != nil {
|
||||
node.parent = tip
|
||||
node.workSum = node.workSum.Add(tip.workSum,
|
||||
|
|
|
@ -99,10 +99,6 @@ const (
|
|||
// transaction.
|
||||
ErrNoTransactions
|
||||
|
||||
// ErrTooManyTransactions indicates the block has more transactions than
|
||||
// are allowed.
|
||||
ErrTooManyTransactions
|
||||
|
||||
// ErrNoTxInputs indicates a transaction does not have any inputs. A
|
||||
// valid transaction must have at least one input.
|
||||
ErrNoTxInputs
|
||||
|
@ -213,6 +209,13 @@ const (
|
|||
// manually computed witness commitment.
|
||||
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
|
||||
// current chain tip. This is not a block validation rule, but is required
|
||||
// for block proposals submitted via getblocktemplate RPC.
|
||||
|
@ -236,7 +239,6 @@ var errorCodeStrings = map[ErrorCode]string{
|
|||
ErrForkTooOld: "ErrForkTooOld",
|
||||
ErrCheckpointTimeTooOld: "ErrCheckpointTimeTooOld",
|
||||
ErrNoTransactions: "ErrNoTransactions",
|
||||
ErrTooManyTransactions: "ErrTooManyTransactions",
|
||||
ErrNoTxInputs: "ErrNoTxInputs",
|
||||
ErrNoTxOutputs: "ErrNoTxOutputs",
|
||||
ErrTxTooBig: "ErrTxTooBig",
|
||||
|
@ -262,6 +264,8 @@ var errorCodeStrings = map[ErrorCode]string{
|
|||
ErrUnexpectedWitness: "ErrUnexpectedWitness",
|
||||
ErrInvalidWitnessCommitment: "ErrInvalidWitnessCommitment",
|
||||
ErrWitnessCommitmentMismatch: "ErrWitnessCommitmentMismatch",
|
||||
ErrPreviousBlockUnknown: "ErrPreviousBlockUnknown",
|
||||
ErrInvalidAncestorBlock: "ErrInvalidAncestorBlock",
|
||||
ErrPrevBlockNotBest: "ErrPrevBlockNotBest",
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ func TestErrorCodeStringer(t *testing.T) {
|
|||
}{
|
||||
{ErrDuplicateBlock, "ErrDuplicateBlock"},
|
||||
{ErrBlockTooBig, "ErrBlockTooBig"},
|
||||
{ErrBlockWeightTooHigh, "ErrBlockWeightTooHigh"},
|
||||
{ErrBlockVersionTooOld, "ErrBlockVersionTooOld"},
|
||||
{ErrInvalidTime, "ErrInvalidTime"},
|
||||
{ErrTimeTooOld, "ErrTimeTooOld"},
|
||||
|
@ -28,7 +29,6 @@ func TestErrorCodeStringer(t *testing.T) {
|
|||
{ErrForkTooOld, "ErrForkTooOld"},
|
||||
{ErrCheckpointTimeTooOld, "ErrCheckpointTimeTooOld"},
|
||||
{ErrNoTransactions, "ErrNoTransactions"},
|
||||
{ErrTooManyTransactions, "ErrTooManyTransactions"},
|
||||
{ErrNoTxInputs, "ErrNoTxInputs"},
|
||||
{ErrNoTxOutputs, "ErrNoTxOutputs"},
|
||||
{ErrTxTooBig, "ErrTxTooBig"},
|
||||
|
@ -52,6 +52,11 @@ func TestErrorCodeStringer(t *testing.T) {
|
|||
{ErrBadCoinbaseHeight, "ErrBadCoinbaseHeight"},
|
||||
{ErrScriptMalformed, "ErrScriptMalformed"},
|
||||
{ErrScriptValidation, "ErrScriptValidation"},
|
||||
{ErrUnexpectedWitness, "ErrUnexpectedWitness"},
|
||||
{ErrInvalidWitnessCommitment, "ErrInvalidWitnessCommitment"},
|
||||
{ErrWitnessCommitmentMismatch, "ErrWitnessCommitmentMismatch"},
|
||||
{ErrPreviousBlockUnknown, "ErrPreviousBlockUnknown"},
|
||||
{ErrInvalidAncestorBlock, "ErrInvalidAncestorBlock"},
|
||||
{ErrPrevBlockNotBest, "ErrPrevBlockNotBest"},
|
||||
{0xffff, "Unknown ErrorCode (65535)"},
|
||||
}
|
||||
|
|
|
@ -482,11 +482,12 @@ func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, timeSource Median
|
|||
"any transactions")
|
||||
}
|
||||
|
||||
// A block must not have more transactions than the max block payload.
|
||||
if numTx > wire.MaxBlockPayload {
|
||||
// A block must not have more transactions than the max block payload or
|
||||
// else it is certainly over the weight limit.
|
||||
if numTx > MaxBlockBaseSize {
|
||||
str := fmt.Sprintf("block contains too many transactions - "+
|
||||
"got %d, max %d", numTx, wire.MaxBlockPayload)
|
||||
return ruleError(ErrTooManyTransactions, str)
|
||||
"got %d, max %d", numTx, MaxBlockBaseSize)
|
||||
return ruleError(ErrBlockTooBig, str)
|
||||
}
|
||||
|
||||
// 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).
|
||||
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
|
||||
if !fastAdd {
|
||||
// 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).
|
||||
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.
|
||||
header := &block.MsgBlock().Header
|
||||
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 "+
|
||||
"too high - got %v, max %v",
|
||||
blockWeight, MaxBlockWeight)
|
||||
return ruleError(ErrBlockVersionTooOld, str)
|
||||
return ruleError(ErrBlockWeightTooHigh, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
16
rpcserver.go
16
rpcserver.go
|
@ -1969,7 +1969,9 @@ func chainErrToGBTErrString(err error) string {
|
|||
case blockchain.ErrDuplicateBlock:
|
||||
return "duplicate"
|
||||
case blockchain.ErrBlockTooBig:
|
||||
return "bad-block-size"
|
||||
return "bad-blk-length"
|
||||
case blockchain.ErrBlockWeightTooHigh:
|
||||
return "bad-blk-weight"
|
||||
case blockchain.ErrBlockVersionTooOld:
|
||||
return "bad-version"
|
||||
case blockchain.ErrInvalidTime:
|
||||
|
@ -1994,8 +1996,6 @@ func chainErrToGBTErrString(err error) string {
|
|||
return "checkpoint-time-too-old"
|
||||
case blockchain.ErrNoTransactions:
|
||||
return "bad-txns-none"
|
||||
case blockchain.ErrTooManyTransactions:
|
||||
return "bad-txns-toomany"
|
||||
case blockchain.ErrNoTxInputs:
|
||||
return "bad-txns-noinputs"
|
||||
case blockchain.ErrNoTxOutputs:
|
||||
|
@ -2040,6 +2040,16 @@ func chainErrToGBTErrString(err error) string {
|
|||
return "bad-script-malformed"
|
||||
case blockchain.ErrScriptValidation:
|
||||
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:
|
||||
return "inconclusive-not-best-prvblk"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue