You can also read the English version of this article.
Teil 1: Wie funktioniert Bitcoin Script, und warum ist es so komplex?
Teil 2: Was ist Miniscript, und wie macht es Bitcoin Scripting einfacher?
Teil 3: Einen Miniscript-Parser und -Analysator in Go schreiben

Im vorangegangenen Teil dieser Serie haben wir uns angesehen, was Miniscript ist und wie es in Bitcoin Script übersetzt wird.

Um zu verstehen, wie Miniscript im Detail funktioniert, ist ein konkretes Implementierungsbeispiel hilfreich, einschließlich der Analyse auf Korrektheit, der Erstellung von Empfangsadressen und dem Ausgeben von Bitcoins.

Lass uns also eintauchen und eine Implementierung in Go schreiben.

Wir werden https://bitcoin.sipa.be/miniscript/ als Referenz nutzen, da es die Spezifikationen und Eigenschaften aller Miniscript-Fragmente enthält.

Jeder Abschnitt enthält oben und unten einen Link zum Go Playground, wo du den Code ausführen, dessen Ausgabe sehen und mit dem konkreten Beispiel herumspielen kannst.

Kurz: wir wandeln ein Miniscript in einen Abstract Syntax Tree um, und führen dann eine Reihe von Baumtransformationen und Tree Walks durch. Dabei führen wir eine Korrektheitsanalyse aus, erstellen das entsprechende Bitcoin Script, generieren Empfangsadressen, und vieles mehr.

⚠️ Haftungsausschluss: Die folgende Implementierung ist weder geprüft und noch getestet. Verwende sie nicht in der Produktion. Sie ist nur für Lehrzwecke gedacht. ⚠️


Inhalt

  1. Umwandlung in einen Abstract Syntax Tree
  2. Fragmente und Anzahl der Argumente überprüfen
  3. Wrapper expandieren
  4. Entzuckern
  5. Typprüfung
  6. Bitcoin-Skript erstellen
  7. Empfangsadresse generieren

Schritt 1: Umwandlung in einen Abstract Syntax Tree

Klicke hier, um den Code im Go Playground auszuführen.

Miniscript-Ausdrücke sind einfach und lassen sich leicht in einen Abstract Syntax Tree (AST) umwandeln. Im Gegensatz zu mathematischen/algebraischen Ausdrücken enthalten Miniscript-Ausdrücke keine Infix-Operatoren, keine gruppierenden Klammern und nur Klammern zum Einschließen von Argumenten eines Fragments. Daher sind sie leicht abzubilden und auch einfach zu parsen.

Lass uns unseren AST definieren:

// AST is the abstract syntax tree representing a Miniscript expression.
type AST struct {
	wrappers   string
	identifier string
	args       []*AST
}

Bezeichner wie or_b werden im Feld identifier gespeichert. Wenn es Wrapper gibt, z. B. ascd:x, werden die Wrapper abgetrennt und im Feld wrappers gespeichert. Schließlich werden die Argumente der Fragmente rekursiv in args gespeichert.

Um einen Ausdruck während des Parsens in einen Baum zu verwandeln, benötigen wir die bewährte alte Stack-Datenstruktur:

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)
}

Um einen Ausdruck mit Hilfe eines Stack in einen Baum zu verwandeln, zerlegen wir den Ausdruck zunächst in durch Klammern und Kommata getrennte Token. Leider gibt es in der Go-Standardbibliothek keine geeignete Funktion, also habe ich ChatGPT gebeten, sie für mich zu schreiben - mit großem Erfolg:

// - Written by ChatGPT.
// splitString keeps separators as individual slice elements and splits a string
// into a slice of strings based on multiple separators. It removes any empty
// elements from the output slice.
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
}

Ein kurzer Unit-Test bestätigt, dass es funktioniert:

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))
}

Jetzt können wir die Token und Klammern/Kommas durchgehen und einen Ausdrucksbaum erstellen.

Immer wenn wir einen Bezeichner sehen (irgendetwas zwischen einer Klammer und einem Komma), schieben wir den Bezeichner auf den Stack, der das Elternteil aller untergeordneten Argumente sein wird. Wann immer wir ein Komma oder eine schließende Klammer sehen, wissen wir, dass es das Ende eines Arguments ist, also holen wir das Argument vom Stapel und fügen es zu seinem Elternteil hinzu. Einige ungültige Sequenzen werden ausdrücklich ausgeschlossen, wie z.B. "()" oder "(,", die niemals in gültigen Miniskripten vorkommen können.

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
}

