我们知道,比特币中使用交易ID
(TxID
) 来作为交易在全网的唯一标识。
在此语境下,绝大多数人都认为TxID
一定是全网唯一的。
绝大多数情况是这样。
但事实上,曾经两起出现过在不同区块中的交易的TxID相同的情况,如下所示:
TxID
:e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468block 91,722
: 00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08eblock 91,880
: 00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721TxID
: d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599block 91,812
: 00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2fblock 91,842
: 00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec这两起事件都和区块的coinbase
交易有关。
简单来说:交易的TxID
是由该交易的内容决定的,包括input
, output
等。
coinbase
交易中是没有input
的,其output
也是由矿工的账号决定的。如果两个区块的矿工采用了相同的地址,极有可能出现两个coinbase
交易的内容相同,从而TxID
也相同的情况。
从区块浏览器中查看这两起事件,可以发现区块91,722和91,880中的矿工地址都为1GktTvnY8KGfAS72DhzGYJRyaQNvYrK9Fg
,而区块91,812
和91,842
的矿工地址都为16va6NxJrMGe5d2LP6wUzuVnzBBoKQZKom
. 这也验证了我们的解释。
处理该问题包含了两个方面:
TxID
的coinbase
?TxID
的coinbase
?比特币团队通过了两项BIP
:BIP30和BIP34。前者在2012年3月15日在主网实施,后者在2013年3月24日在主网上完全升级。
BIP30
BIP30
的核心内容如下:
Blocks are not allowed to contain a transaction whose identifier matches that of an earlier, not-fully-spent transaction in the same chain.
翻译成中文就是说:实施BIP30
之后的区块不允许包含和之前某个交易的TxID
相同的交易,除非之前的那个交易的output
都已经被花费过了。否则,该区块就被判定为无效区块。
BIP30
实施的源代码如下所示:
// checkBIP0030 [validate.go]
func (b *BlockChain) checkBIP0030(node *blockNode, block *btcutil.Block, view *UtxoViewpoint) error {
...
fetchSet := make(map[wire.OutPoint]struct{})
for _, tx := range block.Transactions() {
prevOut := wire.OutPoint{Hash: *tx.Hash()}
for txOutIdx := range tx.MsgTx().TxOut {
prevOut.Index = uint32(txOutIdx)
fetchSet[prevOut] = struct{}{}
}
}
err := view.fetchUtxos(b.db, fetchSet)
...
for outpoint := range fetchSet {
utxo := view.LookupEntry(outpoint)
if utxo != nil && !utxo.IsSpent() {
str := fmt.Sprintf("tried to overwrite transaction %v "+
"at block height %d that is not fully spent",
outpoint.Hash, utxo.BlockHeight())
return ruleError(ErrOverwriteTx, str)
}
}
return nil
}
以上的代码实现中,实际上是借助于output
(即:TxID
+index
)来进行检查的。
具体而言,对于当前区块中的所有output
进行UTXO
的检查。只要存在某个UTXO
和该区块中的某个output
相同,说明该UTXO
的TxID
和该output
的TxID
也相同,也即:该output
所在的交易和之前某个交易的TxID
相同。从而检查结果为失败,返回ruleError
错误。
但源码的实现貌似忽略了一种特殊情况。
由于checkBIP0030
的函数实现中,是基于output
(TxID
+index
)进行比较的,考虑一种可能存在的情况:尽管TxID
相同但index
不同。
举例来说:当前区块中某个交易的TxID
和之前某个交易的TxID
相同。当前交易只存在一个output
,之前交易存在两个output
但第一个output
已被花费,第二个output
未被花费。因此当前交易的output
和之前交易的第二个output
就会出现:TxID
相同而index
不同的情况。如下图所示:
而在以上的代码实现中,这种情况应该也会判断该区块为有效区块。
但这种情况基本上是不会存在的,因为TxID
是基于交易内容计算来的。如果TxID
相同,也就默认了交易内容相同,也即拥有相同的output
。因此只要之前交易中有一个output
未被花费,都一定会和当前交易中的某个output
重合,从而被检查出来,相应的区块被判断为无效区块。
BIP34
简单来说,BIP34
要求矿工将该coinbase
所在的区块高度加入到coinbase
的input
的scriptSig
中,从而可以计算出全网唯一的TxID
。
为实现该目的,需要进行三步走:
coinbase
交易中。2)矿工通过在新区块中设置版本为1或者2进行投票。3)在此阶段,版本为1的区块会被接受,版本定义为2但未包含区块高度的区块也会被接受,版本定义为2且包含区块高度的区块也会被接受由于比特币主网中早已完成了BIP34
的软分叉,源代码中只保留了最后的检查,即:版本定义为2且coinbase
中包含区块高度。相应的源代码如下所示:
// checkBlockContext [validate.go]
func (b *BlockChain) checkBlockContext(...) error {
...
if ShouldHaveSerializedBlockHeight(header) &&
blockHeight >= b.chainParams.BIP0034Height {
coinbaseTx := block.Transactions()[0]
err := checkSerializedHeight(coinbaseTx, blockHeight)
if err != nil {
return err
}
}
...
}
此外,需要多说两句的是:BIP34
开启了一种比较优雅的“软分叉”的方式:三阶段软分叉,后面的BIP66
和BIP65
都采用了类似的方式实现了软分叉。
对于已出现的两起相同TxID
的事件,比特币协议中采取“认可”的态度。即认为这两起事件中产生的区块和相应的output
都是合法的。
相应的源代码如下所示:
// checkConnectBlock [validate.go]
func (b *BlockChain) checkConnectBlock(...) error {
...
if !isBIP0030Node(node) && (node.height < b.chainParams.BIP0034Height) {
err := b.checkBIP0030(node, block, view)
if err != nil {
return err
}
}
...
}
其中isBIP0030Node
函数代码如下所示:
// checkConnectBlock [validate.go] -> isBIP0030Node
func isBIP0030Node(node *blockNode) bool {
if node.height == 91842 && node.hash.IsEqual(block91842Hash) {
return true
}
if node.height == 91880 && node.hash.IsEqual(block91880Hash) {
return true
}
return false
}
也即:对于两起事件中的后一个区块(区块91842和91880), 省略对其进行BIP30
的检查。
此外,通过在区块浏览器Blockchair上跟踪两个相关的地址1GktTvnY8KGfAS72DhzGYJRyaQNvYrK9Fg和16va6NxJrMGe5d2LP6wUzuVnzBBoKQZKom,我们发现这两个地址在接收了两次挖矿奖励后,并没有使用过这些奖励。