作者:benma
來源:https://shiftcrypto.ch/blog/understanding-bitcoin-miniscript-part-3/
本文為 “理解比特幣腳本” 系列的第三篇。前篇見此處。

在本系列的上一篇文章中,我們介紹了 Miniscript 是什麼,以及它如何映射成 Bitcoin Script。
要在細節上理解 Miniscript 的工作原理,看一種它的實現案例 —— 包括如何分析代碼的正確性、如何創建收款地址以及如何花費資金 —— 會有所幫助。
那麼,我們來了解和編寫一種 Go 語言的實現。
我們將始終以 https://bitcoin.sipa.be/miniscript/ 為參考,因為它包含了所有 Miniscript 片段的詳述和特性。
每一個章節的頂部和底部都會包含一個跳到 Go 在線運行環境的鏈接,你可以在那裡運行代碼、檢查結果以及修補它。
簡而言之,我們將把一段 Miniscript 轉化成一個抽象語法樹(AST),然後執行一系列的樹轉換和樹遍歷,以執行正確性分析、創建對應的 Bitcoin Script、創建收款地址,等等。
聲明:下文中的實現沒有經過審核和測試。請不要用在生產環境中。它只能用於教學目的。
第一步:轉化成抽象語法樹
Miniscript 表達式很簡單,也容易轉化成一棵 AST。不像 數學/代數 表達式,Miniscript 表達式不包含任何中置操作符(infix operator)、分組圓括號,而且圓括號僅用來包圍片段的參數。因此,它是易於表達也易於解析的。
我們來定義所需的 AST:
// AST 是用來表示一個 Miniscript 表達式的抽象語法樹。type AST struct { wrappers string identifier string args []*AST}像 or_b 這樣的標識符將被存儲在 identifier 字段中。如有任何封裝器,例如 ascd:X,封裝其都會被分離,然後存儲在 wrappers 字段中。最後,片段的參數將遞歸存儲在 args 中。
為了將一個表達式轉化成一棵樹,我們需要老舊而可靠的堆棧數據結構:
type stack struct { elements []*AST}func (s *stack) push(element *AST) { s.elements = append(s.elements, element)}func (s *stack) pop() *AST { if len(s.elements) == 0 { return nil } top := s.elements[len(s.elements)-1] s.elements = s.elements[:len(s.elements)-1] return top}func (s *stack) top() *AST { if len(s.elements) == 0 { return nil } return s.elements[len(s.elements)-1]}func (s *stack) size() int { return len(s.elements)}為了將表達式使用堆棧轉化成一棵樹,我們先將一個表達式拆分成以圓括號和逗號分割的部分。不走運的是,Go 標準庫裡面沒有線程的函數,所以我讓 ChatGPT 給我寫了一段代碼,而且很成功:
/ - 使用 ChatGPT 編寫。// splitString 函數使用 separator 作為分割元素單元,將一個字數串// 基於多種 separator 分割成字符串切片。它也將// 把輸出切片中的空元素移除。func splitString(s string, isSeparator func(c rune) bool) []string { // Create a slice to hold the substrings substrs := make([]string, 0) // Set the initial index to zero i := 0 // Iterate over the characters in the string for i < len(s) { // Find the index of the first separator in the string j := strings.IndexFunc(s[i:], isSeparator) if j == -1 { // If no separator was found, append the remaining substring and return substrs = append(substrs, s[i:]) return substrs } j += i // If a separator was found, append the substring before it if j > i { substrs = append(substrs, s[i:j]) } // Append the separator as a separate element substrs = append(substrs, s[j:j+1]) i = j + 1 } return substrs}一個快速的單元測試,確認了這段代碼是能工作的:
func TestSplitString(t *testing.T) { separators := func(c rune) bool { return c == '(' || c == ')' || c == ',' } require.Equal(t, []string{}, splitString("", separators)) require.Equal(t, []string{"0"}, splitString("0", separators)) require.Equal(t, []string{"0", ")", "(", "1", "("}, splitString("0)(1(", separators)) require.Equal(t, []string{"or_b", "(", "pk", "(", "key_1", ")", ",", "s:pk", "(", "key_2", ")", ")"}, splitString("or_b(pk(key_1),s:pk(key_2))", separators))}我們已經準備號遍歷這些碎片和 圓括號/逗號,然後建立一棵表達式樹了。
無論什麼時候,只要見到標識符(圓括號和逗號以外的任何東西),我們就把標識符推入棧中,它將成為它所有的子參數的父節點。無論什麼時候,只要遇到逗號或者後圓括號,我們就知道這表示參數的結尾,所以我們從參數從堆棧中彈出,加入到父節點中。一些無效的序列會被明確排除,例如 “()” 和 “(”,它們不會是有效的 miniscript。
func createAST(miniscript string) (*AST, error) { tokens := splitString(miniscript, func(c rune) bool { return c == '(' || c == ')' || c == ',' }) if len(tokens) > 0 { first, last := tokens[0], tokens[len(tokens)-1] if first == "(" || first == ")" || first == "," || last == "(" || last == "," { return nil, errors.New("invalid first or last character") } } // Build abstract syntax tree. var stack stack for i, token := range tokens { switch token { case "(": // Exclude invalid sequences, which cannot appear in valid miniscripts: "((", ")(", ",(". if i > 0 && (tokens[i-1] == "(" || tokens[i-1] == ")" || tokens[i-1] == ",") { return nil, fmt.Errorf("the sequence %s%s is invalid", tokens[i-1], token) } case ",", ")": // End of a function argument - take the argument and add it to the parent's argument // list. If there is no parent, the expression is unbalanced, e.g. `f(X))``. // // Exclude invalid sequences, which cannot appear in valid miniscripts: "(,", "()", ",,", ",)". if i > 0 && (tokens[i-1] == "(" || tokens[i-1] == ",") { return nil, fmt.Errorf("the sequence %s%s is invalid", tokens[i-1], token) } arg := stack.pop() parent := stack.top() if arg == nil || parent == nil { return nil, errors.New("unbalanced") } parent.args = append(parent.args, arg) default: if i > 0 && tokens[i-1] == ")" { return nil, fmt.Errorf("the sequence %s%s is invalid", tokens[i-1], token) } // Split wrappers from identifier if they exist, e.g. in "dv:older", "dv" are wrappers // and "older" is the identifier. wrappers, identifier, found := strings.Cut(token, ":") if !found { // No colon => Cut returns `identifier, ""`, not `"", identifier"`. wrappers, identifier = identifier, wrappers } else if wrappers == "" { return nil, fmt.Errorf("no wrappers found before colon before identifier: %s", identifier) } else if identifier == "" { return nil, fmt.Errorf("no identifier found after colon after wrappers: %s", wrappers) } stack.push(&AST{wrappers: wrappers, identifier: identifier}) } } if stack.size() != 1 { return nil, errors.New("unbalanced") } return stack.top(), nil}Let's also add a function to draw the tree, so we can visualize it more easily:func (a *AST) drawTree(w io.Writer, indent string) { if a.wrappers != "" { fmt.Fprintf(w, "%s:", a.wrappers) } fmt.Fprint(w, a.identifier) fmt.Fprintln(w) for i, arg := range a.args { mark := "" delim := "" if i == len(a.args)-1 { mark = "└──" } else { mark = "├──" delim = "|" } fmt.Fprintf(w, "%s%s", indent, mark) arg.drawTree(w, indent+delim+strings.Repeat(" ", len([]rune(arg.identifier))+len([]rune(mark))-1-len(delim))) }}func (a *AST) DrawTree() string { var b strings.Builder a.drawTree(&b, "") return b.String()}我們用一個複雜的表達式來試一下:
func main() { node, err := createAST("andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(H)),older(1008)),pk(key_revocation))") if err != nil { panic(err) } fmt.Println(node.DrawTree())}成功!輸出是這樣的:
andor├──pk| └──key_remote├──or_i| ├──and_v| | ├──v:pkh| | | └──key_local| | └──hash160| | └──H| └──older| └──1008└──pk └──key_revocation當然,解析器還沒有檢查過,所以 unknownFragment(foo,bar) 這樣的表達式也能被轉化成 AST:
unknownFragment├──foo└──bar第二步:檢查片段和參數號
作為多次樹遍歷的第一步,我們要作第一種容易的檢查:樹上的每一個片段標識符都是有效的嗎?每一種都有正確的參數號碼?
根據規範,所有的片段排列如下:
const ( // 所有的片段標識符 f_0 = "0" // 0 f_1 = "1" // 1 f_pk_k = "pk_k" // pk_k(key) f_pk_h = "pk_h" // pk_h(key) f_pk = "pk" // pk(key) = c:pk_k(key) f_pkh = "pkh" // pkh(key) = c:pk_h(key) f_sha256 = "sha256" // sha256(h) f_ripemd160 = "ripemd160" // ripemd160(h) f_hash256 = "hash256" // hash256(h) f_hash160 = "hash160" // hash160(h) f_older = "older" // older(n) f_after = "after" // after(n) f_andor = "andor" // andor(X,Y,Z) f_and_v = "and_v" // and_v(X,Y) f_and_b = "and_b" // and_b(X,Y) f_and_n = "and_n" // and_n(X,Y) = andor(X,Y,0) f_or_b = "or_b" // or_b(X,Z) f_or_c = "or_c" // or_c(X,Z) f_or_d = "or_d" // or_d(X,Z) f_or_i = "or_i" // or_i(X,Z) f_thresh = "thresh" // thresh(k,X1,...,Xn) f_multi = "multi" // multi(k,key1,...,keyn))older、after、thresh 和 multi 的第一個參數都是數字。在我們需要解析它、檢查它是不是一個有效的數字時,我們要將它轉化成一個數字並存儲在我們的 AST 中,以備後續使用。因此,我們給 AST 增加一個新的字段:
// AST is the abstract syntax tree representing a Miniscript expression.type AST struct { wrappers string identifier string // 在標識符預計是個數字時解析出來的整數。 // 比如 older/after/multi/thresh 的第一個參數。否則不使用 num uint64 args []*AST}我們也需要一種函數,可以遞歸地遍歷整棵樹,然後給每一個 Miniscript 表達式/子表達式 使用一個函數。這種轉化函數可以修改一個節點,或者直接將它替換成一個新節點,這對最後一個階段的解析是有用的:
func (a *AST) apply(f func(*AST) (*AST, error)) (*AST, error) { for i, arg := range a.args { // 我們並不遞歸進入不是 Miniscript 表達式的參數: // key/hash 標量以及 older/after/multi/thresh 的數值參數。 switch a.identifier { case f_pk_k, f_pk_h, f_pk, f_pkh, f_sha256, f_hash256, f_ripemd160, f_hash160, f_older, f_after, f_multi: // 這些函數的變量都不是 Miniscript 表達式,只是 // 變量(或者說具體的指定)或者數字。 continue case f_thresh: // 第一個參數是一個數字,其它的參數是子表達式, // 就是我們想要遍歷的東西,所以我們只跳過第一個參數。 if i == 0 { continue } } new, err := arg.apply(f) if err != nil { return nil, err } a.args[i] = new } return f(a)}案例:
node, _ := createAST("andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(H)),older(1008)),pk(key_revocation))")node.apply(func(node *AST) (*AST, error) { fmt.Println("Visiting node:", node.identifier) return node, nil })輸出:
Visiting node: pkVisiting node: pkhVisiting node: hash160Visiting node: and_vVisiting node: olderVisiting node: or_iVisiting node: pkVisiting node: andor現在,我們加入一個 Parse 函數,它會創建 AST 並連續應用轉化函數,其中第一種轉化函數就是片段和參數檢查器:
func Parse(miniscript string) (*AST, error) { node, err := createAST(miniscript) if err != nil { return nil, err } for _, transform := range []func(*AST) (*AST, error){ argCheck, // More stages to come } { node, err = node.apply(transform) if err != nil { return nil, err } } return node, nil}這個 argCheck 函數會對樹的每一個節點使用,而且我們可以簡單枚舉所有的有效片段標識符,來執行這種基礎檢查:
// argCheck 會檢查每一個標識符都是一種已知的 Miniscript 標識符,並且// 具有正確數量的參數,例如 `andor(X,Y,Z)` 必須有三個參數,等等。func argCheck(node *AST) (*AST, error) { // Helper function to check that this node has a specific number of arguments. expectArgs := func(num int) error { if len(node.args) != num { return fmt.Errorf("%s expects %d arguments, got %d", node.identifier, num, len(node.args)) } return nil } switch node.identifier { case f_0, f_1: if err := expectArgs(0); err != nil { return nil, err } case f_pk_k, f_pk_h, f_pk, f_pkh, f_sha256, f_ripemd160, f_hash256, f_hash160: if err := expectArgs(1); err != nil { return nil, err } if len(node.args[0].args) > 0 { return nil, fmt.Errorf("argument of %s must not contain subexpressions", node.identifier) } case f_older, f_after: if err := expectArgs(1); err != nil { return nil, err } _n := node.args[0] if len(_n.args) > 0 { return nil, fmt.Errorf("argument of %s must not contain subexpressions", node.identifier) } n, err := strconv.ParseUint(_n.identifier, 10, 64) if err != nil { return nil, fmt.Errorf( "%s(k) => k must be an unsigned integer, but got: %s", node.identifier, _n.identifier) } _n.num = n if n < 1 || n >= (1<<31) { return nil, fmt.Errorf("%s(n) -> n must 1 ≤ n < 2^31, but got: %s", node.identifier, _n.identifier) } case f_andor: if err := expectArgs(3); err != nil { return nil, err } case f_and_v, f_and_b, f_and_n, f_or_b, f_or_c, f_or_d, f_or_i: if err := expectArgs(2); err != nil { return nil, err } case f_thresh, f_multi: if len(node.args) < 2 { return nil, fmt.Errorf("%s must have at least two arguments", node.identifier) } _k := node.args[0] if len(_k.args) > 0 { return nil, fmt.Errorf("argument of %s must not contain subexpressions", node.identifier) } k, err := strconv.ParseUint(_k.identifier, 10, 64) if err != nil { return nil, fmt.Errorf( "%s(k, ...) => k must be an integer, but got: %s", node.identifier, _k.identifier) } _k.num = k numSubs := len(node.args) - 1 if k < 1 || k > uint64(numSubs) { return nil, fmt.Errorf( "%s(k) -> k must 1 ≤ k ≤ n, but got: %s", node.identifier, _k.identifier) } if node.identifier == f_multi { // 一個 multisig 可以擁有的最大公鑰數量。 const multisigMaxKeys = 20 if numSubs > multisigMaxKeys { return nil, fmt.Errorf("number of multisig keys cannot exceed %d", multisigMaxKeys) } // Multisig 的密鑰是一種變量,無法擁有子表達式。 for _, arg := range node.args { if len(arg.args) > 0 { return nil, fmt.Errorf("arguments of %s must not contain subexpressions", node.identifier) } } } default: return nil, fmt.Errorf("unrecognized identifier: %s", node.identifier) } return node, nil}有了這些檢查,我們已經可以排除大部分的無效 Miniscript 了。我們來看一些案例:
func main() { for _, expr := range []string{ "invalid", "pk(key1,tooManyArgs)", "pk(key1(0))", "and_v(0)", "after(notANumber)", "after(-1)", "multi(0,k1)", "multi(2,k1)", "multi(1,k1,k2,k3,k4,k5,k6,k7,k8,k9,k10,k11,k12,k13,k14,k15,k16,k17,k18,k19,k20,k21)", } { _, err := Parse(expr) fmt.Println(expr, " -- ", err) }}輸出:
invalid -- unrecognized identifier: invalidpk(key1,tooManyArgs) -- pk expects 1 arguments, got 2pk(key1(0)) -- argument of pk must not contain subexpressionsand_v(0) -- and_v expects 2 arguments, got 1after(notANumber) -- after(k) => k must be an unsigned integer, but got: notANumberafter(-1) -- after(k) => k must be an unsigned integer, but got: -1multi(0,k1) -- multi(k) -> k must 1 ≤ k ≤ n, but got: 0multi(2,k1) -- multi(k) -> k must 1 ≤ k ≤ n, but got: 2multi(1,k1,k2,k3,k4,k5,k6,k7,k8,k9,k10,k11,k12,k13,k14,k15,k16,k17,k18,k19,k20,k21) -- number of multisig keys cannot exceed 20第三步:展開封裝器
每一個片段都可以被封裝器封起來,封裝器使用冒號 “:” 表明。例子來自此處。
dv:older(144) 表示 d: 封裝器應用在了 v: 封裝器上,而 v: 封裝器用來封裝參數為 144 區塊的 older 片段。
在解析器的下一個階段,我們希望同時對封裝器操作,因為它們的動作跟普通的片段一樣:它們可以映射成 Bitcoin Script,也有自己的正確性規則,等等。一句話,dv:older(144) 就只是 d(v(older(144))) 的語法糖。
在這個案例中,我們希望將 AST 從這樣:
dv:older└──144變成這樣:
d└──v └──older └──144為了執行這種轉化,我們要把這個函數加入轉化列表。注意,我們按反序迭代封裝器中的字母,因為它們是從右到左使用的。
// expandWrappers 應用在封裝器(引號以前的字母)上,例如 `ascd:X` =>// `a(s(c(d(X))))`.func expandWrappers(node *AST) (*AST, error) { const allWrappers = "asctdvjnlu" wrappers := []rune(node.wrappers) node.wrappers = "" for i := len(wrappers) - 1; i >= 0; i-- { wrapper := wrappers[i] if !strings.ContainsRune(allWrappers, wrapper) { return nil, fmt.Errorf("unknown wrapper: %s", string(wrapper)) } node = &AST{identifier: string(wrapper), args: []*AST{node}} } return node, nil}第四步:解開糖衣
Miniscript 定義了 6 種語法糖。如果一個 Miniscript 片段包含了下列等式的座標的表達式,那麼這些表達式可以換成等號右邊的表達式。為了節約在後續階段處理這 6 種片段的功夫,我們添加一種去除糖衣的轉化函數,替代這些表達式。
替代關係如下:
pk(key) = c:pk_k(key)pkh(key) = c:pk_h(key)and_n(X,Y) = andor(X,Y,0)t:X = and_v(X,1)l:X = or_i(0,X)u:X = or_i(X,0)現在,我們恰好可以使用我們一開始在封裝器片段中定義的標識符,來擴充這個列表:
const ( // [...] f_wrap_a = "a" // a:X f_wrap_s = "s" // s:X f_wrap_c = "c" // c:X f_wrap_d = "d" // d:X f_wrap_v = "v" // v:X f_wrap_j = "j" // j:X f_wrap_n = "n" // n:X f_wrap_t = "t" // t:X = and_v(X,1) f_wrap_l = "l" // l:X = or_i(0,X) f_wrap_u = "u" // u:X = or_i(X,0)))轉化函數將變成這樣:
// desugar 使用最終的形式替換了語法糖func desugar(node *AST) (*AST, error) { switch node.identifier { case f_pk: // pk(key) = c:pk_k(key) return &AST{ identifier: f_wrap_c, args: []*AST{ { identifier: f_pk_k, args: node.args, }, }, }, nil case f_pkh: // pkh(key) = c:pk_h(key) return &AST{ identifier: f_wrap_c, args: []*AST{ { identifier: f_pk_h, args: node.args, }, }, }, nil case f_and_n: // and_n(X,Y) = andor(X,Y,0) return &AST{ identifier: f_andor, args: []*AST{ node.args[0], node.args[1], {identifier: f_0}, }, }, nil case f_wrap_t: // t:X = and_v(X,1) return &AST{ identifier: f_and_v, args: []*AST{ node.args[0], {identifier: f_1}, }, }, nil case f_wrap_l: // l:X = or_i(0,X) return &AST{ identifier: f_or_i, args: []*AST{ {identifier: f_0}, node.args[0], }, }, nil case f_wrap_u: // u:X = or_i(X,0) return &AST{ identifier: f_or_i, args: []*AST{ node.args[0], {identifier: f_0}, }, }, nil } return node, nil}我們嘗試所有這些方法,並運用視覺檢查:
func main() { for _, expr := range []string{ "pk(key)", "pkh(key)", "and_n(pk(key),sha256(H))", "tv:pk(key)", "l:pk(key)", "u:pk(key)", } { node, err := Parse(expr) if err != nil { panic(err) } fmt.Printf("Tree for \"%v\"\n", expr) fmt.Println(node.DrawTree()) }}從下面的輸出可知,去除糖衣的函數奏效了:
Tree for "pk(key)"c└──pk_k └──keyTree for "pkh(key)"c└──pk_h └──keyTree for "and_n(pk(key),sha256(H))"andor├──c| └──pk_k| └──key├──sha256| └──H└──0Tree for "tv:pk(key)"and_v├──v| └──c| └──pk_k| └──key└──1Tree for "l:pk(key)"or_i├──0└──c └──pk_k └──keyTree for "u:pk(key)"or_i├──c| └──pk_k| └──key└──0第五步:類型檢查
並非所有片段都能相互組合:有些組合無法產生有效的比特幣腳本和有效的見證數據。
但是,因為 Miniscript 表達式和片段是充分結構化、層級式的,所以我們容易靜態分析一段 Miniscript 表達式是否在所有條件下都有效。
舉個例子,or_b(pk(key1),pk(key2)) 和 or_b(v:pk(key1),v:pk(key2)) 不是有效的組合,但 or_b(pk(key1),s:pk(key2)) 是有效的。
根據 Miniscript 規範,每一種片段都可能是四種基礎類型 B、V、K 和 W 之一;每一種片段都可以有額外的類型特性(z、o、n、d 和 u)。
元件片段(不能包含任何子表達式的片段)擁有固定的基礎類型和固定的類型特性。舉個例子,哈希函數片段 sha256(h) 會被轉化成比特幣腳本 SIZE <32> EQUALVERIFY SHA256 <h> EQUAL,可以使用見證數據 <32 byte preimage> 來滿足(其中的數值滿足 sha256(preimage)=h),它的類型為 Bondu,意思是:
B:成功時,向堆棧推入非零值;失敗時推入準確的 0 值。消耗棧頂的元素(如果有的話)。o:消耗一個對戰元素(在這個例子中就是那個原像)n:非零屬性 —— 不能用 0 來滿足。sha256(h)的正確原像必須是 32 字節長,所以無法是 0。d:可以無條件地避開。在sha256()這個例子中,任何 32 字節但並非真正原像的數據都是無效的,這總是可以構造出來的。注意,非 32 字節的值不是有效的避開,因為它會在EQUALVERIFY的時候導致腳本執行終止,而不是繼續執行。u:在滿足的時候,會向堆棧推入 1。
基本的類型和類型屬性是精心定義的,以保證組合出來的片段的正確性。這些屬性可以根據腳本和見證數據的推理,分配給每一種片段。類似地,對於居於子表達式的的片段,例如 and_b(X,Y),你可以分析 X 和 Y 必須滿足什麼樣類型,以及 and_b(X,Y) 自身必須具備什麼樣的衍生類型和屬性。幸運的是,Miniscript 的作者已經完成了這部分工作,而且已經在規範中記錄了正確性表格。
頂層的片段必須具備類型 B,否則這個片段就是無效的。
我們使用基本的類型和類型屬性來擴充我們的 AST:
type basicType stringconst ( typeB basicType = "B" typeV basicType = "V" typeK basicType = "K" typeW basicType = "W")type properties struct { // Basic type properties z, o, n, d, u bool}func (p properties) String() string { s := strings.Builder{} if p.z { s.WriteRune('z') } if p.o { s.WriteRune('o') } if p.n { s.WriteRune('n') } if p.d { s.WriteRune('d') } if p.u { s.WriteRune('u') } return s.String()}// AST is the abstract syntax tree representing a Miniscript expression.type AST struct { basicType basicType props properties wrappers string identifier string // Parsed integer for when identifer is a expected to be a number, i.e. the first argument of // older/after/multi/thresh. Otherwise unused. num uint64 args []*AST}// typeRepr returns the basic type (B, V, K or W) followed by all type properties.func (a *AST) typeRepr() string { return fmt.Sprintf("%s%s", a.basicType, a.props)}然後,我們加入另一個函數來遍歷整棵樹。這個函數將檢查子表達式的類型要求,並根據規範的正確性表格設定類型和類型屬性。
由於這個函數非常長,我們就僅展示它處理少數片段的簡縮版本,僅演示它是如何工作的。片段類型既跟元件有關,也跟參數有關。它根據規範中的表格直接編碼類型規則。舉個例子,為使 s:X 是有效的,X 必須是類型 Bo;而整個片段將具有類型 W,並獲得 X 的 d 和 u 屬性。
你可以在線上環境中看到和運行能夠處理每一種片段的完整版本。
// expectBasicType is a helper function to check that this node has a specific type.func (a *AST) expectBasicType(typ basicType) error { if a.basicType != typ { return fmt.Errorf("expression `%s` expected to have type %s, but is type %s", a.identifier, typ, a.basicType) } return nil}func typeCheck(node *AST) (*AST, error) { switch node.identifier { case f_0: node.basicType = typeB node.props.z = true node.props.u = true node.props.d = true // [...] case f_pk_k: node.basicType = typeK node.props.o = true node.props.n = true node.props.d = true node.props.u = true // [...] case f_or_d: _x, _z := node.args[0], node.args[1] if err := _x.expectBasicType(typeB); err != nil { return nil, err } if !_x.props.d || !_x.props.u { return nil, fmt.Errorf( "wrong properties on `%s`, the first argument of `%s`", _x.identifier, node.identifier) } if err := _z.expectBasicType(typeB); err != nil { return nil, err } node.basicType = typeB node.props.z = _x.props.z && _z.props.z node.props.o = _x.props.o && _z.props.z node.props.d = _z.props.d node.props.u = _z.props.u // [...] case f_wrap_s: _x := node.args[0] if err := _x.expectBasicType(typeB); err != nil { return nil, err } if !_x.props.o { return nil, fmt.Errorf( "wrong properties on `%s`, the first argument of `%s`", _x.identifier, node.identifier) } node.props.d = _x.props.d node.props.u = _x.props.u // [...] } return node, nil}現在,我們已經推導出了所有的類型和類型屬性,我們也需要將頂層表達式必須具有類型 B 的檢查加入最終檢查中:
func Parse(miniscript string) (*AST, error) { node, err := createAST(miniscript) if err != nil { return nil, err } for _, transform := range []func(*AST) (*AST, error){ argCheck, expandWrappers, desugar, typeCheck, // More stages to come } { node, err = node.apply(transform) if err != nil { return nil, err } } // Top-level expression must be of type "B". if err := node.expectBasicType(typeB); err != nil { return nil, err } return node, nil}我們用有效和無效的 Miniscript 片段來檢驗一下:
func main() { expr := "or_b(pk(key1),s:pk(key2))" node, err := Parse(expr) if err == nil { fmt.Println("miniscript valid:", expr) fmt.Println(node.DrawTree()) } for _, expr := range []string{"pk_k(key)", "or_b(pk(key1),pk(key2))"} { _, err = Parse(expr) fmt.Println("miniscript invalid:", expr, "-", err) }}成功!輸出是這樣的:
miniscript valid: or_b(pk(key1),s:pk(key2))or_b [Bdu]├──c [Bondu]| └──pk_k [Kondu]| └──key1└──s [Wdu] └──c [Bondu] └──pk_k [Kondu] └──key2miniscript invalid: pk_k(key) - expression `pk_k` expected to have type B, but is type Kminiscript invalid: or_b(pk(key1),pk(key2)) - expression `c` expected to have type W, but is type B(我們修改了 draw() 函數,以在每個片段旁邊展示其類型。)
第六步:產生比特幣腳本
我們還沒有實現能夠拒絕所有無效 Miniscript 片段的檢查,但現在,可以實驗一下用它來生成比特幣腳本了。
規範中的轉換表定義了 Miniscript 碎片如何映射成比特幣腳本。舉個例子,and_b(X, Y) 映射成 [X] [Y] BOOLAND,等等。
我們會先製作一個函數,它會創建一個可以直接閱讀的腳本的字符串表示,就像那張轉換表一樣。這換我們容易製作出原型並消除 bug,因為你可以容易檢查輸出。實際的腳本是一串字節,我們後面會實現的。
我們加入這個函數,根據轉換表將每一種片段映射成腳本:
func scriptStr(node *AST) string { switch node.identifier { case f_0, f_1: return node.identifier case f_pk_k: return fmt.Sprintf("<%s>", node.args[0].identifier) case f_pk_h: return fmt.Sprintf("DUP HASH160 <HASH160(%s)> EQUALVERIFY", node.args[0].identifier) case f_older: return fmt.Sprintf("<%s> CHECKSEQUENCEVERIFY", node.args[0].identifier) case f_after: return fmt.Sprintf("<%s> CHECKLOCKTIMEVERIFY", node.args[0].identifier) case f_sha256, f_hash256, f_ripemd160, f_hash160: return fmt.Sprintf( "SIZE <32> EQUALVERIFY %s <%s> EQUAL", strings.ToUpper(node.identifier), node.args[0].identifier) case f_andor: return fmt.Sprintf("%s NOTIF %s ELSE %s ENDIF", scriptStr(node.args[0]), scriptStr(node.args[2]), scriptStr(node.args[1]), ) case f_and_v: return fmt.Sprintf("%s %s", scriptStr(node.args[0]), scriptStr(node.args[1])) case f_and_b: return fmt.Sprintf("%s %s BOOLAND", scriptStr(node.args[0]), scriptStr(node.args[1]), ) case f_or_b: return fmt.Sprintf("%s %s BOOLOR", scriptStr(node.args[0]), scriptStr(node.args[1]), ) case f_or_c: return fmt.Sprintf("%s NOTIF %s ENDIF", scriptStr(node.args[0]), scriptStr(node.args[1]), ) case f_or_d: return fmt.Sprintf("%s IFDUP NOTIF %s ENDIF", scriptStr(node.args[0]), scriptStr(node.args[1]), ) case f_or_i: return fmt.Sprintf("IF %s ELSE %s ENDIF", scriptStr(node.args[0]), scriptStr(node.args[1]), ) case f_thresh: s := []string{} for i := 1; i < len(node.args); i++ { s = append(s, scriptStr(node.args[i])) if i > 1 { s = append(s, "ADD") } } s = append(s, node.args[0].identifier) s = append(s, "EQUAL") return strings.Join(s, " ") case f_multi: s := []string{node.args[0].identifier} for _, arg := range node.args[1:] { s = append(s, fmt.Sprintf("<%s>", arg.identifier)) } s = append(s, fmt.Sprint(len(node.args)-1)) s = append(s, "CHECKMULTISIG") return strings.Join(s, " ") case f_wrap_a: return fmt.Sprintf("TOALTSTACK %s FROMALTSTACK", scriptStr(node.args[0])) case f_wrap_s: return fmt.Sprintf("SWAP %s", scriptStr(node.args[0])) case f_wrap_c: return fmt.Sprintf("%s CHECKSIG", scriptStr(node.args[0])) case f_wrap_d: return fmt.Sprintf("DUP IF %s ENDIF", scriptStr(node.args[0])) case f_wrap_v: return fmt.Sprintf("%s VERIFY", scriptStr(node.args[0])) case f_wrap_j: return fmt.Sprintf("SIZE 0NOTEQUAL IF %s ENDIF", scriptStr(node.args[0])) case f_wrap_n: return fmt.Sprintf("%s 0NOTEQUAL", scriptStr(node.args[0])) default: return "<unknown>" }}試一試:
func main() { node, err := Parse("or_d(pk(pubkey1),and_v(v:pk(pubkey2),older(52560)))") if err != nil { panic(err) } fmt.Println(scriptStr(node))}輸出:
<pubkey1> CHECKSIG IFDUP NOTIF <pubkey2> CHECKSIG VERIFY <52560> CHECKSEQUENCEVERIFY ENDIF這是正確的,但還有一處需要優化。v:X 封裝器會映射成 [X] VERIFY。操作符 EQUALVERIFY、CHECKSIGVERIFY 和 CHECKMULTISIGVERIFY 分別是 EQUAL VERIFY、CHECKSIG VERIFY 和 CHECKMULTISIG VERIFY 的縮寫,所以在上述腳本中,CHECKSIG VERIFY 應該被縮寫成 CHECKSIGVERITY,以在腳本中節省一個字節。
如果在 v:X 中,[X] 的最後一個操作符是 EQUAL/CHECKSIG/CHECKMULTISIG,可以替換成 VERITY 版本。
因為 X 可以是任意的表達式,我們需要另一次樹遍歷來確定每個片段的最後一個操作符是否是上述三者之一。
我們把這個屬性加入屬性結構體中:
type properties struct { // Basic type properties z, o, n, d, u bool // Check if the rightmost script byte produced by this node is OP_EQUAL, OP_CHECKSIG or // OP_CHECKMULTISIG. // // If so, it can be be converted into the VERIFY version if an ancestor is the verify wrapper // `v`, i.e. OP_EQUALVERIFY, OP_CHECKSIGVERIFY and OP_CHECKMULTISIGVERIFY instead of using two // opcodes, e.g. `OP_EQUAL OP_VERIFY`. canCollapseVerify bool}而且,這個函數為每一個片段設置了這個字段,我們需要添加到轉化函數列表中:
func canCollapseVerify(node *AST) (*AST, error) { switch node.identifier { case f_sha256, f_ripemd160, f_hash256, f_hash160, f_thresh, f_multi, f_wrap_c: node.props.canCollapseVerify = true case f_and_v: node.props.canCollapseVerify = node.args[1].props.canCollapseVerify case f_wrap_s: node.props.canCollapseVerify = node.args[0].props.canCollapseVerify } return node, nil}and_v 片段和 s- 封裝器是僅有的可以把子表達式作為結尾的可組合片段:and_v(X,Y) => [X] [Y] 和 s:X => SWAP [X],所以,它們會直接獲得來自子節點的特性。腳本中的哈希片段和 thresh/multi/c 最終都會以 EQUAL/CHECKSIG/CHECKMULTISIG 結尾,例如 c:X => [X] CHECKSIG。這些是將被分解為這些操作符的 VERIFY 版本的候選。
然後,我們可以修改我們的scriptStr 函數,在允許的時候使用操作符的 VERIFY 版本。為了簡潔,我們只在下面展示了兩種情形。你可以在這個網站上看到和運行完整的版本。
// collapseVerify is true if the `v` wrapper (VERIFY wrapper) is an ancestor of the node. If so, the// two opcodes `OP_CHECKSIG VERIFY` can be collapsed into one opcode `OP_CHECKSIGVERIFY` (same for// OP_EQUAL and OP_CHECKMULTISIG).func scriptStr(node *AST, collapseVerify bool) string { switch node.identifier { // [...] case f_wrap_c: opVerify := "CHECKSIG" if node.props.canCollapseVerify && collapseVerify { opVerify = "CHECKSIGVERIFY" } return fmt.Sprintf("%s %s", scriptStr(node.args[0], collapseVerify), opVerify, ) // [...] case f_wrap_v: s := scriptStr(node.args[0], true) if !node.args[0].props.canCollapseVerify { s += " VERIFY" } return s}使用修改後的函數運行程序,你會得到這樣的輸出:
<pubkey1> CHECKSIG IFDUP NOTIF <pubkey2> CHECKSIGVERIFY <52560> CHECKSEQUENCEVERIFY ENDIF成功將 CHECKSIG VERITY 簡化為 CHECKSIGVERIFY。
第七步:生成收款地址
在上一個接種,我們為比特幣腳本創建了一個可讀的表示。為了生成一個 P2WSH 地址,我們需要開發出實際的、作為字節序列的腳本,然後編碼成一個地址。
為此,我們先要將公鑰和哈希值變量替換成真實的公鑰和哈希值。我們為 AST 增加一個新的字段:value。
type AST struct { // [...] identifier string // For key arguments, this will be the 33 bytes compressed pubkey. // For hash arguments, this will be the 32 bytes (sha256, hash256) or 20 bytes (ripemd160, hash160) hash. value []byte args []*AST}現在,我們可以增加一個新的函數 ApplyVars,它會將 Miniscript 表達式中的所有變量都替換成真實的數值。調用者可以提供一個回調函數來提供這些數值。
Miniscript 也指明公鑰不可重複(可以簡化腳本的分析),所以我們要檢查複製。
// ApplyVars replaces key and hash values in the miniscript.//// The callback should return `nil, nil` if the variable is unknown. In this case, the identifier// itself will be parsed as the value (hex-encoded pubkey, hex-encoded hash value).func (a *AST) ApplyVars(lookupVar func(identifier string) ([]byte, error)) error { // Set of all pubkeys to check for duplicates allPubKeys := map[string]struct{}{} _, err := a.apply(func(node *AST) (*AST, error) { switch node.identifier { case f_pk_k, f_pk_h, f_multi: var keyArgs []*AST if node.identifier == f_multi { keyArgs = node.args[1:] } else { keyArgs = node.args[:1] } for _, arg := range keyArgs { key, err := lookupVar(arg.identifier) if err != nil { return nil, err } if key == nil { // If the key was not a variable, assume it's the key value directly encoded as // hex. key, err = hex.DecodeString(arg.identifier) if err != nil { return nil, err } } if len(key) != pubKeyLen { return nil, fmt.Errorf("pubkey argument of %s expected to be of size %d, but got %d", node.identifier, pubKeyLen, len(key)) } pubKeyHex := hex.EncodeToString(key) if _, ok := allPubKeys[pubKeyHex]; ok { return nil, fmt.Errorf( "duplicate key found at %s (key=%s, arg identifier=%s)", node.identifier, pubKeyHex, arg.identifier) } allPubKeys[pubKeyHex] = struct{}{} arg.value = key } case f_sha256, f_hash256, f_ripemd160, f_hash160: arg := node.args[0] hashLen := map[string]int{ f_sha256: 32, f_hash256: 32, f_ripemd160: 20, f_hash160: 20, }[node.identifier] hashValue, err := lookupVar(arg.identifier) if err != nil { return nil, err } if hashValue == nil { // If the hash value was not a variable, assume it's the hash value directly encoded // as hex. hashValue, err = hex.DecodeString(node.args[0].identifier) if err != nil { return nil, err } } if len(hashValue) != hashLen { return nil, fmt.Errorf("%s len must be %d, got %d", node.identifier, hashLen, len(hashValue)) } arg.value = hashValue } return node, nil }) return err}看看它的實際情況:
func main() { node, err := Parse("or_d(pk(pubkey1),and_v(v:pk(pubkey2),older(52560)))") if err != nil { panic(err) } unhex := func(s string) []byte { b, _ := hex.DecodeString(s) return b } // Two arbitrary pubkeys. _, pubKey1 := btcec.PrivKeyFromBytes( unhex("2c3931f593f26037a8b8bf837363831b18bbfb91a712dd9d862db5b9b06dc5df")) _, pubKey2 := btcec.PrivKeyFromBytes( unhex("f902f94da618721e516d0a2a2666e2ec37079aaa184ee5a2c00c835c5121b3eb")) err = node.ApplyVars(func(identifier string) ([]byte, error) { switch identifier { case "pubkey1": return pubKey1.SerializeCompressed(), nil case "pubkey2": return pubKey2.SerializeCompressed(), nil } return nil, nil }) if err != nil { panic(err) } fmt.Println(node.DrawTree())}輸出,公鑰已成功替換:
or_d [B]├──c [Bonduv]| └──pk_k [Kondu]| └──pubkey1 [03469d685c3445e83ee6e3cfb30382795c249c91955523c25f484d69379c7a7d6f]└──and_v [Bon] ├──v [Von] | └──c [Bonduv] | └──pk_k [Kondu] | └──pubkey2 [03ba991cc359438fdd8cf43e3cf7894f90cf4d0e040314a6bba82963fa77b7a434] └──older [Bz] └──52560(我們修改 drawTree() 函數,以在每個變量旁邊顯示實際數值。)
有了這個絕妙的 btcd 庫的幫助,我們現在可以構建出實際的腳本了。它看起來非常像上面的 scriptStr(),但將它編碼成了一個字節串,關注於將整數和數據推入堆棧的實際問題。我們在這裡使用了一個縮減版本。完整版本可見這個網站。
// Script creates the witness script from a parsed miniscript.func (a *AST) Script() ([]byte, error) { b := txscript.NewScriptBuilder() if err := buildScript(a, b, false); err != nil { return nil, err } return b.Script()}// collapseVerify is true if the `v` wrapper (VERIFY wrapper) is an ancestor of the node. If so, the// two opcodes `OP_CHECKSIG VERIFY` can be collapsed into one opcode `OP_CHECKSIGVERIFY` (same for// OP_EQUAL and OP_CHECKMULTISIGVERIFY).func buildScript(node *AST, b *txscript.ScriptBuilder, collapseVerify bool) error { switch node.identifier { case f_0: b.AddOp(txscript.OP_FALSE) case f_1: b.AddOp(txscript.OP_TRUE) case f_pk_h: arg := node.args[0] key := arg.value if key == nil { return fmt.Errorf("empty key for %s (%s)", node.identifier, arg.identifier) } b.AddOp(txscript.OP_DUP) b.AddOp(txscript.OP_HASH160) b.AddData(btcutil.Hash160(key)) b.AddOp(txscript.OP_EQUALVERIFY) case f_older: b.AddInt64(int64(node.args[0].num)) b.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) case f_after: b.AddInt64(int64(node.args[0].num)) b.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) case f_and_b: if err := buildScript(node.args[0], b, collapseVerify); err != nil { return err } if err := buildScript(node.args[1], b, collapseVerify); err != nil { return err } b.AddOp(txscript.OP_BOOLAND) case f_wrap_c: if err := buildScript(node.args[0], b, collapseVerify); err != nil { return err } if node.props.canCollapseVerify && collapseVerify { b.AddOp(txscript.OP_CHECKSIGVERIFY) } else { b.AddOp(txscript.OP_CHECKSIG) } case f_wrap_v: if err := buildScript(node.args[0], b, true); err != nil { return err } if !node.args[0].props.canCollapseVerify { b.AddOp(txscript.OP_VERIFY) } // More cases [...] default: return fmt.Errorf("unknown identifier: %s", node.identifier) } return nil}運行它吧:
func main() { // [...] script, err := node.Script() if err != nil { panic(err) } fmt.Println("Script", hex.EncodeToString(script))}輸出:
Script 2103469d685c3445e83ee6e3cfb30382795c249c91955523c25f484d69379c7a7d6fac73642103ba991cc359438fdd8cf43e3cf7894f90cf4d0e040314a6bba82963fa77b7a434ad0350cd00b268根據 BIP141 和 BIP173,P2WSH 地址應該是 0 <sha256(script)> 的 bech32 編碼,其中 0 表示隔離見證版本 0。我們將使用這個 btcd 庫來幫助我們創建地址:
addr, err := btcutil.NewAddressWitnessScriptHash(chainhash.HashB(script), &chaincfg.TestNet3Params)if err != nil { panic(err)}fmt.Println("Address:", addr.String())我們的測試網收款地址準備好了:
Address: tb1q4q3cw0mausmamm7n7fn2phh0fpca4n0vmkc7rdh6hxnkz9rd8l0qcpefrj這個地址收到的資金將使用花費條件 or_d(pk(pubkey1),and_v(v:pk(pubkey2),older(52560))) 鎖定,即,公鑰 1 可以隨時花費;公鑰 2 可以在資金進入這個地址的 52560 個區塊(大約一年)後花費。
結論
評論是,我們已經創建了一個 Miniscript 代碼庫,可以解析 Miniscript 表達式並執行類型檢查,並生成收款地址。
還有很多情形需要考慮。在下一篇文章中,我們將學習如何根據 Miniscript 表達式生成見證數據,從而可以花費資金;如何保證 Miniscript 遵守比特幣的共識以及標準,例如腳本的體積和操作符限制。
如果你希望這個系列繼續,請在 Twitter 上告訴我:@benma。




