Btcd区块链的构建(四)

上一篇文章我们介绍了maybeAcceptBlock()中将区块连入主链的主要步骤,其中checkConnectBlock()在区块最终写入主链前作了较为复杂的检查,本文将对它涉及到的CheckTransactionInputs()、UtxoViewpoint的fetchInputUtxos()和connectTransaction()、BlockChain的calcSequenceLock()、SequenceLockActive()等方法进一步展开分析。

//btcd/blockchain/validate.go

func CheckTransactionInputs(tx *btcutil.Tx, txHeight int32, utxoView *UtxoViewpoint, chainParams *chaincfg.Params) (int64, error) {
    // Coinbase transactions have no inputs.
    if IsCoinBase(tx) {                                                        (1)
        return 0, nil
    }

    txHash := tx.Hash()
    var totalSatoshiIn int64
    for txInIndex, txIn := range tx.MsgTx().TxIn {                             (2)
        // Ensure the referenced input transaction is available.
        originTxHash := &txIn.PreviousOutPoint.Hash
        utxoEntry := utxoView.LookupEntry(originTxHash)                        (3)
        if utxoEntry == nil {                                         
            str := fmt.Sprintf("unable to find unspent output "+
                "%v referenced from transaction %s:%d",
                txIn.PreviousOutPoint, tx.Hash(), txInIndex)
            return 0, ruleError(ErrMissingTx, str)
        }

        // Ensure the transaction is not spending coins which have not
        // yet reached the required coinbase maturity.
        if utxoEntry.IsCoinBase() {
            originHeight := utxoEntry.BlockHeight()
            blocksSincePrev := txHeight - originHeight
            coinbaseMaturity := int32(chainParams.CoinbaseMaturity)            (4)
            if blocksSincePrev < coinbaseMaturity {                      
                str := fmt.Sprintf("tried to spend coinbase "+
                    "transaction %v from height %v at "+
                    "height %v before required maturity "+
                    "of %v blocks", originTxHash,
                    originHeight, txHeight,
                    coinbaseMaturity)
                return 0, ruleError(ErrImmatureSpend, str)
            }
        }

        // Ensure the transaction is not double spending coins.
        originTxIndex := txIn.PreviousOutPoint.Index
        if utxoEntry.IsOutputSpent(originTxIndex) {                            (5)
            str := fmt.Sprintf("transaction %s:%d tried to double "+
                "spend output %v", txHash, txInIndex,
                txIn.PreviousOutPoint)
            return 0, ruleError(ErrDoubleSpend, str)
        }

        // Ensure the transaction amounts are in range.  Each of the
        // output values of the input transactions must not be negative
        // or more than the max allowed per transaction.  All amounts in
        // a transaction are in a unit value known as a satoshi.  One
        // bitcoin is a quantity of satoshi as defined by the
        // SatoshiPerBitcoin constant.
        originTxSatoshi := utxoEntry.AmountByIndex(originTxIndex)              (6)
        if originTxSatoshi < 0 {
            str := fmt.Sprintf("transaction output has negative "+
                "value of %v", btcutil.Amount(originTxSatoshi))
            return 0, ruleError(ErrBadTxOutValue, str)
        }
        if originTxSatoshi > btcutil.MaxSatoshi {
            str := fmt.Sprintf("transaction output value of %v is "+
                "higher than max allowed value of %v",
                btcutil.Amount(originTxSatoshi),
                btcutil.MaxSatoshi)
            return 0, ruleError(ErrBadTxOutValue, str)
        }

        // The total of all outputs must not be more than the max
        // allowed per transaction.  Also, we could potentially overflow
        // the accumulator so check for overflow.
        lastSatoshiIn := totalSatoshiIn
        totalSatoshiIn += originTxSatoshi
        if totalSatoshiIn < lastSatoshiIn ||                                    (7)
            totalSatoshiIn > btcutil.MaxSatoshi {
            str := fmt.Sprintf("total value of all transaction "+
                "inputs is %v which is higher than max "+
                "allowed value of %v", totalSatoshiIn,
                btcutil.MaxSatoshi)
            return 0, ruleError(ErrBadTxOutValue, str)
        }
    }

    // Calculate the total output amount for this transaction.  It is safe
    // to ignore overflow and out of range errors here because those error
    // conditions would have already been caught by checkTransactionSanity.
    var totalSatoshiOut int64
    for _, txOut := range tx.MsgTx().TxOut { 
        totalSatoshiOut += txOut.Value                                          (8)
    }

    // Ensure the transaction does not spend more than its inputs.
    if totalSatoshiIn < totalSatoshiOut {                                       (9)
        str := fmt.Sprintf("total value of all transaction inputs for "+
            "transaction %v is %v which is less than the amount "+
            "spent of %v", txHash, totalSatoshiIn, totalSatoshiOut)
        return 0, ruleError(ErrSpendTooHigh, str)
    }

    // NOTE: bitcoind checks if the transaction fees are < 0 here, but that
    // is an impossible condition because of the check above that ensures
    // the inputs are >= the outputs.
    txFeeInSatoshi := totalSatoshiIn - totalSatoshiOut                          (10)
    return txFeeInSatoshi, nil
}

