0%

并不唯一的交易ID

我们知道,比特币中使用交易ID (TxID) 来作为交易在全网的唯一标识。在此语境下,绝大多数人都认为TxID一定是全网唯一的。 绝大多数情况是这样。但事实上,曾经两起出现过在不同区块中的交易的TxID相同的情况。本篇博客即来介绍这两起TxID碰撞的故事。

I. 前言

在比特币系统中,曾经出现过两起在不同交易的TxID相同的情况,如下所示:

  1. TxID:e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468
    • block 91,722: 00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08e
    • block 91,880: 00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721
  2. TxID: d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599
    • block 91,812: 00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f
    • block 91,842: 00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec

II. 解释

这两起事件都和区块的coinbase交易有关。 简单来说:交易的TxID是由该交易的内容决定的,包括input, output等。 coinbase交易中是没有input的,其output也是由矿工的账号决定的。如果两个区块的矿工采用了相同的地址,极有可能出现两个coinbase交易的内容相同,从而TxID也相同的情况。 从区块浏览器中查看这两起事件,可以发现区块91,722和91,880中的矿工地址都为1GktTvnY8KGfAS72DhzGYJRyaQNvYrK9Fg,而区块91,81291,842的矿工地址都为16va6NxJrMGe5d2LP6wUzuVnzBBoKQZKom. 这也验证了我们的解释。

III. 处理

处理该问题包含了两个方面:

  1. 如何让矿工生成不相同TxIDcoinbase
  2. 如何处理已有的两起事件?

A. 如何让矿工生成不相同TxIDcoinbase

比特币团队通过了两项BIPBIP30BIP34。前者在2012年3月15日在主网实施,后者在2013年3月24日在主网上完全升级。

1. 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实施的源代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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相同,说明该UTXOTxID和该outputTxID也相同,也即:该output所在的交易和之前某个交易的TxID相同。从而检查结果为失败,返回ruleError错误。 但源码的实现貌似忽略了一种特殊情况。

1) 特殊情况的考虑

由于checkBIP0030的函数实现中,是基于output (TxID+index)进行比较的,考虑一种可能存在的情况:尽管TxID相同但index不同。

举例来说:当前区块中某个交易的TxID和之前某个交易的TxID相同。当前交易只存在一个output,之前交易存在两个output但第一个output已被花费,第二个output未被花费。因此当前交易的output和之前交易的第二个output就会出现:TxID相同而index不同的情况。如下图所示: 而在以上的代码实现中,这种情况应该也会判断该区块为有效区块。

但这种情况基本上是不会存在的,因为TxID是基于交易内容计算来的。如果TxID相同,也就默认了交易内容相同,也即拥有相同的output。因此只要之前交易中有一个output未被花费,都一定会和当前交易中的某个output重合,从而被检查出来,相应的区块被判断为无效区块。

2. BIP34

简单来说,BIP34要求矿工将该coinbase所在的区块高度加入到coinbaseinputscriptSig中,从而可以计算出全网唯一的TxID

为实现该目的,需要进行三步走:

  1. 启动协议:1)对区块进行版本的定义,旧协议的区块版本定义为1,新协议的区块版本定义为2,且在新协议中需要将区块高度的加入到coinbase交易中。2)矿工通过在新区块中设置版本为1或者2进行投票。3)在此阶段,版本为1的区块会被接受,版本定义为2但未包含区块高度的区块也会被接受,版本定义为2且包含区块高度的区块也会被接受
  2. 75%阶段(当在过去的1000个区块中有超过750个区块的版本标识为2时):版本为1的区块会被接受,版本定义为2且包含区块高度的区块也会被接受,但版本定义为2却未包含区块高度的区块不会被接受
  3. 95%阶段(当在过去的1000个区块中有超过950个区块的版本标识为2时): 只有版本定义为2且包含区块高度的区块会被接受,其他两种区块都不会被接受了。此时便完成了软分叉。

由于比特币主网中早已完成了BIP34的软分叉,源代码中只保留了最后的检查,即:版本定义为2且coinbase中包含区块高度。相应的源代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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开启了一种比较优雅的“软分叉”的方式:三阶段软分叉,后面的BIP66BIP65都采用了类似的方式实现了软分叉。

B. 如何处理已有的两起事件?

对于已出现的两起相同TxID的事件,比特币协议中采取“认可”的态度。即认为这两起事件中产生的区块和相应的output都是合法的。 相应的源代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
// 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函数代码如下所示:
1
2
3
4
5
6
7
8
9
10
// 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上跟踪两个相关的地址1GktTvnY8KGfAS72DhzGYJRyaQNvYrK9Fg16va6NxJrMGe5d2LP6wUzuVnzBBoKQZKom,我们发现这两个地址在接收了两次挖矿奖励后,并没有使用过这些奖励。

参考文献

  1. TXID, https://learnmeabitcoin.com/guide/txid
  2. BIP-0030, https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki
  3. BIP-0034, https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki
  4. "Mastering Bitcoin 2nd", Chapter 10, Soft Fork Signaling with Block Version, BIP-34 Signaling and Activation