3.2 比特幣的腳本

每個交易輸出不僅確定了一個公鑰,其實同時指定了一個腳本。那腳本是什麼?為什麼我們要用一個腳本?在這一節我們要學習比特幣的工作控制語言,也叫腳本。之後,我們就會懂得為什麼要用一個腳本,而不是簡單地分配一個公鑰。

最常見的比特幣交易,就是通過某人的簽名去取得他在前一筆交易中獲得的資金。這種情況下,我們希望交易的輸出包含這樣的信息:「憑借地址X的所有者的簽名,才可以獲得這筆資金。」我們知道地址其實就是一個公鑰的哈希值,所以僅僅說地址X並沒有告訴我們公鑰在哪裡,也沒有給我們一個檢查簽名的方法。所以,交易輸出必須這樣描述:「憑借哈希值為X的公鑰,以及這個公鑰所有者的簽名,才可以獲得這筆資金。」這實際上就是最常見的比特幣腳本,如圖3.4所示。

圖3.4 P2PH腳本範例

註:一個常見的比特幣輸出腳本範例。

那麼誰執行這個腳本?這一系列指令是如何完成的呢?秘密在於,交易的輸入包括了腳本(而不是簽名)。為了確認一筆交易正確地獲取了上一筆交易所輸出的資金,我們把交易的輸入腳本和上一筆交易的輸出腳本串聯起來,這個串聯腳本必須被成功地執行後才可以獲取資金。這兩個腳本,一個是輸出腳本(scriptPubKey),另一個是輸入腳本(scriptSig)。輸出腳本只是指定了一個公鑰(或是公鑰哈希值的地址),輸入腳本指定了一個對應公鑰的簽名。圖3.5就是兩個腳本結合的案例。

比特幣腳本語言

這個腳本語言是為比特幣開發的。在比特幣裡只叫作「腳本」。它和另一種Forth語言有很多相似的地方,Forth是一種簡單的堆棧式編程語言(stack-based programming language),但你並不需要先學習Forth語言才會使用比特幣的腳本語言。比特幣的腳本語言設計原則就是簡明扼要,並內生地支持加密操作。比如,腳本裡面有目的性的指令用來計算哈希值和檢驗簽名。

這種腳本語言是堆棧式的,意味著每個指令只被執行一次,是線性的,無法循環執行。所以指令的數目給了我們一個執行時間與內存使用的上限。這個語言不是圖靈完備的,意味著不能隨意運行強大函數功能。[1]但這是有意設計的,因為礦工需要去執行這些網絡上任意交易提交者所遞交的腳本,設計者並不希望讓他們提交可能無限循環的腳本。

圖3.5 結合輸入腳本和輸出腳本範例

註:為了確認當前交易是否正確地獲取了前一筆交易輸出的資金,我們把兩個腳本鏈接起來,把上一筆交易的輸出腳本(圖中虛線下方)添加到當前交易的輸入腳本(虛線上方)之後,形成一個新的腳本。請注意<pubKeyHash?>裡面有一個「?」,用作標識——我們後面會來確認它是否與當前交易提供的公鑰的哈希值一致。

執行比特幣腳本只能產生兩個結果:要麼被成功執行,這種情況下,交易有效;要麼腳本執行出現錯誤,這種情況下,整個交易無效,拒絕記入區塊鏈。

這個腳本語言十分簡單。只有256個指令,每個只用一個字節。256個指令中,有15個目前不可用,有75個被保留還沒有具體定義(以後或許可以被用來擴展),剩下的才是可用的。

許多在其他語言裡常見的基本指令這裡面都有。例如,基本的算數、邏輯語句(如If-then)、拋出錯誤、過早返回等。而且,還有密碼指令,比如哈希函數語句、簽名驗證語句,還有一個重要的特殊指令是「CHECKMULTISIG」——可以查證多個簽名。表3.1列舉了一些比特幣工作控制語言裡的常用語句。

CHECKMULTISIG指令要求指定n個公鑰和一個參數t(作為一個臨界值)。這個指令正確執行的條件是:在n個公鑰中,至少可以選出t個現時有效的簽名。我們在本章3.3節會示範這個指令的用法,但現在我們需要認識到這個原生指令是非常強大的,它以一種極其精練的方式協助我們查驗交易中的多方簽名。