其中的主要步骤是:

  1. 如果是coinbase交易,则直接返回,因为它没有有效输入;
  2. 随后开始检查交易的每一项输入,先从utxoset中查找输入的交易是否存在。请注意,在checkConnectBlock()的实现中,调用CheckTransactionInputs()之前已经通过UtxoViewpoint的fetchInputUtxos()方法将区块中所有交易花费的输入都加载到内存中。如果交易的输入不在utxoset中,则它试图花费一个无效的交易或者一个已经花费的交易,将不能通过验证;
  3. 如果交易花费的是一个coinbase交易,则需要检查该coinbase交易是否已经“成熟”,即当前区块的高度减去coinbase生成的区块是否大于设定的CoinbaseMaturity值,当前该值为100,也就是说,coinbase交易至少要有100个确认后才能被花费,如代码(4)处所示。读者应该注意到,这里只检查了coinbase交易的确认数,而没有检查非coinbase交易的确认数,大家可以想一想为什么?
  4. 接下来检查交易的输入(可能是coinbase交易或非coinbase交易)是否已经花费,即检查是否存在双重支付,如代码(5)处所示;
  5. 代码(6)处检查交易花费的utxo的输出币值是否在0 ~ 2100万BTC之间,我们在前面分析的CheckTransactionSanity()的实现中也对交易的输出币值作了相同的检查;
  6. 代码(7)处检查交易花费的所有utxo的币值总和不超过2100万BTC;
  7. 代码(8)处计算交易的所有输出的币值总和,它不能大于所花费的utxo币值总和;
  8. 最后,通过计算交易的总输入币值与总输出币值的差得到交易的费用,费用应该等或者大于零;

可以看出,上述的检查过程均依赖于utxoset,utxoset中的utxoentry在区块加入主链时写入数据库,当需要访问utxoentry时再从数据库中读出,我们可以从UtxoViewpoint的fetchInputUtxos()方法入手来分析utxo的存取:

//btcd/blockchain/utxoviewpoint.go

// fetchInputUtxos loads utxo details about the input transactions referenced
// by the transactions in the given block into the view from the database as
// needed.  In particular, referenced entries that are earlier in the block are
// added to the view and entries that are already in the view are not modified.
func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *btcutil.Block) error {
    // Build a map of in-flight transactions because some of the inputs in
    // this block could be referencing other transactions earlier in this
    // block which are not yet in the chain.
    txInFlight := map[chainhash.Hash]int{}
    transactions := block.Transactions()
    for i, tx := range transactions {                                              (1)
        txInFlight[*tx.Hash()] = i
    }

    // Loop through all of the transaction inputs (except for the coinbase
    // which has no inputs) collecting them into sets of what is needed and
    // what is already known (in-flight).
    txNeededSet := make(map[chainhash.Hash]struct{})
    for i, tx := range transactions[1:] {
        for _, txIn := range tx.MsgTx().TxIn {
            // It is acceptable for a transaction input to reference
            // the output of another transaction in this block only
            // if the referenced transaction comes before the
            // current one in this block.  Add the outputs of the
            // referenced transaction as available utxos when this
            // is the case.  Otherwise, the utxo details are still
            // needed.
            //
            // NOTE: The >= is correct here because i is one less
            // than the actual position of the transaction within
            // the block due to skipping the coinbase.
            originHash := &txIn.PreviousOutPoint.Hash
            if inFlightIndex, ok := txInFlight[*originHash]; ok &&
                i >= inFlightIndex {

                originTx := transactions[inFlightIndex]
                view.AddTxOuts(originTx, block.Height())                           (2)
                continue
            }

            // Don't request entries that are already in the view
            // from the database.
            if _, ok := view.entries[*originHash]; ok {                            (3)    
                continue
            }

            txNeededSet[*originHash] = struct{}{}                                  (4)
        }
    }

    // Request the input utxos from the database.
    return view.fetchUtxosMain(db, txNeededSet)                                    (5)
}

