Add a new behavior flag to provide a dry run.

This commit adds a new behavior flag, BFDryRun which allows the caller
to indicate all checks should be performed against the block as normal
except it will not modify any state.  This is useful to test that a block
is valid without actually modifying the current chain or memory state.

This commit also adds a few additional checks which were elided before
since they are implicitly handled by btcwire.  However, with the ability
to propose blocks which didn't necessarily come through the btcwire path,
these checks need to be enforced in the chain code as well.

As a part of adding the checks, three new error codes named
ErrBlockTooBig, ErrTooManyTransactions, and ErrTxTooBig have been
introduced.

Closes #5.
This commit is contained in:
Dave Collins 2014-06-29 15:11:13 -05:00
parent ae51c3e6e0
commit dbca1d59c3
7 changed files with 138 additions and 36 deletions

View file

@ -16,8 +16,11 @@ import (
//
// The flags modify the behavior of this function as follows:
// - BFFastAdd: The somewhat expensive BIP0034 validation is not performed.
// - BFDryRun: The memory chain index will not be pruned and no accept
// notification will be sent since the block is not being accepted.
func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags) error {
fastAdd := flags&BFFastAdd == BFFastAdd
dryRun := flags&BFDryRun == BFDryRun
// Get a block node for the block previous to this one. Will be nil
// if this is the genesis block.
@ -105,7 +108,7 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags)
if !fastAdd {
// Reject version 1 blocks once a majority of the network has
// upgraded. This is part of BIP0034.
if blockHeader.Version == 1 {
if blockHeader.Version < 2 {
if b.isMajorityVersion(2, prevNode,
b.netParams.BlockV1RejectNumRequired,
b.netParams.BlockV1RejectNumToCheck) {
@ -143,9 +146,11 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags)
// Prune block nodes which are no longer needed before creating
// a new node.
err = b.pruneBlockNodes()
if err != nil {
return err
if !dryRun {
err = b.pruneBlockNodes()
if err != nil {
return err
}
}
// Create a new block node for the block and add it to the in-memory
@ -168,7 +173,9 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags)
// Notify the caller that the new block was accepted into the block
// chain. The caller would typically want to react by relaying the
// inventory to other peers.
b.sendNotification(NTBlockAccepted, block)
if !dryRun {
b.sendNotification(NTBlockAccepted, block)
}
return nil
}

View file

@ -69,7 +69,7 @@ type blockNode struct {
inMainChain bool
// Some fields from block headers to aid in best chain selection.
version uint32
version int32
bits uint32
timestamp time.Time
}
@ -605,7 +605,7 @@ func (b *BlockChain) pruneBlockNodes() error {
// isMajorityVersion determines if a previous number of blocks in the chain
// starting with startNode are at least the minimum passed version.
func (b *BlockChain) isMajorityVersion(minVer uint32, startNode *blockNode, numRequired, numToCheck uint64) bool {
func (b *BlockChain) isMajorityVersion(minVer int32, startNode *blockNode, numRequired, numToCheck uint64) bool {
numFound := uint64(0)
iterNode := startNode
for i := uint64(0); i < numToCheck && iterNode != nil; i++ {
@ -811,7 +811,11 @@ func (b *BlockChain) disconnectBlock(node *blockNode, block *btcutil.Block) erro
// disconnected must be in reverse order (think of popping them off
// the end of the chain) and nodes the are being attached must be in forwards
// order (think pushing them onto the end of the chain).
func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error {
//
// The flags modify the behavior of this function as follows:
// - BFDryRun: Only the checks which ensure the reorganize can be completed
// successfully are performed. The chain is not reorganized.
func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List, flags BehaviorFlags) error {
// Ensure all of the needed side chain blocks are in the cache.
for e := attachNodes.Front(); e != nil; e = e.Next() {
n := e.Value.(*blockNode)
@ -842,6 +846,12 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
}
}
// Skip disconnecting and connecting the blocks when running with the
// dry run flag set.
if flags&BFDryRun == BFDryRun {
return nil
}
// Disconnect blocks from the main chain.
for e := detachNodes.Front(); e != nil; e = e.Next() {
n := e.Value.(*blockNode)
@ -892,8 +902,12 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
// The flags modify the behavior of this function as follows:
// - BFFastAdd: Avoids the call to checkConnectBlock which does several
// expensive transaction validation operations.
// - BFDryRun: Prevents the block from being connected and avoids modifying the
// state of the memory chain index. Also, any log messages related to
// modifying the state are avoided.
func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, flags BehaviorFlags) error {
fastAdd := flags&BFFastAdd == BFFastAdd
dryRun := flags&BFDryRun == BFDryRun
// We haven't selected a best chain yet or we are extending the main
// (best) chain with a new block. This is the most common case.
@ -910,6 +924,11 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
}
}
// Don't connect the block if performing a dry run.
if dryRun {
return nil
}
// Connect the block to the main chain.
err := b.connectBlock(node, block)
if err != nil {
@ -932,7 +951,9 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
// become the main chain, but in either case we need the block stored
// for future processing, so add the block to the side chain holding
// cache.
log.Debugf("Adding block %v to side chain cache", node.hash)
if !dryRun {
log.Debugf("Adding block %v to side chain cache", node.hash)
}
b.blockCache[*node.hash] = block
b.index[*node.hash] = node
@ -940,9 +961,27 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
node.inMainChain = false
node.parent.children = append(node.parent.children, node)
// Remove the block from the side chain cache and disconnect it from the
// parent node when the function returns when running in dry run mode.
if dryRun {
defer func() {
children := node.parent.children
children = removeChildNode(children, node)
node.parent.children = children
delete(b.index, *node.hash)
delete(b.blockCache, *node.hash)
}()
}
// We're extending (or creating) a side chain, but the cumulative
// work for this new side chain is not enough to make it the new chain.
if node.workSum.Cmp(b.bestChain.workSum) <= 0 {
// Skip Logging info when the dry run flag is set.
if dryRun {
return nil
}
// Find the fork point.
fork := node
for ; fork.parent != nil; fork = fork.parent {
@ -975,8 +1014,11 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
detachNodes, attachNodes := b.getReorganizeNodes(node)
// Reorganize the chain.
log.Infof("REORGANIZE: Block %v is causing a reorganize.", node.hash)
err := b.reorganizeChain(detachNodes, attachNodes)
if !dryRun {
log.Infof("REORGANIZE: Block %v is causing a reorganize.",
node.hash)
}
err := b.reorganizeChain(detachNodes, attachNodes, flags)
if err != nil {
return err
}

View file

@ -17,6 +17,10 @@ const (
// exists.
ErrDuplicateBlock ErrorCode = iota
// ErrBlockTooBig indicates the serialized block size exceeds the
// maximum allowed size.
ErrBlockTooBig
// ErrBlockVersionTooOld indicates the block version is too old and is
// no longer accepted since the majority of the network has upgraded
// to a newer version.
@ -67,6 +71,10 @@ 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
@ -75,6 +83,10 @@ const (
// valid transaction must have at least one output.
ErrNoTxOutputs
// ErrTxTooBig indicates a transaction exceeds the maximum allowed size
// when serialized.
ErrTxTooBig
// ErrBadTxOutValue indicates an output value for a transaction is
// invalid in some way such as being out of range.
ErrBadTxOutValue
@ -167,6 +179,7 @@ const (
// Map of ErrorCode values back to their constant names for pretty printing.
var errorCodeStrings = map[ErrorCode]string{
ErrDuplicateBlock: "ErrDuplicateBlock",
ErrBlockTooBig: "ErrBlockTooBig",
ErrBlockVersionTooOld: "ErrBlockVersionTooOld",
ErrInvalidTime: "ErrInvalidTime",
ErrTimeTooOld: "ErrTimeTooOld",
@ -178,8 +191,10 @@ var errorCodeStrings = map[ErrorCode]string{
ErrBadCheckpoint: "ErrBadCheckpoint",
ErrForkTooOld: "ErrForkTooOld",
ErrNoTransactions: "ErrNoTransactions",
ErrTooManyTransactions: "ErrTooManyTransactions",
ErrNoTxInputs: "ErrNoTxInputs",
ErrNoTxOutputs: "ErrNoTxOutputs",
ErrTxTooBig: "ErrTxTooBig",
ErrBadTxOutValue: "ErrBadTxOutValue",
ErrDuplicateTxInputs: "ErrDuplicateTxInputs",
ErrBadTxInput: "ErrBadTxInput",

View file

@ -16,6 +16,7 @@ func TestErrorCodeStringer(t *testing.T) {
want string
}{
{btcchain.ErrDuplicateBlock, "ErrDuplicateBlock"},
{btcchain.ErrBlockTooBig, "ErrBlockTooBig"},
{btcchain.ErrBlockVersionTooOld, "ErrBlockVersionTooOld"},
{btcchain.ErrInvalidTime, "ErrInvalidTime"},
{btcchain.ErrTimeTooOld, "ErrTimeTooOld"},
@ -27,8 +28,10 @@ func TestErrorCodeStringer(t *testing.T) {
{btcchain.ErrBadCheckpoint, "ErrBadCheckpoint"},
{btcchain.ErrForkTooOld, "ErrForkTooOld"},
{btcchain.ErrNoTransactions, "ErrNoTransactions"},
{btcchain.ErrTooManyTransactions, "ErrTooManyTransactions"},
{btcchain.ErrNoTxInputs, "ErrNoTxInputs"},
{btcchain.ErrNoTxOutputs, "ErrNoTxOutputs"},
{btcchain.ErrTxTooBig, "ErrTxTooBig"},
{btcchain.ErrBadTxOutValue, "ErrBadTxOutValue"},
{btcchain.ErrDuplicateTxInputs, "ErrDuplicateTxInputs"},
{btcchain.ErrBadTxInput, "ErrBadTxInput"},

View file

@ -26,6 +26,11 @@ const (
// not be performed.
BFNoPoWCheck
// BFDryRun may be set to indicate the block should not modify the chain
// or memory chain index. This is useful to test that a block is valid
// without modifying the current state.
BFDryRun
// BFNone is a convenience value to specifically indicate no flags.
BFNone BehaviorFlags = 0
)
@ -110,6 +115,7 @@ func (b *BlockChain) processOrphans(hash *btcwire.ShaHash, flags BehaviorFlags)
// when the error is nil.
func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bool, error) {
fastAdd := flags&BFFastAdd == BFFastAdd
dryRun := flags&BFDryRun == BFDryRun
blockHash, err := block.Sha()
if err != nil {
@ -179,9 +185,11 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bo
// Handle orphan blocks.
prevHash := &blockHeader.PrevBlock
if !prevHash.IsEqual(zeroHash) && !b.blockExists(prevHash) {
log.Infof("Adding orphan block %v with parent %v", blockHash,
prevHash)
b.addOrphanBlock(block)
if !dryRun {
log.Infof("Adding orphan block %v with parent %v",
blockHash, prevHash)
b.addOrphanBlock(block)
}
return true, nil
}
@ -193,14 +201,18 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bo
return false, err
}
// Accept any orphan blocks that depend on this block (they are no
// longer orphans) and repeat for those accepted blocks until there are
// no more.
err = b.processOrphans(blockHash, flags)
if err != nil {
return false, err
// Don't process any orphans or log when the dry run flag is set.
if !dryRun {
// Accept any orphan blocks that depend on this block (they are
// no longer orphans) and repeat for those accepted blocks until
// there are no more.
err := b.processOrphans(blockHash, flags)
if err != nil {
return false, err
}
log.Debugf("Accepted block %v", blockHash)
}
log.Debugf("Accepted block %v", blockHash)
return false, nil
}

View file

@ -49,6 +49,9 @@ func connectTransactions(txStore TxStore, block *btcutil.Block) error {
originHash := &txIn.PreviousOutpoint.Hash
originIndex := txIn.PreviousOutpoint.Index
if originTx, exists := txStore[*originHash]; exists {
if originIndex > uint32(len(originTx.Spent)) {
continue
}
originTx.Spent[originIndex] = true
}
}
@ -82,6 +85,9 @@ func disconnectTransactions(txStore TxStore, block *btcutil.Block) error {
originIndex := txIn.PreviousOutpoint.Index
originTx, exists := txStore[*originHash]
if exists && originTx.Tx != nil && originTx.Err == nil {
if originIndex > uint32(len(originTx.Spent)) {
continue
}
originTx.Spent[originIndex] = false
}
}

View file

@ -189,10 +189,14 @@ func CheckTransactionSanity(tx *btcutil.Tx) error {
return ruleError(ErrNoTxOutputs, "transaction has no outputs")
}
// NOTE: bitcoind does size limits checking here, but the size limits
// have already been checked by btcwire for incoming transactions.
// Also, btcwire checks the size limits on send too, so there is no need
// to double check it here.
// A transaction must not exceed the maximum allowed block payload when
// serialized.
serializedTxSize := tx.MsgTx().SerializeSize()
if serializedTxSize > btcwire.MaxBlockPayload {
str := fmt.Sprintf("serialized transaction is too big - got "+
"%d, max %d", serializedTxSize, btcwire.MaxBlockPayload)
return ruleError(ErrTxTooBig, str)
}
// Ensure the transaction amounts are in range. Each transaction
// output must not be negative or more than the max allowed per
@ -414,10 +418,29 @@ func CountP2SHSigOps(tx *btcutil.Tx, isCoinBaseTx bool, txStore TxStore) (int, e
// The flags do not modify the behavior of this function directly, however they
// are needed to pass along to checkProofOfWork.
func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, flags BehaviorFlags) error {
// NOTE: bitcoind does size limits checking here, but the size limits
// have already been checked by btcwire for incoming blocks. Also,
// btcwire checks the size limits on send too, so there is no need
// to double check it here.
// A block must have at least one transaction.
msgBlock := block.MsgBlock()
numTx := len(msgBlock.Transactions)
if numTx == 0 {
return ruleError(ErrNoTransactions, "block does not contain "+
"any transactions")
}
// A block must not have more transactions than the max block payload.
if numTx > btcwire.MaxBlockPayload {
str := fmt.Sprintf("block contains too many transactions - "+
"got %d, max %d", numTx, btcwire.MaxBlockPayload)
return ruleError(ErrTooManyTransactions, str)
}
// A block must not exceed the maximum allowed block payload when
// serialized.
serializedSize := msgBlock.SerializeSize()
if serializedSize > btcwire.MaxBlockPayload {
str := fmt.Sprintf("serialized block is too big - got %d, "+
"max %d", serializedSize, btcwire.MaxBlockPayload)
return ruleError(ErrBlockTooBig, str)
}
// Ensure the proof of work bits in the block header is in min/max range
// and the block hash is less than the target value described by the
@ -446,14 +469,8 @@ func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, flags BehaviorFla
return ruleError(ErrTimeTooNew, str)
}
// A block must have at least one transaction.
transactions := block.Transactions()
if len(transactions) == 0 {
return ruleError(ErrNoTransactions, "block does not contain "+
"any transactions")
}
// The first transaction in a block must be a coinbase.
transactions := block.Transactions()
if !IsCoinBase(transactions[0]) {
return ruleError(ErrFirstTxNotCoinbase, "first transaction in "+
"block is not a coinbase")