Skip to content

Commit 233220b

Browse files
Merge pull request #55 from BitGo/BTC-1829.add-tree-formatting
feat: support taproot tree nodes in descriptor AST
2 parents ceae5a6 + 0a4a83b commit 233220b

File tree

7 files changed

+366
-3
lines changed

7 files changed

+366
-3
lines changed

packages/wasm-miniscript/js/ast/formatNode.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ type Miniscript =
4848
| Wrap<{ older: number }>
4949
| Wrap<{ after: number }>;
5050

51+
type TapTree = [TapTree, TapTree] | Miniscript;
52+
5153
// Top level descriptor expressions
5254
// https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md#reference
5355
type Descriptor =
@@ -57,13 +59,22 @@ type Descriptor =
5759
| { pkh: Key }
5860
| { wpkh: Key }
5961
| { combo: Key }
60-
| { tr: [Key, Miniscript] }
62+
| { tr: [Key, TapTree] }
6163
| { addr: string }
6264
| { raw: string }
6365
| { rawtr: string };
6466

6567
type Node = Miniscript | Descriptor | number | string;
6668

69+
/**
70+
* Format a TapTree as a string
71+
* If the argument is an array, descend recursively with formatTr and wrap result in curly braces
72+
* Otherwise, format the node with formatN
73+
*/
74+
function formatTr(tree: TapTree): string {
75+
return Array.isArray(tree) ? `{` + tree.map(formatTr).join(",") + `}` : formatN(tree);
76+
}
77+
6778
function formatN(n: Node | Node[]): string {
6879
if (typeof n === "string") {
6980
return n;
@@ -80,11 +91,16 @@ function formatN(n: Node | Node[]): string {
8091
throw new Error(`Invalid node: ${n}`);
8192
}
8293
const [name, value] = entries[0];
94+
if (name === "tr" && Array.isArray(value)) {
95+
const [key, tree] = value;
96+
return formatN({ tr: formatN([key, formatTr(tree)]) });
97+
}
8398
return `${name}(${formatN(value)})`;
8499
}
85100
throw new Error(`Invalid node: ${n}`);
86101
}
87102

103+
export type TapTreeNode = TapTree;
88104
export type MiniscriptNode = Miniscript;
89105
export type DescriptorNode = Descriptor;
90106

packages/wasm-miniscript/js/ast/fromWasmNode.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DescriptorNode, MiniscriptNode } from "./formatNode";
1+
import { DescriptorNode, MiniscriptNode, TapTreeNode } from "./formatNode";
22
import { Descriptor, Miniscript } from "../index";
33

44
function getSingleEntry(v: unknown): [string, unknown] {
@@ -22,7 +22,7 @@ function wrap(type: string, value: unknown): MiniscriptNode {
2222
return { [`${type}:${name}`]: inner } as MiniscriptNode;
2323
}
2424

25-
type Node = DescriptorNode | MiniscriptNode | string | number;
25+
type Node = DescriptorNode | MiniscriptNode | TapTreeNode | string | number;
2626

2727
function fromUnknown(v: unknown): Node | Node[] {
2828
if (typeof v === "number" || typeof v === "string") {
@@ -113,6 +113,12 @@ function fromUnknown(v: unknown): Node | Node[] {
113113
return node("multi", value);
114114
case "MultiA":
115115
return node("multi_a", value);
116+
117+
case "Tree":
118+
if (!Array.isArray(value) || value.length !== 2) {
119+
throw new Error(`Invalid Tree node: ${JSON.stringify(value)}`);
120+
}
121+
return [fromUnknown(value[0]), fromUnknown(value[1])] as TapTreeNode;
116122
}
117123
}
118124

packages/wasm-miniscript/test/ast/formatNode.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,14 @@ describe("formatNode", function () {
1010
formatNode({ and_v: [{ after: 1 }, { after: 1 }] }),
1111
"and_v(after(1),after(1))",
1212
);
13+
// taproot single key
14+
assert.strictEqual(formatNode({ tr: "k" }), "tr(k)");
15+
// key with single-node taproot tree
16+
assert.strictEqual(formatNode({ tr: ["k", { pk: "k1" }] }), "tr(k,pk(k1))");
17+
// key with multi-node taproot tree
18+
assert.strictEqual(
19+
formatNode({ tr: ["k", [{ pk: "k1" }, { pk: "k2" }]] }),
20+
"tr(k,{pk(k1),pk(k2)})",
21+
);
1322
});
1423
});

