Skip to content

Commit bde2415

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 40fed57 commit bde2415

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

packages/wasm-utxo/test/sbtc.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as assert from "assert";
2+
import * as utxolib from "@bitgo/utxo-lib";
3+
import { Descriptor } from "../js/index.js";
4+
import type { ExtParamsConfig } from "../js/index.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 DEPOSIT_LEAF =
11+
"c:and_v(payload_drop(" +
12+
"0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5" +
13+
"),pk_k(c9c2312ca406dcb8eed50b829b5292f5fb3e846db0a556af61cc53834ce75421))";
14+
15+
const RECLAIM_LEAF =
16+
"and_v(r:older(1),multi_a(2," +
17+
"4d838759b2a74616a2298e0580ca815874f5e5a9d2dd1b2f0203b68c66fc6c1e," +
18+
"639779c4b700dc51ece012a0e20325fcafada22a4a122ffaa04d0c0ccae83943," +
19+
"d1d6084eac98303e9d28e082bfd9eadf0b8be033e223a17ad01df81bdaa8c7b2))";
20+
21+
const SIGNERS_KEY = "c9c2312ca406dcb8eed50b829b5292f5fb3e846db0a556af61cc53834ce75421";
22+
23+
function getSbtcDescriptor(signersKey: string, depositLeaf: string, reclaimLeaf: string) {
24+
return `tr(${signersKey},{${depositLeaf},${reclaimLeaf}})`;
25+
}
26+
27+
// Types matching the node() structure for the sBTC taproot descriptor
28+
type DefiniteKey = { Single: string };
29+
30+
type SbtcDepositLeaf = {
31+
Check: {
32+
AndV: [{ PayloadDrop: string }, { PkK: DefiniteKey }];
33+
};
34+
};
35+
36+
type SbtcReclaimLeaf = {
37+
AndV: [{ Drop: { Older: { relLockTime: number } } }, { MultiA: DefiniteKey[] }];
38+
};
39+
40+
type SbtcDescriptorNode = {
41+
Tr: [DefiniteKey, { Tree: [SbtcDepositLeaf, SbtcReclaimLeaf] }];
42+
};
43+
44+
describe("sBTC taproot descriptor", function () {
45+
// Use fromStringExt with { drop: true } to enable r:older() in taproot
46+
const descriptor = Descriptor.fromStringExt(
47+
getSbtcDescriptor(SIGNERS_KEY, DEPOSIT_LEAF, RECLAIM_LEAF),
48+
"definite",
49+
{ drop: true } satisfies ExtParamsConfig,
50+
);
51+
52+
it("parses successfully with fromStringExt", () => {
53+
// Key test: Descriptor.fromStringExt({ drop: true }) handles r:older() with targeted drop permission
54+
assert.ok(descriptor, "Descriptor should parse successfully");
55+
});
56+
57+
it("has expected taproot structure", () => {
58+
const node = descriptor.node() as SbtcDescriptorNode;
59+
// Definite descriptors wrap keys in { Single: "..." }
60+
assert.deepStrictEqual(node.Tr[0], { Single: SIGNERS_KEY }, "Should have correct internal key");
61+
assert.ok(node.Tr[1].Tree, "Should have taproot tree structure");
62+
assert.strictEqual(node.Tr[1].Tree.length, 2, "Should have two leaves");
63+
});
64+
65+
describe("deposit leaf", function () {
66+
it("has correct structure with payload_drop", () => {
67+
const node = descriptor.node() as SbtcDescriptorNode;
68+
const depositLeaf = node.Tr[1].Tree[0];
69+
70+
assert.deepStrictEqual(depositLeaf, {
71+
Check: {
72+
AndV: [
73+
{ PayloadDrop: "0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5" },
74+
{ PkK: { Single: "c9c2312ca406dcb8eed50b829b5292f5fb3e846db0a556af61cc53834ce75421" } },
75+
],
76+
},
77+
});
78+
});
79+
});
80+
81+
describe("reclaim leaf", function () {
82+
it("has correct structure with r:older (Drop wrapper)", () => {
83+
const node = descriptor.node() as SbtcDescriptorNode;
84+
const reclaimLeaf = node.Tr[1].Tree[1];
85+
86+
// Verify the r:older pattern creates a Drop wrapper
87+
assert.ok(reclaimLeaf.AndV, "Should have AndV structure");
88+
assert.ok(reclaimLeaf.AndV[0].Drop, "Should have Drop wrapper for r:older");
89+
assert.ok(reclaimLeaf.AndV[0].Drop.Older, "Should contain Older inside Drop");
90+
assert.strictEqual(
91+
reclaimLeaf.AndV[0].Drop.Older.relLockTime,
92+
1,
93+
"Should have locktime of 1",
94+
);
95+
96+
// Verify the multi_a is the second part
97+
assert.ok(reclaimLeaf.AndV[1].MultiA, "Should have MultiA as second element");
98+
});
99+
});
100+
101+
describe("P2TR output", function () {
102+
it("produces correct address", () => {
103+
const scriptPubkeyBytes = descriptor.scriptPubkey();
104+
105+
// P2TR scripts are 34 bytes: OP_1 (0x51) + OP_PUSHBYTES_32 (0x20) + 32-byte pubkey
106+
assert.strictEqual(scriptPubkeyBytes.length, 34, "Should be 34 bytes");
107+
assert.strictEqual(scriptPubkeyBytes[0], 0x51, "Should start with OP_1");
108+
assert.strictEqual(scriptPubkeyBytes[1], 0x20, "Should have 32-byte push");
109+
110+
const addr = utxolib.address.fromOutputScript(
111+
Buffer.from(scriptPubkeyBytes) as Buffer<ArrayBufferLike>,
112+
utxolib.networks.bitcoin,
113+
);
114+
assert.strictEqual(addr, "bc1p04m0dcwy53627k03x67wjzfn77x7zu85pmwnz653u4uzpl4qsg9qgp3nqd");
115+
});
116+
});
117+
});

0 commit comments

Comments
 (0)