不過,目前比特幣多方簽名功能實現過程中有一個缺陷,CHECKMULTISIG指令在執行的時候會返回一個沒用的值,而且系統還必須要安排一個堆棧中的變量去儲存它,然後再忽略掉。由於修復這個缺陷成本很高,兩害相權取其輕,這個缺陷就一直沒被修復,我們在第3章3.6節會再做討論。但目前,這個程序缺陷也算是比特幣的一個特性。

表3.1 一些比特幣腳本工作語言中的指令及其功能

執行一個腳本

在堆棧語言裡執行一個腳本,我們只需要一個堆棧來壘積數據,不需要分配任何內存與變量。因此,堆棧語言中計算相當容易。總共有兩類指令:數據指令和工作碼指令。數據指令的作用是把數據推到堆棧的最上面;工作碼指令則通常是用堆棧頂部的數據作為輸入值,用來計算一個函數。

我們現在來一起看一下,圖3.5這段腳本是怎麼執行的。圖3.6給我們展示了每一條指令執行後的堆棧狀態。腳本中的前兩條指令屬於數據指令,分別是輸入腳本(包含在交易的輸入項)中的簽名和用來驗證簽名的公鑰。我們前面提到過,一看到數據指令,系統就把它堆到堆棧最上面。後面幾個指令是輸出腳本(包含在上一交易的輸出項中)裡的指令。

首先,我們複製指令OP_DUP,這一步僅僅是將堆棧最上層的公鑰複製,並置於堆棧最上層;下一個指令是OP_HASH160,該指令取得堆棧最上層的數據,並計算其哈希值,然後將結果再堆到堆棧最上層。當指令執行完成後,我們將堆棧最上層的公鑰替換成了公鑰的哈希值。

圖3.6 比特幣腳本的執行堆棧狀態圖

註:圖中底部列出了相對應的指令:尖括號裡的是數據指令,以OP開頭的是工作碼指令,指令上方對應的是指令執行之後的堆棧狀態。

接下來,我們還要在堆棧頂層再推送一些數據:此筆交易發送者指定的公鑰的哈希值,以及對應的私鑰,這樣才可完成簽名,取得資金。此時,堆棧頂部有兩個數值,一個是發送者指定的公鑰的哈希值,另一個是接收者想要取得資金時提交的公鑰的哈希值。

這個時候,我們就要執行EQUALVERIFY命令了,這個命令是用來檢查堆棧頂部兩個數值是否相等的。如果不相等,就會拋出一個失敗信號,並且停止執行腳本。不過現在我們假設其相等,也就代表著接收者使用的是正確的公鑰。這條指令會移除堆棧頂部的兩條數據,這時,堆棧還剩下兩個數據:公鑰以及簽名。

我們已經證實接收者使用的公鑰確實就是交易裡指定的公鑰,但現在我們必須證實這個簽名是真的。這時,使用OP_CHECKSIG指令即可。這裡我們可以看出比特幣的腳本語言雖然簡單,但很強大。它只用「OP_CHECKSIG」就能實現一個很複雜的事情:移除堆棧裡兩個數值,然後用公鑰來證實整個交易的簽名是真的。

但這裡的簽名究竟是對什麼的簽名?簽名函數的輸入是什麼?實際上,在比特幣中,我們只可以對一個事情進行簽名——就是整個交易。所以,CHECKSIG指令從堆棧中取出兩個數據(公鑰以及簽名),並驗證簽名對於整個交易(使用對應公鑰發起的交易)來說是有效的。現在我們完成了所有的指令,堆棧裡面什麼也不剩。假設沒有碰到任何差錯的話,這個腳本的輸出就是一個「真」表示這個交易是正當有效的。

實際情況

理論上來講,通過腳本,我們可以隨意地為比特幣支付設定條件。當然,從2015年的情況看,這些特性也並不太常用到。如果我們回顧比特幣歷史中曾經實際用到的腳本,絕大多數的比特幣使用的腳本都非常基礎,像前文的例子一樣:指定一個公鑰,然後通過驗證簽名來使用這個幣。

