Skip to content

Commit bf5ebc5

Browse files
benmaguggero
andcommitted
descriptors: add support for miniscript
This commit adds a fully-featured miniscript implementation that is modeled after the rust-bitcoin/rust-miniscript repository. The miniscript functionality is equivalent to the rust-miniscript implementation at version 13.1.0 and all test vectors (introduced in the next commit) are extracted from the Rust reference implementation to ensure full compatibility and feature-completeness. Co-authored-by: Oli <gugger@gmail.com>
1 parent dd79887 commit bf5ebc5

18 files changed

Lines changed: 5920 additions & 1 deletion

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ DEV_TAGS := rpctest
2222
GOTEST_DEV = go test -v -tags=$(DEV_TAGS)
2323
GOTEST := go test -v
2424
COVER_FLAGS = -coverprofile=coverage.txt -covermode=atomic -coverpkg=$(PKG)/...
25-
MODULES := address btcec btcutil chaincfg chainhash psbt txscript v2transport wire
25+
MODULES := address btcec btcutil chaincfg chainhash descriptors psbt txscript v2transport wire
2626

2727
# Linting uses a lot of memory, so keep it under control by limiting the number
2828
# of workers if requested.

descriptors/go.mod

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module github.com/btcsuite/btcd/descriptors
2+
3+
go 1.25.0
4+
5+
require (
6+
github.com/btcsuite/btcd/address/v2 v2.0.0
7+
github.com/btcsuite/btcd/txscript/v2 v2.0.0
8+
github.com/btcsuite/btcd/wire/v2 v2.0.0
9+
github.com/stretchr/testify v1.10.0
10+
)
11+
12+
require (
13+
github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect
14+
github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 // indirect
15+
github.com/btcsuite/btcd/chainhash/v2 v2.0.0 // indirect
16+
github.com/btcsuite/btclog v1.0.0 // indirect
17+
github.com/davecgh/go-spew v1.1.1 // indirect
18+
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
19+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
20+
github.com/pmezard/go-difflib v1.0.0 // indirect
21+
golang.org/x/crypto v0.45.0 // indirect
22+
golang.org/x/sys v0.38.0 // indirect
23+
gopkg.in/yaml.v3 v3.0.1 // indirect
24+
)

