Skip to content

Commit cafbace

Browse files
committed
descriptors: add basic descriptor functionality
This commit adds descriptor parsing, checksum calculation and validation and several methods to the Descriptor struct: - AddressAt() - Keys() - DescType() - MaxWeightToSatisfy() - MultipathLen() - ScriptCodeAt() - String()
1 parent ac213e7 commit cafbace

24 files changed

Lines changed: 6488 additions & 0 deletions

descriptors/address.go

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package descriptors
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"sort"
7+
8+
"github.com/btcsuite/btcd/address/v2"
9+
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
11+
"github.com/btcsuite/btcd/chaincfg/v2"
12+
"github.com/btcsuite/btcd/chainhash/v2"
13+
"github.com/btcsuite/btcd/txscript/v2"
14+
)
15+
16+
// AddressAt derives and returns the address at the given multipath and
17+
// derivation index for the given network.
18+
func (d *Descriptor) AddressAt(params *chaincfg.Params, multipathIndex,
19+
derivationIndex uint32) (string, error) {
20+
21+
if uint64(multipathIndex) >= uint64(d.multipath) {
22+
return "", fmt.Errorf("multipath index out of bounds")
23+
}
24+
25+
addr, err := d.address(
26+
d.root, params, multipathIndex, derivationIndex,
27+
)
28+
if err != nil {
29+
return "", err
30+
}
31+
return addr.String(), nil
32+
}
33+
34+
// address builds the address for the given top-level node.
35+
func (d *Descriptor) address(n *node, params *chaincfg.Params, mp,
36+
idx uint32) (address.Address, error) {
37+
38+
switch n.kind {
39+
case nodePkh:
40+
pk, err := n.keys[0].derive(mp, idx)
41+
if err != nil {
42+
return nil, err
43+
}
44+
return address.NewAddressPubKeyHash(
45+
address.Hash160(pk), params,
46+
)
47+
48+
case nodeWpkh:
49+
pk, err := n.keys[0].derive(mp, idx)
50+
if err != nil {
51+
return nil, err
52+
}
53+
return address.NewAddressWitnessPubKeyHash(
54+
address.Hash160(pk), params,
55+
)
56+
57+
case nodeWsh:
58+
script, err := d.innerScript(n.sub, mp, idx)
59+
if err != nil {
60+
return nil, err
61+
}
62+
return address.NewAddressWitnessScriptHash(
63+
chainhash.HashB(script), params,
64+
)
65+
66+
case nodeSh:
67+
redeem, err := d.redeemScript(n.sub, mp, idx)
68+
if err != nil {
69+
return nil, err
70+
}
71+
return address.NewAddressScriptHash(redeem, params)
72+
73+
case nodeTr:
74+
outputKey, err := d.taprootOutputKey(n, mp, idx)
75+
if err != nil {
76+
return nil, err
77+
}
78+
return address.NewAddressTaproot(
79+
schnorr.SerializePubKey(outputKey), params,
80+
)
81+
82+
default:
83+
return nil, fmt.Errorf("descriptor of type %q has no address",
84+
d.DescType())
85+
}
86+
}
87+
88+
// redeemScript returns the script that a P2SH output commits to for the given
89+
// inner node.
90+
func (d *Descriptor) redeemScript(n *node, mp, idx uint32) ([]byte, error) {
91+
switch n.kind {
92+
case nodeWpkh:
93+
pk, err := n.keys[0].derive(mp, idx)
94+
if err != nil {
95+
return nil, err
96+
}
97+
return witnessV0Script(address.Hash160(pk))
98+
99+
case nodeWsh:
100+
script, err := d.innerScript(n.sub, mp, idx)
101+
if err != nil {
102+
return nil, err
103+
}
104+
return witnessV0Script(chainhash.HashB(script))
105+
106+
default:
107+
return d.innerScript(n, mp, idx)
108+
}
109+
}
110+
111+
// innerScript builds the script of a wsh/sh content node (pk, pkh, multi,
112+
// sortedmulti or a miniscript expression).
113+
func (d *Descriptor) innerScript(n *node, mp, idx uint32) ([]byte, error) {
114+
b := txscript.NewScriptBuilder()
115+
116+
switch n.kind {
117+
case nodePk:
118+
pk, err := n.keys[0].derive(mp, idx)
119+
if err != nil {
120+
return nil, err
121+
}
122+
b.AddData(pk)
123+
b.AddOp(txscript.OP_CHECKSIG)
124+
return b.Script()
125+
126+
case nodePkh:
127+
pk, err := n.keys[0].derive(mp, idx)
128+
if err != nil {
129+
return nil, err
130+
}
131+
b.AddOp(txscript.OP_DUP)
132+
b.AddOp(txscript.OP_HASH160)
133+
b.AddData(address.Hash160(pk))
134+
b.AddOp(txscript.OP_EQUALVERIFY)
135+
b.AddOp(txscript.OP_CHECKSIG)
136+
return b.Script()
137+
138+
case nodeMulti, nodeSortedMulti:
139+
return d.multiScript(n, mp, idx)
140+
141+
case nodeMs:
142+
return d.miniscriptScript(n, mp, idx)
143+
144+
default:
145+
return nil, fmt.Errorf("cannot build script for node %q",
146+
n.kind)
147+
}
148+
}
149+
150+
// multiScript builds a bare CHECKMULTISIG script for a multi/sortedmulti node.
151+
func (d *Descriptor) multiScript(n *node, mp, idx uint32) ([]byte, error) {
152+
pubKeys := make([][]byte, len(n.keys))
153+
for i, k := range n.keys {
154+
pk, err := k.derive(mp, idx)
155+
if err != nil {
156+
return nil, err
157+
}
158+
pubKeys[i] = pk
159+
}
160+
161+
// sortedmulti sorts the keys lexicographically (BIP67) before building
162+
// the script.
163+
if n.kind == nodeSortedMulti {
164+
sort.Slice(pubKeys, func(i, j int) bool {
165+
return bytes.Compare(pubKeys[i], pubKeys[j]) < 0
166+
})
167+
}
168+
169+
b := txscript.NewScriptBuilder()
170+
b.AddInt64(int64(n.thresh))
171+
for _, pk := range pubKeys {
172+
b.AddData(pk)
173+
}
174+
b.AddInt64(int64(len(pubKeys)))
175+
b.AddOp(txscript.OP_CHECKMULTISIG)
176+
return b.Script()
177+
}
178+
179+
// miniscriptScript compiles a miniscript node into its script, substituting
180+
// each key with its concrete derived public key.
181+
func (d *Descriptor) miniscriptScript(n *node, mp, idx uint32) ([]byte, error) {
182+
ast := n.clonedMsAST()
183+
184+
err := ast.ApplyVars(d.lookupKey(mp, idx))
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
return ast.Script()
190+
}
191+
192+
// lookupKey returns the variable lookup that substitutes the descriptor's keys
193+
// into a miniscript expression at the given multipath and derivation index.
194+
//
195+
// A miniscript also holds the hash values of its hash fragments as variables,
196+
// which are not descriptor keys. Returning nil for an identifier the descriptor
197+
// does not know lets the miniscript layer decode it as the literal value it is,
198+
// as its ApplyVars contract prescribes; erroring instead made every descriptor
199+
// containing a hash fragment fail at script build time.
200+
func (d *Descriptor) lookupKey(mp, idx uint32) func(string) ([]byte, error) {
201+
return func(id string) ([]byte, error) {
202+
k, ok := d.keyByRaw[id]
203+
if !ok {
204+
return nil, nil
205+
}
206+
207+
return k.derive(mp, idx)
208+
}
209+
}
210+
211+
// taprootOutputKey derives the taproot output key of a tr node: the internal
212+
// key tweaked with the merkle root of its script tree (or without a tweak
213+
// script for a key-path-only output).
214+
func (d *Descriptor) taprootOutputKey(n *node, mp, idx uint32) (
215+
*btcec.PublicKey, error) {
216+
217+
internal, err := n.keys[0].derivePub(mp, idx)
218+
if err != nil {
219+
return nil, err
220+
}
221+
222+
if n.tapTree == nil {
223+
return txscript.ComputeTaprootKeyNoScript(internal), nil
224+
}
225+
226+
root, err := d.tapNode(n.tapTree, mp, idx)
227+
if err != nil {
228+
return nil, err
229+
}
230+
rootHash := root.TapHash()
231+
232+
return txscript.ComputeTaprootOutputKey(internal, rootHash[:]), nil
233+
}
234+
235+
// tapNode builds the txscript taproot tree node for a descriptor tap tree,
236+
// deriving each leaf's script at the given multipath and derivation index. Its
237+
// TapHash is the merkle root committed to by the taproot output key.
238+
func (d *Descriptor) tapNode(t *tapTree, mp, idx uint32) (txscript.TapNode,
239+
error) {
240+
241+
if t.leaf != nil {
242+
leafScript, err := d.innerScript(t.leaf, mp, idx)
243+
if err != nil {
244+
return nil, err
245+
}
246+
return txscript.NewBaseTapLeaf(leafScript), nil
247+
}
248+
249+
left, err := d.tapNode(t.left, mp, idx)
250+
if err != nil {
251+
return nil, err
252+
}
253+
right, err := d.tapNode(t.right, mp, idx)
254+
if err != nil {
255+
return nil, err
256+
}
257+
258+
return txscript.NewTapBranch(left, right), nil
259+
}
260+
261+
// witnessV0Script builds a version-0 witness program script (OP_0 <program>),
262+
// used both as the scriptPubKey of a P2WPKH/P2WSH output and as the redeem
263+
// script of a P2SH-wrapped one.
264+
func witnessV0Script(program []byte) ([]byte, error) {
265+
return txscript.NewScriptBuilder().
266+
AddOp(txscript.OP_0).
267+
AddData(program).
268+
Script()
269+
}
270+
271+
// ScriptCodeAt derives and returns the script code (the raw compiled Bitcoin
272+
// script used for sighash computation) at the given multipath and derivation
273+
// index.
274+
func (d *Descriptor) ScriptCodeAt(multipathIndex, derivationIndex uint32) (
275+
[]byte, error) {
276+
277+
if uint64(multipathIndex) >= uint64(d.multipath) {
278+
return nil, fmt.Errorf("multipath index out of bounds")
279+
}
280+
return d.scriptCode(d.root, multipathIndex, derivationIndex)
281+
}
282+
283+
// scriptCode returns the script code for the given top-level node.
284+
func (d *Descriptor) scriptCode(n *node, mp, idx uint32) ([]byte, error) {
285+
switch n.kind {
286+
// A bare descriptor commits to its script directly, so the script code
287+
// is the output script itself. Besides pk() and pkh(), that covers the
288+
// bare multisigs of BIP383 and a bare miniscript.
289+
case nodePkh, nodePk, nodeMulti, nodeSortedMulti, nodeMs:
290+
return d.innerScript(n, mp, idx)
291+
292+
case nodeWpkh:
293+
// The script code of a P2WPKH is the corresponding P2PKH
294+
// script.
295+
return d.innerScript(&node{
296+
kind: nodePkh,
297+
keys: []*descKey{n.keys[0]},
298+
}, mp, idx)
299+
300+
case nodeWsh:
301+
return d.innerScript(n.sub, mp, idx)
302+
303+
case nodeSh:
304+
if n.sub.kind == nodeWsh {
305+
return d.innerScript(n.sub.sub, mp, idx)
306+
}
307+
if n.sub.kind == nodeWpkh {
308+
return d.scriptCode(n.sub, mp, idx)
309+
}
310+
return d.innerScript(n.sub, mp, idx)
311+
312+
default:
313+
return nil, fmt.Errorf("descriptor of type %q has no script "+
314+
"code", d.DescType())
315+
}
316+
}

descriptors/address_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package descriptors
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// TestWitnessV0Script checks that a version-0 witness program is wrapped as
11+
// OP_0 followed by the pushed program, for both the 20-byte (P2WPKH) and
12+
// 32-byte (P2WSH) program sizes.
13+
func TestWitnessV0Script(t *testing.T) {
14+
t.Parallel()
15+
16+
program20 := bytes.Repeat([]byte{0xab}, 20)
17+
script, err := witnessV0Script(program20)
18+
require.NoError(t, err)
19+
require.Equal(t, append([]byte{0x00, 0x14}, program20...), script)
20+
21+
program32 := bytes.Repeat([]byte{0xcd}, 32)
22+
script, err = witnessV0Script(program32)
23+
require.NoError(t, err)
24+
require.Equal(t, append([]byte{0x00, 0x20}, program32...), script)
25+
}

0 commit comments

Comments
 (0)