fetchInputUtxos()将区块中所有交易的输入utxo加载到内存中,其主要步骤为:

  1. 代码(1)处记录所有交易的序号;
  2. 遍历除coinbase交易外的其它交易,进而遍历每个交易中的所有输入,如果交易花费的是当前区块中排在前面的某一个交易,则将花费的交易加入到utxoset中,如代码(2)处所示;
  3. 如果交易的输入utxo已经在utxoset中,则继续遍历剩下的交易输入,如代码(3)处所示;
  4. 将所有花费的且不在uxtoset中的交易的Hash记录到txNeededSet中,准备在Db中根据Hash查找utxoentry,如代码(4)处所示;
  5. 调用fetchUtxosMain从数据库中查询uxtoentry,并加载到utxoset中;

fetchUtxosMain的实现如下:

//btcd/blockchain/utxoviewpoint.go

// fetchUtxosMain fetches unspent transaction output data about the provided
// set of transactions from the point of view of the end of the main chain at
// the time of the call.
//
// Upon completion of this function, the view will contain an entry for each
// requested transaction.  Fully spent transactions, or those which otherwise
// don't exist, will result in a nil entry in the view.
func (view *UtxoViewpoint) fetchUtxosMain(db database.DB, txSet map[chainhash.Hash]struct{}) error {
    // Nothing to do if there are no requested hashes.
    if len(txSet) == 0 {
        return nil
    }

    // Load the unspent transaction output information for the requested set
    // of transactions from the point of view of the end of the main chain.
    //
    // NOTE: Missing entries are not considered an error here and instead
    // will result in nil entries in the view.  This is intentionally done
    // since other code uses the presence of an entry in the store as a way
    // to optimize spend and unspend updates to apply only to the specific
    // utxos that the caller needs access to.
    return db.View(func(dbTx database.Tx) error {
        for hash := range txSet {
            hashCopy := hash
            entry, err := dbFetchUtxoEntry(dbTx, &hashCopy)
            if err != nil {
                return err
            }

            view.entries[hash] = entry
        }

        return nil
    })
}

它的实现比较简单,主要是通过db.View()获取db的只读Transacion,并调用dbFetchUtxoEntry()来执行具体的查找过程:

//btcd/blockchain/chainio.go

// dbFetchUtxoEntry uses an existing database transaction to fetch all unspent
// outputs for the provided Bitcoin transaction hash from the utxo set.
//
// When there is no entry for the provided hash, nil will be returned for the
// both the entry and the error.
func dbFetchUtxoEntry(dbTx database.Tx, hash *chainhash.Hash) (*UtxoEntry, error) {
    // Fetch the unspent transaction output information for the passed
    // transaction hash.  Return now when there is no entry.
    utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName)
    serializedUtxo := utxoBucket.Get(hash[:])

    ......

    // Deserialize the utxo entry and return it.
    entry, err := deserializeUtxoEntry(serializedUtxo)
    ......

    return entry, nil
}

可以看到,所有的utxoentry是存在名为“utxoset”(utxoSetBucketName)的Bucket中,其中的Key为交易的Hash,Value为utxoentry的序列化结果。读者可以回顾《Btcd区块的存取之ffldb》中对DB的操作分析。相应地,当区块写入主链或者从主链中被移除时,节点将通过dbPutUtxoView()来更新Bucket中的utxoentry记录。

接下来,我们开始分析connectTransaction()的实现,它主要是将交易输入花费的utxo标记为spent,同时将交易输出产生的utxo添加到utxoset中。

//btcd/blockchain/utxoviewpoint.go