Fügen wir noch eine Funktion hinzu, um den Baum zu visualisieren:

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()
}

Probieren wir es mal mit einem komplizierten Ausdruck:

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())
}

Erfolg! Die Ausgabe ist:

andor
├──pk
|   └──key_remote
├──or_i
|     ├──and_v
|     |      ├──v:pkh
|     |      |    └──key_local
|     |      └──hash160
|     |               └──H
|     └──older
|            └──1008
└──pk
    └──key_revocation

Natürlich führt der Parser noch keinerlei Prüfungen durch, so dass Ausdrücke wie unknownFragment(foo,bar) ebenfalls zu einem AST werden:

unknownFragment
├──foo
└──bar

Klicke hier, um den Code im Go Playground auszuführen.


Schritt 2: Fragmente und Anzahl der Argumente überprüfen

Klicke hier, um den Code im Go Playground auszuführen.

Der erste von mehreren Tree Walks ist eine einfache Prüfung: Ist jeder Fragmentbezeichner im Baum ein gültiges Miniscript-Fragment, und hat jedes Fragment die richtige Anzahl von Argumenten?

Dies sind alle Fragmente, die der Spezifikation entsprechen:

const (
	// All fragment identifiers.

	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 und multi haben ein numerisches erstes Argument. Da wir es parsen müssen, um zu sehen, ob es eine gültige Zahl ist, wandeln wir es in eine Zahl um und speichern es in unserem AST zur späteren Verwendung. Fügen wir ein neues Feld zu unserem AST hinzu:

// AST is the abstract syntax tree representing a Miniscript expression.
type AST struct {
	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
}

Außerdem benötigen wir eine Funktion, die den Baum rekursiv durchläuft und eine Funktion auf jeden Miniscript-Ausdruck/Unterausdruck anwendet. Die Transformationsfunktion kann einen Knoten verändern oder ganz durch einen neuen Knoten ersetzen, was in späteren Phasen des Parsers nützlich sein wird:

func (a *AST) apply(f func(*AST) (*AST, error)) (*AST, error) {
	for i, arg := range a.args {
		// We don't rescurse into arguments which are not Miniscript subexpressions themselves:
		// key/hash variables and the numeric arguments of 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:
			// None of the arguments of these functions are Miniscript subexpressions - they are
			// variables (or concrete assignments) or numbers.
			continue
		case f_thresh:
			// First argument is a number. The other arguments are subexpressions, which we want to
			// visit, so only skip the first argument.
			if i == 0 {
				continue
			}
		}

		new, err := arg.apply(f)
		if err != nil {
			return nil, err
		}
		a.args[i] = new
	}
	return f(a)
}

Beispiel:

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
	})

Ausgabe:

Visiting node: pk
Visiting node: pkh
Visiting node: hash160
Visiting node: and_v
Visiting node: older
Visiting node: or_i
Visiting node: pk
Visiting node: andor

Wir fügen nun eine Parse-Funktion hinzu, die den AST erstellt und eine Reihe von Transformationen darauf anwendet. Die erste Transformation ist die Fragment- und Argumentprüfung:

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
}

Die argCheck-Funktion wird auf jeden Knoten des Baums angewendet, und wir können einfach alle gültigen Fragmentbezeichner aufzählen, um diese grundlegenden Prüfungen durchzuführen.

// argCheck checks that each identifier is a known Miniscript identifier and that it has the correct
// number of arguments, e.g. `andor(X,Y,Z)` must have three arguments, etc.
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 {
			// Maximum number of keys in a multisig.
			const multisigMaxKeys = 20
			if numSubs > multisigMaxKeys {
				return nil, fmt.Errorf("number of multisig keys cannot exceed %d", multisigMaxKeys)
			}
			// Multisig keys are variables, they can't have subexpressions.
			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
}

Mit diesen Prüfungen schließen wir bereits viele ungültige Miniskripte aus. Schauen wir uns einige Beispiele an:

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)
	}
}

Ausgabe:

