From 2a1417f49397a0132270137cd42853d48ef0e169 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 21 Aug 2024 15:28:30 +0200 Subject: [PATCH] feat: add fixedScriptToDescriptor with tests This is useful in testing Issue: BTC-1348 --- package-lock.json | 1 + packages/wasm-miniscript/package.json | 1 + .../test/fixedScriptToDescriptor.ts | 109 ++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 packages/wasm-miniscript/test/fixedScriptToDescriptor.ts diff --git a/package-lock.json b/package-lock.json index 4ce0817..edfff09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18047,6 +18047,7 @@ "name": "@bitgo/wasm-miniscript", "version": "0.0.0-semantic-release-managed", "devDependencies": { + "@bitgo/utxo-lib": "^10.1.0", "@types/mocha": "^10.0.7", "@types/node": "^20.14.10", "mocha": "^10.6.0", diff --git a/packages/wasm-miniscript/package.json b/packages/wasm-miniscript/package.json index e464f4b..405c2ff 100644 --- a/packages/wasm-miniscript/package.json +++ b/packages/wasm-miniscript/package.json @@ -43,6 +43,7 @@ "check-fmt": "prettier --check ." }, "devDependencies": { + "@bitgo/utxo-lib": "^10.1.0", "@types/mocha": "^10.0.7", "@types/node": "^20.14.10", "mocha": "^10.6.0", diff --git a/packages/wasm-miniscript/test/fixedScriptToDescriptor.ts b/packages/wasm-miniscript/test/fixedScriptToDescriptor.ts new file mode 100644 index 0000000..ec1648a --- /dev/null +++ b/packages/wasm-miniscript/test/fixedScriptToDescriptor.ts @@ -0,0 +1,109 @@ +import * as assert from "assert"; +import * as utxolib from "@bitgo/utxo-lib"; +import { Descriptor } from "../js"; + +/** Expand a template with the given root wallet keys and chain code */ +function expand(template: string, rootWalletKeys: utxolib.bitgo.RootWalletKeys, chainCode: number) { + return template.replace(/\$([0-9])/g, (_, i) => { + const keyIndex = parseInt(i, 10); + if (keyIndex !== 0 && keyIndex !== 1 && keyIndex !== 2) { + throw new Error("Invalid key index"); + } + const xpub = rootWalletKeys.triple[keyIndex].neutered().toBase58(); + const prefix = rootWalletKeys.derivationPrefixes[keyIndex]; + return xpub + "/" + prefix + "/" + chainCode + "/*"; + }); +} + +/** + * Get a standard output descriptor that corresponds to the proprietary HD wallet setup + * used in BitGo wallets. + * Only supports a subset of script types. + */ +function getDescriptorForScriptType( + rootWalletKeys: utxolib.bitgo.RootWalletKeys, + scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3, + scope: "internal" | "external", +): string { + const chain = + scope === "external" + ? utxolib.bitgo.getExternalChainCode(scriptType) + : utxolib.bitgo.getInternalChainCode(scriptType); + switch (scriptType) { + case "p2sh": + return expand("sh(multi(2,$0,$1,$2))", rootWalletKeys, chain); + case "p2shP2wsh": + return expand("sh(wsh(multi(2,$0,$1,$2)))", rootWalletKeys, chain); + case "p2wsh": + return expand("wsh(multi(2,$0,$1,$2))", rootWalletKeys, chain); + default: + throw new Error(`Unsupported script type ${scriptType}`); + } +} + +const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm")); +const scriptTypes = ["p2sh", "p2shP2wsh", "p2wsh"] as const; +const scope = ["external", "internal"] as const; +const index = [0, 1, 2]; + +function runTest( + scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3, + index: number, + scope: "internal" | "external", +) { + describe(`scriptType=${scriptType}, index=${index}, scope=${scope}`, function () { + const chainCode = + scope === "external" + ? utxolib.bitgo.getExternalChainCode(scriptType) + : utxolib.bitgo.getInternalChainCode(scriptType); + const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chainCode, index); + const scriptUtxolib = utxolib.bitgo.outputScripts.createOutputScript2of3( + derivedKeys.publicKeys, + scriptType, + ).scriptPubKey; + + it("descriptor should have expected format", function () { + const descriptor = Descriptor.fromString( + getDescriptorForScriptType(rootWalletKeys, scriptType, scope), + "derivable", + ); + const [x1, x2, x3] = rootWalletKeys.triple.map((xpub) => xpub.neutered().toBase58()); + if (scriptType === "p2sh" && scope === "external") { + // spot check + assert.ok( + descriptor + .toString() + .startsWith(`sh(multi(2,${x1}/0/0/0/*,${x2}/0/0/0/*,${x3}/0/0/0/*))`), + ); + } + if (scriptType === "p2shP2wsh" && scope === "internal") { + // spot check + assert.ok( + descriptor + .toString() + .startsWith(`sh(wsh(multi(2,${x1}/0/0/11/*,${x2}/0/0/11/*,${x3}/0/0/11/*)))`), + ); + } + }); + + it("address should match descriptor", function () { + const scriptFromDescriptor = Buffer.from( + Descriptor.fromString( + getDescriptorForScriptType(rootWalletKeys, scriptType, scope), + "derivable", + ) + .atDerivationIndex(index) + .scriptPubkey(), + ); + assert.deepStrictEqual(scriptUtxolib.toString("hex"), scriptFromDescriptor.toString("hex")); + }); + }); +} + +scriptTypes.forEach((scriptType) => { + index.forEach((index) => { + scope.forEach((scope) => { + runTest(scriptType, index, scope); + }); + }); +});