packages/wasm-miniscript/test/descriptorFixtures.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,21 @@ export const fixtures = {
357357
script: "0020823bcb22035958d32afe8ec04357535a3e73da3ed9cd90a4251970f9995077a5",
358358
checksumRequired: false,
359359
},
360+
{
361+
descriptor:
362+
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(200)))#t9nj25pu",
363+
script: "5120a887274080b8b01b124bc0f3b7e180e2910e30ef6c319b17bf878cb2576397e4",
364+
},
365+
{
366+
descriptor:
367+
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{and_v(and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),v:multi_a(1,850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900,a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3)),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),{and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(100))}})#cfz66mzw",
368+
script: "5120230320466c8eb821585e10e1ca35f98ea064aea8d972db994a50ce14a5d49689",
369+
},
370+
{
371+
descriptor:
372+
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{and_v(and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),v:multi_a(1,850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900,a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3)),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(200))})#ruusuuvn",
373+
script: "51207f0160320dcb290c130b93cf18d445c3f3c8208fccd6cf6e6b2790a360378d15",
374+
},
360375
],
361376
invalid: [
362377
{
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"descriptor": "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(200)))#t9nj25pu",
3+
"wasmNode": {
4+
"Tr": [
5+
{
6+
"Single": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
7+
},
8+
{
9+
"AndV": [
10+
{
11+
"Verify": {
12+
"Check": {
13+
"PkK": {
14+
"Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
15+
}
16+
}
17+
}
18+
},
19+
{
20+
"Older": {
21+
"relLockTime": 200
22+
}
23+
}
24+
]
25+
}
26+
]
27+
},
28+
"ast": {
29+
"tr": [
30+
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
31+
{
32+
"and_v": [
33+
{
34+
"v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
35+
},
36+
{
37+
"older": 200
38+
}
39+
]
40+
}
41+
]
42+
}
43+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
{
2+
"descriptor": "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{and_v(and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),v:multi_a(1,850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900,a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3)),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),{and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(100))}})#cfz66mzw",
3+
"wasmNode": {
4+
"Tr": [
5+
{
6+
"Single": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
7+
},
8+
{
9+
"Tree": [
10+
{
11+
"AndV": [
12+
{
13+
"AndV": [
14+
{
15+
"Verify": {
16+
"Check": {
17+
"PkK": {
18+
"Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
19+
}
20+
}
21+
}
22+
},
23+
{
24+
"Verify": {
25+
"MultiA": [
26+
1,
27+
{
28+
"Single": "850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900"
29+
},
30+
{
31+
"Single": "a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3"
32+
}
33+
]
34+
}
35+
}
36+
]
37+
},
38+
{
39+
"MultiA": [
40+
2,
41+
{
42+
"Single": "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b"
43+
},
44+
{
45+
"Single": "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f"
46+
}
47+
]
48+
}
49+
]
50+
},
51+
{
52+
"Tree": [
53+
{
54+
"AndV": [
55+
{
56+
"Verify": {
57+
"Check": {
58+
"PkK": {
59+
"Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
60+
}
61+
}
62+
}
63+
},
64+
{
65+
"MultiA": [
66+
2,
67+
{
68+
"Single": "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b"
69+
},
70+
{
71+
"Single": "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f"
72+
}
73+
]
74+
}
75+
]
76+
},
77+
{
78+
"AndV": [
79+
{
80+
"Verify": {
81+
"Check": {
82+
"PkK": {
83+
"Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
84+
}
85+
}
86+
}
87+
},
88+
{
89+
"Older": {
90+
"relLockTime": 100
91+
}
92+
}
93+
]
94+
}
95+
]
96+
}
97+
]
98+
}
99+
]
100+
},
101+
"ast": {
102+
"tr": [
103+
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
104+
[
105+
{
106+
"and_v": [
107+
{
108+
"and_v": [
109+
{
110+
"v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
111+
},
112+
{
113+
"v:multi_a": [
114+
1,
115+
"850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900",
116+
"a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3"
117+
]
118+
}
119+
]
120+
},
121+
{
122+
"multi_a": [
123+
2,
124+
"2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b",
125+
"6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f"
126+
]
127+
}
128+
]
129+
},
130+
[
131+
{
132+
"and_v": [
133+
{
134+
"v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
135+
},
136+
{
137+
"multi_a": [
138+
2,
139+
"2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b",
140+
"6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f"
141+
]
142+
}
143+
]
144+
},
145+
{
146+
"and_v": [
147+
{
148+
"v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9"
149+
},
150+
{
151+
"older": 100
152+
}
153+
]
154+
}
155+
]
156+
]
157+
]
158+
}
159+
}

0 commit comments

Comments
 (0)