並不唯一的交易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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章