文章目錄
1. 前言
我們知道,比特幣中使用交易ID
(TxID
) 來作爲交易在全網的唯一標識。
在此語境下,絕大多數人都認爲TxID
一定是全網唯一的。
2. 事實
絕大多數情況是這樣。
但事實上,曾經兩起出現過在不同區塊中的交易的TxID相同的情況,如下所示:
TxID
:e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468
1)block 91,722
: 00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08e
2)block 91,880
: 00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721TxID
: 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,812
和91,842
的礦工地址都爲16va6NxJrMGe5d2LP6wUzuVnzBBoKQZKom
. 這也驗證了我們的解釋。
4. 處理
處理該問題包含了兩個方面:
- 如何讓礦工生成不相同
TxID
的coinbase
? - 如何處理已有的兩起事件?
4.1. 如何讓礦工生成不相同TxID
的coinbase
?
比特幣團隊通過了兩項BIP
:BIP30和BIP34。前者在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
相同,說明該UTXO
的TxID
和該output
的TxID
也相同,也即:該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
所在的區塊高度加入到coinbase
的input
的scriptSig
中,從而可以計算出全網唯一的TxID
。
爲實現該目的,需要進行三步走:
- 啓動協議:1)對區塊進行版本的定義,舊協議的區塊版本定義爲1,新協議的區塊版本定義爲2,且在新協議中需要將區塊高度的加入到
coinbase
交易中。2)礦工通過在新區塊中設置版本爲1或者2進行投票。3)在此階段,版本爲1的區塊會被接受,版本定義爲2但未包含區塊高度的區塊也會被接受,版本定義爲2且包含區塊高度的區塊也會被接受 - 75%階段(當在過去的1000個區塊中有超過750個區塊的版本標識爲2時):版本爲1的區塊會被接受,版本定義爲2且包含區塊高度的區塊也會被接受,但版本定義爲2卻未包含區塊高度的區塊不會被接受
- 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
開啓了一種比較優雅的“軟分叉”的方式:三階段軟分叉,後面的BIP66
和BIP65
都採用了類似的方式實現了軟分叉。
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上跟蹤兩個相關的地址1GktTvnY8KGfAS72DhzGYJRyaQNvYrK9Fg和16va6NxJrMGe5d2LP6wUzuVnzBBoKQZKom,我們發現這兩個地址在接收了兩次挖礦獎勵後,並沒有使用過這些獎勵。
參考文獻
- TXID, https://learnmeabitcoin.com/guide/txid
- BIP-0030, https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki
- BIP-0034, https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki
- “Mastering Bitcoin 2nd”, Chapter 10, Soft Fork Signaling with Block Version, BIP-34 Signaling and Activation