當然,實際中也會使用一些其他指令,比如MULTISIG,還有一種支付給腳本的哈希值(Pay-to-script-hash,簡稱P2SH,我們很快會談到)等,但除此之外,平時常用的指令真不多,因為每個節點都有一份標準腳本的白名單,它們會拒絕接受不在名單上的腳本。這倒不是說無法運行其他腳本,只是使用起來比較麻煩。事實上這樣的安排也很巧妙,我們會在談論比特幣點對點網絡的時候再進行描述。

銷毀證明

銷毀證明(proof of burn)腳本,用於銷毀比特幣(即防止資金被贖回)。如果交易代碼的運行結果是將比特幣轉到「銷毀證明」腳本,那麼這筆比特幣將被銷毀。實際應用中主要是用來引導客戶使用其他數字貨幣系統,即將比特幣銷毀,以便獲得另一個數字貨幣系統發行的新幣。我們會在第10章展開敘述。銷毀證明腳本使用起來非常簡便:使用OP_RETURN腳本來拋出錯誤;不論之前指令的運行結果是什麼,OP_RETURN指令總會被執行,並相應拋出一個錯誤,腳本返回一個「錯誤」(false)值。

由於OP_RETURN以拋出錯誤的形式結束腳本,其後的所有指令都不會執行。利用這個特性,我們可以往腳本中植入任意信息,這些信息也將被存儲在區塊鏈中。假如你想通過署名或者蓋時間戳的方式來證明你在某個時候知道某件事情,就可以發起一筆極小額的比特幣交易,在腳本中加入上述信息,並使用銷毀證明腳本將幣銷毀,這樣就可以將信息永久地存儲在區塊鏈上。

支付給腳本的哈希值

如前文所述,比特幣的工作機制要求幣的發送者必須在交易時明確指定腳本。這種機制有時候不太適用:假如你在網店看中了一件商品並打算下單,你會問賣家「請把付款地址告訴我,我可以付款了」,但如果賣家使用了多重簽名地址(MULTISIG),那他會說「嘿,我們用了多重簽名地址,你需要支付給一個腳本地址,而不是一個簡單的地址」,但你會說「我不知道怎麼弄,這太複雜了,我只會支付給簡單的地址」。

比特幣用了一種很聰明的辦法來解決這個問題,不僅可以實現多重簽名地址支付,而且還可以實現複雜的資金監管規則。比特幣使用的辦法是:收款方告訴付款方「請把比特幣支付給某個腳本地址,腳本的哈希值是××,在取款的時候,我會提供上述哈希值對應的腳本,同時,提供數據通過腳本的驗證」,而不是「請把比特幣支付給某個公鑰,公鑰的哈希值是××」。付款方通過P2SH即可實現上述交易。

需要說明的是,P2SH腳本只是對堆棧最頂層的數據進行哈希運算,核驗運算結果是否與給定的哈希值一致,核驗通過後,再執行一步特殊的核驗:將堆棧最頂層的數據重新解讀為一系列指令,然後將其作為腳本運行一次,此時,堆棧中的其他數據作為腳本的輸入值。

要做到P2SH還是有點複雜的,因為P2SH不是比特幣的原始設計,是後來加上去的。它解決了兩個重要的問題:讓付款方的支付工作簡單化,收款方只需告訴付款方一個哈希值即可。在我們上面的例子中,你不再需要去關心商家到底用哪種地址,是否用了多重簽名,因為這只是商家在支取這筆款項時需要考慮的事情。

P2SH還實現了效率上的提升:礦工的工作是追蹤那些還沒有被消費掉的輸出腳本。採用P2SH的輸出腳本會變得很小——它們只不過是個哈希值而已。所有的複雜性都被放在輸入腳本中了。

[1] 圖靈是第二次世界大戰時英國數學家,密碼學家。他破譯了納粹的密碼機「謎」,為盟軍取得第二次世界大戰勝利做出重大貢獻,美國好萊塢以此題材拍了一部電影《模仿遊戲》。圖靈完備的意思是語言有能力隨意地執行強大的函數。——譯者注

《區塊鏈:技術驅動金融》