descriptors/go.sum

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
github.com/btcsuite/btcd/address/v2 v2.0.0 h1:UVu8Hal6Siu4XastFe+JX5JkeBYONbDUIY5E+SVTs6I=
2+
github.com/btcsuite/btcd/address/v2 v2.0.0/go.mod h1:htJK1AtaeK3bKNfZY63ep2oN8LbrI6qvmPGe1vekb3I=
3+
github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8=
4+
github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk=
5+
github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 h1:M/RTtXfXA9odC1RUEOyZFXj/NXKVHPYZXVjb60xTOok=
6+
github.com/btcsuite/btcd/chaincfg/v2 v2.0.0/go.mod h1:rHgHIXYYfn70m25a+BJ9f9z7VZAsTiDQGB2XYaippGQ=
7+
github.com/btcsuite/btcd/chainhash/v2 v2.0.0 h1:PMLlSloHJuEeB80XG9EjpXWNEKAZAMLl6YHZ6YsEuoA=
8+
github.com/btcsuite/btcd/chainhash/v2 v2.0.0/go.mod h1:mKxcZ7oGTXE7IRV+sS9hP4EVBwc/SzfNR+52IsOP9j8=
9+
github.com/btcsuite/btcd/txscript/v2 v2.0.0 h1:pEmmHaC8eRx6KSB63zSVJD7qrit9/c9cLSrw++XrYP8=
10+
github.com/btcsuite/btcd/txscript/v2 v2.0.0/go.mod h1:pZXabc11Xr9nz/18kXY3yErdAajYc3gi28Zqb3KqlFo=
11+
github.com/btcsuite/btcd/wire/v2 v2.0.0 h1:mYSKzZZ0a1sK+aMhXzfDSVsSzRkWkU3x2U04TFRS2z8=
12+
github.com/btcsuite/btcd/wire/v2 v2.0.0/go.mod h1:bGxkPkk8IiDvUo1D96wE03llBIk7p2MdWYRyAQwLmqM=
13+
github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns=
14+
github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ=
15+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
16+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17+
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
18+
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
19+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
20+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
21+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
22+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
23+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
24+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
25+
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
26+
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
27+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
28+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
29+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
30+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
31+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
32+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package miniscript
2+
3+
import "fmt"
4+
5+
// execSize holds the maximum number of stack and altstack elements present at
6+
// any point during execution, beyond the initial witness elements, when
7+
// satisfying (sat) or dissatisfying (dsat) a (sub)expression.
8+
//
9+
// It feeds the consensus rule of both segwit contexts that caps the total
10+
// number of stack elements (initial witness elements plus these) at 1000. The
11+
// per-fragment values follow rust-miniscript's max_exec_stack_count, using a
12+
// straightforward peak model: two sub expressions run in sequence take the max
13+
// of their peaks, and if the first leaves its result on the stack while the
14+
// second executes (OP_BOOLAND/OP_BOOLOR, or a threshold's running total), the
15+
// second's peak is one higher.
16+
//
17+
// They deviate from rust's in the three places where rust's value is not the
18+
// true peak: thresh (see threshExecStack), the OP_IFDUP of or_d and the <k> and
19+
// <n> pushes of multi.
20+
type execSize struct {
21+
dsat, sat maxInt
22+
}
23+
24+
// seqExec combines the execution stack sizes of two sub expressions that run in
25+
// sequence. If keepFirst is set, the first sub expression leaves a result on
26+
// the stack while the second executes, so the second's peak counts one extra
27+
// element. The result is invalid if either input is.
28+
func seqExec(a, b maxInt, keepFirst bool) maxInt {
29+
if !a.valid || !b.valid {
30+
return maxInt{}
31+
}
32+
second := b.value
33+
if keepFirst {
34+
second++
35+
}
36+
return maxInt{valid: true, value: max(a.value, second)}
37+
}
38+
39+
// computeExecStack computes the execSize of a node from that of its children.
40+
// It is applied bottom-up as part of Parse.
41+
func computeExecStack(node *AST) (*AST, error) {
42+
invalid := maxInt{valid: false}
43+
fixed := func(v int) maxInt { return maxInt{valid: true, value: v} }
44+
45+
switch node.identifier {
46+
case f_0:
47+
node.execStack = execSize{dsat: fixed(1), sat: invalid}
48+
49+
case f_1:
50+
node.execStack = execSize{dsat: invalid, sat: fixed(1)}
51+
52+
case f_pk_k:
53+
node.execStack = execSize{dsat: fixed(1), sat: fixed(1)}
54+
55+
case f_pk_h:
56+
// OP_DUP and the hash push.
57+
node.execStack = execSize{dsat: fixed(2), sat: fixed(2)}
58+
59+
case f_older, f_after:
60+
node.execStack = execSize{dsat: invalid, sat: fixed(1)}
61+
62+
case f_sha256, f_hash256, f_ripemd160, f_hash160:
63+
// Either <32-byte size> or <hash> <32-byte value>.
64+
node.execStack = execSize{dsat: fixed(2), sat: fixed(2)}
65+
66+
case f_multi:
67+
// The script pushes <k>, then the n public keys one at a time,
68+
// then <n>, all of which are on the stack when
69+
// OP_CHECKMULTISIG runs.
70+
n := len(node.args) - 1
71+
node.execStack = execSize{dsat: fixed(n + 2), sat: fixed(n + 2)}
72+
73+
case f_multi_a:
74+
// The two numbers before the final OP_NUMEQUAL.
75+
node.execStack = execSize{dsat: fixed(2), sat: fixed(2)}
76+
77+
case f_andor:
78+
x, y, z := node.args[0], node.args[1], node.args[2]
79+
node.execStack = execSize{
80+
dsat: seqExec(
81+
x.execStack.dsat, z.execStack.dsat, false,
82+
),
83+
sat: seqExec(x.execStack.sat, y.execStack.sat, false).
84+
or(seqExec(
85+
x.execStack.dsat, z.execStack.sat,
86+
false,
87+
)),
88+
}
89+
90+
case f_and_v:
91+
x, y := node.args[0], node.args[1]
92+
node.execStack = execSize{
93+
dsat: invalid,
94+
sat: seqExec(x.execStack.sat, y.execStack.sat, false),
95+
}
96+
97+
case f_and_b:
98+
x, y := node.args[0], node.args[1]
99+
node.execStack = execSize{
100+
dsat: seqExec(x.execStack.dsat, y.execStack.dsat, true),
101+
sat: seqExec(x.execStack.sat, y.execStack.sat, true),
102+
}
103+
104+
case f_or_b:
105+
x, z := node.args[0], node.args[1]
106+
node.execStack = execSize{
107+
dsat: seqExec(x.execStack.dsat, z.execStack.dsat, true),
108+
sat: seqExec(x.execStack.sat, z.execStack.dsat, true).
109+
or(seqExec(
110+
x.execStack.dsat, z.execStack.sat, true,
111+
)),
112+
}
113+
114+
case f_or_c:
115+
x, z := node.args[0], node.args[1]
116+
node.execStack = execSize{
117+
dsat: invalid,
118+
sat: x.execStack.sat.or(seqExec(
119+
x.execStack.dsat, z.execStack.sat, false,
120+
)),
121+
}
122+
123+
case f_or_d:
124+
x, z := node.args[0], node.args[1]
125+
126+
// or_d compiles to [X] OP_IFDUP OP_NOTIF [Z] OP_ENDIF. On the
127+
// path where X is satisfied, X leaves exactly one non-zero
128+
// element (its u property) which OP_IFDUP duplicates before
129+
// OP_NOTIF consumes the copy, so the peak of that path is at
130+
// least the two elements. On the path where X is dissatisfied
131+
// the top element is zero, which OP_IFDUP does not duplicate,
132+
// and OP_NOTIF pops it before Z runs.
133+
satX := x.execStack.sat
134+
if satX.valid {
135+
satX = maxInt{valid: true, value: max(satX.value, 2)}
136+
}
137+
138+
node.execStack = execSize{
139+
dsat: seqExec(
140+
x.execStack.dsat, z.execStack.dsat, false,
141+
),
142+
sat: satX.or(seqExec(
143+
x.execStack.dsat, z.execStack.sat, false,
144+
)),
145+
}
146+
147+
case f_or_i:
148+
x, z := node.args[0], node.args[1]
149+
node.execStack = execSize{
150+
dsat: x.execStack.dsat.or(z.execStack.dsat),
151+
sat: x.execStack.sat.or(z.execStack.sat),
152+
}
153+
154+
case f_thresh:
155+
node.execStack = threshExecStack(node)
156+
157+
case f_wrap_a, f_wrap_s, f_wrap_c, f_wrap_n:
158+
// These wrappers do not change the peak execution stack size.
159+
node.execStack = node.args[0].execStack
160+
161+
case f_wrap_d:
162+
x := node.args[0]
163+
sat := invalid
164+
if x.execStack.sat.valid {
165+
// OP_DUP OP_IF leaves at least the duplicated element.
166+
sat = fixed(max(1, x.execStack.sat.value))
167+
}
168+
node.execStack = execSize{dsat: fixed(1), sat: sat}
169+
170+
case f_wrap_v:
171+
node.execStack = execSize{
172+
dsat: invalid,
173+
sat: node.args[0].execStack.sat,
174+
}
175+
176+
case f_wrap_j:
177+
node.execStack = execSize{
178+
dsat: fixed(1),
179+
sat: node.args[0].execStack.sat,
180+
}
181+
182+
default:
183+
return nil, fmt.Errorf("unknown identifier: %s",
184+
node.identifier)
185+
}
186+
187+
return node, nil
188+
}
189+
190+
// threshExecStack computes the execSize of a thresh(k, X1, ..., Xn) fragment.
191+
//
192+
// The sub expressions execute in order. The first runs on a clean stack; every
193+
// later one runs with the running total (one element) already on the stack, and
194+
// the final `<k> OP_EQUAL` needs the total plus the pushed k (two elements). So
195+
// the peak of a given (dis)satisfaction is the maximum, over the sub
196+
// expressions, of each one's own peak plus one for the running total when it is
197+
// not the first, and at least two for the final comparison.
198+
//
199+
// This is a tight, sound model of the true execution stack peak. (It does not
200+
// match rust-miniscript's threshold value exactly: rust's is an
201+
// order-dependent, internally inconsistent conservative estimate not worth
202+
// replicating; see the differential test for details.)
203+
func threshExecStack(node *AST) execSize {
204+
n := len(node.args) - 1
205+
k := int(node.args[0].num)
206+
207+
subSat := make([]maxInt, n)
208+
subDsat := make([]maxInt, n)
209+
for i, arg := range node.args[1:] {
210+
subSat[i], subDsat[i] = arg.execStack.sat, arg.execStack.dsat
211+
}
212+
213+
// adjusted returns the contribution of one sub expression's
214+
// (dis)satisfaction to the peak: every sub expression but the first
215+
// runs with the running total already on the stack.
216+
adjusted := func(e maxInt, i int) int {
217+
if i > 0 {
218+
return e.value + 1
219+
}
220+
return e.value
221+
}
222+
223+
// Dissatisfaction dissatisfies every sub expression.
224+
dsat := maxInt{valid: true, value: 2}
225+
for i := range subDsat {
226+
if !subDsat[i].valid {
227+
dsat = maxInt{}
228+
break
229+
}
230+
dsat.value = max(dsat.value, adjusted(subDsat[i], i))
231+
}
232+
233+
// Satisfaction satisfies exactly k sub expressions, taking the worst
234+
// case over all such choices. Since the peak is a maximum over the sub
235+
// expressions rather than a sum, it is enough to know for each of them
236+
// whether some choice satisfies (or dissatisfies) it, so the choices
237+
// don't have to be enumerated.
238+
sel := newThreshSelection(k, subSat, subDsat)
239+
sat := maxInt{}
240+
if sel.possible {
241+
sat = maxInt{valid: true, value: 2}
242+
for i := range subSat {
243+
if sel.canSatisfy(i) {
244+
sat.value = max(
245+
sat.value, adjusted(subSat[i], i),
246+
)
247+
}
248+
if sel.canDissatisfy(i) {
249+
sat.value = max(
250+
sat.value, adjusted(subDsat[i], i),
251+
)
252+
}
253+
}
254+
}
255+
256+
return execSize{dsat: dsat, sat: sat}
257+
}
258+
259+
// maxExecStackSize returns the maximum number of stack elements pushed during
260+
// execution (beyond the initial witness) to satisfy this script.
261+
func (a *AST) maxExecStackSize() int {
262+
return a.execStack.sat.value
263+
}

0 commit comments

Comments
 (0)