作者:Rusty Russell
來源:https://rusty.ozlabs.org/2024/01/19/the-great-opcode-restoration.html
編者注:本文為作者 Rusty Russell 的 “Great Script Restoration”(直譯為 “Script 復興”)提議的概述,公開出版於 2024 年 1 月。“Script 復興” 的想法是恢復比特幣腳本在 2010 年由於 DoS 問題而被禁用的操作碼;為避免重新啟用這些操作碼會造成同樣的 DoS 問題,作者提出了一套新的約束交易的驗證資源使用量的方法,並提出了新的操作碼。
2025 年 9 月,作者已經為本文中涉及的想法撰寫了相關的 4 個 BIP。
在我過去幾篇文章中,我一直在謹慎地思考,如果我們有了 “內省(introspection)”,可能想要給 Bitcoin Script(比特幣腳本編程語言)添加什麼升級。Sccript 因為拒絕服務式攻擊問題而止步於 0.3.1 版本:這一直是人們的一個遺憾,但是,像 OP_TXHASH
這樣的功能,讓 Script 的侷限性變得清晰。
陳舊的 Bitcoin Script
許多人都知道,中本聰在 0.3.1 版本中禁用了 OP_CAT
和其它幾種操作碼,但 Anthony Towns 指出,在 0.3 版本以前,bitcoin 軟件也允許使用 OpenSSL 的 BIGNUM 類型,實現任意長度的數值。
那是比特幣項目的起步階段,我完全理解這種願望:立即、清楚地避免 DoS 問題,等到這個問題經過慎重考慮之後再恢復功能。不幸的是,直到多年以後(也就是我們現在所處的位置),人們才理解為 Script 增強功能的難度。
變長操作碼預算:完全恢復 Script 功能而不引入 DoS
BIP-342 將全局的簽名數量限制替換為一種基於重量(weight)的簽名操作預算,該預算設計為能夠支持任何合理的簽名驗證(比如可以由 miniscript 製作的腳本),又足以避免 DoS 。
我們可以在其它操作上使用這個辦法,只要這種操作的開銷與它們的運算對象(operand)的體積相關,並類似地移除現有腳本系統中的武斷限制。我將這種想法稱為 “變長操作碼(varops)” 預算,因為它適用於在變長對象上的操作。
我的提議草案將變長操作碼預算設得很簡單:
- 交易的重量乘以 520 。
這保證了即使在現有的腳本上強制執行預算,也不會有可以想象到的腳本無法執行(例如,每一個 OP_SHA256 都總可以在最大長度的堆棧元素上操作,它自身的操作碼重量就足以支撐其預算)。
注意:這種預算是在整個交易上生效的,不是以輸入為單位的:這是因為預期會有內存操作碼,它意味著一個非常短的腳本可能會檢查其它非常大的輸入。
每一個操作碼的預算消耗量如下(未列出的操作碼不消耗預算):
操作碼 | 變長操作碼預算消耗量 |
---|---|
OP_CAT | 0 |
OP_SUBSTR | 0 |
OP_LEFT | 0 |
OP_RIGHT | 0 |
OP_INVERT | 1 + len(a) / 8 |
OP_AND | 1 + MAX(len(a), len(b)) / 8 |
OP_OR | 1 + MAX(len(a), len(b)) / 8 |
OP_XOR | 1 + MAX(len(a), len(b)) / 8 |
OP_2MUL | 1 + len(a) / 8 |
OP_2DIV | 1 + len(a) / 8 |
OP_ADD | 1 + MAX(len(a), len(b)) / 8 |
OP_SUB | 1 + MAX(len(a), len(b)) / 8 |
OP_MUL | (1 + len(a) / 8) * (1 + len(b) / 8 |
OP_DIV | (1 + len(a) / 8) * (1 + len(b) / 8 |
OP_MOD | (1 + len(a) / 8) * (1 + len(b) / 8 |
OP_LSHIFT | 1 + len(a) / 8 |
OP_RSHIFT | 1 + len(a) / 8 |
OP_EQUAL | 1 + MAX(len(a), len(b)) / 8 |
OP_NOTEQUAL | 1 + MAX(len(a), len(b)) / 8 |
OP_SHA256 | 1 + len(a) |
OP_RIPEMD160 | 0 (fails if len(a) > 520 bytes) |
OP_SHA1 | 0 (fails if len(a) > 520 bytes) |
OP_HASH160 | 1 + len(a) |
OP_HASH256 | 1 + len(a) |
移除其它限制
Ethan Hilman 的恢復 OP_CAT 的提議保留了 520 字節的限制(譯者注:指單個堆棧元素不得超過 520 字節長的限制)。應用變長操作碼預算之後,就可以移除這一限制,替換成在 taproot v1 上已經應用的堆棧總規模限制(1000 個元素和 520 000 字節)。
此外,如果我們希望引入一個新的隔離見證版本(比如 Anthony Towns 的 “generalized taproot”)或者希望允許 “無密鑰的條件(keyless entry)”,我們可以將這些限制適配為合理的區塊體積上限(也許是 1 0000 個元素,最大 4M 字節)。
稍微改變語義
數值將依然是小端序編碼(little-endian),但變成無符號數(unsigned)。這能簡化實現,而且讓位操作(bit opertations)和算術運算(arithmetic operations)的相互作用變得簡單很多。它允許現有的正數使用這些操作碼而無需修改,也不需要轉換。
如果我們有意使用一個新的隔離見證版本,現有的操作碼可以替換;否則,就需要添加新的操作碼(例如 OP_ADDV
)。
實現細節
v0.3.0 比特幣軟件使用了 OpenSSL 的 BIGNUM 類型的一種簡單的類封裝器,但為了最大限度的簡潔性,我在不使用外部依賴的前提下重新實現了每一個操作碼。
除了 OP_EQUAL
/OP_EQUALVERIFY
,每一個操作碼都轉換為 uint64_t
的一個小端序向量(或從這樣的向量開始轉換)。這可以通過按需轉換來優化。
OP_DIV
、OP_MOD
和 OP_MUL
是粗糙地實現的(與 libgmp 的大數字操作的比較表明,更復雜的方法會快得多)。
基準測試:上述限制是否低到足以防止 DoS ?
上述限制是否高到足以被忽略?
我們可以移除 520 字節的限制。(譯者注:應指單個堆棧對象體積不得超過 520 字節的限制。)
但我們依然需要對堆棧的總體積作一個限制:使用一種新的隔離見證版本,可以將這個限制提高到 400 0000;或者跟當前的限制保持一致:52 0000 字節。
在我之前的《在 Script 中求出腳本公鑰》(中文譯本)一文中,我指出,有些時候,我們希望要求一類特定的腳本條件,但不是一段確切的腳本:一個例子是保險櫃合約類型的限制條款,它要求時延,但不關心腳本里還有沒有別的東西。
問題在於,在 Taproot 腳本中,任何未知的操作碼(OP_SUCCESSx
)都將導致整個腳本通過(完全不會被執行),所以我們需要稍微調整一下。我以往的分隔符的提議很令人尷尬,所以我想出了一個更簡單的新辦法。
加入 OP_SEGMENT
當前,驗證程序會在整個 tapscript 中掃描 OP_SUCCESS
操作碼,一旦找到,腳本就直接通過。這將被修改成:
- 在 tapscript 中掃描
OP_SEGMENT
和OP_SUCCESSx
。 - 如果找到了
OP_SEGMENT
,那就執行位於這個操作碼前面的腳本;如果腳本沒有失敗,那就從這個操作碼開始繼續掃描。 - 如果找到了
OP_SUCCESSx
,那麼腳本直接通過。
基本上,這就將一段腳本分成了幾個 片段,每個片段按順序執行。它並不像 “使用 OP_SEGMENT 將腳本分成幾段,一次只執行一個片段” 這麼簡單,因為 tapscript 允許在 OP_SUCCESSx
之後包含無法解碼的東西,我們希望保留這種能力。
在執行 OP_SEGMENT
的時候,它什麼也不做:它的存在只是為了限制 OP_SUCCESS
操作碼的覆蓋範圍。
實現
ExecuteWitnessScript
將有必要重構(可能是作為一段專門的 ExecuteTapScript
,因為它的 38 行代碼中有 21 行都在 “if Tapscript” 條件下),而且它也暗示了,對當前的 tapscript 的堆棧限制,在遇到了 OP_SEGMENT
時會強制執行,即使後面跟著 OP_SUCCESS
。
有趣的是,核心的 EvalScript
函數不會改變,除了忽視 OP_SEGMENT
,因為它已經非常靈活了。
提醒一句,我還沒完成實現,所以可能會有驚喜,但我計劃在這個想法獲得一些評論之後製作原型。
希望你們喜歡!
(完)