1. 程式人生 > 程式設計 >區塊鏈技術 -- 比特幣隔離見證地址 與 延展性攻擊

區塊鏈技術 -- 比特幣隔離見證地址 與 延展性攻擊

作者:林冠巨集 / 指尖下的幽靈。轉載者,請: 務必標明出處。

掘金:juejin.im/user/587f0d…

部落格:www.cnblogs.com/linguanh/

GitHub : github.com/af913337456…

騰訊雲專欄: cloud.tencent.com/developer/u…


提示:閱讀本文需要一定的比特幣技術基礎知識

目錄

  • 背景簡介
  • 協議
  • 隔離見證地址的生成
  • 擴容原理
  • 交易的驗籤
  • 延展性攻擊
    • 場景
    • 疑問與解答
    • 修改S的程式碼

背景簡介

隔離見證源於比特幣的一次升級,這次升級涉及到了共識的層面,它支援多了一種交易模式 --- 隔離見證交易,也因此導致了一次軟分叉。

它出現背景是為了解決下面兩個問題

  1. 因為橢圓曲線簽名演演算法ECDSA的漏洞導致的比特幣“延展性攻擊”問題;

  2. 在一定程度上達到比特幣區塊擴容的目的。

協議

要注意,“隔離見證地址” 是比特幣 “隔離見證體系” 中的一部分,整個隔離見證體系是由多個部分組成的。完整體系的介紹在比特幣的改進協議(BIP)裡面,主要由下面的協議檔案共同參與:

  1. BIP-141,連結是:github.com/bitcoin/bip… ,141對隔離見證做了詳細的介紹,包含其定義和用途;

  2. BIP-143,連結是:github.com/bitcoin/bip… ,143對版本號為0的隔離見證,在交易中的,其簽名,和驗籤的整體流程做了詳細的介紹;

  3. BIP-144,連結是:github.com/bitcoin/bip… ,144 對隔離見證在點對點(P2P)網路中,是如何被操作參與的,做了詳細的介紹;

  4. BIP-173,連結是:github.com/bitcoin/bip… ,173裡面對隔離見證地址做了介紹,包括它是使用什麼的編碼格式,校驗碼是怎樣的等等。

其他的更多關於隔離見證的官方介紹,可以自行檢視完整的所有改進協議,連結是:github.com/bitcoin/bip…

地址的生成

下面我們來認識下隔離見證地址是如何生成的。 比特幣的地址生成步驟,都是從公鑰開始進行的,需要經過多個位元組拼湊再進行hash演演算法編碼的過程。同樣地,隔離見證地址的生成流程也是類似的,參照BIP-173協議,我們可以總結出它的生成流程:

  1. 準備好比特幣中,解鎖指令碼中的16進位制雜湊位元組流,目前主要有兩種,分別是P2WPKH 和 P2WSH。這兩種指令碼它們最明顯的區別是:P2WPKH 中的雜湊佔了20個位元組,而P2WSH中的雜湊是32個位元組,對應的指令碼結構分別是:
  • P2WPKH:OP_0 <20-byte雜湊>
  • P2WSH:OP_HASH160 <32-byte 雜湊> OP_EQUAL
  1. 選擇好不同比特幣網路所對應的“hrp”字串,分別有,主網:“bc”,測試網路:“tb”,私人regtest網路:“bcrt”。這些資訊定義在原始碼中的配置檔案裡面,比如Go版本的路徑是:github.com\btcsuite\btcd\chaincfg\params.go

  2. 將步驟1的位元組流使用5位一個位元組進行編碼,原本的是8位一個位元組,結果設為 B;

  3. 將0x00位元組新增到 B前面,結果設為 C;

  4. 使用“bech32”的生成校驗碼演演算法對hrp 和 C 的位元組流生成校驗碼 D;

  5. 將D新增到C後面,結果設為 E ;

  6. 組裝:hrp + "1" + E結合編碼表的對映字串,得到地址。

其中,步驟一雖然不是公鑰直接參與生成,但其中的雜湊資料也是由公鑰經過演變來的。第七步驟的“bech32”編碼表字元組合是:qpzry9x8gf2tvdw0s3jn54khce6mua7l。因為不同的指令碼中的雜湊結構的位元組數不一樣,所以結果也是不一樣的。下圖是上面生成步驟對應的流程圖:

地址生成的程式碼實現可以直接使用btcd原始碼中提供的函式,如下圖所示:

擴容原理

在比特幣交易的一般打包流程裡,是會把每筆交易的簽名資料也包含進去的,如下圖所示。

同時我們知道,一個區塊所能容納的資料量大小是有限的,意味著一個區塊所能打包的交易數也是有限的,如果我們能夠想辦法把交易裡面的資料量減少,那麼就能間接地達到了區塊擴容的目的。

而隔離見證的本質含義就是在區塊打包交易的時候,不打包簽名的資料,把簽名資料放到另外一個地方去。如果是這樣,那麼怎樣驗證交易的簽名是否正確,以保證資料沒被篡改?

隔離見證交易的驗籤

做法是這樣的,在將要發起交易,構造指令碼的輸入Vin時,把輸入的解鎖指令碼資料放置到另外一個欄位處,原始碼中,這個欄位的名稱是:Witness。當交易被髮送到交易節點的時候,節點程式碼中會從這個欄位裡面提取出資料恢復出解鎖指令碼,接著在節點進行驗籤操作,驗籤通過後,簽名的資料就不再被打包進區塊裡面,後續要消費這邊交易的時候,解鎖指令碼也是從Witness欄位中恢復。

