Implement a fast path for the Initial Block Download.

It is not necessary to do all of the transaction validation on
blocks if they have been confirmed to be in the block chain leading
up to the final checkpoint in a given blockschain.

This algorithm fetches block headers from the peer, then once it has
established the full blockchain connection, it requests blocks.
Any blocks before the final checkpoint pass true for fastAdd on
btcchain operation, which causes it to do less valiation on the block.
This commit is contained in:
Dale Rahn 2013-11-18 16:23:51 -05:00
parent 55331de532
commit 992d11830c
6 changed files with 138 additions and 111 deletions

183
accept.go
View file

@ -14,7 +14,11 @@ import (
// It performs several validation checks which depend on its position within // It performs several validation checks which depend on its position within
// the block chain before adding it. The block is expected to have already gone // the block chain before adding it. The block is expected to have already gone
// through ProcessBlock before calling this function with it. // through ProcessBlock before calling this function with it.
func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block) error { // The fastAdd argument modifies the behavior of the function by avoiding the
// somewhat expensive operation: BIP34 validation, it also passes the argument
// down to connectBestChain()
func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, fastAdd bool) error {
// Get a block node for the block previous to this one. Will be nil // Get a block node for the block previous to this one. Will be nil
// if this is the genesis block. // if this is the genesis block.
prevNode, err := b.getPrevNodeFromBlock(block) prevNode, err := b.getPrevNodeFromBlock(block)
@ -31,43 +35,49 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block) error {
} }
block.SetHeight(blockHeight) block.SetHeight(blockHeight)
// Ensure the difficulty specified in the block header matches the
// calculated difficulty based on the previous block and difficulty
// retarget rules.
blockHeader := block.MsgBlock().Header blockHeader := block.MsgBlock().Header
expectedDifficulty, err := b.calcNextRequiredDifficulty(prevNode, block)
if err != nil {
return err
}
blockDifficulty := blockHeader.Bits
if blockDifficulty != expectedDifficulty {
str := "block difficulty of %d is not the expected value of %d"
str = fmt.Sprintf(str, blockDifficulty, expectedDifficulty)
return RuleError(str)
}
// Ensure the timestamp for the block header is after the median time of if !fastAdd {
// the last several blocks (medianTimeBlocks). // Ensure the difficulty specified in the block header matches
medianTime, err := b.calcPastMedianTime(prevNode) // the calculated difficulty based on the previous block and
if err != nil { // difficulty
log.Errorf("calcPastMedianTime: %v", err) // retarget rules.
return err expectedDifficulty, err := b.calcNextRequiredDifficulty(prevNode, block)
} if err != nil {
if !blockHeader.Timestamp.After(medianTime) { return err
str := "block timestamp of %v is not after expected %v" }
str = fmt.Sprintf(str, blockHeader.Timestamp, medianTime) blockDifficulty := blockHeader.Bits
return RuleError(str) if blockDifficulty != expectedDifficulty {
} str := "block difficulty of %d is not the expected value of %d"
str = fmt.Sprintf(str, blockDifficulty, expectedDifficulty)
// Ensure all transactions in the block are finalized.
for _, tx := range block.Transactions() {
if !IsFinalizedTransaction(tx, blockHeight, blockHeader.Timestamp) {
str := fmt.Sprintf("block contains unfinalized "+
"transaction %v", tx.Sha())
return RuleError(str) return RuleError(str)
} }
}
// Ensure the timestamp for the block header is after the
// median time of the last several blocks (medianTimeBlocks).
medianTime, err := b.calcPastMedianTime(prevNode)
if err != nil {
log.Errorf("calcPastMedianTime: %v", err)
return err
}
if !blockHeader.Timestamp.After(medianTime) {
str := "block timestamp of %v is not after expected %v"
str = fmt.Sprintf(str, blockHeader.Timestamp,
medianTime)
return RuleError(str)
}
// Ensure all transactions in the block are finalized.
for _, tx := range block.Transactions() {
if !IsFinalizedTransaction(tx, blockHeight,
blockHeader.Timestamp) {
str := fmt.Sprintf("block contains "+
"unfinalized transaction %v", tx.Sha())
return RuleError(str)
}
}
}
// Ensure chain matches up to predetermined checkpoints. // Ensure chain matches up to predetermined checkpoints.
// It's safe to ignore the error on Sha since it's already cached. // It's safe to ignore the error on Sha since it's already cached.
blockHash, _ := block.Sha() blockHash, _ := block.Sha()
@ -83,59 +93,66 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block) error {
return RuleError(str) return RuleError(str)
} }
// Reject version 1 blocks once a majority of the network has upgraded. if !fastAdd {
// Rules: // Reject version 1 blocks once a majority of the network has
// 95% (950 / 1000) for main network // upgraded.
// 75% (75 / 100) for the test network // Rules:
// This is part of BIP_0034. // 95% (950 / 1000) for main network
if blockHeader.Version == 1 { // 75% (75 / 100) for the test network
minRequired := uint64(950) // This is part of BIP_0034.
numToCheck := uint64(1000) if blockHeader.Version == 1 {
if b.btcnet == btcwire.TestNet3 || b.btcnet == btcwire.TestNet { minRequired := uint64(950)
minRequired = 75 numToCheck := uint64(1000)
numToCheck = 100 if b.btcnet == btcwire.TestNet3 || b.btcnet ==
} btcwire.TestNet {
if b.isMajorityVersion(2, prevNode, minRequired, numToCheck) { minRequired = 75
str := "new blocks with version %d are no longer valid" numToCheck = 100
str = fmt.Sprintf(str, blockHeader.Version)
return RuleError(str)
}
}
// Ensure coinbase starts with serialized block heights for blocks
// whose version is the serializedHeightVersion or newer once a majority
// of the network has upgraded.
// Rules:
// 75% (750 / 1000) for main network
// 51% (51 / 100) for the test network
// This is part of BIP_0034.
if blockHeader.Version >= serializedHeightVersion {
minRequired := uint64(750)
numToCheck := uint64(1000)
if b.btcnet == btcwire.TestNet3 || b.btcnet == btcwire.TestNet {
minRequired = 51
numToCheck = 100
}
if b.isMajorityVersion(serializedHeightVersion, prevNode,
minRequired, numToCheck) {
expectedHeight := int64(0)
if prevNode != nil {
expectedHeight = prevNode.height + 1
} }
coinbaseTx := block.Transactions()[0] if b.isMajorityVersion(2, prevNode, minRequired,
err := checkSerializedHeight(coinbaseTx, expectedHeight) numToCheck) {
if err != nil { str := "new blocks with version %d are no longer valid"
return err str = fmt.Sprintf(str, blockHeader.Version)
return RuleError(str)
} }
} }
}
// Prune block nodes which are no longer needed before creating a new // Ensure coinbase starts with serialized block heights for
// node. // blocks whose version is the serializedHeightVersion or
err = b.pruneBlockNodes() // newer once a majority of the network has upgraded.
if err != nil { // Rules:
return err // 75% (750 / 1000) for main network
// 51% (51 / 100) for the test network
// This is part of BIP_0034.
if blockHeader.Version >= serializedHeightVersion {
minRequired := uint64(750)
numToCheck := uint64(1000)
if b.btcnet == btcwire.TestNet3 || b.btcnet ==
btcwire.TestNet {
minRequired = 51
numToCheck = 100
}
if b.isMajorityVersion(serializedHeightVersion,
prevNode, minRequired, numToCheck) {
expectedHeight := int64(0)
if prevNode != nil {
expectedHeight = prevNode.height + 1
}
coinbaseTx := block.Transactions()[0]
err := checkSerializedHeight(coinbaseTx,
expectedHeight)
if err != nil {
return err
}
}
}
// Prune block nodes which are no longer needed before creating
// a new node.
err = b.pruneBlockNodes()
if err != nil {
return err
}
} }
// Create a new block node for the block and add it to the in-memory // Create a new block node for the block and add it to the in-memory
@ -150,7 +167,7 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block) error {
// Connect the passed block to the chain while respecting proper chain // Connect the passed block to the chain while respecting proper chain
// selection according to the chain with the most proof of work. This // selection according to the chain with the most proof of work. This
// also handles validation of the transaction scripts. // also handles validation of the transaction scripts.
err = b.connectBestChain(newNode, block) err = b.connectBestChain(newNode, block, fastAdd)
if err != nil { if err != nil {
return err return err
} }

View file

@ -874,7 +874,10 @@ func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) error
// chain. However, it may also be extending (or creating) a side chain (fork) // chain. However, it may also be extending (or creating) a side chain (fork)
// which may or may not end up becoming the main chain depending on which fork // which may or may not end up becoming the main chain depending on which fork
// cumulatively has the most proof of work. // cumulatively has the most proof of work.
func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block) error { // The fastAdd argument avoids the call to checkConnectBlock which does
// several expensive transaction validation operations.
func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fastAdd bool) error {
// We haven't selected a best chain yet or we are extending the main // 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. // (best) chain with a new block. This is the most common case.
if b.bestChain == nil || node.parent.hash.IsEqual(b.bestChain.hash) { if b.bestChain == nil || node.parent.hash.IsEqual(b.bestChain.hash) {
@ -883,13 +886,15 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block) err
// be necessary to get this node to the main chain) without // be necessary to get this node to the main chain) without
// violating any rules and without actually connecting the // violating any rules and without actually connecting the
// block. // block.
err := b.checkConnectBlock(node, block) if !fastAdd {
if err != nil { err := b.checkConnectBlock(node, block)
return err if err != nil {
return err
}
} }
// Connect the block to the main chain. // Connect the block to the main chain.
err = b.connectBlock(node, block) err := b.connectBlock(node, block)
if err != nil { if err != nil {
return err return err
} }
@ -901,6 +906,10 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block) err
return nil return nil
} }
if fastAdd {
bsha, _ := block.Sha()
log.Warnf("fastAdd set in the side chain case? %v\n", bsha)
}
// We're extending (or creating) a side chain which may or may not // We're extending (or creating) a side chain which may or may not
// become the main chain, but in either case we need the block stored // become the main chain, but in either case we need the block stored

