并不唯一的交易ID

1. 前言

我们知道,比特币中使用交易ID (TxID) 来作为交易在全网的唯一标识。
在此语境下,绝大多数人都认为TxID一定是全网唯一的。

2. 事实

绝大多数情况是这样。
但事实上,曾经两起出现过在不同区块中的交易的TxID相同的情况,如下所示:

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

3. 解释

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

4. 处理

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

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

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

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

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

// 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错误。
但源码的实现貌似忽略了一种特殊情况。

4.1.1.1. 特殊情况的考虑

由于checkBIP0030的函数实现中,是基于output (TxID+index)进行比较的,考虑一种可能存在的情况:尽管TxID相同但index不同。
举例来说:当前区块中某个交易的TxID和之前某个交易的TxID相同。当前交易只存在一个output,之前交易存在两个output但第一个output已被花费,第二个output未被花费。因此当前交易的output和之前交易的第二个output就会出现:TxID相同而index不同的情况。如下图所示:
特殊情况
而在以上的代码实现中,这种情况应该也会判断该区块为有效区块。

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

4.1.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中包含区块高度。相应的源代码如下所示:

// 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都采用了类似的方式实现了软分叉。

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

对于已出现的两起相同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上跟踪两个相关的地址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
发布了53 篇原创文章 · 获赞 24 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/u014633283/article/details/104759834