invalid  --  unrecognized identifier: invalid
pk(key1,tooManyArgs)  --  pk expects 1 arguments, got 2
pk(key1(0))  --  argument of pk must not contain subexpressions
and_v(0)  --  and_v expects 2 arguments, got 1
after(notANumber)  --  after(k) => k must be an unsigned integer, but got: notANumber
after(-1)  --  after(k) => k must be an unsigned integer, but got: -1
multi(0,k1)  --  multi(k) -> k must 1 ≤ k ≤ n, but got: 0
multi(2,k1)  --  multi(k) -> k must 1 ≤ k ≤ n, but got: 2
multi(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

Klicke hier, um den Code im Go Playground auszuführen.


Schritt 3: Wrapper expandieren

Klicke hier, um den Code im Go Playground auszuführen.

Jedes Fragment kann mit Wrappern umschlossen werden, die durch Buchstaben vor dem Doppelpunkt ":" gekennzeichnet sind. Ein Beispiel aus https://bitcoin.sipa.be/miniscript/:

dv:older(144) ist der d: Wrapper, der auf den v: Wrapper angewendet wird, der auf das older Fragment für 144 Blöcke angewendet wird.

In weiteren Versionen des Parsers wollen wir auch mit den Wrappern arbeiten, da sie sich genauso verhalten wie normale Fragmente - sie haben eine Zuordnung zu Bitcoin Script, haben Korrektheitsregeln, etc. In gewisser Weise ist dv:older(144) nur ein syntaktischer Zucker für d(v(older(144))).

In diesem Beispiel wollen wir unseren originalen AST umwandeln von:

dv:older
└──144

nach:

d
└──v
   └──older
          └──144

Um diese Transformation durchzuführen, fügen wir diese Funktion der Liste der Transformationen hinzu. Beachte, dass wir die Buchstaben der Wrapper in umgekehrter Reihenfolge durchlaufen, da sie von rechts nach links angewendet werden.

// expandWrappers applies wrappers (the characters before a colon), e.g. `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
}

Klicke hier, um den Code im Go Playground auszuführen.


Schritt 4: Entzuckern

Klicke hier, um den Code im Go Playground auszuführen.

Miniscript definiert sechs Instanzen von syntaktischem Zucker. Wenn ein Miniscript einen Ausdruck auf der linken Seite der unten aufgeführten Gleichungen enthält, können diese durch die rechte Seite der Gleichung ersetzt werden. Um sich den Aufwand zu ersparen, diese sechs Fragmente in allen späteren Phasen zu behandeln, fügen wir eine Desugar-Transformation hinzu, die diese im Voraus ersetzt.

Die Ersetzungen sind:

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)

Jetzt ist ein guter Zeitpunkt, die Tabelle um die Fragmentbezeichner zu erweitern, die wir zu Beginn mit den Wrapper-Fragmenten definiert haben:

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))
)

Die Transformationsfunktion sieht wie folgt aus:

// desugar replaces syntactic sugar with the final form.
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
}

Probieren wir sie alle aus und untersuchen sie visuell:

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())
	}
}

Wie wir in folgendem Output sehen können, funktioniert die Funktion einwandfrei:

Tree for "pk(key)"
c
└──pk_k
      └──key

Tree for "pkh(key)"
c
└──pk_h
      └──key

Tree for "and_n(pk(key),sha256(H))"
andor
├──c
|  └──pk_k
|        └──key
├──sha256
|       └──H
└──0

Tree for "tv:pk(key)"
and_v
├──v
|  └──c
|     └──pk_k
|           └──key
└──1

Tree for "l:pk(key)"
or_i
├──0
└──c
   └──pk_k
         └──key

Tree for "u:pk(key)"
or_i
├──c
|  └──pk_k
|        └──key
└──0

Klicke hier, um den Code im Go Playground auszuführen.


Schritt 5: Typprüfung

Klicke hier, um den Code im Go Playground auszuführen.

Nicht alle Fragmente passen zueinander, um ein gültiges Bitcoin-Skript und gültige Witness zu erzeugen.

Da Miniscript-Ausdrücke und -Fragmente jedoch gut strukturiert und hierarchisch aufgebaut sind, ist es einfach, statisch zu überprüfen, ob ein Miniscript-Ausdruck unter allen Umständen gültig ist.

Ein Beispiel: or_b(pk(key1),pk(key2)) und or_b(v:pk(key1),v:pk(key2)) sind keine gültigen Kombinationen, or_b(pk(key1),s:pk(key2)) hingegen schon.

Gemäss der Miniskript-Spezifikation kann jedes Fragment einem der vier verschiedenen Grundtypen B, V, K oder W angehören, und jedes Fragment kann zusätzliche Typ-Eigenschaften haben (z, o, n, d und u).