// connectTransaction updates the view by adding all new utxos created by the
// passed transaction and marking all utxos that the transactions spend as
// spent.  In addition, when the 'stxos' argument is not nil, it will be updated
// to append an entry for each spent txout.  An error will be returned if the
// view does not contain the required utxos.
func (view *UtxoViewpoint) connectTransaction(tx *btcutil.Tx, blockHeight int32, stxos *[]spentTxOut) error {
    // Coinbase transactions don't have any inputs to spend.
    if IsCoinBase(tx) {
        // Add the transaction's outputs as available utxos.
        view.AddTxOuts(tx, blockHeight)                                               (1)
        return nil
    }

    // Spend the referenced utxos by marking them spent in the view and,
    // if a slice was provided for the spent txout details, append an entry
    // to it.
    for _, txIn := range tx.MsgTx().TxIn {
        originIndex := txIn.PreviousOutPoint.Index
        entry := view.entries[txIn.PreviousOutPoint.Hash]

        ......

        entry.SpendOutput(originIndex)                                                (2)

        // Don't create the stxo details if not requested.
        if stxos == nil {
            continue
        }

        // Populate the stxo details using the utxo entry.  When the
        // transaction is fully spent, set the additional stxo fields
        // accordingly since those details will no longer be available
        // in the utxo set.
        var stxo = spentTxOut{                                                        (3)              
            compressed: false,
            version:    entry.Version(),
            amount:     entry.AmountByIndex(originIndex),
            pkScript:   entry.PkScriptByIndex(originIndex),
        }
        if entry.IsFullySpent() {
            stxo.height = entry.BlockHeight()
            stxo.isCoinBase = entry.IsCoinBase()
        }

        // Append the entry to the provided spent txouts slice.
        *stxos = append(*stxos, stxo)                                                 (4)
    }

    // Add the transaction's outputs as available utxos.
    view.AddTxOuts(tx, blockHeight)                                                   (5)
    return nil
}

其中的主要步骤是:

  1. 如果交易是coinbase交易,则不用处理其输入,直接调用AddTxOuts将其输出添加到utxoset中;
  2. 代码(2)处调用utxoentry的SpendOutput()方法将交易花费的utxo设为spent;
  3. 随后,如果传入的stxos不变nil,则根据花费的utxo构造spentTxOut,并按照交易的输入的顺序将spentTxOut添加到stxos中。可以看出,stxos按序记录了交易中所有花费的utxo(s),并进而按区块中交易的顺序记录了所有交易花费的utxo(s),也就是说,stxos将会按交易及交易输入的顺序记录区块中交易花费的所有utxo(s)。如果区块因分叉而被从主链上移除,stxos中的记录将被加回到utxoset中,后面我们将会看到,stoxs也被存储到数据库中。uxtoentry若被完全花费,即它的所有输出均被花费,它将被从utxoset中移除,如果需要将其恢复成utxoentry,则需要额外记录区块高度height和isCoinBase字段;
  4. 最后,将交易的所有输出添加到utxoset中,如代码(5)处所示;

AddTxOuts()的实现如下:

//btcd/blockchain/utxoviewpoint.go

// AddTxOuts adds all outputs in the passed transaction which are not provably
// unspendable to the view.  When the view already has entries for any of the
// outputs, they are simply marked unspent.  All fields will be updated for
// existing entries since it's possible it has changed during a reorg.
func (view *UtxoViewpoint) AddTxOuts(tx *btcutil.Tx, blockHeight int32) {
    // When there are not already any utxos associated with the transaction,
    // add a new entry for it to the view.
    entry := view.LookupEntry(tx.Hash())                                         (1)
    if entry == nil {
        entry = newUtxoEntry(tx.MsgTx().Version, IsCoinBase(tx),
            blockHeight)
        view.entries[*tx.Hash()] = entry                                         (2)
    } else {
        entry.blockHeight = blockHeight                                          (3)
    }
    entry.modified = true

    // Loop all of the transaction outputs and add those which are not
    // provably unspendable.
    for txOutIdx, txOut := range tx.MsgTx().TxOut {
        if txscript.IsUnspendable(txOut.PkScript) {
            continue
        }

        // Update existing entries.  All fields are updated because it's
        // possible (although extremely unlikely) that the existing
        // entry is being replaced by a different transaction with the
        // same hash.  This is allowed so long as the previous
        // transaction is fully spent.
        if output, ok := entry.sparseOutputs[uint32(txOutIdx)]; ok {             (4)
            output.spent = false
            output.compressed = false
            output.amount = txOut.Value
            output.pkScript = txOut.PkScript
            continue
        }

        // Add the unspent transaction output.
        entry.sparseOutputs[uint32(txOutIdx)] = &utxoOutput{                     (5)
            spent:      false,
            compressed: false,
            amount:     txOut.Value,
            pkScript:   txOut.PkScript,
        }
    }
    return
}

