Skip to content

Commit 01f214d

Browse files
OttoAllmendingerllm-git
andcommitted
feat: add comprehensive sBTC taproot descriptor tests
Add complete test suite for sBTC protocol's two-leaf taproot tree with deposit (payload_drop) and reclaim (r:older with multi_a) leaves. Verify structure parsing, P2TR output generation, and address derivation. Tests demonstrate proper use of fromStringExt with `drop: true` to enable r:older() in taproot context. Issue: BTC-3357 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent dbbe2b9 commit 01f214d

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

packages/wasm-utxo/test/sbtc.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import * as assert from "assert";
2+
import * as crypto from "crypto";
3+
import { Descriptor, ExtParamsConfig } from "../js/index.js";
4+
import { getUnspendableKey } from "../js/testutils/descriptor/descriptors.js";
5+
6+
// sBTC protocol uses two taproot script leaves:
7+
// 1. Deposit leaf: allows the signers to spend with a protocol payload
8+
// 2. Reclaim leaf: allows the depositors to reclaim after a timelock
9+
10+
const SIGNERS_KEY = "c9c2312ca406dcb8eed50b829b5292f5fb3e846db0a556af61cc53834ce75421";
11+
12+
// BIP341 "nothing up my sleeve" unspendable internal key — used so the taproot address
13+
// can only be spent via script path (no key-path spend).
14+
const UNSPENDABLE_KEY = getUnspendableKey();
15+
16+
const DEPOSIT_LEAF =
17+
"c:and_v(payload_drop(" +
18+
"0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5" +
19+
"),pk_k(" +
20+
SIGNERS_KEY +
21+
"))";
22+
23+
const RECLAIM_LEAF =
24+
"and_v(r:older(1),multi_a(2," +
25+
"4d838759b2a74616a2298e0580ca815874f5e5a9d2dd1b2f0203b68c66fc6c1e," +
26+
"639779c4b700dc51ece012a0e20325fcafada22a4a122ffaa04d0c0ccae83943," +
27+
"d1d6084eac98303e9d28e082bfd9eadf0b8be033e223a17ad01df81bdaa8c7b2))";
28+
29+
// Reference vectors from rust-miniscript test_payload_drop_stacks_vectors.
30+
// Deposit leaf: OP_PUSHBYTES_30 <metadata> OP_DROP OP_PUSHBYTES_32 <key> OP_CHECKSIG
31+
const DEPOSIT_SCRIPT_HEX =
32+
"1e0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5" +
33+
"7520c9c2312ca406dcb8eed50b829b5292f5fb3e846db0a556af61cc53834ce75421ac";
34+
const DEPOSIT_LEAF_HASH = "b14bbf1c6699b64429be4f11e1d4df7b75f16f68e7a86cb91c58daf024d0b379";
35+
// Reclaim leaf: OP_1 OP_CSV OP_DROP + 2-of-3 multi_a
36+
const RECLAIM_SCRIPT_HEX =
37+
"51b275" +
38+
"204d838759b2a74616a2298e0580ca815874f5e5a9d2dd1b2f0203b68c66fc6c1eac" +
39+
"20639779c4b700dc51ece012a0e20325fcafada22a4a122ffaa04d0c0ccae83943ba" +
40+
"20d1d6084eac98303e9d28e082bfd9eadf0b8be033e223a17ad01df81bdaa8c7b2ba529c";
41+
const RECLAIM_LEAF_HASH = "1e379caf8335dc3bd0af785d32d8135647ffa2ee76dd2c1bcc663ff424602ac0";
42+
// P2TR output: OP_1 OP_PUSHBYTES_32 <tweaked-x-only-pubkey>
43+
const SCRIPT_PUBKEY_HEX = "5120f3b3930e1e7103753b62e5cfee821b5bfa942eacb868e1d625243df606882dff";
44+
45+
// BIP341 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data)
46+
function taggedHash(tag: string, data: Buffer): Buffer {
47+
const tagHash = crypto.createHash("sha256").update(tag).digest();
48+
return crypto
49+
.createHash("sha256")
50+
.update(Buffer.concat([tagHash, tagHash, data]))
51+
.digest();
52+
}
53+
54+
// BIP341 tap leaf hash: tagged_hash("TapLeaf", version || compact_size(len) || script)
55+
// version 0xc0 = TapScript; compact_size is a single byte for scripts shorter than 253 bytes.
56+
function tapLeafHash(scriptHex: string): string {
57+
const script = Buffer.from(scriptHex, "hex");
58+
const data = Buffer.concat([Buffer.from([0xc0, script.length]), script]);
59+
return taggedHash("TapLeaf", data).toString("hex");
60+
}
61+
62+
function getSbtcDescriptor(depositLeaf: string, reclaimLeaf: string) {
63+
return `tr(${UNSPENDABLE_KEY},{${depositLeaf},${reclaimLeaf}})`;
64+
}
65+
66+
// Types matching the node() structure for the sBTC taproot descriptor
67+
type DefiniteKey = { Single: string };
68+
69+
type SbtcDepositLeaf = {
70+
Check: {
71+
AndV: [{ PayloadDrop: string }, { PkK: DefiniteKey }];
72+
};
73+
};
74+
75+
type SbtcReclaimLeaf = {
76+
AndV: [{ Drop: { Older: { relLockTime: number } } }, { MultiA: DefiniteKey[] }];
77+
};
78+
79+
type SbtcDescriptorNode = {
80+
Tr: [DefiniteKey, { Tree: [SbtcDepositLeaf, SbtcReclaimLeaf] }];
81+
};
82+
83+
describe("sBTC taproot descriptor", function () {
84+
// Use fromStringExt with { drop: true } to enable r:older() in taproot
85+
const descriptor = Descriptor.fromStringExt(
86+
getSbtcDescriptor(DEPOSIT_LEAF, RECLAIM_LEAF),
87+
"definite",
88+
{ drop: true } satisfies ExtParamsConfig,
89+
);
90+
91+
it("parses successfully with fromStringExt", () => {
92+
// Key test: Descriptor.fromStringExt({ drop: true }) handles r:older() with targeted drop permission
93+
assert.ok(descriptor, "Descriptor should parse successfully");
94+
});
95+
96+
it("has expected taproot structure", () => {
97+
const node = descriptor.node() as SbtcDescriptorNode;
98+
// Definite descriptors wrap keys in { Single: "..." }
99+
assert.deepStrictEqual(
100+
node.Tr[0],
101+
{ Single: UNSPENDABLE_KEY },
102+
"Should have correct internal key",
103+
);
104+
assert.ok(node.Tr[1].Tree, "Should have taproot tree structure");
105+
assert.strictEqual(node.Tr[1].Tree.length, 2, "Should have two leaves");
106+
});
107+
108+
describe("deposit leaf", function () {
109+
it("has correct structure with payload_drop", () => {
110+
const node = descriptor.node() as SbtcDescriptorNode;
111+
const depositLeaf = node.Tr[1].Tree[0];
112+
113+
assert.deepStrictEqual(depositLeaf, {
114+
Check: {
115+
AndV: [
116+
{ PayloadDrop: "0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5" },
117+
{ PkK: { Single: "c9c2312ca406dcb8eed50b829b5292f5fb3e846db0a556af61cc53834ce75421" } },
118+
],
119+
},
120+
});
121+
});
122+
123+
it("has correct script hex and tap leaf hash", () => {
124+
assert.strictEqual(tapLeafHash(DEPOSIT_SCRIPT_HEX), DEPOSIT_LEAF_HASH);
125+
});
126+
});
127+
128+
describe("reclaim leaf", function () {
129+
it("has correct structure with r:older (Drop wrapper)", () => {
130+
const node = descriptor.node() as SbtcDescriptorNode;
131+
const reclaimLeaf = node.Tr[1].Tree[1];
132+
133+
// Verify the r:older pattern creates a Drop wrapper
134+
assert.ok(reclaimLeaf.AndV, "Should have AndV structure");
135+
assert.ok(reclaimLeaf.AndV[0].Drop, "Should have Drop wrapper for r:older");
136+
assert.ok(reclaimLeaf.AndV[0].Drop.Older, "Should contain Older inside Drop");
137+
assert.strictEqual(
138+
reclaimLeaf.AndV[0].Drop.Older.relLockTime,
139+
1,
140+
"Should have locktime of 1",
141+
);
142+
143+
// Verify the multi_a is the second part
144+
assert.ok(reclaimLeaf.AndV[1].MultiA, "Should have MultiA as second element");
145+
});
146+
147+
it("has correct script hex and tap leaf hash", () => {
148+
assert.strictEqual(tapLeafHash(RECLAIM_SCRIPT_HEX), RECLAIM_LEAF_HASH);
149+
});
150+
});
151+
152+
describe("P2TR output", function () {
153+
it("produces correct script pubkey", () => {
154+
const scriptPubkeyBytes = descriptor.scriptPubkey();
155+
assert.strictEqual(Buffer.from(scriptPubkeyBytes).toString("hex"), SCRIPT_PUBKEY_HEX);
156+
});
157+
});
158+
});

0 commit comments

Comments
 (0)