1. 程式人生 > 其它 >BTC-比特幣指令碼(區塊鏈技術與應用)

BTC-比特幣指令碼(區塊鏈技術與應用)

基於棧的語言(stack based language)

比特幣系統中使用的指令碼語言很簡單,唯一能訪問的記憶體空間就是一個棧,這點和通用指令碼語言的區別很大。

這個交易有一個輸入和兩個輸出,其中一個輸出已經被花出去了,另一個沒有被花出去。

輸入指令碼

輸入指令碼包含兩個操作,分別將兩個很長的數壓入棧中。

輸出指令碼

輸出指令碼有兩行,分別對應上面的兩個輸出,即每個輸出有自己單獨的一段指令碼

交易結構

交易的整體結構

這裡可以看到很多meta data。

交易的輸入

交易的輸入是一個列表,這個例子中輸入只有一個,所以這個列表也就只有一個列表項。

如果一個交易有多個輸入,那麼每個輸入都要指明來源,並給出簽名。

交易的輸出

交易的輸出也可以有多個,形成列表。

例子

下圖 上面是一個小型的區塊鏈,兩個交易分屬兩個區塊,中間隔了兩個區塊,B轉給C的這個交易的BTC的來源是前面A轉給B的這個交易。

所以右邊這個交易中的相應輸入的txid是左邊這個交易的id,右邊這個交易中的輸入的vout指向的是左邊這個交易的對應輸出

在早期的比特幣系統中,要驗證這個交易的合法性,就要把B->C這個交易的輸入指令碼,和A->B這個交易的輸出指令碼拼在一起執行(注意拼的順序),看看能不能執行通過。

後來,出於安全因素的考慮,這兩個指令碼改為分別執行,首先執行輸入指令碼,如果沒有出錯,那麼再執行輸出指令碼,如果能順利執行,並且最後得到非零值(true),那麼這個交易就是合法的。

如果一個交易有多個輸入,每個輸入指令碼都要去找到前面特定區塊中所對應的輸出指令碼,匹配之後來進行驗證。全部驗證通過後,這個交易才是合法的。

輸入、輸出指令碼的幾種形式

  1. P2PK(Pay to Public Key)

輸入指令碼中直接給出付款人的簽名(付款人用自己的私鑰對輸入指令碼所在的整個交易的簽名),輸出指令碼中(上一次交易的輸出指令碼)直接給出收款人(指的是上一次交易的收款人,也就是這次交易的付款人,注意在這裡是同一個人)的公鑰,最後的CHECKSIG是檢查簽名時用的指令。

這種形式的指令碼是最簡單的,因為Public Key是直接在輸出指令碼中給出的。

執行情況:一共三條語句,從上往下執行。

第一條語句,將輸入指令碼中的簽名壓入棧:

第二條語句,將輸出指令碼中的公鑰壓入棧:

第三條語句,彈出棧頂的兩個元素,用公鑰PubKey檢查一下簽名Sig是否正確。如果正確,返回True,說明驗證通過:

  1. P2PKH(Pay to Public Key Hash)

P2PKH的輸出指令碼中沒有給出收款人的公鑰,給出的是公鑰的雜湊值。在輸入指令碼中給出了這個人的公鑰(也就是既要給出公鑰又要給出簽名)。輸出指令碼其他一些操作都是為了驗證操作的正確性。

執行情況:一共七條語句,從上往下執行。

第一條語句,將輸入指令碼中的簽名壓入棧:

第二條語句,將輸入指令碼中的公鑰壓入棧:

第三條語句,將棧頂元素複製一遍(所以又壓入了一次公鑰):

第四條語句,將棧頂元素取出來取雜湊,再將得到的雜湊值壓入棧(也就是將棧頂的公鑰變成了其雜湊值):

第五條語句,將輸出指令碼中提供的公鑰的雜湊值壓入棧:

第六條語句,彈出棧頂的兩個元素,比較它們是否相等(防止有人用自己的公鑰冒充 幣的來源的交易 的收款人的公鑰),若他們相等就從棧裡面消失了:

第七條語句,彈出棧頂的兩個元素,用公鑰PubKey檢查一下簽名Sig是否正確。如果正確,返回True,說明驗證通過:

  1. P2SH(Pay to Script Hash)

這是最複雜的一種形式,這種形式下輸出指令碼給出的不是收款人的公鑰的雜湊,而是收款人提供的贖回指令碼(Redeem Script)的雜湊。將來要花這個輸出指令碼的BTC的時候,相應交易的輸入指令碼要給出贖回指令碼的具體內容,同時還要給出讓贖回指令碼能正確執行所需要的簽名。

進一步說明:

輸入指令碼會給出一些簽名(數目不等)及一段序列化的贖回指令碼。在驗證時分為兩步:

  • 驗證輸入指令碼給出的贖回指令碼內容,是否和對應輸出指令碼給出的贖回指令碼雜湊值相匹配
  • 反序列化並執行贖回指令碼,以驗證輸入指令碼給出的簽名是否正確。

兩步驗證都通過這個交易才是合法的。

贖回指令碼的形式:

  • P2PK形式
  • P2PKH形式
  • 多重簽名形式

用P2SH實現P2PK的功能

輸入指令碼中給出交易簽名和序列化的贖回指令碼;贖回指令碼中給出公鑰,然後用checksig檢查簽名;輸出指令碼中給出了贖回指令碼的雜湊值,用來驗證輸入指令碼中給出的贖回指令碼是否正確。

驗證過程:

第一階段的驗證:先驗證輸入指令碼和輸出指令碼在一起執行的結果。

第一步,將輸入指令碼中的交易簽名壓入棧:

第二步,將輸入指令碼中給出的贖回指令碼壓入棧:

第三步,彈出棧頂元素取雜湊再壓棧,也就得到了贖回指令碼的雜湊(Redeem Script Hash):

第四步,將輸出指令碼中給出的贖回指令碼的雜湊值壓入棧:

第五步,比較棧頂兩個元素是否相等,相當於用之前的輸出指令碼給出的贖回指令碼雜湊,驗證了輸入指令碼提供的贖回指令碼是否是正確的,如果相等就從棧頂消失:

第二階段的驗證:對輸入指令碼提供的贖回指令碼的驗證,首先要將其反序列化,得到可以執行的贖回指令碼。然後執行這個贖回指令碼。

第一步,將指令碼中寫死的公鑰壓入棧:

第二步,驗證輸入指令碼中給出的交易簽名的正確性。驗證通過就會返回True:

為什麼要用贖回指令碼:P2SH在最初版本的比特幣系統中是沒有的,後來通過軟分叉的形式加進去了,它常用的一個場景就是多重簽名

多重簽名

比特幣系統中一個交易輸出可能要求使用它的交易輸入提供多個簽名,才能把BTC取出來。

eg:某個公司可能要求5個合夥人中的任意三個提供簽名,才能把公司的錢轉走。這樣設計不但為私鑰的洩露提供了一定安全性保護,也為私鑰的丟失提供了一定的容錯性。

  1. 最早的多重簽名

最早的多重簽名是通過比特幣指令碼中的CEHCKMULTISIG操作來實現的,輸出指令碼中指定N個公鑰,同時指定一個不超過N的閾值M,輸入指令碼中只要提供任意M個簽名,就能夠通過驗證。

圖中有一個紅叉,這是因為比特幣系統中的CEHCKMULTISIG操作的實現有一個bug,這個bug會導致多從堆疊中彈出一個元素,因為這是一個去中心化的系統,這個bug到現在已經沒法修復了,要改只能去硬分叉,代價很大。這個紅叉的意思也就是在輸入腳本里往棧中新增一個沒用的元素,這樣來抵消掉這個bug的影響。

另外,給出的M個簽名的相對順序,要和對應的輸出指令碼中N個公鑰中對應公鑰的相對順序一致才行。

執行情況:

3個簽名中給出2個就行,這兩個簽名的順序和公鑰的順序是一樣的

第一步,將輸入指令碼中的多餘元素(前述的紅叉)壓棧:

第二步,將輸入腳本里的M個簽名依次壓入棧中(這裡M=2):

輸入指令碼到這裡就執行完了

第三步,將輸出指令碼中給定的閾值M壓棧:

第四步,將輸出指令碼中給定的N個公鑰壓棧:

第五步,將輸出指令碼中給定的公鑰數N壓棧:

第六步,執行CEHCKMULTISIG,以檢查堆疊中是否按順序包含了N個簽名中的M個,如果是的話,驗證通過

注意,這是最早的多重簽名,並沒有用到P2SH就是用比特幣指令碼中原生的CEHCKMULTISIG實現的

這樣在實際使用時有些不方便的地方:

eg:電商網站開通了比特幣支付渠道,但要求要有5個合夥人中3個人的簽名才能把BTC轉走。但這樣做之後,使用者在BTC支付的時候,生成的轉賬交易裡也要給出5個合夥人的公鑰,同時還要給出N和M的值。

而這些公鑰,以及N和M的值就要電商網站公佈給使用者,而且不同的電商網站規則也不一樣,這就讓使用者生成轉賬交易變得不方便。因為這些複雜性都暴露給使用者了。

  1. 用P2SH實現多重簽名

相比前面的實現,這樣的本質是將複雜性從輸出指令碼轉移到了贖回指令碼中,輸出指令碼只需要給出贖回指令碼的雜湊值就行了。N個公鑰以及N、M的值都在贖回指令碼中給出來,而贖回指令碼由輸入指令碼提供,也就是收款人提供,這樣也就和支付給它的使用者們隔離開了。

例子:下面B是使用者,A是電商平臺,C是A要把賺到的錢轉出去時候轉給的賬戶:

B要支付給電商平臺A時,不需要A的贖回指令碼,只要在輸出指令碼中寫好A的贖回指令碼的雜湊值(RSH)就可以了。在這種模式下,電商網站只需要在網站上公佈贖回指令碼的雜湊值,使用者生成轉賬交易時,把這個雜湊值包含在輸出腳本里就可以了,至於電商網站採用什麼樣的簽名規則對使用者而言是不可見的。

從使用者的角度來看,採用這種P2SH的支付方式,和採用上節課學的P2PKH支付方式沒有多大區別,只不過輸出指令碼中的是贖回指令碼的雜湊值而不是公鑰的雜湊值罷了(輸出指令碼寫法上也有一些區別,見各自的指令)。

輸入指令碼就是電商網站要把這筆BTC轉出去時候用的,這種方式下輸入指令碼要包含M個簽名,以及贖回指令碼的序列化版本。

如果電商將來改變了採用的多重簽名規則,就只需要改變一下贖回指令碼的內容和輸入指令碼中的內容,然後把新的贖回指令碼的雜湊值公佈出去就可以了。對使用者而言也只是付款時候輸出指令碼中要包含的雜湊值發生了變化。

驗證過程:

第一階段的驗證:先驗證輸入指令碼和輸出指令碼在一起執行的結果。

第一步,將紅叉佔位元素壓棧:

第二步,將輸入指令碼中的M個簽名壓棧:

第三步,將輸入指令碼中儲存的序列化的贖回指令碼壓棧:

輸入指令碼到此就執行完了。

第四步,彈出棧頂元素取雜湊再壓棧,即將棧頂的贖回指令碼取雜湊:

第五步,將輸出指令碼中給出的贖回指令碼雜湊值(RSH)壓棧:

第六步,判斷棧頂兩個元素是否相等,即判斷一下計算出的贖回指令碼雜湊和給定的贖回指令碼雜湊是否相等:

輸出指令碼到此就執行完了,也即第一階段的驗證做完了。

第二階段的驗證:對輸入指令碼提供的贖回指令碼的驗證,首先要將其反序列化,得到可以執行的贖回指令碼。然後執行這個贖回指令碼。

第一步,將閾值M壓棧:

第二步,將N個公鑰壓棧:

第三步,將給定的公鑰數N壓棧:

第四步,使用CEHCKMULTISIG操作檢查多重簽名的正確性:

現在的多重簽名都是採用這種P2SH的形式

Proof of Burn:銷燬比特幣

這是一種特殊的輸出指令碼,開頭是return,後面可以跟任意內容。

RETURN語句的作用是無條件的返回錯誤。所以包含這個操作的指令碼永遠不可能通過驗證,執行到RETURN語句就會出錯,然後驗證就會終止,後面的語句完全沒有機會執行。

為什麼要設計這樣的輸出指令碼?這樣的輸出BTC永遠都花不出去。這是用來證明銷燬比特幣的一種方法。

為什麼要銷燬比特幣?一般有兩種應用場景:

  • 一些小的加密貨幣(AltCoin:Alternative Coin),要求銷燬一定數量的比特幣可以得到一定數量的這種幣。這時Proof of Burn就可以證明自己銷燬了這些比特幣。

  • 往區塊鏈裡寫入一些內容。因為區塊鏈是不可篡改的賬本,有人就利用這個特性向其中寫入一些需要永久儲存的內容。

    比如第一節課學的digital commitment,即需要證明自己在某一時間知道某些內容。例如某些智慧財產權保護,可以將智慧財產權取雜湊之後,將雜湊值放在這種輸出指令碼的return語句的後面。反正雜湊值很小,而且雜湊值沒有洩露原來的內容,將來出現糾紛時,再將原來的內容公佈出去,大家在區塊鏈上找到這個交易的輸出腳本里的雜湊值,就可以證明自己在某個時間點已經掌握了這些知識了。

    回想在前面學習到鑄幣交易時,鑄幣交易的CoinBase域也可以隨便寫什麼內容,為什麼不在那裡寫呢?這種方法很難,必須要獲得記賬權,而且是在CoinBase域設定好內容的情況下,去獲得記賬權。根本來說,是因為 釋出交易不需要有記賬權,但釋出區塊需要取得記賬權

任何使用者都可以用Proof of Burn的方法,銷燬極少量的比特幣,換取向比特幣系統的區塊鏈中寫入一些內容的機會。

例項:

沒有銷燬比特幣,僅僅支付了交易費,也可以向區塊鏈中寫入內容:

看一下輸出指令碼,開頭就是RETURN,後面的內容是要寫進去的內容:

因為輸出永遠不會被花出去,所以不用儲存在UTXO裡面,這對全節點是很友好的。

總結

比特幣系統中使用的指令碼語言很簡單,它也不是圖靈完備的語言,甚至不支援迴圈,這樣設計也有其用意,不支援迴圈也就不會有死迴圈。

後面學的以太坊的指令碼語言就是圖靈完備的,這樣就靠其它機制來防止進入死迴圈等。

比特幣的指令碼語言針對比特幣應用場景做了很好的優化,如檢查多重簽名時的CHECKMULTISIG操作一條就能實現,這是其強大之處。