其主要步骤如下:

  1. 看欲添加的交易是否已经在utxoset中,如果没有,则新建utxoentry,并将其添加进utxoset中;如果已经存在,则直接更新utxoentry的区块高度;
  2. 随后更新utxoentry中的sparseOutputs,如果交易的输出已经在utxoentry的sparseOutputs记录中,则直接更新utxoOutput的各字段;如果交易输出不在sparseOutputs,则新建一个记录项;

在connectTransaction()中,主要通过UtxoEntry的SpendOutput()方法将交易的输入设为了已花费,通过UtxoViewpoint的AddTxOuts()方法将交易的输出添加到utxoset中。可以看到,主链上区块的变化将直接导致utxoset的变化。前面我们提到过,区块链的一致性实际上是主链与utxoset状态的一致,它们之间的关系示意如下图所示:

图中,红色表示已经花费的交易,绿色表示还未花费的交易。到此,我们就完整地了解了checkConnectBlock()中验证区块中交易的主要过程:

  • 首先,根据BIP30的建议,检查区块中是否有重复交易;
  • 随后,将区块中的交易输入引用的utxo从DB加载到内存中;
  • 然后,检查区块中所有交易脚本中(包括P2SH脚本)中操作符的个数是否超过限制;
  • 接着,按区块中交易的顺序,对交易逐个进行双重支持检查,并将交易的输入引用的utxo设为spent,将交易的输出添加到utxoset中,并计算所有交易的费用之和;
  • 检查coinbase的输出是否超过网络预期的奖励与交易费用之和;
  • 如果CSV已经部署,则还要检查各个交易的LockTime是否已经解锁,LockTime小于区块高度或者MTP时间的交易不能被接受;
  • 最后,通过脚本执行引擎运行脚本并检验ECDSA;

其中计算BIP部署状态的deploymentState()方法将在后文中介绍“软分叉”时专门分析,接下来,我们进一步分析与LockTime检查相关的calcSequenceLock()和SequenceLockActive()等方法。根据BIP68的定义,TxIn中的32bit的Sequence Number可以解释成相对锁定时间,也可以解释成相对锁定高度,它的编码规则如下图所示:

Sequence中的第31位表明相对锁定时间机制未开启;第0~15位表示Sequence的有效值,如果第22位为1则该值表示相对锁定时间,为0则该值表示相对锁定高度。如果Sequence的低16位表示相对时间,则其精度为512s,即Sequence表示的时间间隔是 (Sequence & 0x0000FFFF) * 512 (s)。接下来,我们通过calcSequenceLock()来了解具体的实现:

//btcd/blockchain/chain.go