Primitive Fragmente (Fragmente ohne Unterausdrücke) haben einen festen Basistyp und feste Typeigenschaften. Zum Beispiel hat das Hashing-Fragment sha256(h), das dem Bitcoin-Skript SIZE <32> EQUALVERIFY SHA256 <h> EQUAL entspricht und durch den Witness <32 Byte preimage> (der Wert, für den sha256(preimage)=h) erfüllt wird, den Typ Bondu. Das bedeutet:

  • B: ergibt bei Erfolg einen Wert ungleich Null und bei Misserfolg eine exakte 0. Konsumiert Elemente vom oberen Ende des Stacks (falls vorhanden).
  • o: konsumiert genau ein Stapelelement (das Preimage in diesem Beispiel)
  • n: nonzero - die Satisfaction kann nicht Null sein. Das korrekte Preimage für das sha256(h)-Fragment muss 32 Bytes lang sein, kann also nicht Null sein.
  • d: Kann bedingungslos unerfüllt werden. In diesem Fall sind beliebige 32 Bytes außer dem eigentlichen Preimage ungültig, das immer konstruiert werden kann. Beachte, dass ein Wert, der nicht 32 Bytes lang ist, keine gültige Dissatisfaction darstellt, da dieser die Skriptausführung bei EQUALVERIFY abbricht, anstatt sie fortzusetzen.
  • u: Legt eine 1 auf den Stapel wenn erfüllt.

Die Basistypen und Typeigenschaften wurden sorgfältig definiert, um die Korrektheit der zusammengesetzten Fragmente zu gewährleisten. Sie können jedem Fragment auf der Grundlage von Überlegungen über das Skript und die Witnesses, welche sie verkapseln, zugewiesen werden. Auch für Fragmente, die Unterausdrücke wie and_b(X,Y) haben, kann man folgern, welche Typen und Eigenschaften X und Y haben müssen, und welche abgeleiteten Typen und Eigenschaften and_b(X,Y) selbst haben muss. Glücklicherweise haben die Autoren von Miniscript diese Arbeit bereits erledigt und in der Korrektheitstabelle der Spezifikation dokumentiert.

Das Top-Level-Fragment muss vom Typ B sein, sonst ist das Miniscript ungültig.

Erweitern wir nun unseren AST-Typ um den Basistyp und die Typeigenschaften:

type basicType string

const (
	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)
}

Nun fügen wir eine weitere Funktion hinzu, die den Baum durchläuft. Diese Funktion prüft die Typanforderungen von Unterausdrücken und setzt den Typ und die Typ-Eigenschaften gemäß der Korrektheitstabelle der Spezifikation.

Da die Funktion recht lang ist, zeigen wir eine gekürzte Version, die nur einige Fragmente behandelt, aber die Funktionsweise verdeutlicht. Fragmenttypen sind entweder primitiv oder hängen von ihren Argumenten ab. Die Funktion kodiert einfach die Typregeln gemäß der Tabelle in der Spezifikation. Zum Beispiel muss für s:X, damit es gültig ist, X vom Typ Bo sein, den Typ W haben und die Eigenschaften d und u von X erhalten.

Du kannst die vollständige Version, die jedes Fragment behandelt, im Go Playground sehen und ausführen.

// 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
}

Nachdem wir nun alle Typen und Typeigenschaften abgeleitet haben, müssen wir noch die abschließende Typprüfung hinzufügen, dass der Ausdruck der obersten Ebene vom Typ B sein muss:

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
}

Versuchen wir es mal mit gültigen und ungültigen Miniskripten:

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)
	}
}

Erfolg! Die Ausgabe ist:

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]
            └──key2

miniscript invalid: pk_k(key) - expression `pk_k` expected to have type B, but is type K
miniscript invalid: or_b(pk(key1),pk(key2)) - expression `c` expected to have type W, but is type B

(wir haben die Funktion drawTree() geändert, um auch die Typen neben jedem Fragment anzuzeigen).

Klicke hier, um den Code im Go Playground auszuführen.


Schritt 6: Bitcoin-Skript erstellen

Wir haben noch nicht alle Prüfungen implementiert, um ungültige Miniskripte zurückzuweisen. Aber jetzt ist trotzdem ein guter Zeitpunkt, um mit der Erzeugung des Bitcoin-Skripts zu experimentieren.

