저자: 벤마
출처: https: //shiftcrypto.ch/blog/understanding-bitcoin-miniscript-part-3/
이 글은 "비트코인 스크립팅 이해하기" 시리즈의 세 번째 글입니다. 이전 게시물은 여기에서 확인하실 수 있습니다.

이 시리즈의 이전 글 에서는 미니스크립트가 무엇이며 비트코인 스크립트에 어떻게 매핑되는지 다루었습니다.
미니스크립트가 어떻게 작동하는지 자세히 이해하려면 코드의 정확성을 분석하는 방법, 지급 주소를 만드는 방법, 돈을 쓰는 방법 등 구현된 예시를 살펴보는 것이 도움이 됩니다.
이제 Go 언어로 구현에 대해 배우고 작성해 보겠습니다.
여기에는 모든 미니스크립트 조각에 대한 자세한 설명과 기능이 포함되어 있으므로 항상 https://bitcoin.sipa.be/miniscript/ 을 참조로 사용하겠습니다.
각 섹션의 상단과 하단에는 코드를 실행하고, 결과를 확인하고, 수정할 수 있는 Go 온라인 런타임 환경으로 연결되는 링크가 있습니다.
간단히 말해, 미니스크립트 조각을 추상 구문 트리 (AST)로 변환한 다음 일련의 트리 변환과 트리 탐색을 수행하여 정확성 분석을 수행하고, 해당 비트코인 스크립트를 생성하고, 컬렉션 주소를 생성하는 등의 작업을 수행하겠습니다.
고지 사항: 아래 구현은 검토 또는 테스트되지 않았습니다. 프로덕션 환경에서는 사용하지 마시기 바랍니다. 교육 목적으로만 사용해야 합니다.
1단계: 추상 구문 트리로 변환하기
Go 온라인 환경에서 코드를 실행하려면 여기를 클릭하세요.
미니스크립트 표현식은 수학/대수 표현식과 달리 접두사 연산자, 그룹 괄호, 괄호는 조각의 인수를 둘러싸는 데만 사용되므로 간단하고 쉽게 AST로 변환할 수 있습니다. 따라서 표현하기 쉽고 파싱하기 쉽습니다.
필요한 AST를 정의해 보겠습니다:
// AST는 미니스크립트 표현식을 표현하는 데 사용되는 추상 구문 트리입니다. 유형 AST 구조체 { 래퍼 문자열 식별자 문자열 args []*AST}{}or_b와 같은 식별자는 식별자 필드에 저장됩니다. ascd:X와 같은 래퍼가 있는 경우 래퍼가 분리되어 래퍼 필드에 저장됩니다. 마지막으로 조각에 대한 인수는 재귀적으로 args에 저장됩니다.
표현식을 트리로 변환하려면 오래되었지만 신뢰할 수 있는 스택 데이터 구조가 필요합니다:
유형 스택 구조체 { 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 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 함수는 구분 기호를 기본 분리 단위로 사용하여 단어 문자열을 여러 구분 기호에 따라 // 문자열 조각으로 나눕니다. 또한 출력 슬라이스에서 빈 요소를 // 제거합니다. func splitString(string, isSeparator func(c rune) bool) []string { // 하위 문자열을 담을 슬라이스 생성 substrs := make([]) string, 0) // 초기 인덱스를 0으로 설정 i := 0 // 문자열의 문자를 반복합니다 for i < len(s) { // 문자열에서 첫 번째 구분자의 인덱스를 찾습니다 j := strings. string j := strings.IndexFunc(s[i:], isSeparator) if j == -1 { // 구분자를 찾지 못하면 나머지 부분 문자열을 추가하고 substrs = append( substrs, s[i:]) return substrs } j += i // 구분자가 발견되면, 그 앞에 부분 문자열을 추가합니다 if j > i { substrs = append(substrs, s[i:j]) } // 구분자를 별도의 요소 substrs로 추가합니다. 구분자를 별도의 요소로 추가 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(" ", 구분자)) require.Equal(t, []string{"0"}, splitString("0", 구분자)) 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))", 구분자))})이제 조각과 괄호/쉼표를 반복하여 표현식 트리를 구축할 준비가 되었습니다.
식별자(괄호와 쉼표 이외의 모든 것)를 발견할 때마다 식별자를 스택에 밀어넣고, 이 식별자는 모든 하위 인자의 부모가 됩니다. 쉼표나 뒷 괄호를 발견할 때마다 인수의 끝을 나타내는 것으로 알고 있으므로 인수를 스택에서 꺼내 부모에 추가합니다. 유효한 미니스크립트가 아닌 "()"와 "(" 등 일부 유효하지 않은 시퀀스는 명시적으로 제외됩니다.
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. errors.New("유효하지 않은 첫 번째 또는 마지막 문자") } } } // 추상 구문 트리를 구축합니다. var stack stack for i, token := 범위 토큰 { switch token { case "(": // 유효하지 않은, 유효하지 않은, 유효하지 않은 유효하지 않은 유효하지 않은 시퀀스를 제외합니다. 유효한 미니 스크립트에 나타날 수 없는 유효하지 않은 시퀀스 제외: "((", ")(", ",("). if i > 0 && (tokens[i-1] == "(" || tokens[i-1] == ")" || tokens[i-1] == ",") { return nil, fmt.Errorf("%s%s 시퀀스가 유효하지 않습니다", tokens[i-. 1], 토큰) } case ",", ")": // 함수 인자의 끝 - 인자를 가져와서 부모 인자의 // 목록에 추가합니다. 부모가 없는 경우, 표현식( 표현식은 불균형합니다(예: `f(X))``). // // 유효한 미니 스크립트에 나타날 수 없는 유효하지 않은 시퀀스를 제외합니다: "(,", "()", ",", ",", ",)". if i > 0 && (tokens[i-1] == "(" || tokens[i-1] == ",") { return nil, fmt.Errorf("시퀀스 %s%s가 유효하지 않습니다", tokens[i-1], token) } arg := stack.pop() parent := stack.top() if arg == nil || parent == nil { return nil, errors.New("불균형") } parent.args = append(parent.args, arg) default: if i > 0 && tokens[i-1] == ")" { return nil, fmt.Errorf("시퀀스 %s%s가 유효하지 않습니다.", tokens[i-1], token) } // 래퍼가 존재할 경우 식별자가 존재하는 경우, 예를 들어 "dv:older"에서 "dv"는 래퍼이고 // "older"는 식별자입니다. wrappers, 식별자, found := strings.Cut(token, ":") ) if !found { // 콜론 없음 => Cut은 `"", 식별자"`가 아닌 `identifier, ""`를 반환합니다. wrappers, identifier = 식별자, 래퍼 } else if wrappers == "" { return nil, fmt.Errorf("식별자 앞에 콜론 앞에 래퍼가 없습니다: %s", 식별자) } else if identifier == "" { return nil, fmt. 식별자) } else if identifier == "" { return nil, fmt.Errorf("wrappers 뒤 콜론 뒤에 식별자를 찾을 수 없음: %s", wrappers) } stack.push(&AST{. wrappers: 래퍼, 식별자: 식별자}) } } if stack.size() ! = 1 { return nil, errors.New("불균형") } return stack.top(), nil}더 쉽게 시각화할 수 있도록 트리를 그리는 함수도 추가해 봅시다: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 := "" 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온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
2단계: 조각 및 인수 번호 확인
온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
다중 트리 탐색의 첫 번째 단계로, 트리의 모든 조각 식별자가 유효한가요? 각각의 식별자가 올바른 매개변수 번호를 가지고 있나요?
사양에 따르면 모든 조각은 다음과 같이 배열되어 있습니다:
const ( // 모든 조각 식별자 F_0 = "0" // 0 F_1 = "1" // 1 F_PK_K = "PK_K" // PK_K(키) F_PK_H = "PK_H" // PK_H(키) F_PK = "PK" // PK(키) = C:PK_K(키) F_PKH = "PKH" // PKH. (키) = c:pk_h(키) 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 구조체 { 래퍼 문자열 식별자 문자열 // 식별자가 숫자일 것으로 예상되는 경우 구문 분석할 정수입니다. // 예를 들어, 이전/이후/다중/임계값의 첫 번째 인수입니다. 그렇지 않으면 숫자 uint64 인수 []*AST}가 없습니다.또한 전체 트리를 재귀적으로 탐색한 다음 각 미니스크립트 표현식/서브 표현식에 대해 함수를 사용하는 함수가 필요합니다. 이 변환 함수는 노드를 수정하거나 새 노드로 대체할 수 있으므로 구문 분석의 마지막 단계에 유용합니다:
func (a *AST) apply(f func(*AST) (*AST, error)) (*AST, error) { for i, arg := range a.args { // 미니스크립트 표현식이 아닌 인수를 재귀적으로 입력하지 않음: // 키/해시 스칼라 및 이전/이후 /다중/임계값. 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: // 이 함수들 중 어느 것도 a의 일부가 아닌 변수를 가지고 있지 않습니다. 변수(또는 특정 지정) 또는 숫자만 있습니다. 계속 케이스 f_thresh: // 첫 번째 인수는 숫자이고, 나머지는 하위 표현식으로 // 우리가 트래버스하려는 것이므로 첫 번째 인수는 건너뛰겠습니다. if i == 0 { 계속 } } new, err := arg.apply(f) if err ! = nil { return nil, err } a.args[i] = new } return f(a)}Case:
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("방문 노드:", node.identifier) return node, nil })출력:
방문 노드: pk방문 노드: pkh방문 노드: hash160방문 노드: and_v방문 노드: older방문 노드: or_i방문 노드: pkVisiting 노드: andor이제 AST를 생성하고 연속적인 변환을 적용하는 Parse 함수를 추가하고, 그 중 첫 번째는 조각 및 인수 검사기입니다:
func Parse(miniscript string) (*AST, error) { node, err := createAST(miniscript) if err ! = nil { return nil, err } for _, transform := range []func(*AST) (*AST, error){ argCheck, // 앞으로 더 많은 단계 } { node, err = node.apply(transform) if err ! = nil { return nil, err } } return node, nil} 이 argCheck 함수는 트리의 모든 노드에 사용되며, 유효한 조각 식별자를 모두 열거하는 것만으로 이 기본 검사를 수행할 수 있습니다:
// 각 식별자가 알려진 미니스크립트 식별자인지, // 인자 수가 올바른지, 예를 들어 `andor(X,Y,Z)`는 인자가 3개여야 하는 등입니다. func argCheck(node *AST) (*AST, error) { // 헬퍼 함수를 사용하여 이 노드에 특정 수의 인자가 있는지 확인합니다. expectArgs := func(num int) error { if len(node.args) ! = num { return fmt.Errorf("%s가 %d 인자를 기대합니다, %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("%s의 인수는 하위 표현식을 포함하지 않아야 합니다", 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("%s의 인수는 하위 표현식을 포함하지 않아야 합니다.", node. 식별자) } n, err := strconv.ParseUint(_n.식별자, 10, 64) if err ! = nil { return nil, fmt.Errorf("%s(k) => k는 부호 없는 정수여야 하지만: %s를 얻었습니다.", node.identifier, _n.identifier) } _n.num = n if n < 1 || n >= (1 <<31) { return nil, fmt.Errorf("%s(n) -> n은 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: len(node.args) < 2 { return nil, fmt.Errorf("%s에는 적어도 두 개의 인자가 있어야 합니다", node.identifier ) } _k := node.args[0] len(_k.args) > 0 { return nil, fmt.Errorf("%s의 인수는 하위 표현식을 포함하지 않아야 합니다", node.identifier) } k, err := strconv.ParseUint(_k.identifier, 10, 64) if err ! = nil { return nil, fmt.Errorf( "%s(k, ...) => k는 정수여야 합니다만: %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는 1 ≤ k ≤ n이어야 하지만: %s", node.identifier, _k.identifier) } if node.identifier == f_multi { // 멀티시그가 가질 수 있는 공개키의 최대 개수입니다. 다중서명이 가질 수 있는 공개키의 최대 개수입니다. const multisigMaxKeys = 20 if numSubs > multisigMaxKeys { return nil, fmt.Errorf("다중서명 키의 개수가 %d를 초과할 수 없습니다", multisigMaxKeys) } // 다중서명의 키는 변수이며 하위 표현식을 가질 수 없습니다. for _, arg := 범위 node.args { if len(arg.args) > 0 { return nil, fmt.Errorf("%s의 인수는 하위 표현식을 포함하지 않아야 합니다", node.identifier) } } } 기본값: 반환 nil, fmt.Errorf("인식할 수 없는 식별자: %s", node.identifier) } 반환 node, nil}이러한 검사를 통해 이미 대부분의 잘못된 미니스크립트를 제외할 수 있습니다. 몇 가지 예를 살펴보겠습니다:
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 -- 인식할 수 없는 식별자: invalidpk(key1,tooManyArgs) -- pk는 1 개의 인자를 기대합니다, 2pk(key1(0)) -- pk의 인자에는 subexpressionsand_v(0) -- and_v는 2 개의 인자를 기대, got 1after(notANumber) -- after(k) => k는 부호 없는 정수여야 하지만, got: notANumberafter. (-1) -- after(k) => k는 부호 없는 정수여야 하지만, got: -1multi(0,k1) -- multi(k) -> k는 1 ≤ k ≤ n이어야 하지만, got: 0multi(2,k1) -- multi(k) -> k는 1 ≤ k ≤ n이어야 하지만, got: 0multi(2,k1) -- multi(k) -> k는 1 ≤ k ≤ n이어야 하지만, got: 0 ≤ 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) -- 다중 서명 키의 개수는 초과할 수 없습니다. 20온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
3단계: 래퍼 확장
온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
각 조각은 콜론 ":"으로 표시되는 래퍼로 래핑할 수 있습니다. 여기 예시입니다.
dv:older(144 )는 d: 래퍼가 블록 144의 인수로 이전 조각을 캡슐화하는 데 사용되는 v: 래퍼에 적용됨을 의미합니다.
구문 분석기의 다음 단계에서는 래퍼가 비트코인 스크립트에 매핑될 수 있고, 자체적인 정확성 규칙이 있는 등 일반 조각과 똑같이 작동하기 때문에 래퍼에 대한 작업을 동시에 수행하고자 합니다. 간단히 말해, 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("알 수 없는 래퍼: %s", string(wrapper)) } node = &AST{identifier: string( wrapper), args: []*AST{node}} } 반환 노드, nil}온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
4단계: 슈가 래핑 풀기
온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
미니스크립트는 6가지 유형의 구문 설탕을 정의합니다. 미니스크립트 조각에 다음 방정식의 좌표에 대한 표현식이 포함되어 있는 경우 이러한 표현식은 등호 기호 오른쪽에 있는 표현식으로 대체할 수 있습니다. 이후 단계에서 이 6개의 조각을 처리하는 수고를 덜기 위해 이러한 표현식을 대체하는 설탕 코팅 변환 함수를 추가했습니다.
치환 관계는 다음과 같습니다:
PK(키) = C:PK_K(키)PKH(키) = C:PK_H(키)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(키) = c:pk_k(키) 반환 &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, {identifier: f_0}, }, nil 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()) }}아래 출력에서 볼 수 있듯이 설탕 코팅을 제거하는 함수가 작동했습니다:
"pk(키)"에 대한 트리 c└──pk_k └──키트리 "pkh(키)"에 대한 트리 c└──pk_h └──키트리 "and_n(pk(키),sha256(H))"에 대한 트리 andor├──c|└──pk_k| └──키├──sha256| └── ─H└──0 "tv:pk(키)"에 대한 트리 and_v├──v| └──c| └──pk_k| └──key└──1 "l:pk(키)"에 대한 트리 or_i├──0└──c └──pk_k └──key "u:pk(키)"에 대한 트리 or_i├──c| └──pk_k ─pk_k| └──key └──0온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
5단계: 유형 검사
온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
모든 조각을 서로 결합할 수 있는 것은 아닙니다. 일부 조합은 유효한 비트코인 스크립트와 유효한 감시 데이터를 생성하지 못합니다.
그러나 미니스크립트 표현식과 조각은 충분히 구조화되고 계층적이기 때문에 모든 조건에서 미니스크립트 표현식이 유효한지 정적으로 쉽게 분석할 수 있습니다.
예를 들어, or_b(pk(key1),pk(key2)) 및 or_b(v:pk(key1),v:pk(key2 ))는 유효한 조합이 아니지만, or_b(pk(key1),s:pk(key2 ))는 유효한 조합입니다.
미니스크립트 사양에 따르면 각 조각은 네 가지 기본 유형 B, V, K, W 중 하나일 수 있으며, 각 조각은 추가 유형 특성 (z, o, n, d, u )을 가질 수 있습니다.
구성 조각(하위 표현식을 포함할 수 없는 조각)은 고정된 기본 유형과 고정된 유형 특성을 가집니다. 예를 들어, 해시 함수 조각 sha256(h) 는 감시 데이터 <32바이트 프리이미지>를 사용하여 만족할 수 있는 비트코인 스크립트 SIZE <32> EQUALVERIFY SHA256 <h> EQUAL로 변환됩니다(여기서 값은 sha256(( preimage)=h ), 즉 Bondu 타입을 만족합니다:
B: 성공하면 0이 아닌 값을 스택에 밀어넣고, 실패하면 정확한 0 값을 밀어넣습니다. 스택의 맨 위에 있는 요소를 소비합니다(있는 경우).o: 스파링 요소(이 경우 원본 이미지)를 소비합니다.n: 0이 아닌 속성 - 0으로 만족할 수 없음.sha256(h)의 올바른 원본 이미지는 길이가 32바이트여야 하므로 0이 될 수 없습니다.d: 는 무조건 피할 수 있습니다.sha256()예제에서 32바이트 길이이지만 실제 원본 이미지가 아닌 모든 데이터는 유효하지 않으며, 이는 언제나 생성될 수 있습니다. 32바이트가 아닌 값은 스크립트 실행이 계속되지 않고EQUALVERIFY에서종료되므로 유효한 회피가 아닙니다.u: 1이 만족되면 스택에 푸시됩니다.
기본 유형 및 유형 속성은 조립된 조각의 정확성을 보장하기 위해 신중하게 정의됩니다. 이러한 속성은 스크립트 및 증인 데이터의 추론에 따라 각 조각 유형에 할당할 수 있습니다. 마찬가지로, and_b(X,Y )와 같은 하위 표현식에 존재하는 조각의 경우, X와 Y가 어떤 유형을 만족해야 하는지, 그리고 and_b(X,Y ) 자체가 어떤 파생 유형과 속성을 가져야 하는지를 분석할 수 있습니다. 다행히도 미니스크립트 작성자는 이미 이 작업을 수행했으며 사양에 정확성 표를 문서화했습니다.
최상위 조각은 반드시 타입 B를 가져야 하며, 그렇지 않으면 해당 조각은 유효하지 않습니다.
기본 유형과 유형 속성을 사용하여 AST를 확장합니다:
type basicType stringconst ( typeB 기본 유형 = "B" typeV 기본 유형 = "V" typeK 기본 유형 = "K" typeW 기본 유형 = "W") 유형 속성 구조체 { // 기본 유형 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는 미니스크립트를 표현하는 추상 구문 트리입니다. 표현식.유형 AST 구조체 { 기본 유형 기본 유형 프로퍼티 래퍼 문자열 식별자 문자열 // 식별자가 숫자일 때 파싱된 정수입니다. 숫자, 즉 // 이전/이후/다중/임계값의 첫 번째 인자가 될 것으로 예상됩니다. 그렇지 않으면 사용되지 않습니다. num uint64 args []*AST}// typeRepr은 기본 유형 (B, V , K 또는 W) 뒤에 모든 유형 속성을 반환합니다. func (a *AST) typeRepr() string { return fmt.Sprintf("%s%s", a.basicType, a.props)}그런 다음 전체 트리를 트래버스하는 함수를 하나 더 추가합니다. 이 함수는 하위 표현식의 유형 요구 사항을 확인하고 표준 정확도 표에 따라 유형 및 유형 속성을 설정합니다.
이 함수는 매우 길기 때문에 몇 가지 조각을 처리하는 단축 버전만 보여드리고 작동 방식만 설명하겠습니다. 조각 유형은 컴포넌트 및 매개변수 모두와 관련이 있습니다. 이는 사양의 표에 따라 유형 규칙을 직접 인코딩합니다. 예를 들어, s:X가 유효하려면 X는 Bo 유형이어야 하고, 전체 조각은 W 유형을 가지며 X의 d 및 u 속성을 가져야 합니다.
온라인 환경에서 각 조각 유형을 처리하는 정식 버전을 확인하고 실행할 수 있습니다.
// expectBasicType은 이 노드가 특정 타입을 가지고 있는지 확인하는 헬퍼 함수입니다. func (a *AST) expectBasicType(typ basicType) error { if a.basicType ! = typ { return fmt.Errorf("표현식 `%s`가 %s 타입을 가질 것으로 예상되었지만 %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( "`%s`의 첫 번째 인수인 `%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( "`%s`의 첫 번째 인수인 `%s`에 잘못된 프로퍼티가 있습니다.", _x.identifier, node.identifier) } node.props.d = _x. props.d node.props.u = _x.props.u // [...] } 반환 노드, 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, // 앞으로의 더 많은 단계 } { node, err = node.apply(transform) if err ! = nil { return nil, err } } // 최상위 표현식은 "B" 유형이어야 합니다. if err := node. expect node.apply(transform), if err ! if err := node.expectBasicType(typeB); err ! = nil { return nil, err } return node, nil}유효한 미니스크립트 조각과 유효하지 않은 조각으로 이를 확인해 봅시다:
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] └── 키2미니스크립트 유효하지 않음: pk_k(키) - 표현식 `pk_k`는 타입 B를 가질 것으로 예상되지만, 타입 K미니스크립트 유효하지 않음: or_b(pk(키1),pk(키2)) - 표현식 c`는 유형이 W일 것으로 예상되지만 유형이 B입니다. (각 조각 옆에 유형을 표시하도록 draw() 함수를 수정했습니다.)
온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
6단계: 비트코인 스크립트 생성
아직 유효하지 않은 미니스크립트 조각을 모두 거부하는 검사를 구현하지는 않았지만, 지금은 이를 이용해 비트코인 스크립트를 생성하는 실험을 해볼 때입니다.
사양의 변환 테이블은 미니스크립트 조각이 비트코인 스크립트에 매핑되는 방법을 정의합니다. 예를 들어, and_b(X, Y) 는 [X] [Y] BOOLAND 등으로 매핑됩니다.
먼저 변환 테이블처럼 직접 읽을 수 있는 스크립트의 문자열 표현을 생성하는 함수를 만들어 보겠습니다. 이렇게 하면 출력을 쉽게 검사할 수 있으므로 프로토타입을 쉽게 만들고 버그를 제거할 수 있습니다. 실제 스크립트는 나중에 구현할 바이트 문자열입니다.
변환 테이블을 기반으로 각 종류의 조각을 스크립트에 매핑하는 이 함수를 추가합니다:
func scriptStr(node *AST) string { switch node.identifier { case f_0, f_1: 반환 node.identifier case f_pk_k: 반환 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_ 이전: 반환 fmt.Sprintf("<%s> CHECKSEQUENCEVERIFY", node.args[0].identifier) case f_after: 반환 fmt. 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. 스크립트Str(node.args[0]), 스크립트Str(node.args[1])) case f_and_b: 반환 fmt.Sprintf("%s %s BOOLAND", 스크립트Str(node.args[0]), 스크립트Str(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. ("%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 := 범위 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_v: return fmt. )) case f_wrap_j: return fmt.Sprintf("SIZE 0NOTEQUAL IF %s ENDIF", scriptStr(node.args[0])) case f_wrap_n: return fmt. 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의 줄임말이므로 위 스크립트에서 1바이트를 저장하려면 CHECKSIGVERIFY를 CHECKSIGVERITY로 줄여야 합니다.
v:X에서[X] 의 마지막 연산자가 EQUAL/CHECKSIG/CHECKMULTISIG인 경우, VERITY 버전으로 대체할 수 있습니다.
X는 임의의 표현식일 수 있으므로 각 조각의 마지막 연산자가 위의 세 가지 중 하나인지 확인하기 위해 또 다른 트리 탐색이 필요합니다.
이 속성을 속성 구조체에 추가합니다:
유형 속성 구조체 { // 기본 유형 속성 z, o, n, d, u bool // 이 노드에서 생성된 가장 오른쪽 스크립트 바이트가 OP_EQUAL, OP_CHECKSIG 또는 //. OP_CHECKMULTISIG. // // 만약 그렇다면, 조상이 검증 래퍼 // `v`, 즉 OP_EQUALVERIFY, OP_CHECKSIGVERIFY 및 // `v`인 경우 VERIFY 버전으로 변환할 수 있습니다. 두 개의 // 연산자 코드(예: `OP_EQUAL OP_VERIFY`)를 사용하는 대신 CHECKSIGVERIFY 및 OP_CHECKMULTISIGVERIFY를 사용할 수 있습니다. 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 } 반환 node, nil}.and_v 조각과 s- 래퍼는 하위 표현식을 코다로 취할 수 있는 유일한 컴포저블 조각입니다: and_v(X,Y) => [X] [Y] 및 s:X => SWAP [X], 따라서 자식 노드에서 직접 프로퍼티를 가져옵니다. 스크립트의 해시 조각과 thresh/multi/c는 c:X => [X] CHECKSIG와 같이 EQUAL/CHECKSIG/CHECKMULTISIG로 끝납니다. 이들은 이러한 연산자의 VERIFY 버전으로 분류될 후보입니다.
그런 다음 허용되는 경우 VERIFY 버전의 연산자를 사용하도록 scriptStr 함수를 수정할 수 있습니다. 간결성을 위해 아래에는 두 가지 시나리오만 표시했습니다. 전체 버전은 이 사이트에서 확인하고 실행할 수 있습니다.
// v` 래퍼(VERIFY 래퍼)가 노드의 조상인 경우 collapseVerify는 참입니다. 그렇다면, // 두 개의 옵코드 `OP_CHECKSIG VERIFY`는 하나의 옵코드 `OP_CHECKSIGVERIFY`로 축소할 수 있습니다(OP_EQUAL과 OP_CHECKMULTISIG도 동일).func scriptStr(node *AST, collapseVerify bool) 문자열 { 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로 단순화했습니다.
7단계: 결제 주소 생성
온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
이전 접종에서 비트코인 스크립트의 읽기 가능한 표현을 만들었습니다. P2WSH 주소를 생성하려면 실제 스크립트를 바이트 시퀀스로 개발한 다음 주소로 인코딩해야 합니다.
이를 위해 먼저 공개 키와 해시 변수를 실제 공개 키와 해시로 바꿉니다. AST: 값에 새 필드를 추가합니다.
유형 AST 구조체 { // [...]] 식별자 문자열 // 키 인자의 경우 33바이트의 압축된 공개키가 됩니다. // 해시 인자의 경우 32바이트(sha256, hash256) 또는 20. 바이트(ripemd160, hash160) 해시 값 []바이트 인자 []*AST}가 됩니다. 이제 미니스크립트 표현식의 모든 변수를 실제 값으로 대체하는 새 함수인 ApplyVars를 추가할 수 있습니다. 호출자는 이러한 값을 제공하기 위해 콜백 함수를 제공할 수 있습니다.
미니스크립트는 또한 공개 키를 복제할 수 없도록 지정하므로(스크립트 분석을 단순화하기 위해) 중복 여부를 확인해야 합니다.
//// 변수를 알 수 없는 경우 콜백은 `nil, nil`을 반환해야 합니다. 이 경우 식별자 자체의 식별자// 자체가 값(헥스로 인코딩된 공개키, 헥스로 인코딩된 해시 값)으로 파싱됩니다.func (a *AST) ApplyVars(lookupVar func(identifier 문자열) ([]바이트, 오류)) 오류 { // 중복 여부를 확인할 모든 공개키 집합 allPubKeys := map[문자열]구조{}{} _, 오류 := 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 { // 키가 변수가 아니라면, // 헥스로 직접 인코딩된 키 값이라고 가정합니다. key, err = hex.DecodeString(arg.identifier) if err ! DecodeString(arg. identifier) if err ! = nil { return nil, err } } if len(key) ! = pubKeyLen { return nil, fmt.Errorf("%s의 pubkey 인수가 %d 크기일 것으로 예상되었으나 %d를 얻었습니다.", node.identifier, pubKeyLen, len(key)) } pubKeyHex := hex.EncodeToString(key) if _, ok := allPubKeys[pubKeyHex]; ok { return nil, fmt.Errorf("%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 { // 해시 값이 변수가 아니라면, // 직접 인코딩된 해시 값이라고 가정합니다. hashValue, err = hex .디코딩 문자열(node.args[0].식별자) if err ! hashValue, err = hex .DecodeString(node.args[0].identifier) if err ! = hashLen { return nil, fmt.Errorf("%s len은 %d여야 합니다, %d를 얻었습니다", node.identifier, hashLen, len(hashValue)) } arg.value = hashValue } return node, nil })) 반환 오류}실제로 사용해 보세요:
func main() { node, err := Parse("or_d(pk(pubkey1),and_v(v:pk(pubkey2),older(52560)))") if err ! = nil { panic(err) } unhex := func(s 문자열) []byte { b, _ := hex.DecodeString(s) return b } // 두 개의 임의의 공개키. _, pubKey1 := btcec.PrivKeyFromBytes ( unhex("2c3931f593f26037a8b8b8bf837363831b18bbfb91a712dd9d862db5b9b06dc5df")) _, pubKey2 := btcec.PrivKeyFromBytes( unhex(" f902f94da618721e516d0a2a2666e2ec37079aaa184ee5a2c00c835c5121b3eb")) err = node.ApplyVars(func(식별자 문자열) ([]바이트, 오류) { switch identifier { case "pubkey1": return pubKey1.SerializeCompressed(), nil case "pubkey2": return pubKey2.SerializeCompressed(), nil } return nil, nil })) if err ! nil }) if err ! = nil { panic(err) } fmt.Println(node.DrawTree())}출력 결과, 공개키가 성공적으로 교체되었습니다:
or_d [B]├──c [본두브]| └──pk_k [콘두]| └──pubkey1 [03469d685c3445e83ee6e3cfb30382795c249c91955523c25f484d69379c7a7d6f] └──and_v [봉] ├──v [본] | └──c [본두브] | └──pk_k [콘두] | └──pubkey2 [03ba991cc359438fdd8cf43e3cf7894f90cf4d0e040314a6bba82963fa77b7a434] └──older [Bz] └─── 52560 (각 변수 옆에 실제 값을 표시하도록 drawTree() 함수를 수정합니다).
이 멋진 btcd 라이브러리의 도움으로 이제 실제 스크립트를 작성할 수 있습니다. 위의 scriptStr() 과 매우 비슷해 보이지만, 정수와 데이터를 스택에 푸시하는 실용성에 초점을 맞춰 바이트 문자열로 인코딩합니다. 여기서는 단축 버전을 사용했습니다. 전체 버전은 이 사이트에서 확인할 수 있습니다.
// 스크립트는 구문 분석된 미니스크립트에서 감시 스크립트를 생성합니다.func (a *AST) Script() ([]byte, error) { b := txscript.NewScriptBuilder() if err :=. buildScript(a, b, false); err ! = nil { return nil, err } return b.Script()}// `v` 래퍼(VERIFY 래퍼)가 노드의 조상인 경우 collapseVerify는 참입니다. 만약 그렇다면, // 두 개의 옵코드 OP_CHECKSIG VERIFY`는 하나의 옵코드 `OP_CHECKSIGVERIFY`로 축소될 수 있습니다(OP_EQUAL과 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("%s (%s)에 대한 빈 키", node.identifier, arg. 식별자) } 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) } // 더 많은 경우 [...] 기본값: 반환 fmt.Errorf("알 수 없는 식별자: %s", node.identifier) } 반환 nil}실행해 봅시다:
func main() { // [...]. script, err := node.Script() if err ! = nil { panic(err) } fmt.Println("Script", hex.EncodeToString(script))}출력합니다:
Script 2103469d685c3445e83ee6e3cfb30382795c249c91955523c25f484d69379c7a7d6fac73642103ba991cc359438fdd8cf43e3cf7894f90cf4d0e040314a6bba82963fa77b7a434ad0350cd00b268BIP141 및 BIP173에 따르면 P2WSH 주소는 0 <sha256(스크립트)>로 인코딩된 bech32여야 하며, 여기서 0은 검역 증인 버전 0을 나타냅니다. 이 btcd 라이브러리를 사용하여 주소를 생성할 수 있도록 하겠습니다:
addr, err := btcutil.NewAddressWitnessScriptHash(chainhash.HashB(스크립트), &chaincfg.TestNet3Params) if err ! = nil { panic(err)}fmt.Println("Address:", addr.String())테스트넷 수집 주소가 준비되었습니다:
주소: tb1q4q3cw0mausmamm7n7fn2phh0fpca4n0vmkc7rdh6hxnkz9rd8l0qcpefrj 이 주소로 받은 자금은 or_d(pk(pubkey1),and_v(v:pk(pubkey2),older(52560))), 즉 공개키 1은 언제든지 사용할 수 있고 공개키 2는 이 주소에 자금을 입력한 후 52560블록(약 1년) 후에 사용할 수 있는 지출 조건을 사용하여 잠깁니다.
온라인 Go 환경에서 코드를 실행하려면 여기를 클릭하세요.
결론
결론적으로 미니스크립트 표현식을 파싱하고 타입 검사를 수행하며 컬렉션 주소를 생성하는 미니스크립트 코드베이스를 만들었습니다.
고려해야 할 더 많은 시나리오가 있습니다. 다음 글에서는 돈을 사용할 수 있도록 미니스크립트 표현식을 기반으로 증인 데이터를 생성하는 방법, 미니스크립트가 비트코인 합의와 스크립트 크기 및 연산자 제한과 같은 표준을 준수하는지 확인하는 방법에 대해 알아보겠습니다.
이 시리즈가 계속 이어지길 원하시면 트위터( @benma )로 알려주세요.