// 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(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
    // at any given height or time. This value is returned as the relative
    // lock time in the case that BIP 68 is disabled, or has not yet been
    // activated.
    sequenceLock := &SequenceLock{Seconds: -1, BlockHeight: -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)
        ......

        // If the input height is set to the mempool height, then we
        // assume the transaction makes it into the next block when
        // evaluating its sequence blocks.
        inputHeight := utxo.BlockHeight()
        if inputHeight == 0x7fffffff {
            inputHeight = nextHeight
        }

        // Given a sequence number, we apply the relative time lock
        // mask in order to obtain the time lock delta required before
        // this input can be spent.
        sequenceNum := txIn.Sequence
        relativeLock := int64(sequenceNum & wire.SequenceLockTimeMask)                         (1)

        switch {
        // Relative time locks are disabled for this input, so we can
        // skip any further calculation.
        case sequenceNum&wire.SequenceLockTimeDisabled == wire.SequenceLockTimeDisabled:       (2)
            continue
        case sequenceNum&wire.SequenceLockTimeIsSeconds == wire.SequenceLockTimeIsSeconds:     (3)
            // This input requires a relative time lock expressed
            // in seconds before it can be spent. Therefore, we
            // need to query for the block prior to the one in
            // which this input was included within so we can
            // 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(node.height-inputHeight) + 1
            blockNode, err := b.index.RelativeNode(node, inputDepth)                           (4)
            ......

            // With all the necessary block headers loaded into
            // memory, we can now finally calculate the MTP of the
            // block prior to the one which included the output
            // being spent.
            medianTime, err := b.index.CalcPastMedianTime(blockNode)                           (5)
            ......

            // Time based relative time-locks as defined by BIP 68
            // have a time granularity of RelativeLockSeconds, so
            // we shift left by this amount to convert to the
            // proper relative time-lock. We also subtract one from
            // the relative lock to maintain the original lockTime
            // semantics.
            timeLockSeconds := (relativeLock << wire.SequenceLockTimeGranularity) - 1          (6)
            timeLock := medianTime.Unix() + timeLockSeconds                                    (7)                          
            if timeLock > sequenceLock.Seconds {
                sequenceLock.Seconds = timeLock                                                (8)
            }
        default:
            // The relative lock-time for this input is expressed
            // in blocks so we calculate the relative offset from
            // the input's height as its converted absolute
            // lock-time. We subtract one from the relative lock in
            // order to maintain the original lockTime semantics.
            blockHeight := inputHeight + int32(relativeLock-1)                                 (9)
            if blockHeight > sequenceLock.BlockHeight {
                sequenceLock.BlockHeight = blockHeight                                         (10)
            }
        }
    }

    return sequenceLock, nil
}

其主要步骤是:

  1. 按顺序遍历交易的所有输入,并计算每个输入对应的交易所在的区块高度和每个输入的Sequence Number里的低16位值,如代码(1)处所示;
  2. 如果相对锁定时间机制未开启,则不计算该输入的相对锁定时间,如代码(2)处所示;
  3. 如果Sequence的第22位置位,则将Sequence值解析成相对时间。代码(4)、(5)处计算输入交易所在区块的MTP,代码(6)处计算相对时间,以秒为单位,代码(7)处计算输入对应的“绝对解锁时间”,代码(8)处将交易中的所有输入的“绝对解锁时间”的最大值作为交易的解锁时间;
  4. 如果Sequence的第22位未置位,则将Sequence值解析成相对高度,代码(9)处计算“绝对解锁高度”,代码(10)处将交易中的所有输入的“绝对解锁高度”的最大值作为交易的解锁高度;

在checkConnectBlock()中,通过calcSequenceLock()计算出区块中交易的解锁时间后,就调用SequenceLockActive()来判断交易是否能被打包进当前区块:

//btcd/blockchain/validate.go

// SequenceLockActive determines if a transaction's sequence locks have been
// met, meaning that all the inputs of a given transaction have reached a
// height or time sufficient for their relative lock-time maturity.
func SequenceLockActive(sequenceLock *SequenceLock, blockHeight int32,
    medianTimePast time.Time) bool {

    // If either the seconds, or height relative-lock time has not yet
    // reached, then the transaction is not yet mature according to its
    // sequence locks.
    if sequenceLock.Seconds >= medianTimePast.Unix() ||
        sequenceLock.BlockHeight >= blockHeight {
        return false
    }

    return true
}

可以看到,只有当交易的“绝对解锁时间”和“绝对解锁高度”均小于当前区块的MTP和高度时,交易的“sequence lock”才算“解锁”,也就表明,交易输入花费的所有交易均满足了一定“成熟度”要求。如果SequenceLock中的高度和时间均为-1,则表明交易可以被打包进任何区块中。值得注意的是,与交易中的LockTime不同,LockTime是直接指定了交易能被打包的最小时间或高度,而交易输入中的Sequence代表的“相对锁定时间”或“相对锁定高度”指定了交易花费的其它交易必须满足的“解锁”时间或高度。例如,如果LockTime值为10000,则交易只能被打包进10001及以后的区块中,如果该交易的输入交易所在的区块高度为9999,且Sequence指定的相对高度为100,则该交易只能被打包进10098及以后的区块中。回顾我们之前的分析,checkBlockContext()中调用IsFinalizedTransaction()对交易的LockTime进行了检查,在checkConnectBlock()中如果BIP68已经部署,又通过SequenceLockActive()对交易输入的Sequnece表示的“相对锁定时间”或“相对锁定高度”进行了检查,这是为了兼容两种锁定时间。