Die Übersetzungstabelle der Spezifikation definiert, wie Miniscript-Fragmente auf Bitcoin Script abgebildet werden. Zum Beispiel wird and_b(X,Y) zu [X] [Y] BOOLAND, etc.

Wir werden zuerst eine Funktion erstellen, die eine menschenlesbare Text-Repräsentation des Skripts erzeugt, genau wie in der Übersetzungstabelle. Dies erleichtert die Erstellung eines Prototyps und die Fehlersuche, da man die Ausgabe leicht überprüfen kann. Das eigentliche Skript wird eine Folge von Bytes sein, was wir später umsetzen werden.

Wir fügen diese Funktion hinzu, die jedes Fragment gemäß der Übersetzungstabelle auf seine Skript-Darstellung abbildet:

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>"
	}
}

Versuchen wir es:

Klicke hier, um den Code im Go Playground auszuführen.

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))
}

Ausgabe:

<pubkey1> CHECKSIG IFDUP NOTIF <pubkey2> CHECKSIG VERIFY <52560> CHECKSEQUENCEVERIFY ENDIF

Dies ist korrekt, aber es gibt noch eine Optimierungsmöglichkeit. Der v:X-Wrapper entspricht [X] VERIFY. Die Opcodes EQUALVERIFY, CHECKSIGVERIFY und CHECKMULTISIGVERIFY sind Abkürzungen für EQUAL VERIFY, CHECKSIG VERIFY und CHECKMULTISIG VERIFY, so dass in der obigen Ausgabe CHECKSIG VERIFY zu CHECKSIGVERIFY abgekürzt werden kann, was ein Byte im Skript spart.

Wenn in v:X der letzte OP-Code in [X] EQUAL/CHECKSIG/CHECKMULTISIG ist, kann er durch die VERIFY-Version ersetzt werden.

Da X ein beliebiger Ausdruck sein kann, brauchen wir einen weiteren Tree Walk, um für jedes Fragment zu entscheiden, ob sein letzter OP-Code einer dieser drei ist.

Fügen wir diese Eigenschaft zur properties struct hinzu:

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
}

Und die Funktion, die dieses Feld für jedes Fragment setzt, das wir zu unserer Liste der Transformationen hinzufügen:

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
}

Das and_v-Fragment und der s-wrapper sind die einzigen zusammengesetzten Fragmente, die mit einem Unterausdruck enden: and_v(X,Y) => [X] [Y] und s:X => SWAP [X], so dass diese einfach die Eigenschaft von diesem Unterknoten übernehmen. Die Skripte der Hash-Fragmente und thresh/multi/c enden tatsächlich auf EQUAL/CHECKSIG/CHECKMULTISIG, z. B. c:X => [X] CHECKSIG. Dies sind Kandidaten, die in die VERIFY-Version des Opcodes eingefügt werden können.

Dann können wir unsere scriptStr-Funktion so ändern, dass sie wo möglich die VERIFY-Version des Opcodes verwendet. Der Kürze halber zeigen wir im Folgenden nur zwei Fälle. Die vollständige Version kannst du im Go Playground sehen und ausführen.

// 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

}

Das Ausführen des Programms mit der geänderten Funktion gibt nun folgendes aus:

<pubkey1> CHECKSIG IFDUP NOTIF <pubkey2> CHECKSIGVERIFY <52560> CHECKSEQUENCEVERIFY ENDIF

was erfolgreich CHECKSIG VERIFY zu CHECKSIGVERIFY zusammengefügt hat.


Schritt 7: Empfangsadresse generieren

Klicke hier, um den Code im Go Playground auszuführen.

Im vorigen Abschnitt haben wir eine menschenlesbare Darstellung für das Bitcoin-Skript erstellt. Um eine P2WSH-Adresse zu generieren, müssen wir das eigentliche Skript als Byte-Sequenz aufbauen und dann in eine Adresse umwandeln.

Dazu müssen wir zunächst alle Schlüssel- und Hash-Variablen durch tatsächliche Pubkeys und Hash-Werte ersetzen. Wir fügen ein neues Feld value zu unserem AST hinzu:

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
}

Nun können wir eine neue Funktion ApplyVars hinzufügen, mit der wir alle Variablen im Miniskript durch aktuelle Werte ersetzen können. Der Aufrufer kann eine Callback-Funktion bereitstellen, welche die Werte liefert.

