是誰控制了比特幣,是你?還是錢包? BTC地址與交易原理大剖析
安比(SECBIT)实验室
2018-09-29 02:58
本文约7152字,阅读全文需要约29分钟
如果你在使用比特幣錢包,那麼這篇文章是為你而寫的。

正文

  • 正文

  • 正文

  • 編者按:本文來自安比實驗室,作者:安比實驗室,經授權發布。

  • 比特幣地址有1 打頭的地址,也有3 打頭的地址,這兩者有什麼區別嗎?

    在哪種情況下,地址上的比特幣會被鎖死?

    到底是誰擁有比特幣的控制權,是你?還是你的錢包?

    如果你在使用比特幣錢包,但卻無法回答上面三個問題,那麼這篇文章是為你而寫。

    安比(SECBIT)實驗室在對數字錢包源碼審計時,發現一個名為pywallet 的比特幣錢包開源庫包含了一個嚴重缺陷。如果向pywallet 生成的OmniLayer 收款地址轉賬,將導致資產永久丟失。

    但是,開源庫pywallet 在生成OmniLayer 錢包地址的時候,誤將地址的前綴寫反了,若干資產被鎖死在無效的地址內!https://github.com/ranaroussi/pywallet/commit/eb784ea4dd62fe2a50e1352e7d24438fc66a4ac0#diff-ca3a8be6f2ab4be3bfd69a49f5f4122a

    文件地址:

    下面是pywallet 相關錯誤代碼截圖:

    文件地址:

    插隊科普一下:比特幣網絡上最常見的地址類型有三種:普通公鑰地址(1-地址),腳本哈希地址(3-地址)和隔離見證地址(bc1-地址),地址類型通過地址的前綴來區分。其中1-地址的前綴為0x00,3-地址的前綴為0x05。

    • 1-地址:這是最常見的比特幣地址,通常用於普通轉賬收款。 1-地址實際上為公鑰Hash的編碼。驗證1-地址的簽名後便可解鎖收款。

    • 3-地址:這個地址為腳本(Script)哈希地址。這類地址實際對應為一段比特幣腳本Hash的編碼。

    • bc1-地址:bech32編碼地址,用於隔離見證交易。

    開源庫pywallet 顛倒了地址前綴,將1-地址錯誤地設置為3-地址。因此原本要轉給1-地址的資產會誤轉入3-地址。當賬戶持有者以1-地址的驗證方式,也就是私鑰簽名去取出資產的時候,區塊鍊網絡卻以3-地址執行腳本的方式去執行驗證,導致用戶無法正常取出資產!

    請慎重使用pywallet 開源庫! !

    真相:比特幣從未真正實現過轉賬功能

    這一點出乎很多人的意料,由於比特幣的實現基於UTXO 模型,與我們直觀理解的賬戶模型不一樣。 zer0to0ne 解釋說,實際上比特幣從未真正實現過通常意義上的轉賬功能。中本聰只給比特幣設計了一系列比特幣腳本操作符和比特幣腳本執行器,而所謂的轉賬過程實際是由一段比特幣腳本鎖定、解鎖過程來模擬。這與日常生活中的賬本概念(或稱之為賬戶模型)不一樣。

    為了便於理解,我們可以把比特幣區塊鏈上的資產交易比喻成將資產鎖進保險箱,只有持有保險箱鑰匙的人(即收款人)才能拿出保險箱中的資產進行交易。舉個例子,如果Alice 要向Bob 支付一筆資產,Alice 將這筆資產鎖進一個保險箱中,只有Bob 才有這個保險箱的鑰匙,即只有Bob才能取出這筆資產。如果Bob 要取出資產,那麼要求Bob 必須同時花掉這筆資產(即鎖入另一個保險箱)。在Bob 沒有取出資產前,資產並不真正屬於Bob。設想如果Bob 丟了鑰匙,那麼將無法再取出資產。換句話說,這筆資產還在保險箱中保存的時候,既不屬於Alice,也不完全屬於Bob。當然,Alice 也可以把資產放入任何人都可以打開的保險箱中,這也被稱之為Anyone-Can-Spend 交易。

    由於比特幣區塊鏈上的收款地址不同,保險箱的類型也有所不同。不同類型的保險箱需要不同類型的鑰匙來開啟。付款人為收款人定制一個保險箱,將資產放入保險箱中並上鎖,再將保險箱丟到公共場所。而保險箱有兩種開啟方法:

    • 若收款人為1-地址,我們稱保險箱為1-類保險箱。而相應的鑰匙必須是指定收款地址對應的私鑰。解鎖保險箱的過程也就是驗證1-地址公鑰以及公鑰對應的數字簽名,這也是我們通常所理解的向普通賬戶地址轉賬的驗證過程。

    • 若收款人為3-地址,我們稱為3-類保險箱。開啟鑰匙必須為一段可以執行的比特幣腳本。解鎖保險箱的過程是:比特幣腳本的Hash值對應到3-地址,同時比特幣腳本執行器運行該腳本後成功返回。也就是說只有擁有腳本原文並可以成功執行的人才可以提取這個保險箱裡的資產。

    回到這一節的問題:為什麼說比特幣從未實現真正意義上的轉賬功能。答案很簡單,因為比特幣系統中根本就不存在賬戶的概念,賬戶之間的轉賬也無從談起。一個人能在未來打開多少個保險箱,也是未知數。

    通過上面的解釋,我們可知:當pywallet 開源庫誤將1-地址識別為3-地址時,就好像將原本的1-類保險箱改造成了3-類保險箱,而賬戶持有者還是拿著1 -類保險箱的鑰匙去解鎖,那麼自然無法打開保險箱。那麼之前zer0to0ne 發現的被誤鎖住的OmniLayer 數字資產是否能恢復?

    是否存在一種可能性,採用1-地址的鑰匙去開啟3-保險箱 ?

    zer0to0ne 接著向我們詳細解釋了兩個重要概念P2PKH(Pay to Public Key Hash) 與P2SH (Pay to Script Hash)的來龍去脈。這兩個名詞分別代表了兩種不同的比特幣交易類型。

    Base58(0x00 + + Checksum)

    下面是zer0to0ne 的精彩技術細節分析

    P2PKH——中本聰的偉大發明

    OP_DUP 

    OP_HASH160 

    Pay to Public Key Hash 顧名思義,是將比特幣放入一個保險箱,鑰匙孔為公鑰Hash(Public Key Hash)。我們最常見到的1-地址本質上就是Public Key Hash 的一種編碼。 1-地址的生成過程也很簡單,將公鑰經過Hash160運算得到Public Key Hash,在Public Key Hash 頭部補上前綴0x00,Hash 尾部補上校驗和,經過Base58便得到了1開頭的比特幣地址。

    OP_EQUALVERIFY 

    OP_CHECKSIG

    我們來看看P2PKH交易類型的保險箱構造過程,Alice發送比特幣給Bob為例:

    付款方Alice 在構造保險箱的時候需要設置一個鎖定腳本:

    (Bob收款地址蘊含的Public Key Hash)

    注:我們可以把這一步理解為Alice 為Bob 定制了一個保險箱,把比特幣放入保險箱並用Bob 的公鑰PubKey Hash上鎖。現在這把鎖除了持有私鑰的Bob,誰都無法打開。

    當Bob 需要花費Alice 給他的比特幣時,需要提供必要的參數:交易簽名+ 公鑰(技術黑話:scriptSig)來開啟保險箱,使得鎖定腳本執行後返回True,這一步通常由錢包自動完成。 我們來看看比特幣節點是如何校驗scriptSig 合法性的。

    (圖片來自Mastering Bitcoin)腳本執行過程如圖所示,Bob將交易簽名後得到的數據(實際上還要包含數據長度信息),真正的scriptSig 應該為,比特幣腳本執行器從PUSH 數據開始,PUSH 操作會讀取第一個字節獲取將要入棧的數據長度信息,然後持續執行比特幣腳本,直到最後執行完畢檢查執行結果。,HASH160彈出棧頂的併計算Hash,將結果壓回棧中,之後使用EQUALVERIFY 彈出Hash對比是否合相等,如果相等則返回True,不相等便標記交易為無效。執行到這一步,暴露了公鑰,確保了簽名者的身份的正確性,但是黑客或礦工可以通過暴露的公鑰構造出一個新的交易替換原始交易,無法保證安全,那麼便需要下一步來保證交易無法偽造。此時棧上還有

    ,執行CHECKSIG,將校驗數字簽名的正確性,確保了簽名者擁有地址對應的私鑰。

    數字簽名除了持有私鑰的人,誰也無法偽造,執行至此,一筆比特幣P2PKH交易已經安全地完成了。

    再解釋一遍:當Bob 要花費Alice 給他的比特幣時,Bob 只有用正確的鑰匙才能打開Alice 留給他的保險箱,把錢放入Bob 新構造的一個保險箱裡。

    這時候一些聰明的讀者會注意到一個細節:如果Bob 取出鑰匙,在還未打開保險箱的時刻,區塊鏈上的任何礦工都能看得見這把鑰匙的形狀,理論上他們是可以立即復制一把鑰匙,把Alice 留給Bob 的保險箱打開並花掉(俗稱Front-running 攻擊)。真的可以這樣做嗎?顯然中本聰考慮了這個問題,這把鑰匙中的交易簽名是Bob 發起的交易的完整簽名。假設Bob 要將Alice 構造的保險箱中的比特幣裝入一個新的保險箱(留給Charlie),這時候Bob 出示的鑰匙包含了Charlie 的公鑰Hash,礦工雖然可以復制Bob 的鑰匙,但是這把鑰匙已經隱藏了下一個新保險箱的關鍵信息,因此礦工無法使用這個複制鑰匙來完成別的動作(無法挪用數字簽名)。

    OP_HASH160

    OP_EQUAL

    P2SH——後中本聰時代的重大創新中本聰設計了一個這麼強大的腳本系統,只用來構造轉賬交易似乎太浪費了,我們試試用其他指令構造一些特別的鎖定腳本,並使用其他方式來解鎖。

    例如我們可以構造一個用Hash 原象(Pre-image)來解鎖交易的腳本:

    這個腳本的含義是:當滿足Hash160(Pre-image)==

    這個條件時,便可成功將腳本解鎖。

    我們繼續通過保險箱的例子來解釋,並給這類保險箱起名為3-類保險箱。現在Alice 給Bob 的比特幣鎖定在一個由上述Hash160保護的保險箱裡,我們姑且稱之為哈希鎖吧。

    這把鎖依然需要正確的形狀才能開啟,但是安全性卻弱很多,缺少數字簽名機制導致鑰匙隱藏的關鍵信息不會隨著Bob 新建的保險箱而變化。任何礦工都能在Bob 亮出鑰匙的一瞬間複製出一摸一樣的鑰匙,搶著去開Alice留給Bob的保險箱(Front-running),將幣轉給另一個人Eve,於是原本屬於Bob 的比特幣會被洗劫一空。

    雖然這個腳本非常不安全,但是它卻有兩個非常神奇的功能:

    1. 交易構造的輸出足夠短,意味著比特幣節點維護的UTXO 緩存佔用空間將會大大減小

    2. Pre-image 總是在交易被花費時作為input 來引用,不會在交易的output 側出現,UTXO依然保持精簡,同時可以把手續費負擔轉嫁給接收方。