checkConnectBlock()中的各项检查通过通过后,节点会调用connectBlock()将区块相关状态写入数据库。

//btcd/blockchain/chain.go

// connectBlock handles connecting the passed node/block to the end of the main
// (best) chain.
//
// This passed utxo view must have all referenced txos the block spends marked
// as spent and all of the new txos the block creates added to it.  In addition,
// the passed stxos slice must be populated with all of the information for the
// spent txos.  This approach is used because the connection validation that
// must happen prior to calling this function requires the same details, so
// it would be inefficient to repeat it.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) connectBlock(node *blockNode, block *btcutil.Block, view *UtxoViewpoint, stxos []spentTxOut) error {
    // Make sure it's extending the end of the best chain.
    prevHash := &block.MsgBlock().Header.PrevBlock
    if !prevHash.IsEqual(&b.bestNode.hash) {                                         (1)
        return AssertError("connectBlock must be called with a block " +
            "that extends the main chain")
    }

    // Sanity check the correct number of stxos are provided.
    if len(stxos) != countSpentOutputs(block) {                                      (2)
        return AssertError("connectBlock called with inconsistent " +
            "spent transaction out information")
    }

    // No warnings about unknown rules or versions until the chain is
    // current.
    if b.isCurrent() {
        // Warn if any unknown new rules are either about to activate or
        // have already been activated.
        if err := b.warnUnknownRuleActivations(node); err != nil {                   (3)
            return err
        }

        // Warn if a high enough percentage of the last blocks have
        // unexpected versions.
        if err := b.warnUnknownVersions(node); err != nil {                          (4)
            return err
        }
    }

    // Calculate the median time for the block.
    medianTime, err := b.index.CalcPastMedianTime(node)
    if err != nil {
        return err
    }

    // Generate a new best state snapshot that will be used to update the
    // database and later memory if all database updates are successful.
    b.stateLock.RLock()
    curTotalTxns := b.stateSnapshot.TotalTxns
    b.stateLock.RUnlock()
    numTxns := uint64(len(block.MsgBlock().Transactions))
    blockSize := uint64(block.MsgBlock().SerializeSize())
    state := newBestState(node, blockSize, numTxns, curTotalTxns+numTxns,            (5)
        medianTime)

    // Atomically insert info into the database.
    err = b.db.Update(func(dbTx database.Tx) error {
        // Update best block state.
        err := dbPutBestState(dbTx, state, node.workSum)                             (6)
        if err != nil {
            return err
        }

        // Add the block hash and height to the block index which tracks
        // the main chain.
        err = dbPutBlockIndex(dbTx, block.Hash(), node.height)                       (7)
        if err != nil {
            return err
        }

        // Update the utxo set using the state of the utxo view.  This
        // entails removing all of the utxos spent and adding the new
        // ones created by the block.
        err = dbPutUtxoView(dbTx, view)
        if err != nil {
            return err
        }

        // Update the transaction spend journal by adding a record for
        // the block that contains all txos spent by it.
        err = dbPutSpendJournalEntry(dbTx, block.Hash(), stxos)                      (8)
        if err != nil {
            return err
        }

        // Allow the index manager to call each of the currently active
        // optional indexes with the block being connected so they can
        // update themselves accordingly.
        if b.indexManager != nil {
            err := b.indexManager.ConnectBlock(dbTx, block, view)                    (9)
            if err != nil {
                return err
            }
        }

        // Update the cached threshold states in the database as needed.
        return b.putThresholdCaches(dbTx)                                            (10)
    })
    if err != nil {
        return err
    }

    // Mark all modified entries in the threshold caches as flushed now that
    // they have been committed to the database.
    b.markThresholdCachesFlushed()                                                   (11)

    // Prune fully spent entries and mark all entries in the view unmodified
    // now that the modifications have been committed to the database.
    view.commit()                                                                    (12)

    // Add the new node to the memory main chain indices for faster lookups.
    node.inMainChain = true
    b.index.AddNode(node)                                                            (13)

    // This node is now the end of the best chain.
    b.bestNode = node                                                                (14)

    // Update the state for the best block.  Notice how this replaces the
    // entire struct instead of updating the existing one.  This effectively
    // allows the old version to act as a snapshot which callers can use
    // freely without needing to hold a lock for the duration.  See the
    // comments on the state variable for more details.
    b.stateLock.Lock()
    b.stateSnapshot = state                                                          (15)
    b.stateLock.Unlock()

    // Notify the caller that the block was connected to the main chain.
    // The caller would typically want to react with actions such as
    // updating wallets.
    b.chainLock.Unlock()
    b.sendNotification(NTBlockConnected, block)                                      (16)
    b.chainLock.Lock()

    return nil
}

