|
| 1 | +/** |
| 2 | + * Descriptor test utilities for building common descriptor templates. |
| 3 | + * Ported from @bitgo/utxo-core/testutil/descriptor/descriptors.ts. |
| 4 | + */ |
| 5 | +import assert from "assert"; |
| 6 | + |
| 7 | +import { BIP32 } from "../../bip32.js"; |
| 8 | +import type { BIP32Interface } from "../../bip32.js"; |
| 9 | +import { Descriptor, Miniscript, ast } from "../../index.js"; |
| 10 | +import type { Triple } from "../../triple.js"; |
| 11 | +import { DescriptorMap, PsbtParams } from "../../descriptorWallet/index.js"; |
| 12 | +import { getKeyTriple } from "../keys.js"; |
| 13 | + |
| 14 | +type KeyTriple = Triple<BIP32Interface>; |
| 15 | + |
| 16 | +export type DescriptorTemplate = |
| 17 | + | "Wsh2Of3" |
| 18 | + | "Tr1Of3-NoKeyPath-Tree" |
| 19 | + // no xpubs, just plain keys |
| 20 | + | "Tr1Of3-NoKeyPath-Tree-Plain" |
| 21 | + | "Tr2Of3-NoKeyPath" |
| 22 | + | "Wsh2Of2" |
| 23 | + /** |
| 24 | + * Wrapped segwit 2of3 multisig with a relative locktime OP_DROP |
| 25 | + * (requiring a miniscript extension). Used in CoreDao staking transactions. |
| 26 | + */ |
| 27 | + | "Wsh2Of3CltvDrop"; |
| 28 | + |
| 29 | +/** |
| 30 | + * Get the BIP-341 "Nothing Up My Sleeve" (NUMS) unspendable key. |
| 31 | + * This is the x-only public key with unknown discrete logarithm |
| 32 | + * constructed by hashing the uncompressed secp256k1 base point G. |
| 33 | + * |
| 34 | + * @see https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs |
| 35 | + */ |
| 36 | +export function getUnspendableKey(): string { |
| 37 | + return "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; |
| 38 | +} |
| 39 | + |
| 40 | +export function getDefaultXPubs(seed?: string): Triple<string> { |
| 41 | + return getKeyTriple(seed ?? "default").map((k) => k.neutered().toBase58()) as Triple<string>; |
| 42 | +} |
| 43 | + |
| 44 | +function toDescriptorMap(v: Record<string, string>): DescriptorMap { |
| 45 | + return new Map(Object.entries(v).map(([k, v]) => [k, Descriptor.fromString(v, "derivable")])); |
| 46 | +} |
| 47 | + |
| 48 | +function toXPub(k: BIP32Interface | string, path: string): string { |
| 49 | + if (typeof k === "string") { |
| 50 | + return k + "/" + path; |
| 51 | + } |
| 52 | + return k.neutered().toBase58() + "/" + path; |
| 53 | +} |
| 54 | + |
| 55 | +function toPlain(k: BIP32Interface | string, { xonly = false } = {}): string { |
| 56 | + if (typeof k === "string") { |
| 57 | + if (k.startsWith("xpub") || k.startsWith("xprv")) { |
| 58 | + return toPlain(BIP32.fromBase58(k), { xonly }); |
| 59 | + } |
| 60 | + return k; |
| 61 | + } |
| 62 | + return toHex(k.publicKey.subarray(xonly ? 1 : 0)); |
| 63 | +} |
| 64 | + |
| 65 | +function toHex(bytes: Uint8Array): string { |
| 66 | + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); |
| 67 | +} |
| 68 | + |
| 69 | +function toXOnly(k: BIP32Interface | string): string { |
| 70 | + return toPlain(k, { xonly: true }); |
| 71 | +} |
| 72 | + |
| 73 | +function multiArgs( |
| 74 | + m: number, |
| 75 | + n: number, |
| 76 | + keys: BIP32Interface[] | string[], |
| 77 | + path: string, |
| 78 | +): [number, ...string[]] { |
| 79 | + if (n < m) { |
| 80 | + throw new Error(`Cannot create ${m} of ${n} multisig`); |
| 81 | + } |
| 82 | + if (keys.length < n) { |
| 83 | + throw new Error(`Not enough keys for ${m} of ${n} multisig: keys.length=${keys.length}`); |
| 84 | + } |
| 85 | + keys = keys.slice(0, n); |
| 86 | + return [m, ...keys.map((k: BIP32Interface | string) => toXPub(k, path))]; |
| 87 | +} |
| 88 | + |
| 89 | +export function getPsbtParams(t: DescriptorTemplate): Partial<PsbtParams> { |
| 90 | + switch (t) { |
| 91 | + case "Wsh2Of3CltvDrop": |
| 92 | + return { locktime: 1 }; |
| 93 | + default: |
| 94 | + return {}; |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +export function getDescriptorNode( |
| 99 | + template: DescriptorTemplate, |
| 100 | + keys: KeyTriple | string[] = getDefaultXPubs(), |
| 101 | + path = "0/*", |
| 102 | +): ast.DescriptorNode { |
| 103 | + switch (template) { |
| 104 | + case "Wsh2Of3": |
| 105 | + return { |
| 106 | + wsh: { multi: multiArgs(2, 3, keys, path) }, |
| 107 | + }; |
| 108 | + case "Wsh2Of3CltvDrop": { |
| 109 | + const { locktime } = getPsbtParams(template); |
| 110 | + assert(locktime); |
| 111 | + return { |
| 112 | + wsh: { |
| 113 | + and_v: [{ "r:after": locktime }, { multi: multiArgs(2, 3, keys, path) }], |
| 114 | + }, |
| 115 | + }; |
| 116 | + } |
| 117 | + case "Wsh2Of2": |
| 118 | + return { |
| 119 | + wsh: { multi: multiArgs(2, 2, keys, path) }, |
| 120 | + }; |
| 121 | + case "Tr2Of3-NoKeyPath": |
| 122 | + return { |
| 123 | + tr: [getUnspendableKey(), { multi_a: multiArgs(2, 3, keys, path) }], |
| 124 | + }; |
| 125 | + case "Tr1Of3-NoKeyPath-Tree": |
| 126 | + return { |
| 127 | + tr: [ |
| 128 | + getUnspendableKey(), |
| 129 | + [ |
| 130 | + { pk: toXPub(keys[0], path) }, |
| 131 | + [{ pk: toXPub(keys[1], path) }, { pk: toXPub(keys[2], path) }], |
| 132 | + ], |
| 133 | + ], |
| 134 | + }; |
| 135 | + case "Tr1Of3-NoKeyPath-Tree-Plain": |
| 136 | + return { |
| 137 | + tr: [ |
| 138 | + getUnspendableKey(), |
| 139 | + [{ pk: toXOnly(keys[0]) }, [{ pk: toXOnly(keys[1]) }, { pk: toXOnly(keys[2]) }]], |
| 140 | + ], |
| 141 | + }; |
| 142 | + } |
| 143 | + throw new Error(`Unknown descriptor template: ${template as string}`); |
| 144 | +} |
| 145 | + |
| 146 | +type TapTree = [TapTree, TapTree] | ast.MiniscriptNode; |
| 147 | + |
| 148 | +function getTapLeafScriptNodes(t: ast.DescriptorNode | TapTree): ast.MiniscriptNode[] { |
| 149 | + if (Array.isArray(t)) { |
| 150 | + if (t.length !== 2) { |
| 151 | + throw new Error(`expected tuple, got: ${JSON.stringify(t)}`); |
| 152 | + } |
| 153 | + return t.map((v) => (Array.isArray(v) ? getTapLeafScriptNodes(v) : v)).flat(); |
| 154 | + } |
| 155 | + |
| 156 | + if (typeof t === "object") { |
| 157 | + const node = t; |
| 158 | + if (!("tr" in node)) { |
| 159 | + throw new Error( |
| 160 | + `TapLeafScripts are only supported for Taproot descriptors, got: ${JSON.stringify(t)}`, |
| 161 | + ); |
| 162 | + } |
| 163 | + if (!Array.isArray(node.tr) || node.tr.length !== 2) { |
| 164 | + throw new Error(`expected tuple, got: ${JSON.stringify(node.tr)}`); |
| 165 | + } |
| 166 | + const tapscript = node.tr[1]; |
| 167 | + if (!Array.isArray(tapscript)) { |
| 168 | + throw new Error(`expected tapscript to be an array, got: ${JSON.stringify(tapscript)}`); |
| 169 | + } |
| 170 | + return getTapLeafScriptNodes(tapscript); |
| 171 | + } |
| 172 | + |
| 173 | + throw new Error(`Invalid input: ${JSON.stringify(t)}`); |
| 174 | +} |
| 175 | + |
| 176 | +export function containsKey( |
| 177 | + script: Miniscript | ast.MiniscriptNode, |
| 178 | + key: BIP32Interface | string, |
| 179 | +): boolean { |
| 180 | + if (script instanceof Miniscript) { |
| 181 | + script = ast.fromMiniscript(script); |
| 182 | + } |
| 183 | + if ("pk" in script) { |
| 184 | + return script.pk === toXOnly(key); |
| 185 | + } |
| 186 | + throw new Error(`Unsupported script type: ${JSON.stringify(script)}`); |
| 187 | +} |
| 188 | + |
| 189 | +export function getTapLeafScripts(d: Descriptor): string[] { |
| 190 | + return getTapLeafScriptNodes(ast.fromDescriptor(d)).map((n) => |
| 191 | + Miniscript.fromString(ast.formatNode(n), "tap").toString(), |
| 192 | + ); |
| 193 | +} |
| 194 | + |
| 195 | +export function getDescriptor( |
| 196 | + template: DescriptorTemplate, |
| 197 | + keys: KeyTriple | string[] = getDefaultXPubs(), |
| 198 | + path = "0/*", |
| 199 | +): Descriptor { |
| 200 | + return Descriptor.fromStringDetectType(ast.formatNode(getDescriptorNode(template, keys, path))); |
| 201 | +} |
| 202 | + |
| 203 | +export function getDescriptorMap( |
| 204 | + template: DescriptorTemplate, |
| 205 | + keys: KeyTriple | string[] = getDefaultXPubs(), |
| 206 | +): DescriptorMap { |
| 207 | + return toDescriptorMap({ |
| 208 | + external: getDescriptor(template, keys, "0/*").toString(), |
| 209 | + internal: getDescriptor(template, keys, "1/*").toString(), |
| 210 | + }); |
| 211 | +} |
0 commit comments