View file

@ -47,7 +47,7 @@ func TestHaveBlock(t *testing.T) {
btcchain.TstSetCoinbaseMaturity(1) btcchain.TstSetCoinbaseMaturity(1)
for i := 1; i < len(blocks); i++ { for i := 1; i < len(blocks); i++ {
err = chain.ProcessBlock(blocks[i]) err = chain.ProcessBlock(blocks[i], false)
if err != nil { if err != nil {
t.Errorf("ProcessBlock fail on block %v: %v\n", i, err) t.Errorf("ProcessBlock fail on block %v: %v\n", i, err)
return return
@ -55,7 +55,7 @@ func TestHaveBlock(t *testing.T) {
} }
// Insert an orphan block. // Insert an orphan block.
if err := chain.ProcessBlock(btcutil.NewBlock(&Block100000)); err != nil { if err := chain.ProcessBlock(btcutil.NewBlock(&Block100000), false); err != nil {
t.Errorf("Unable to process block: %v", err) t.Errorf("Unable to process block: %v", err)
return return
} }

2
doc.go
View file

@ -112,7 +112,7 @@ intentionally causes an error by attempting to process a duplicate block.
// Process a block. For this example, we are going to intentionally // Process a block. For this example, we are going to intentionally
// cause an error by trying to process the genesis block which already // cause an error by trying to process the genesis block which already
// exists. // exists.
err = chain.ProcessBlock(genesisBlock) err = chain.ProcessBlock(genesisBlock, false)
if err != nil { if err != nil {
fmt.Printf("Failed to process block: %v\n", err) fmt.Printf("Failed to process block: %v\n", err)
return return

View file

@ -74,7 +74,7 @@ func (b *BlockChain) processOrphans(hash *btcwire.ShaHash) error {
i-- i--
// Potentially accept the block into the block chain. // Potentially accept the block into the block chain.
err := b.maybeAcceptBlock(orphan.block) err := b.maybeAcceptBlock(orphan.block, false)
if err != nil { if err != nil {
return err return err
} }
@ -92,7 +92,7 @@ func (b *BlockChain) processOrphans(hash *btcwire.ShaHash) error {
// the block chain. It includes functionality such as rejecting duplicate // the block chain. It includes functionality such as rejecting duplicate
// blocks, ensuring blocks follow all rules, orphan handling, and insertion into // blocks, ensuring blocks follow all rules, orphan handling, and insertion into
// the block chain along with best chain selection and reorganization. // the block chain along with best chain selection and reorganization.
func (b *BlockChain) ProcessBlock(block *btcutil.Block) error { func (b *BlockChain) ProcessBlock(block *btcutil.Block, fastAdd bool) error {
blockHash, err := block.Sha() blockHash, err := block.Sha()
if err != nil { if err != nil {
return err return err
@ -138,22 +138,23 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block) error {
blockHeader.Timestamp, checkpointTime) blockHeader.Timestamp, checkpointTime)
return RuleError(str) return RuleError(str)
} }
if !fastAdd {
// Even though the checks prior to now have already ensured the // Even though the checks prior to now have already ensured the
// proof of work exceeds the claimed amount, the claimed amount // proof of work exceeds the claimed amount, the claimed amount
// is a field in the block header which could be forged. This // is a field in the block header which could be forged. This
// check ensures the proof of work is at least the minimum // check ensures the proof of work is at least the minimum
// expected based on elapsed time since the last checkpoint and // expected based on elapsed time since the last checkpoint and
// maximum adjustment allowed by the retarget rules. // maximum adjustment allowed by the retarget rules.
duration := blockHeader.Timestamp.Sub(checkpointTime) duration := blockHeader.Timestamp.Sub(checkpointTime)
requiredTarget := CompactToBig(b.calcEasiestDifficulty( requiredTarget := CompactToBig(b.calcEasiestDifficulty(
checkpointHeader.Bits, duration)) checkpointHeader.Bits, duration))
currentTarget := CompactToBig(blockHeader.Bits) currentTarget := CompactToBig(blockHeader.Bits)
if currentTarget.Cmp(requiredTarget) > 0 { if currentTarget.Cmp(requiredTarget) > 0 {
str := fmt.Sprintf("block target difficulty of %064x "+ str := fmt.Sprintf("block target difficulty of %064x "+
"is too low when compared to the previous "+ "is too low when compared to the previous "+
"checkpoint", currentTarget) "checkpoint", currentTarget)
return RuleError(str) return RuleError(str)
}
} }
} }
@ -172,7 +173,7 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block) error {
// The block has passed all context independent checks and appears sane // The block has passed all context independent checks and appears sane
// enough to potentially accept it into the block chain. // enough to potentially accept it into the block chain.
err = b.maybeAcceptBlock(block) err = b.maybeAcceptBlock(block, fastAdd)
if err != nil { if err != nil {
return err return err
} }

View file

@ -58,7 +58,7 @@ func TestReorganization(t *testing.T) {
btcchain.TstSetCoinbaseMaturity(1) btcchain.TstSetCoinbaseMaturity(1)
for i := 1; i < len(blocks); i++ { for i := 1; i < len(blocks); i++ {
err = chain.ProcessBlock(blocks[i]) err = chain.ProcessBlock(blocks[i], false)
if err != nil { if err != nil {
t.Errorf("ProcessBlock fail on block %v: %v\n", i, err) t.Errorf("ProcessBlock fail on block %v: %v\n", i, err)
return return