在maybeAcceptBlock()中我们分析过,调用connectBestChain()将区块连入区块链之前,区块本身就已经写入区块文件了,所以connectBlock()并不负责将区块持久化,而是将区块链的最新状态更新到数据库中,其具体实现是:

  1. 代码(1)、(2)处作了基本检查,保证区块的父区块是主链上的“尾节点”,同时区块中花费的交易数量与待记录的spentTxOuts数量一致;
  2. 紧接着,如果有节点未知的软分叉部署在区块中激活(状态为ThresholdActive或ThresholdLockedIn),则打印告警log;同时,统计区块前100个区块中有未知版本号的区块个数,超过50%时,打印告警log,这是为了提示手动升级节点版本;
  3. 代码(5)处用新区块的Hash、高度、难度Bits、交易数量、MTP及主链上总的交易数据构造新的主链的BestState;
  4. 随后开始更新数据库,先将主链的新的BestState,随同总的工作量之和更新到键值“chainstate”中,如代码(6)处所示;
  5. 然后将区块的block和高度之间的对应关系分别写入Bucket “hashidx” 和Bucket “heightidx”,它们分别记录区块Hash与高度、区块高度与Hash之间的映射关系;
  6. 接着调用dbPutUtxoView()更新Bucket “utxoset”,删除已经被花费的utxoentry,增加或者更新新的utxoentry;
  7. 接着调用dbPutSpendJournalEntry向Bucket “spendjournal”添加一条记录,它记录区块Hash与区块花费的交易的对应关系;
  8. 代码(9)处调用与BlockChain关联的IndexManager的ConnectBlock()接口,来更新Indexers中的记录,当前版本中可以启用AddrIndex和TxIndex,AddrIndex用于索引交易和Bitcoin地址的关系,TxIndex用于索引交易和其所在的区块的关系;
  9. 然后调用putThresholdCaches将缓存的warningCaches和deploymentCaches更新到数据库中,如代码(10)处所示;
  10. 在更新完数据库后,开始更新内存中的一些状态。首先将内存缓存的deploymentCaches和warningCaches清空,如代码(11)处所示;
  11. 然后更新内存中的utxoset,将已经花费的utxo删除,如代码(12)处所示;
  12. 接着将新的区块添加到BlockChain的索引器中,用于后续查找,如代码(13)处所示;
  13. 随后将主链的尾节点更新为新的区块节点,并将主链的快照更新,如代码(14)、(15)处所示;
  14. 最后,向外发出NTBlockConnected事件通知,mempool将更新交易池中的交易,矿工将停止当前“挖矿”过程并开始“求解”下一个区块;

从connectBlock()中我们可以看到,除了区块本身需要持久化外,与区块链相关的状态也以MetaData的形式存入数据库,这些状态包含: utxoset、spendjournal、hashidx、heightidx及threshholdstate等等。到此,我们就完整地分析了区块添加到主链上的全部过程。如果新的区块是扩展了侧链,而且扩展后的侧链的工作量之和大于主链的工作量之和,那么就需要通过reorganizeChain()将侧链变成主链。同时,区块被添加到主链或者由于reorganizeChain()区块从主链上移除时,均需要进一步处理“孤儿池”中的“孤儿”区块,这些过程我们将在下一篇文章《Btcd区块链的构建(五)》中介绍分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容