From e1ef2f899b61dfbf703c7ff33f4e9feea8dea75c Mon Sep 17 00:00:00 2001 From: Jim Posen Date: Tue, 5 Sep 2017 20:42:54 -0700 Subject: [PATCH] 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. --- blockchain/accept.go | 17 +++++++--- blockchain/blockindex.go | 40 +++++++++++++++++++++++ blockchain/chain.go | 69 ++++++++++++++++++++++++++++++++++++++-- blockchain/chainio.go | 2 ++ blockchain/error.go | 14 +++++--- blockchain/error_test.go | 7 +++- blockchain/validate.go | 21 ++++-------- rpcserver.go | 16 ++++++++-- 8 files changed, 156 insertions(+), 30 deletions(-) diff --git a/blockchain/accept.go b/blockchain/accept.go index a11fc94d6..897c4ddbf 100644 --- a/blockchain/accept.go +++ b/blockchain/accept.go @@ -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 diff --git a/blockchain/blockindex.go b/blockchain/blockindex.go index 63d021d8d..8fa1fb99e 100644 --- a/blockchain/blockindex.go +++ b/blockchain/blockindex.go @@ -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 diff --git a/blockchain/chain.go b/blockchain/chain.go index e14056583..83bfe883f 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -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 diff --git a/blockchain/chainio.go b/blockchain/chainio.go index e4c95666c..70277edf3 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -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, diff --git a/blockchain/error.go b/blockchain/error.go index 0f3f2b591..1e7c879ba 100644 --- a/blockchain/error.go +++ b/blockchain/error.go @@ -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", } diff --git a/blockchain/error_test.go b/blockchain/error_test.go index dda8ad266..c0e56ab89 100644 --- a/blockchain/error_test.go +++ b/blockchain/error_test.go @@ -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)"}, } diff --git a/blockchain/validate.go b/blockchain/validate.go index ba20d7b37..1e9660496 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -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) } } } diff --git a/rpcserver.go b/rpcserver.go index 1033b20f6..0ba2c4bc5 100644 --- a/rpcserver.go +++ b/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" }