而不是隔離見證的交易型別,Witness欄位是沒有資料的,解鎖指令碼的資料被攜帶在簽名裡面,導致要恢復,只能從簽名中恢復,意味著簽名資料必須要被打包在區塊裡面。下面是Vin的和上述相關的結構:

type Vin struct {
    // 省略無關欄位
    ScriptSig *ScriptSig  `json:"scriptSig"`
    Witness   []string   `json:"txinwitness"`
}
複製程式碼

ScriptSig 放置的就是簽名資料Witness 放置的就是解鎖指令碼資料。原始碼中的指令碼恢復操作函式在比特幣操作碼虛擬機器器部分,如下圖所示。

延展性攻擊

上面說到了隔離見證的出現背景之一是為瞭解決“延展性攻擊”,這個攻擊又被稱為“可鍛性攻擊”,英文名稱:transaction malleability attack。

2014年,MT.GOX交易所(門頭溝)發生的85萬個比特幣丟失事件,事後當事人把此次事故,歸罪於比特幣的交易延展性攻擊。下面我們來看看它是怎樣達到攻擊目的的。 在比特幣中,區分一筆交易的憑據是交易的id,即TxId。如果兩筆交易的TxId不一樣,那麼會被認為是兩筆不同的交易。在我們日常使用區塊鏈瀏覽器去檢視交易的時候,也會根據TxId去查詢交易,兩個不同的TxId,就肯定不是同一筆交易。

現在假設這麼一個場景:

A 使用橢圓曲線簽名演演算法ECDSA傳送了一筆交易T去比特幣節點N,此時N會根據T的id校驗T是否已經存在交易池裡面及其相關邏輯,如果發現已經存在且不滿足替換條件,那麼就會返回錯誤資訊給 A。如果不存在,就會被放置到交易池裡面,等待被處理,同時返回了 T 的 id 給A。 此時假設 B 自己編寫了個程式,對比特幣節點交易池進行了監聽,發現了交易 T,便將 T 提取了出來,並獲取了 Vin 結構中的ScriptSig 欄位。 上面場景對應的流程圖如下圖所示。

B 獲取了T 的 ScriptSig 後,要做什麼呢?首先可以肯定的是,B 肯定不能篡改 T 的資料,那麼他要怎樣進行“延展性攻擊”

B 接下來這樣做:

B 將ScriptSig 使用ECDSA的相關程式碼恢復出簽名資訊的 R 和 S 整形大數。然後根據橢圓曲線加密演演算法的漏洞修改了 S,再重新生成簽名的 ScriptSig,然後將這筆交易重放。注意這裡!重放的時候,節點N 會重新根據整筆交易的資訊,包含 ScriptSig 生成一個TxId_2,但是因為 ScriptSig 被改過了,所以導致了 TxId不一樣,因為TxId 是使用hash 演演算法生成的,hash 演演算法在不同的輸入情況下,輸出是肯定不一樣的。

B 操作部分的描述留下了兩個疑問:
  1. 相同交易的發到鏈上,輸入部分(UTXO)不會被檢測嗎?

  2. 為什麼 ScriptSig 被改過了,還能驗籤成功?

回答:
  1. 因為比特幣的賬戶模型是基於UTXO的,如果某筆 UTXO 還沒被消費,那麼它可以被嘗試雙花。而當它已經被消費了,此時再消費,就會出錯。在上面的例子中,交易T及其被 B 修改過的複製版本中的UTXO,雖然是一樣的,但是它們都是處於沒被花費的,所以可以被多次引用。即檢測,只檢測是否已被上鍊花費了。

  2. 之所以能驗籤成功,是因為橢圓曲線的簽名演演算法ECDSA中,對於 S 和 R 可以驗籤,負S 和 R 也可以驗籤成功。但一個負S 卻導致了 TxId的不一樣。 B 的攻擊流程圖如下圖所示:

至此,節點中同樣的交易內容,卻出現了不同的 TxId,交易的手續費和收款人完全相同。意味著,這兩筆交易,具備都有被打包的可能,當其中一筆被打包了,另一筆就不會被打包,因為其中涉及的UTXO已經被花費了。

此時假設 A 是某交易所,而它此時正在幫使用者提現,而B 是這個使用者,A 在對賬的時候,會根據自己傳送交易時候拿到的TxId去核對,但它並不知道此時還有一個 TxId_2 是做了同樣的提現操作。然後此時 TxId_2 被打包了,B 作為攻擊者,發現自己攻擊成功了,就會去向交易所說,自己的提現怎麼還沒成功。而A 會進行核實,發現自己的TxId失敗了,然後就會重新給使用者發起提現操作。 至此,B 收到了兩筆或多筆鏈上的轉賬。

上面的整個過程,就是一次“延展性攻擊”。

修改S的程式碼

在程式碼中的具體實現也是很簡單的,只需要新增一行程式碼就能修改簽名中的S,使得驗籤依然有效。下圖是我實現的一個可行的函式,為了不被濫用,關鍵行數已打碼

修改簽名中的S 的程式碼: