1. 程式人生 > >精通比特幣(第七章)【高階交易和指令碼】

精通比特幣(第七章)【高階交易和指令碼】

7.1介紹

在上一章中,我們介紹了比特幣交易的基本元素,並且查看了最常見的交易指令碼型別,即P2PKH指令碼。在本章中,我們將介紹更高階的指令碼,以及如何使用它來構建具有複雜條件的交易。

首先,我們將看看多重簽名指令碼。接下來,我們將檢查第二個最常見的交易指令碼Pay-to-Script-Hash,它打開了一個複雜指令碼的整個世界。然後,我們將檢查新的指令碼操作符,通過時間鎖定將比特幣新增時間維度。

7.2多重簽名

多重簽名指令碼設定了一個條件,其中N個公鑰被記錄在指令碼中,並且至少有M個必須提供簽名來解鎖資金。這也稱為M-N方案,其中N是金鑰的總數,M是驗證所需的簽名的數量。例如,2/3的多重簽名是三個公鑰被列為潛在簽名人,至少有2個有效的簽名才能花費資金。

當前,標準多重簽名指令碼限制在最多3個公鑰,這意味著你可以使用1-1到3-3多重簽名,或這個範圍的任意組合。3這個數量限制可能會改變,所以,檢查IsStandard()函式,看看網路當前接受的是什麼值。注意,限制3只應用於標準簽名指令碼,而不應用於P2SH指令碼中包裝的多重簽名指令碼。P2SH簽名指令碼限制為15個金鑰,允許最多15-15多重簽名。我們將在後面學習P2SH。

設定M-N多重簽名條件的鎖定指令碼的一般形式是:

M <Public Key 1> <Public Key 2> ... <Public Key N> N CHECKMULTISIG


M是花費輸出所需的簽名的數量,N是列出的公鑰的總數。 設定2到3多重簽名條件的鎖定指令碼如下所示:

2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG


上述鎖定指令碼可由含有簽名和公鑰的指令碼予以解鎖: 或者由3個存檔公鑰中的任意2個相一致的私鑰簽名組合予以解鎖。 兩個指令碼組合將形成一個驗證指令碼:

<Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG


當執行時,只有當未解鎖版指令碼與解鎖指令碼設定條件相匹配時,組合指令碼才顯示得到結果為真(Ture)。

上述例子中相應的設定條件即為:解鎖指令碼是否含有3個公鑰中的任意2個相對應的私鑰的有效簽名。

CHECKMULTISIG執行中的bug

CHECKMULTISIG的執行中有一個bug,需要一些輕微的解決方法。 當CHECKMULTISIG執行時,它應該消耗 堆疊(stack) 上的M + N + 2個專案作為引數。 然而,由於該錯誤,CHECKMULTISIG將彈出(pop)超出預期的額外值或一個值。 我們來看看這個更詳細的/使用以前的/驗證示例:

<Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG


首先,CHECKMULTISIG彈出最上面的專案,這是N(在這個例子中N是“3”)。然後它彈出N個專案,這是可以簽名的公鑰。在這個例子中,公鑰A,B和C.然後,它彈出一個專案,即M,仲裁(需要多少個簽名)。這裡M = 2。此時,CHECKMULTISIG應彈出最終的M個專案,這些是簽名,並檢視它們是否有效。

然而,不幸的是,實施中的錯誤導致CHECKMULTISIG再彈出一個專案(總共M + 1個)。檢查簽名時,不考慮額外的專案,因此它對CHECKMULTISIG本身沒有直接影響。但是,必須存在額外的值,因為如果不存在,則當CHECKMULTISIG嘗試彈出空堆疊時,會導致堆疊錯誤和指令碼失敗(將交易標記為無效)。因為額外的專案被忽略,它可以是任何東西,但通常使用0。

因為這個bug成為共識規則的一部分,所以現在它必須永遠被複制。因此,正確的指令碼驗證將如下所示:

0 <Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG


這樣解鎖指令碼就不是下面的:

<Signature B> <Signature C>


而是:

0 <Signature B> <Signature C>


從現在開始,如果你看到一個multisig解鎖指令碼,你應該期望看到一個額外的0開始,其唯一的目的是解決一個bug,意外地成為一個共識規則的解決方法。 【譯者注:即保證例子中有3個私鑰簽名(其中2有效簽名,其中1個為0的無效簽名)對應3個公鑰用於檢查多重簽名,從而保證指令碼不產生bug。】

7.3 P2SH(Pay-to-Script-Hash)

P2SH在2012年被作為一種新型、強大、且能大大簡化複雜交易指令碼的交易型別而引入。為進一步解釋P2SH的必要性,讓我們先看一個實際的例子。

在第1章中,我們曾介紹過Mohammed,一個迪拜的電子產品進口商。Mohammed的公司採用比特幣多重簽名作為其公司會計賬簿記賬要求。多重簽名指令碼是比特幣高階指令碼最為常見的運用之一,是一種具有相當大影響力的指令碼。針對所有的顧客支付(即應收賬款),Mohammed的公司要求採用多重簽名交易。基於多重簽名機制,顧客的任何支付都需要至少兩個簽名才能解鎖,一個來自Mohammed,另一個來自其合夥人或擁有備份鑰匙的代理人。這樣的多重簽名機制能為公司治理提供管控便利,同時也能有效防範盜竊、挪用和遺失。 最終的指令碼非常長:

2 <Mohammed's Public Key> <Partner1 Public Key> <Partner2 Public Key> <Partner3 Public Key> <Attorney Public Key> 5 OP_C HECKMULTISIG


雖然多重簽名十分強大,但其使用起來還是多有不便。基於之前的指令碼,Mohammed必須在客戶付款前將該指令碼傳送給每一位客戶,而每一位顧客也必須使用特製的能產生客戶交易指令碼的比特幣錢包軟體,每位顧客還得學會如何利用指令碼來完成交易。

此外,由於指令碼可能包含特別長的公鑰,最終的交易指令碼可能是最初交易指令碼長度的5倍之多。額外長度的指令碼將給客戶造成費用負擔。最後,一個長的交易指令碼將一直記錄在所有節點的隨機儲存器的UTXO集中,直到該筆資金被使用。採用這種複雜輸出指令碼使得在實際交易中變得困難重重。

P2SH正是為了解決這一實際難題而被引入的,它旨在使複雜指令碼的運用能與直接向比特幣地址支付一樣簡單。在P2SH 支付中,複雜的鎖定指令碼被電子指紋所取代,電子指紋是指密碼學中的雜湊值。

當一筆交易試圖支付UTXO時,要解鎖支付指令碼,它必須含有與雜湊相匹配的指令碼。P2SH的含義是,向與該雜湊匹配的指令碼支付,當輸出被支付時,該指令碼將在後續呈現。

在P2SH交易中,鎖定指令碼由雜湊運算後的20位元組的雜湊值取代,被稱為贖回指令碼。因為它在系統中是在贖回時出現而不是以鎖定指令碼模式出現。表7-1列示了非P2SH指令碼,表7-2列示了P2SH指令碼。

表7-1不含P2SH的複雜指令碼

表7-1 不含P2SH的複雜指令碼

表7-2 P2SH複雜指令碼 表7-2 P2SH複雜指令碼

從表中可以看出,對於P2SH,詳細描述了輸出(贖回指令碼)的條件的複雜指令碼不會在鎖定指令碼中顯示。相反,只有它的雜湊值在鎖定指令碼中呈現,並且兌換指令碼本身之後作為解鎖指令碼在輸出花費時的一部分出現。 這使得給礦工的交易費用從傳送方轉移到收款方,複雜的計算工作也從傳送方轉移到收款方。

[譯者注]贖回指令碼(Redeem Script)中的“2 Pubkey1 Pubkey2 Pubkey3 Pubkey4 Pubkey5 5 CHECKMULTISIG”的內容,沒有出現在鎖定指令碼(Locking Script 表中第二行內容)中,但使用“2 Pubkey1 Pubkey2 Pubkey3 Pubkey4 Pubkey5 5 CHECKMULTISIG”(有520位元組)進行雜湊運算後的20位元組的雜湊值取代之,然後將之(“2 Pubkey1 Pubkey2 Pubkey3 Pubkey4 Pubkey5 5 CHECKMULTISIG”)放到解鎖指令碼中(Unlocking Script)。這使得給礦工的交易費用從傳送方轉移到收款方,並且令複雜的計算工作也從從傳送方轉移到收款方。

讓我們再看下Mohammed公司的例子,複雜的多重簽名指令碼和相應的P2SH指令碼。 首先,Mohammed公司對所有顧客訂單採用多重簽名指令碼:

2 <Mohammed’s Public Key> 5 CHECKMULTISIG

如果佔位符由實際的公鑰(以04開頭的520位元組)替代,你將會看到的指令碼會非常地長:

2 04C16B8698A9ABF84250A7C3EA7EEDEF9897D1C8C6ADF47F06CF73370D74DCCA01CDCA79DCC5C395D7EEC6984D83F1F50C900A24DD47F569FD4193AF5DE762C58704A2192968D8655D6A935BEAF2CA23E3FB87A3495E7AF308EDF08DAC3C1FCBFC2C75B4B0F4D0B1B70CD2423657738C0C2B1D5CE65C97D78D0E34224858008E8B49047E63248B75DB7379BE9CDA8CE5751D16485F431E46117B9D0C1837C9D5737812F393DA7D4420D7E1A9162F0279CFC10F1E8E8F3020DECDBC3C0DD389D99779650421D65CBD7149B255382ED7F78E946580657EE6FDA162A187543A9D85BAAA93A4AB3A8F044DADA618D087227440645ABE8A35DA8C5B73997AD343BE5C2AFD94A5043752580AFA1ECED3C68D446BCAB69AC0BA7DF50D56231BE0AABF1FDEEC78A6A45E394BA29A1EDF518C022DD618DA774D207D137AAB59E0B000EB7ED238F4D800 5 CHECKMULTISIG

整個指令碼都可由僅為20個位元組的密碼雜湊所取代,首先採用SH256雜湊演算法,隨後對其運用RIPEMD160演算法。20位元組 的指令碼為:

 54c557e07dde5bb6cb791c7a540e0a4796f5e97 

一筆P2SH交易運用鎖定指令碼將輸出與雜湊關聯,而不是與前面特別長的指令碼所關聯。使用的鎖定指令碼為:

HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e EQUAL

正如你所看到的,這個指令碼比前面的長指令碼簡短多了。取代“向該5個多重簽名指令碼支付”,這個P2SH等同於“向含該雜湊的指令碼支付”。顧客在向Mohammed公司支付時,只需在其支付指令中納入這個非常簡短的鎖定指令碼即可。當 Mohammed想要花費這筆UTXO時,附上原始贖回指令碼(與UTXO鎖定的雜湊)和必要的解鎖簽名即可,如:

<Sig1> <Sig2> <2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG>

兩個指令碼經由兩步實現組合。 首先,將贖回指令碼與鎖定指令碼比對以確認其與雜湊是否匹配:

<2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG> HASH160 <redeem scriptHash> EQUAL

假如贖回指令碼與雜湊匹配,解鎖指令碼會被執行以釋放贖回指令碼:

<Sig1> <Sig2> 2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG

本章中描述的幾乎所有指令碼只能以P2SH指令碼來實現。 它們不能直接用在UTXO的鎖定指令碼中。

7.3.1 P2SH地址

P2SH的另一重要特徵是它能將指令碼雜湊編譯為一個地址(其定義請見BIP0013 /BIP-13)。P2SH地址是基於Base58編碼的一 個含有20個位元組雜湊的指令碼,就像比特幣地址是基於Base58編碼的一個含有20個位元組的公鑰。由於P2SH地址採用5作為字首,這導致基於Base58編碼的地址以“3”開頭。例如,Mohammed的指令碼,基於Base58編碼下的P2SH地址變 為“39RF6JqABiHdYHkfChV6USGMe6Nsr66Gzw”。

此時,Mohammed可以將該地址傳送給他的客戶,這些客戶可以 採用任何的比特幣錢包實現簡單支付,就像這是一個比特幣地址一樣。以“3”為字首給予客戶這是一種特殊型別的地址的暗示,該地址與一個指令碼相對應而非與一個公鑰相對應,但是它的效果與比特幣地址支付別無二致。 P2SH地址隱藏了所有的複雜性,因此,運用其進行支付的人將不會看到指令碼。

7.3.2 P2SH的優點

與直接使用複雜指令碼以鎖定輸出的方式相比,P2SH具有以下特點:

  • 在交易輸出中,複雜指令碼由簡短電子指紋取代,使得交易程式碼變短。
  • 指令碼能被編譯為地址,支付指令的發出者和支付者的比特幣錢包不需要複雜工序就可以執行P2SH。
  • P2SH將構建指令碼的重擔轉移至接收方,而非傳送方。
  • P2SH將長指令碼資料儲存的負擔從輸出方(儲存於UTXO集,影響記憶體)轉移至輸入方(儲存在區塊鏈裡面)。
  • P2SH將長指令碼資料儲存的重擔從當前(支付時)轉移至未來(花費時)。
  • P2SH將長指令碼的交易費成本從傳送方轉移至接收方,接收方在使用該筆資金時必須含有贖回指令碼。

7.3.3贖回指令碼和標準確認

在0.9.2版比特幣核心客戶端之前,P2SH僅限於標準比特幣交易指令碼型別(即通過標準函式檢驗的指令碼)。這也意味著使用該筆資金的交易中的贖回指令碼只能是標準化的P2PK、P2PKH或者多重簽名,而非RETURN 和P2SH。

作為0.9.2版的比特幣核心客戶端,P2SH交易能包含任意有效的指令碼,這使得P2SH標準更為靈活,也可以用於多種新的或複雜型別的交易。

請記住不能將P2SH植入P2SH贖回指令碼,因為P2SH不能自迴圈。雖然在技術上可以將RETURN包含在贖回指令碼中,但由於規則中沒有策略阻止您執行此操作,因此在驗證期間執行RETURN將導致交易被標記為無效,因此這是不實際的。

需要注意的是,因為贖回指令碼只有在你試圖傳送一個P2SH輸出時才會在比特幣網路中出現,假如你將輸出與一個無效的交易雜湊鎖定,則它將會被忽略。該UTXO將會被成功鎖定,但是你將不能使用該筆資金,因為交易中含有贖回指令碼,該指令碼因是一個無效的指令碼而不能被接受。這樣的處理機制也衍生出一個風險,你可能將比特幣鎖定在一個未來不能被花費的P2SH中。因為比特幣網路本身會接受這一P2SH,即便它與無效的贖回指令碼所對應(因為該贖回指令碼雜湊沒有對其所表徵的指令碼給出指令)。

註釋 P2SH鎖定指令碼包含一個贖回指令碼雜湊,該指令碼對於贖回指令碼本身未提供任何描述。P2SH交易即便在贖回指令碼無效的情況下也會被認為有效。如果處理不當,有可能會出現一個事故,即你的比特幣可能會被鎖死在P2SH這個交易中,導致你以後再也不能花費這筆比特幣了。

7.4 資料記錄輸出(RETURN操作符)

比特幣的去中心特點和時間戳賬本機制,即區塊鏈技術,其潛在運用將大大超越支付領域。許多開發者試圖充分發揮交易指令碼語言的安全性和可恢復性優勢,將其運用於電子公證服務、證券認證和智慧合約等領域。很多早期的開發者利用比特幣這種能將交易資料放到區塊鏈上的技術進行了很多嘗試 ,例如,為檔案記錄電子指紋,則任何人都可以通過該機制在特定的日期建立關於文件存在性的證明。

運用比特幣的區塊鏈技術儲存與比特幣支付不相關資料的做法是一個有爭議的話題。許多開發者認為其有濫用的嫌疑,因而試圖予以阻止。另一些開發者則將之視為區塊鏈技術強大功能的有力證明,從而試圖給予大力支援。那些反對非支付相關應用的開發者認為這樣做將引致“區塊鏈膨脹”,因為所有的區塊鏈節點都將以消耗磁碟儲存空間為成本,負擔儲存此類 資料的任務。

更為嚴重的是,此類交易僅將比特幣地址當作自由組合的20個位元組而使用,進而會產生不能用於交易的UTXO。因為比特幣地址只是被當作資料使用,並不與私鑰相匹配,所以會導致UTXO不能被用於交易,因而是一種偽支付行為。因此,這些交易永遠不會被花費,所以永遠不會從UTXO集中刪除,並導致UTXO資料庫的大小永遠增加或“膨脹”。

在0.9版的比特幣核心客戶端上,通過採用Return操作符最終實現了妥協。Return允許開發者在交易輸出上增加80位元組的非交易資料。然後,與偽交易型的UTXO不同,Return創造了一種明確的可複查的非交易型輸出,此類資料無需儲存於UTXO集。Return輸出被記錄在區塊鏈上,它們會消耗磁碟空間,也會導致區塊鏈規模的增加,但 它們不儲存在UTXO集中,因此也不會使得UTXO記憶體膨脹,更不會以消耗代價高昂的記憶體為代價使全節點都不堪重負。 RETURN 指令碼的樣式:

  RETURN <data>

“data”部分被限制為80位元組,且多以雜湊方式呈現,如32位元組的SHA256演算法輸出。許多應用都在其前面加上字首以輔助認定。例如,電子公正服務的證明材料採用8個位元組的字首“DOCPROOF”,在十六進位制演算法中,相應的ASCII碼為 44 4f 43 50 52 4f 4f 46。

請記住 RETURN 不涉及可用於支付的解鎖指令碼的特點, RETURN 不能使用其輸出中所鎖定的資金,因此它也就沒有必要記錄在蘊含潛在成本的UTXO集中,所以 RETURN 實際是沒有成本的。

RETURN 常為一個金額為0的比特幣輸出, 因為任何與該輸出相對應的比特幣都會永久消失。假如一筆 RETURN 被作為一筆交易的輸入,指令碼驗證引擎將會阻止驗證指令碼的執行,將標記交易為無效。如果你碰巧將 RETURN 的輸出作為另一筆交易的輸入,則該交易是無效的。

一筆標準交易(通過了 isStandard() 函式檢驗的)只能有一個 RETURN 輸出。但是單個RETURN 輸出能與任意型別的輸出交易進行組合。

Bitcoin Core中添加了兩個新版本的命令列選項。 選項datacarrier控制RETURN交易的中繼和挖掘,預設設定為“1”以允許它們。 選項datacarriersize採用一個數字引數,指定RETURN指令碼的最大大小(以位元組為單位),預設為83位元組,允許最多80個位元組的RETURN資料加上一個位元組的RETURN操作碼和兩個位元組的PUSHDATA操作碼。

註釋 最初提出了RETURN,限制為80位元組,但是當功能被釋放時,限制被減少到40位元組。 2015年2月,在Bitcoin Core的0.10版本中,限制提高到80位元組。 節點可以選擇不中繼或重新啟動RETURN,或者只能中繼和挖掘包含少於80位元組資料的RETURN。

7.5時間鎖(Timelocks)

時間鎖是隻允許在一段時間後才允許支出的交易。比特幣從一開始就有一個交易級的時間鎖定功能。它由交易中的nLocktime欄位實現。在2015年底和2016年中期推出了兩個新的時間鎖定功能,提供UTXO級別的時間鎖定功能。這些是CHECKLOCKTIMEVERIFY和CHECKSEQUENCEVERIFY。

時間鎖對於後期交易和將資金鎖定到將來的日期很有用。更重要的是,時間鎖將比特幣指令碼擴充套件到時間的維度,為複雜的多級智慧合同打開了大門。

7.5.1交易鎖定時間(nLocktime)

比特幣從一開始就有一個交易級的時間鎖功能。交易鎖定時間是交易級設定(交易資料結構中的一個欄位),它定義交易有效的最早時間,並且可以在網路上中繼或新增到區塊鏈中。

鎖定時間也稱為nLocktime,是來自於Bitcoin Core程式碼庫中使用的變數名稱。在大多數交易中將其設定為零,以指示即時傳播和執行。如果nLocktime不為零,低於5億,則將其解釋為塊高度,這意味著交易無效,並且在指定的塊高度之前未被中繼或包含在塊鏈中。

如果超過5億,它被解釋為Unix紀元時間戳(自Jan-1-1970之後的秒數),並且交易在指定時間之前無效。指定未來塊或時間的nLocktime的交易必須由始發系統持有,並且只有在有效後才被髮送到比特幣網路。如果交易在指定的nLocktime之前傳輸到網路,那麼第一個節點就會拒絕該交易,並且不會被中繼到其他節點。使用nLocktime等同於一張延期支票。

7.5.1.1交易鎖定時間限制

nLocktime就是一個限制,雖然它可以在將來花費,但是到現在為止,它並不能使用它們。我們來解釋一下,下面的例子。

Alice簽署了一筆交易,支付給Bob的地址,並將交易nLocktime設定為3個月。Alice把這筆交易傳送給Bob。有了這個交易,Alice和Bob知道:

  • 在3個月過去之前,Bob不能完成交易進行變現。
  • Bob可以在3個月後接受交易。

然而:

  • Alice可以建立另一個交易,雙重花費相同的輸入,而不需要鎖定時間。 因此,Alice可以在3個月過去之前花費相同的UTXO。
  • Bob不能保證Alice不會這樣做。

瞭解交易nLocktime的限制很重要。 唯一的保證是Bob在3個月過去之前無法兌換它。 不能保證Bob得到資金。 為了實現這樣的保證,時間限制必須放在UTXO本身上,併成為鎖定指令碼的一部分,而不是交易。

這是通過下一種形式的時間鎖定來實現的,稱為檢查鎖定時間驗證(CLTV)。

7.5.2檢查鎖定時間驗證Check Lock Time Verify (CLTV)

2015年12月,引入了一種新形式的時間鎖進行比特幣軟分叉升級。根據BIP-65中的規範,指令碼語言添加了一個名為CHECKLOCKTIMEVERIFY(CLTV)的新指令碼操作符。 CLTV是每個輸出的時間鎖定,而不是每個交易的時間鎖定,與nLocktime的情況一樣。這允許在應用時間鎖的方式上具有更大的靈活性。 簡單來說,通過在輸出的贖回指令碼中新增CLTV操作碼來限制輸出,從而只能在指定的時間過後使用。

註釋 當nLocktime是交易級時間鎖定時,CLTV是基於輸出的時間鎖。

CLTV不會取代nLocktime,而是限制特定的UTXO,並通過將nLocktim設定為更大或相等的值,從而達到在未來才能花費這筆錢的目的。

CLTV操作碼採用一個引數作為輸入,表示為與nLocktime(塊高度或Unix紀元時間)相同格式的數字 。如VERIFY字尾所示,CLTV如果結果為FALSE,則停止執行指令碼的操作碼型別。如果結果為TRUE,則繼續執行。

為了使用CLTV鎖定輸出,將其插入到建立輸出的交易中的輸出的贖回指令碼中。例如,如果Alice支付Bob的地址,輸出通常會包含一個這樣的P2PKH指令碼:

DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

要鎖定一段時間,比如說3個月以後,交易將是一個P2SH交易,其中包含一個贖回指令碼:

<now + 3 months> CHECKLOCKTIMEVERIFY DROP DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

其中<now +3個月>是從交易開始被挖礦時間起計3個月的塊高度或時間值:當前塊高度+12,960(塊)或當前Unix紀元時間+7,760,000(秒)。現在,不要擔心CHECKLOCKTIMEVERIFY之後的DROP操作碼,下面很快就會解釋。

當Bob嘗試花費這個UTXO時,他構建了一個引用UTXO作為輸入的交易。他使用他的簽名和公鑰在該輸入的解鎖指令碼,並將交易nLocktime設定為等於或更大於Alice設定的CHECKLOCKTIMEVERIFY 時間鎖。然後,Bob在比特幣網路上廣播交易。

Bob的交易評估如下。如果Alice設定的CHECKLOCKTIMEVERIFY引數小於或等於支出交易的nLocktime,指令碼執行將繼續(就好像執行“無操作”或NOP操作碼一樣)。否則,指令碼執行停止,並且該交易被視為無效。 更確切地說,CHECKLOCKTIMEVERIFY失敗並停止執行,標記交易無效(來源:BIP-65):

  1. 堆疊是空的要麼
  2. 堆疊中的頂部項小於0;要麼
  3. 頂層堆疊項和nLocktime欄位的鎖定時間型別(高度或者時間戳)不相同;要麼
  4. 頂層堆疊項大於交易的nLocktime欄位;要麼
  5. 輸入的nSequence欄位為0xffffffff。

註釋 CLTV和nLocktime使用相同的格式來描述時間鎖定,無論是塊高度還是自Unix紀元以秒鐘以來所經過的時間。 最重要的是,在一起使用時,nLocktime的格式必須與輸入中的CLTV格式相匹配,它們必須以秒為單位引用塊高度或時間。

執行後,如果滿足CLTV,則其之前的時間引數仍然作為堆疊中的頂級項,並且可能需要使用DROP進行刪除,才能正確執行後續指令碼操作碼。 為此,您將經常在指令碼中看到CHECKLOCKTIMEVERIFY+DROP在一起使用。

通過將nLocktime與CLTV結合使用,交易鎖定時間限制中描述的情況發生變化。 因為Alice鎖定了UTXO本身,所以現在Bob或Alice在3個月的鎖定時間到期之前不可能花費它。

通過將時間鎖定功能直接引入到指令碼語言中,CLTV允許我們開發一些非常有趣的複雜指令碼。 該標準在BIP-65(CHECKLOCKTIMEVERIFY)中定義(附錄部分)。

7.5.3相對時間鎖

nLocktime和CLTV都是絕對時間鎖定,它們指定絕對時間點。接下來的兩個時間鎖定功能,我們將要考察的是相對時間鎖定,因為它們將消耗輸出的條件指定為從塊連結中的輸出確認起的經過時間。

相對時間鎖是有用的,因為它們允許將兩個或多個相互依賴的交易連結在一起,同時對依賴於從先前交易的確認所經過的時間的一個交易施加時間約束。換句話說,在UTXO被記錄在塊狀塊之前,時鐘不開始計數。這個功能在雙向狀態通道和閃電網路中特別有用,我們將在後面章節[state_channels]中看到。

相對時間鎖,如絕對時間鎖定,同時具有交易級功能和指令碼級操作碼。交易級相對時間鎖定是作為對每個交易輸入中設定的交易欄位nSequence的值的共識規則實現的。指令碼級相對時間鎖定使用CHECKSEQUENCEVERIFY(CSV)操作碼實現。

相對時間鎖是根據BIP-68與BIP-112的規範共同實現的,其中BIP-68通過與相對時間鎖運用一致性增強的數字序列實現,BIP-112中是運用到了CHECKSEQUENCEVERIFY這個操作碼實現。

BIP-68和BIP-112是在2016年5月作為軟分叉升級時被啟用的一個共識規則。

7.5.4 nSequence相對時間鎖

相對時間鎖定可以在每個輸入中設定好,其方法是在每個輸入中加多一個nSequence欄位。

7.5.4.1 nSequence的本義

nnSequence欄位的設計初心是想讓交易能在在記憶體中修改,可惜後面從未運用過,使用nSequence這個欄位時,如果輸入的交易的序列值小於2^32 (0xFFFFFFFF),就表示尚未“確定”的交易。

這樣的交易將在記憶體池中儲存,直到被另一個交易消耗相同輸入並具有較大nSequence值的代替。一旦收到一個交易,其投入的nSequence值為2^32,那麼它將被視為“最終確定”並開採。 nSequence的原始含義從未被正確實現,並且在不利用時間鎖定的交易中nSequence的值通常設定為232。對於具有nLocktime或CHECKLOCKTIMEVERIFY的交易,nSequence值必須設定為小於232,以使時間鎖定器有效。通常設定為2^32 - 1(0xFFFFFFFE)。

7.5.4.2 nSequence作為一個共同執行的相對時間鎖定

由於BIP-68的啟用,新的共識規則適用於任何包含nSequence值小於2^31的輸入的交易(bit 1<<31 is not set)。以程式設計方式,這意味著如果沒有設定最高有效(bit 1<<31),它是一個表示“相對鎖定時間”的標誌。否則(bit 1<<31set),nSequence值被保留用於其他用途,例如啟用CHECKLOCKTIMEVERIFY,nLocktime,Opt-In-Replace-By-Fee以及其他未來的新產品。

一筆輸入交易,當輸入指令碼中的nSequence值小於2^31時,就是相對時間鎖定的輸入交易。這種交易只有到了相對鎖定時間後才生效。例如,具有30個區塊的nSequence相對時間鎖的一個輸入的交易只有在從輸入中引用的UTXO開始的時間起至少有30個塊時才有效。由於nSequence是每個輸入欄位,因此交易可能包含任何數量的時間鎖定輸入,所有這些都必須具有足夠的時間以使交易有效。

交易可以包括時間鎖定輸入(nSequence <2^31)和沒有相對時間鎖定(nSequence> = 2^31)的輸入。 nSequence值以塊或秒為單位指定,但與nLocktime中使用的格式略有不同。型別標誌用於區分計數塊和計數時間(以秒為單位)的值。型別標誌設定在第23個最低有效位(即值1 << 22)。如果設定了型別標誌,則nSequence值將被解釋為512秒的倍數。如果未設定型別標誌,則nSequence值被解釋為塊數。

當將nSequence解釋為相對時間鎖定時,只考慮16個最低有效位。一旦評估了標誌(位32和23),nSequence值通常用16位掩碼(例如nSequence&0x0000FFFF)“遮蔽”。 下圖顯示由BIP-68定義的nSequence值的二進位制佈局。 BIP-68 definition of nSequence encoding Figure 1. BIP-68 definition of nSequence encoding (Source: BIP-68)

7.5.5 帶CSV的相對時間鎖

就像CLTV和nLocktime一樣,有一個指令碼操作碼用於相對時間鎖定,它利用指令碼中的nSequence值。該操作碼是CHECKSEQUENCEVERIFY,通常簡稱為CSV。 在UTXO的贖回指令碼中評估時,CSV操作碼僅允許在輸入nSequence值大於或等於CSV引數的交易中進行消耗。實質上,這限制了UTXO的消耗,直到UTXO開採時間過了一定數量的塊或秒。

與CLTV一樣,CSV中的值必須與相應nSequence值中的格式相匹配。如果CSV是根據塊指定的,那麼nSequence也是如此。如果以秒為單位指定CSV,那麼nSequence也是如此。

當幾個(已經形成鏈)交易被保留為“脫鏈”時,建立和簽名這幾個(已經形成鏈)交易但不傳播時,CSV的相對時間鎖特別有用。在父交易已被傳播,直到消耗完相對鎖定時間,才能使用子交易。這個用例的一個應用可以在state_channelslightning_network/請加上鍊接 章節中看到。 CSV 細節參見 BIP-112, CHECKSEQUENCEVERIFY.

7.5.6中位時間過去Median-Time-Past

作為啟用相對時間鎖定的一部分,時間鎖定(絕對和相對)的“時間”方式也發生了變化。在比特幣中,牆上時間(wall time)和共識時間之間存在微妙但非常顯著的差異。比特幣是一個分散的網路,這意味著每個參與者都有自己的時間觀。網路上的事件不會隨時隨地發生。網路延遲必須考慮到每個節點的角度。最終,所有內容都被同步,以建立一個共同的分類帳。比特幣在過去存在的分類賬狀態中每10分鐘達成一個新的共識。

區塊頭中設定的時間戳由礦工設定。共識規則允許一定的誤差來解決分散節點之間時鐘精度的問題。然而,這誘惑了礦工去說謊,以便通過包括還不在範圍內的時間交易來賺取額外礦工費。有關詳細資訊,請參閱以下部分。

為了杜絕礦工說謊,加強時間安全性,在相對時間鎖的基礎上又新增了一個BIP。這是BIP-113,它定義了一個稱為“中位時間過去 /(Median-Time-Past)”的新的共識測量機制。通過取最後11個塊的時間戳並計算其中位數作為“中位時間過去”的值。這個中間時間值就變成了共識時間,並被用於所有的時間計算。過去約兩個小時的中間點,任何一個塊的時間戳的影響減小了。通過這個方法,沒有一個礦工可以利用時間戳從具有尚未成熟的時間段的交易中獲取非法礦工費。

Median-Time-Past更改了nLocktime,CLTV,nSequence和CSV的時間計算的實現。由Median-Time-Past計算的共識時間總是大約在掛鐘時間後一個小時。如果建立時間鎖交易,那麼要在nLocktime,nSequence,CLTV和CSV中進行編碼的估計所需值時,應該考慮它。 Median-Time-Past細節參見BIP-113.

7.5.7針對費用狙擊(Fee Sniping)的時間鎖定

費用狙擊是一種理論攻擊情形,礦工試圖從將來的塊(挑選手續費較高的交易)重寫過去的塊,實現“狙擊”更高費用的交易,以最大限度地提高盈利能力。

例如,假設存在的最高塊是塊#100,000。如果不是試圖把#100,001號的礦區擴大到區塊鏈,那麼一些礦工們會試圖重新挖礦#100,000。這些礦工可以選擇在候選塊#100,000中包括任何有效的交易(尚未開採)。他們不必使用相同的交易來恢復塊。事實上,他們有動力選擇最有利可圖(最高每kBB)的交易來包含在其中。它們可以包括處於“舊”塊#100,000中的任何交易,以及來自當前記憶體池的任何交易。當他們重新建立塊#100,000時,他們本質上可以將交易從“現在”提取到重寫的“過去”中。

今天,這種襲擊並不是非常有利可圖,因為回報獎勵(因為包括一定數量的比特幣獎勵)遠遠高於每個區塊的總費用。但在未來的某個時候,交易費將是獎勵的大部分(甚至是獎勵的整體)。那時候這種情況變得不可避免了。

為了防止“費用狙擊”,當Bitcoin Core /錢包 建立交易時,預設情況下,它使用nLocktime將它們限制為“下一個塊”。在我們的環境中,Bitcoin Core /錢包將在任何建立的交易上將nLocktime設定為100,001。在正常情況下,這個nLocktime沒有任何效果 - 交易只能包含在#100,001塊中,這是下一個區塊。 但是在區塊鏈分叉攻擊的情況下,由於所有這些交易都將被時間鎖阻止在#100,001,所以礦工們無法從籌碼中提取高額交易。他們只能在當時有效的任何交易中重新挖礦#100,000,這導致實質上不會獲得新的費用。 為了實現這一點,Bitcoin Core/錢包將所有新交易的nLocktime設定為<current block #+ 1>,並將所有輸入上的nSequence設定為0xFFFFFFFE以啟用nLocktime。

7.6具有流量控制的指令碼(條件子句 (Conditional Clauses))

比特幣指令碼的一個更強大的功能是流量控制,也稱為條件條款。您可能熟悉使用構造IF … THEN … ELSE的各種程式語言中的流控制。比特幣條件條款看起來有點不同,但是基本上是相同的結構。

在基本層面上,比特幣條件操作碼允許我們構建一個具有兩種解鎖方式的贖回指令碼,這取決於評估邏輯條件的TRUE / FALSE結果。例如,如果x為TRUE,則贖回指令碼為A,ELSE贖回指令碼為B. 此外,比特幣條件表示式可以無限期地“巢狀”,這意味著這個條件語句可以包含其中的另外一個條件,另外一個條件其中包含別的條件等等 。Bitcoin指令碼流控制可用於構造非常複雜的指令碼,具有數百甚至數千個可能的執行路徑。巢狀沒有限制,但協商一致的規則對指令碼的最大大小(以位元組為單位)施加限制。

比特幣使用IF,ELSE,ENDIF和NOTIF操作碼實現流量控制。此外,條件表示式可以包含布林運算子,如BOOLAND,BOOLOR和NOT。

乍看之下,您可能會發現比特幣的流量控制指令碼令人困惑。那是因為比特幣指令碼是一種堆疊語言。同樣的方式,當1+1看起來“向後”當表示為1 1 ADD時,比特幣中的流控制條款也看起來“向後”(backward)。 在大多數傳統(程式)程式語言中,流控制如下所示: 大多數程式語言中的流控制虛擬碼

if (condition):
  code to run when condition is true
else:
  code to run when condition is false
code to run in either case

在基於堆疊的語言中,比如比特幣指令碼,邏輯條件出現在IF之前,這使得它看起來像“向後”,如下所示: Bitcoin指令碼流控制

 condition
IF
  code to run when condition is true
ELSE
  code to run when condition is false
ENDIF
code to run in either case 

閱讀Bitcoin指令碼時,請記住,評估的條件是在IF操作碼之前。

7.6.1帶有VERIFY操作碼的條件子句

比特幣指令碼中的另一種條件是任何以VERIFY結尾的操作碼。 VERIFY字尾表示如果評估的條件不為TRUE,指令碼的執行將立即終止,並且該交易被視為無效。 與提供替代執行路徑的IF子句不同,VERIFY字尾充當保護子句,只有在滿足前提條件的情況下才會繼續。

例如,以下指令碼需要Bob的簽名和產生特定雜湊的前影象(祕密地)。

解鎖時必須滿足這兩個條件:

1)具有EQUALVERIFY保護子句的贖回指令碼。

HASH160 <expected hash> EQUALVERIFY <Bob's Pubkey> CHECKSIG

為了兌現這一點,Bob必須構建一個解鎖指令碼,提供有效的前影象和簽名:

2)一個解鎖指令碼以滿足上述贖回指令碼。

<Bob's Sig> <hash pre-image>

沒有前影象,Bob無法訪問檢查其簽名的指令碼部分。

該指令碼可以用IF編寫: 具有IF保護條款的兌換指令碼

HASH160 <expected hash> EQUAL
IF
   <Bob's Pubkey> CHECKSIG
ENDIF

Bob的解鎖指令碼是一樣的: 解鎖指令碼以滿足上述兌換指令碼

<Bob's Sig> <hash pre-image>

使用IF的指令碼與使用具有VERIFY字尾的操作碼相同; 他們都作為保護條款。 然而,VERIFY的構造更有效率,使用較少的操作碼。

那麼,我們什麼時候使用VERIFY,什麼時候使用IF? 如果我們想要做的是附加一個前提條件(保護條款),那麼驗證是更好的。 然而,如果我們想要有多個執行路徑(流控制),那麼我們需要一個IF … ELSE流控制子句。

提示 諸如EQUAL之類的操作碼會將結果(TRUE / FALSE)推送到堆疊上,留下它用於後續操作碼的評估。 相比之下,操作碼EQUALVERIFY字尾不會在堆疊上留下任何東西。 在VERIFY中結束的操作碼不會將結果留在堆疊上。

7.6.2在指令碼中使用流控制

比特幣指令碼中流量控制的一個非常常見的用途是構建一個提供多個執行路徑的贖回指令碼,每個指令碼都有一種不同的贖回UTXO的方式。

我們來看一個簡單的例子,我們有兩個簽名人,Alice和Bob,兩人中任何一個都可以兌換。 使用多重簽名,這將被表示為1-of-2 多重簽名指令碼。 為了示範,我們將使用IF子句做同樣的事情:

IF
 <Alice's Pubkey> CHECKSIG
ELSE
 <Bob's Pubkey> CHECKSIG
ENDIF

看這個贖回指令碼,你可能會想:“條件在哪裡?”IF子句之前沒有什麼!“ 條件不是贖回指令碼的一部分。

相反,該解鎖指令碼將提供該條件,允許Alice和Bob“選擇”他們想要的執行路徑。

Alice用解鎖指令碼兌換了這個:

<Alice's Sig> 1

最後的1作為條件(TRUE),將使IF子句執行Alice具有簽名的第一個兌換路徑。

為了兌換這個Bob,他必須通過給IF子句賦一個FALSE值來選擇第二個執行路徑:

<Bob's Sig> 0

Bob的解鎖指令碼在堆疊中放置一個0,導致IF子句執行第二個(ELSE)指令碼,這需要Bob的簽名。

由於可以巢狀IF子句,所以我們可以建立一個“迷宮”的執行路徑。 解鎖指令碼可以提供一個選擇執行路徑實際執行的“地圖”:

IF
    script A
ELSE
   IF
script B
  ELSE
script C
  ENDIF
ENDIF

在這種情況下,有三個執行路徑(指令碼A,指令碼B和指令碼C)。 解鎖指令碼以TRUE或FALSE值的形式提供路徑。

要選擇路徑指令碼B,例如,解鎖指令碼必須以1 0(TRUE,FALSE)結束。

這些值將被推送到堆疊,以便第二個值(FALSE)結束於堆疊的頂部。 外部IF子句彈出FALSE值並執行第一個ELSE子句。 然後,TRUE值移動到堆疊的頂部,並通過內部(巢狀)IF來評估,選擇B執行路徑。

使用這個結構,我們可以用數十或數百個執行路徑構建贖回指令碼,每個指令碼提供了一種不同的方式來兌換UTXO。 要花費,我們構建一個解鎖指令碼,通過在每個流量控制點的堆疊上放置相應的TRUE和FALSE值來導航執行路徑。

7.7複雜的指令碼示例

在本節中,我們將本章中的許多概念合併成一個例子。 我們的例子使用了迪拜公司所有者Mohammed的故事,他們正在經營進出口業務。

在這個例子中,Mohammed希望用靈活的規則建立公司資本賬戶。他建立的方案需要不同級別的授權,具體取決於時間鎖定。

多重簽名的計劃的參與者是Mohammed,他的兩個合作伙伴Saeed和Zaira,以及他們的公司律師Abdul。三個合作伙伴根據多數規則作出決定,因此三者中的兩個必須同意。然而,如果他們的鑰匙有問題,他們希望他們的律師能夠用三個合作伙伴簽名之一收回資金。最後,如果所有的合作伙伴一段時間都不可用或無行為能力,他們希望律師能夠直接管理該帳戶。

這是Mohammed設計的指令碼: 具有時間鎖定(Timelock)變數的多重簽名

IF
  IF
    2
  ELSE
    <30 days> CHECKSEQUENCEVERIFY DROP
    <Abdul the Lawyer's Pubkey> CHECKSIGVERIFY
    1
  ENDIF
  <Mohammed's Pubkey> <Saeed's Pubkey> <Zaira's Pubkey> 3 CHECKMULTISIG
ELSE
  <90 days> CHECKSEQUENCEVERIFY DROP
  <Abdul the Lawyer's Pubkey> CHECKSIG
ENDIF

Mohammed的指令碼使用巢狀的IF … ELSE流控制子句來實現三個執行路徑。

在第一個執行路徑中,該指令碼作為三個合作伙伴的簡單的2-of-3 multisig操作。

該執行路徑由第3行和第9行組成。第3行將multisig的定額設定為2(2 - 3)。

該執行路徑可以通過在解鎖指令碼的末尾設定TRUE TRUE來選擇: 解鎖第一個執行路徑的指令碼(2-of-3 multisig)

0 <Mohammed's Sig> <Zaira's Sig> TRUE TRUE

提示 此解鎖指令碼開頭的0是因為CHECKMULTISIG中的錯誤從堆疊中彈出一個額外的值。 額外的值被CHECKMULTISIG忽略,否則指令碼簽名將失敗。 推送0(通常)是解決bug的方法,如CHECKMULTISIG執行中的錯誤章節所述。

第二個執行路徑只能在UTXO建立30天后才能使用。 那時候,它需要簽署Abdul(律師)和三個合作伙伴之一(三分之一)。

這是通過第7行實現的,該行將多選的法定人數設定為1。要選擇此執行路徑,解鎖指令碼將以FALSE TRUE結束: 解鎖第二個執行路徑的指令碼(Lawyer + 1-of-3)

0 <Saeed's Sig> <Abdul's Sig> FALSE TRUE

提示 為什麼先FALSE後TRUE? 反了嗎?這是因為這兩個值被推到堆疊,所以先push FALSE,然後push TRUE。 因此,第一個IF操作碼首先彈出的是TRUE。

最後,第三個執行路徑允許律師單獨花費資金,但只能在90天之後。 要選擇此執行路徑,解鎖指令碼必須以FALSE結束: 解鎖第三個執行路徑的指令碼(僅適用於律師)

<Abdul's Sig> FALSE

在紙上執行指令碼來檢視它在堆疊(stack)上的行為。

閱讀這個例子還需要考慮幾件事情。 看看你能找到答案嗎?

  • 為什麼律師可以隨時通過在解鎖指令碼中選擇FALSE來兌換第三個執行路徑?
  • 在UTXO開採後分別有多少個執行路徑可以使用5,35與105天?
  • 如果律師失去鑰匙,資金是否流失? 如果91天過去了,你的答案是否會改變?
  • 合作伙伴如何每隔29天或89天“重置”一次,以防止律師獲得資金?
  • 為什麼這個指令碼中的一些CHECKSIG操作碼有VERIFY字尾,而其他的沒有?