上一篇文章我们介绍了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
}
其中的主要步骤是:
- 如果是coinbase交易,则直接返回,因为它没有有效输入;
- 随后开始检查交易的每一项输入,先从utxoset中查找输入的交易是否存在。请注意,在checkConnectBlock()的实现中,调用CheckTransactionInputs()之前已经通过UtxoViewpoint的fetchInputUtxos()方法将区块中所有交易花费的输入都加载到内存中。如果交易的输入不在utxoset中,则它试图花费一个无效的交易或者一个已经花费的交易,将不能通过验证;
- 如果交易花费的是一个coinbase交易,则需要检查该coinbase交易是否已经“成熟”,即当前区块的高度减去coinbase生成的区块是否大于设定的CoinbaseMaturity值,当前该值为100,也就是说,coinbase交易至少要有100个确认后才能被花费,如代码(4)处所示。读者应该注意到,这里只检查了coinbase交易的确认数,而没有检查非coinbase交易的确认数,大家可以想一想为什么?
- 接下来检查交易的输入(可能是coinbase交易或非coinbase交易)是否已经花费,即检查是否存在双重支付,如代码(5)处所示;
- 代码(6)处检查交易花费的utxo的输出币值是否在0 ~ 2100万BTC之间,我们在前面分析的CheckTransactionSanity()的实现中也对交易的输出币值作了相同的检查;
- 代码(7)处检查交易花费的所有utxo的币值总和不超过2100万BTC;
- 代码(8)处计算交易的所有输出的币值总和,它不能大于所花费的utxo币值总和;
- 最后,通过计算交易的总输入币值与总输出币值的差得到交易的费用,费用应该等或者大于零;
可以看出,上述的检查过程均依赖于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)处记录所有交易的序号;
- 遍历除coinbase交易外的其它交易,进而遍历每个交易中的所有输入,如果交易花费的是当前区块中排在前面的某一个交易,则将花费的交易加入到utxoset中,如代码(2)处所示;
- 如果交易的输入utxo已经在utxoset中,则继续遍历剩下的交易输入,如代码(3)处所示;
- 将所有花费的且不在uxtoset中的交易的Hash记录到txNeededSet中,准备在Db中根据Hash查找utxoentry,如代码(4)处所示;
- 调用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
}
其中的主要步骤是:
- 如果交易是coinbase交易,则不用处理其输入,直接调用AddTxOuts将其输出添加到utxoset中;
- 代码(2)处调用utxoentry的SpendOutput()方法将交易花费的utxo设为spent;
- 随后,如果传入的stxos不变nil,则根据花费的utxo构造spentTxOut,并按照交易的输入的顺序将spentTxOut添加到stxos中。可以看出,stxos按序记录了交易中所有花费的utxo(s),并进而按区块中交易的顺序记录了所有交易花费的utxo(s),也就是说,stxos将会按交易及交易输入的顺序记录区块中交易花费的所有utxo(s)。如果区块因分叉而被从主链上移除,stxos中的记录将被加回到utxoset中,后面我们将会看到,stoxs也被存储到数据库中。uxtoentry若被完全花费,即它的所有输出均被花费,它将被从utxoset中移除,如果需要将其恢复成utxoentry,则需要额外记录区块高度height和isCoinBase字段;
- 最后,将交易的所有输出添加到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
}
其主要步骤如下:
- 看欲添加的交易是否已经在utxoset中,如果没有,则新建utxoentry,并将其添加进utxoset中;如果已经存在,则直接更新utxoentry的区块高度;
- 随后更新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
}
其主要步骤是:
- 按顺序遍历交易的所有输入,并计算每个输入对应的交易所在的区块高度和每个输入的Sequence Number里的低16位值,如代码(1)处所示;
- 如果相对锁定时间机制未开启,则不计算该输入的相对锁定时间,如代码(2)处所示;
- 如果Sequence的第22位置位,则将Sequence值解析成相对时间。代码(4)、(5)处计算输入交易所在区块的MTP,代码(6)处计算相对时间,以秒为单位,代码(7)处计算输入对应的“绝对解锁时间”,代码(8)处将交易中的所有输入的“绝对解锁时间”的最大值作为交易的解锁时间;
- 如果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)、(2)处作了基本检查,保证区块的父区块是主链上的“尾节点”,同时区块中花费的交易数量与待记录的spentTxOuts数量一致;
- 紧接着,如果有节点未知的软分叉部署在区块中激活(状态为ThresholdActive或ThresholdLockedIn),则打印告警log;同时,统计区块前100个区块中有未知版本号的区块个数,超过50%时,打印告警log,这是为了提示手动升级节点版本;
- 代码(5)处用新区块的Hash、高度、难度Bits、交易数量、MTP及主链上总的交易数据构造新的主链的BestState;
- 随后开始更新数据库,先将主链的新的BestState,随同总的工作量之和更新到键值“chainstate”中,如代码(6)处所示;
- 然后将区块的block和高度之间的对应关系分别写入Bucket “hashidx” 和Bucket “heightidx”,它们分别记录区块Hash与高度、区块高度与Hash之间的映射关系;
- 接着调用dbPutUtxoView()更新Bucket “utxoset”,删除已经被花费的utxoentry,增加或者更新新的utxoentry;
- 接着调用dbPutSpendJournalEntry向Bucket “spendjournal”添加一条记录,它记录区块Hash与区块花费的交易的对应关系;
- 代码(9)处调用与BlockChain关联的IndexManager的ConnectBlock()接口,来更新Indexers中的记录,当前版本中可以启用AddrIndex和TxIndex,AddrIndex用于索引交易和Bitcoin地址的关系,TxIndex用于索引交易和其所在的区块的关系;
- 然后调用putThresholdCaches将缓存的warningCaches和deploymentCaches更新到数据库中,如代码(10)处所示;
- 在更新完数据库后,开始更新内存中的一些状态。首先将内存缓存的deploymentCaches和warningCaches清空,如代码(11)处所示;
- 然后更新内存中的utxoset,将已经花费的utxo删除,如代码(12)处所示;
- 接着将新的区块添加到BlockChain的索引器中,用于后续查找,如代码(13)处所示;
- 随后将主链的尾节点更新为新的区块节点,并将主链的快照更新,如代码(14)、(15)处所示;
- 最后,向外发出NTBlockConnected事件通知,mempool将更新交易池中的交易,矿工将停止当前“挖矿”过程并开始“求解”下一个区块;
从connectBlock()中我们可以看到,除了区块本身需要持久化外,与区块链相关的状态也以MetaData的形式存入数据库,这些状态包含: utxoset、spendjournal、hashidx、heightidx及threshholdstate等等。到此,我们就完整地分析了区块添加到主链上的全部过程。如果新的区块是扩展了侧链,而且扩展后的侧链的工作量之和大于主链的工作量之和,那么就需要通过reorganizeChain()将侧链变成主链。同时,区块被添加到主链或者由于reorganizeChain()区块从主链上移除时,均需要进一步处理“孤儿池”中的“孤儿”区块,这些过程我们将在下一篇文章《Btcd区块链的构建(五)》中介绍分析。