Miniscript legt auch fest, dass sich öffentliche Schlüssel nicht wiederholen dürfen (das vereinfacht die Argumentation in den Skripten), also prüfen wir auf Duplikate.

// 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
}

Schauen wir uns dies in Aktion an:

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())
}

Ausgabe, wobei die Pubkeys erfolgreich ersetzt wurden:

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

(wir haben die Funktion drawTree() so geändert, dass der Knotenwert neben jeder Variablen angezeigt wird).

Mit Hilfe der ausgezeichneten btcd-Bibliothek werden wir nun das eigentliche Skript erstellen. Es sieht ähnlich aus wie die obige Version scriptStr(), kodiert es aber als Byte-String und kümmert sich um die Feinheiten der Kodierung von Integer und Daten-Pushes auf dem Stack. Wir zeigen hier eine verkürzte Version mit nur wenigen Fällen. Die vollständige Version kannst du im Go Playground sehen und ausführen.

// 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
}

Führen wir es aus:

func main() {
	// [...]
	script, err := node.Script()
	if err != nil {
		panic(err)
	}
	fmt.Println("Script", hex.EncodeToString(script))
}

Ausgabe:

Script 2103469d685c3445e83ee6e3cfb30382795c249c91955523c25f484d69379c7a7d6fac73642103ba991cc359438fdd8cf43e3cf7894f90cf4d0e040314a6bba82963fa77b7a434ad0350cd00b268

Gemäss BIP141 und BIP173 ist eine P2WSH-Adresse die bech32-Kodierung von 0 <sha256(script)>, wobei 0 die Segwit-Version 0 ist. Wir werden die btcd-Bibliothek verwenden, um die Adresse zu erstellen:

addr, err := btcutil.NewAddressWitnessScriptHash(chainhash.HashB(script), &chaincfg.TestNet3Params)
if err != nil {
   	panic(err)
}
fmt.Println("Address:", addr.String())

Unsere Testnet-Empfangsadresse ist bereit:

Address: tb1q4q3cw0mausmamm7n7fn2phh0fpca4n0vmkc7rdh6hxnkz9rd8l0qcpefrj

Die an dieser Adresse empfangenen Bitcoins sind mit der Ausgabebedingung or_d(pk(pubkey1),and_v(v:pk(pubkey2),older(52560))) gesichert, d.h. pubkey1 kann jederzeit ausgeben, oder pubkey2 kann ausgeben, wenn der an der Adresse empfangene Coin 52560 Blöcke alt ist (etwa 1 Jahr).

Klicke hier, um den Code im Go Playground auszuführen.


Fazit

Wir haben eine Miniscript-Bibliothek erstellt, die einen Miniscript-Ausdruck parsen und typisieren und daraus eine Empfangsadresse generieren kann.

Es gibt noch eine Menge zu tun. In einem weiteren Teil könnten wir untersuchen, wie man Witnesses generiert, welche Coins ausgeben können, die an ein Miniscript gesendet werden. Oder wie man sicherstellt, dass Miniscripts dem Bitcoin-Konsens und den Standardgrenzen entsprechen, wie z.B. Skriptgröße und OP-Code-Grenzen.

Wenn du möchtest, dass diese Serie fortgesetzt wird, dann lasse es mich doch auf Twitter @_benma_ wissen.


Du hast noch keine BitBox?

Deine Kryptowährungen sicher zu halten muss nicht schwer sein. Die BitBox02 Hardware Wallet speichert die privaten Schlüssel deiner Kryptowährungen offline. So kannst du deine Coins sicher verwalten.

Die BitBox02 gibt es auch als Bitcoin-only-Version mit einer radikal fokussierten Firmware: weniger Code bedeutet weniger Angriffsfläche, was deine Sicherheit weiter verbessert, wenn du nur Bitcoin speicherst.

Hol dir eine in unserem Shop!‌


Shift Crypto ist ein privates Unternehmen mit Sitz in Zürich, Schweiz. Unser Team aus Bitcoin-Entwicklern, Krypto-Experten und Sicherheitsingenieuren entwickelt Produkte, die unseren Kunden eine stressfreie Reise vom Anfänger zum Meister der Kryptowährung ermöglichen. Die BitBox02, unsere Hardware-Wallet der zweiten Generation, ermöglicht es den Nutzern, Bitcoin und andere Kryptowährungen zu speichern, zu schützen und mit Leichtigkeit zu handeln - zusammen mit der dazugehörigen Software, der BitBoxApp.