From 75569f599f9612a94976d6af15f9751eb4ce41b6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 26 Sep 2016 19:00:28 -0700 Subject: [PATCH] blockchain: enforce CSV soft-fork validation based on versionbits state This commit modifies the existing block validation logic to examine the current version bits state of the CSV soft-fork, enforcing the new validation rules (BIPs 68, 112, and 113) accordingly based on the current `ThesholdState`. --- blockchain/chain.go | 45 +++++++++++++++++-------- blockchain/chain_test.go | 10 +++--- blockchain/thresholdstate.go | 27 ++++++++++++--- blockchain/validate.go | 65 +++++++++++++++++++++++++++++++++++- 4 files changed, 125 insertions(+), 22 deletions(-) diff --git a/blockchain/chain.go b/blockchain/chain.go index 16c54b7b..e53d3f36 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -351,17 +351,15 @@ func (b *BlockChain) CalcSequenceLock(tx *btcutil.Tx, utxoView *UtxoViewpoint, b.chainLock.Lock() defer b.chainLock.Unlock() - return b.calcSequenceLock(tx, utxoView, mempool) + return b.calcSequenceLock(b.bestNode, tx, utxoView, mempool) } // calcSequenceLock computes the relative lock-times for the passed // transaction. See the exported version, CalcSequenceLock for further details. // // This function MUST be called with the chain state lock held (for writes). -func (b *BlockChain) calcSequenceLock(tx *btcutil.Tx, utxoView *UtxoViewpoint, - mempool bool) (*SequenceLock, error) { - - mTx := tx.MsgTx() +func (b *BlockChain) calcSequenceLock(node *blockNode, tx *btcutil.Tx, + utxoView *UtxoViewpoint, mempool bool) (*SequenceLock, error) { // A value of -1 for each relative lock type represents a relative time // lock value that will allow a transaction to be included in a block @@ -370,20 +368,42 @@ func (b *BlockChain) calcSequenceLock(tx *btcutil.Tx, utxoView *UtxoViewpoint, // activated. sequenceLock := &SequenceLock{Seconds: -1, BlockHeight: -1} + // The sequence locks semantics are always active for transactions + // within the mempool. + csvSoftforkActive := mempool + + // If we're performing block validation, then we need to query the BIP9 + // state. + if !csvSoftforkActive { + prevNode, err := b.index.PrevNodeFromNode(node) + if err != nil { + return nil, err + } + + // Obtain the latest BIP9 version bits state for the + // CSV-package soft-fork deployment. The adherence of sequence + // locks depends on the current soft-fork state. + csvState, err := b.deploymentState(prevNode, chaincfg.DeploymentCSV) + if err != nil { + return nil, err + } + csvSoftforkActive = csvState == ThresholdActive + } + // If the transaction's version is less than 2, and BIP 68 has not yet // been activated then sequence locks are disabled. Additionally, // sequence locks don't apply to coinbase transactions Therefore, we // return sequence lock values of -1 indicating that this transaction // can be included within a block at any given height or time. - // TODO(roasbeef): check version bits state or pass as param - // * true should be replaced with a version bits state check - sequenceLockActive := mTx.Version >= 2 && (mempool || true) + mTx := tx.MsgTx() + sequenceLockActive := mTx.Version >= 2 && csvSoftforkActive if !sequenceLockActive || IsCoinBase(tx) { return sequenceLock, nil } - // Grab the next height to use for inputs present in the mempool. - nextHeight := b.BestSnapshot().Height + 1 + // Grab the next height from the PoV of the passed blockNode to use for + // inputs present in the mempool. + nextHeight := node.height + 1 for txInIndex, txIn := range mTx.TxIn { utxo := utxoView.LookupEntry(&txIn.PreviousOutPoint.Hash) @@ -421,9 +441,8 @@ func (b *BlockChain) calcSequenceLock(tx *btcutil.Tx, utxoView *UtxoViewpoint, // compute the past median time for the block prior to // the one which included this referenced output. // TODO: caching should be added to keep this speedy - inputDepth := uint32(b.bestNode.height-inputHeight) + 1 - blockNode, err := b.index.RelativeNode(b.bestNode, - inputDepth) + inputDepth := uint32(node.height-inputHeight) + 1 + blockNode, err := b.index.RelativeNode(node, inputDepth) if err != nil { return sequenceLock, err } diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index 5a68c9fc..3bd4bc3b 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -122,7 +122,7 @@ func TestCalcSequenceLock(t *testing.T) { netParams := &chaincfg.SimNetParams // Create a new database and chain instance to run tests against. - chain, teardownFunc, err := chainSetup("haveblock", netParams) + chain, teardownFunc, err := chainSetup("calcseqlock", netParams) if err != nil { t.Errorf("Failed to setup chain instance: %v", err) return @@ -190,7 +190,8 @@ func TestCalcSequenceLock(t *testing.T) { prevBlock = block } - // Create with all the utxos within the create created above. + // Create a utxo view with all the utxos within the blocks created + // above. utxoView := blockchain.NewUtxoViewpoint() for blockHeight, blockWithMTP := range blocksWithMTP { for _, tx := range blockWithMTP.block.Transactions() { @@ -200,8 +201,9 @@ func TestCalcSequenceLock(t *testing.T) { utxoView.SetBestHash(blocksWithMTP[len(blocksWithMTP)-1].block.Hash()) // The median time calculated from the PoV of the best block in our - // test chain. For unconfirmed inputs, this value will be used since th - // MTP will be calculated from the PoV of the yet-to-be-mined block. + // test chain. For unconfirmed inputs, this value will be used since + // the MTP will be calculated from the PoV of the yet-to-be-mined + // block. nextMedianTime := int64(1401292712) // We'll refer to this utxo within each input in the transactions diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index ff080bdd..cc7fcde1 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -309,14 +309,33 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit // // This function is safe for concurrent access. func (b *BlockChain) ThresholdState(deploymentID uint32) (ThresholdState, error) { + b.chainLock.Lock() + state, err := b.deploymentState(b.bestNode, deploymentID) + b.chainLock.Unlock() + + return state, err +} + +// deploymentState returns the current rule change threshold for a given +// deploymentID. The threshold is evaluated from the point of view of the block +// node passed in as the first argument to this method. +// +// It is important to note that, as the variable name indicates, this function +// expects the block node prior to the block for which the deployment state is +// desired. In other words, the returned deployment state is for the block +// AFTER the passed node. +// +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) deploymentState(prevNode *blockNode, + deploymentID uint32) (ThresholdState, error) { + if deploymentID > uint32(len(b.chainParams.Deployments)) { return ThresholdFailed, DeploymentError(deploymentID) } + deployment := &b.chainParams.Deployments[deploymentID] checker := deploymentChecker{deployment: deployment, chain: b} cache := &b.deploymentCaches[deploymentID] - b.chainLock.Lock() - state, err := b.thresholdState(b.bestNode, checker, cache) - b.chainLock.Unlock() - return state, err + + return b.thresholdState(prevNode, checker, cache) } diff --git a/blockchain/validate.go b/blockchain/validate.go index 7980cc5a..bb291639 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -752,6 +752,27 @@ func (b *BlockChain) checkBlockContext(block *btcutil.Block, prevNode *blockNode fastAdd := flags&BFFastAdd == BFFastAdd if !fastAdd { + // Obtain the latest state of the deployed CSV soft-fork in + // order to properly guard the new validation behavior based on + // the current BIP 9 version bits state. + csvState, err := b.deploymentState(prevNode, chaincfg.DeploymentCSV) + if err != nil { + return err + } + + // Once the CSV soft-fork is fully active, we'll switch to + // using the current median time past of the past block's + // timestamps for all lock-time based checks. + blockTime := header.Timestamp + if csvState == ThresholdActive { + medianTime, err := b.index.CalcPastMedianTime(prevNode) + if err != nil { + return err + } + + blockTime = medianTime + } + // The height of this block is one more than the referenced // previous block. blockHeight := prevNode.height + 1 @@ -759,7 +780,7 @@ func (b *BlockChain) checkBlockContext(block *btcutil.Block, prevNode *blockNode // Ensure all transactions in the block are finalized. for _, tx := range block.Transactions() { if !IsFinalizedTransaction(tx, blockHeight, - header.Timestamp) { + blockTime) { str := fmt.Sprintf("block contains unfinalized "+ "transaction %v", tx.Hash()) @@ -1133,6 +1154,48 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block *btcutil.Block, vi scriptFlags |= txscript.ScriptVerifyCheckLockTimeVerify } + // Enforce CHECKSEQUENCEVERIFY during all block validation checks once + // the soft-fork deployment is fully active. + csvState, err := b.deploymentState(node.parent, chaincfg.DeploymentCSV) + if err != nil { + return err + } + if csvState == ThresholdActive { + // If the CSV soft-fork is now active, then modify the + // scriptFlags to ensure that the CSV op code is properly + // validated during the script checks bleow. + scriptFlags |= txscript.ScriptVerifyCheckSequenceVerify + + // We obtain the MTP of the *previous* block in order to + // determine if transactions in the current block are final. + medianTime, err := b.index.CalcPastMedianTime(node.parent) + if err != nil { + return err + } + + // Additionally, if the CSV soft-fork package is now active, + // then we also enforce the relative sequence number based + // lock-times within the inputs of all transactions in this + // candidate block. + for _, tx := range block.Transactions() { + // A transaction can only be included within a block + // once the sequence locks of *all* its inputs are + // active. + sequenceLock, err := b.calcSequenceLock(node, tx, view, + false) + if err != nil { + return err + } + if !SequenceLockActive(sequenceLock, node.height, + medianTime) { + str := fmt.Sprintf("block contains " + + "transaction whose input sequence " + + "locks are not met") + return ruleError(ErrUnfinalizedTx, str) + } + } + } + // Now that the inexpensive checks are done and have passed, verify the // transactions are actually allowed to spend the coins by running the // expensive ECDSA signature check scripts. Doing this last helps