From bc0fb096ce29dae1e6cde984f3d54f9a292d4996 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 14 Feb 2025 13:07:52 +0100 Subject: [PATCH 1/3] feat(utxo-staking): use miniscript AST for descriptor creation Issue: BTC-1829 --- modules/utxo-staking/src/coreDao/descriptor.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/utxo-staking/src/coreDao/descriptor.ts b/modules/utxo-staking/src/coreDao/descriptor.ts index af753208bd..a807731a86 100644 --- a/modules/utxo-staking/src/coreDao/descriptor.ts +++ b/modules/utxo-staking/src/coreDao/descriptor.ts @@ -1,4 +1,5 @@ import { BIP32Interface } from '@bitgo/utxo-lib'; +import { ast } from '@bitgo/wasm-miniscript'; /** * Script type for a descriptor. @@ -39,13 +40,13 @@ export function createMultiSigDescriptor( throw new Error(`locktime (${locktime}) must be greater than 0`); } const keys = orderedKeys.map((key) => asDescriptorKey(key, neutered)); - const inner = `and_v(r:after(${locktime}),multi(${m},${keys.join(',')}))`; + const inner: ast.MiniscriptNode = { and_v: [{ 'r:after': locktime }, { multi: [m, ...keys] }] }; switch (scriptType) { case 'sh': - return `sh(${inner})`; + return ast.formatNode({ sh: inner }); case 'sh-wsh': - return `sh(wsh(${inner}))`; + return ast.formatNode({ sh: { wsh: inner } }); case 'wsh': - return `wsh(${inner})`; + return ast.formatNode({ wsh: inner }); } } From 79f9c5c8e8650b494ec1afa2fa215328d3d1facb Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 14 Feb 2025 12:10:06 +0100 Subject: [PATCH 2/3] feat(utxo-staking): use TapTreeNode type for Babylon descriptor Update descriptor builder to use correct AST types and method for creating tap tree script descriptors. Issue: BTC-1829 --- modules/utxo-staking/package.json | 2 +- .../utxo-staking/src/babylon/descriptor.ts | 37 ++++++++----------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/modules/utxo-staking/package.json b/modules/utxo-staking/package.json index b2273e5777..8ab6d7fb20 100644 --- a/modules/utxo-staking/package.json +++ b/modules/utxo-staking/package.json @@ -42,7 +42,7 @@ "@bitgo/utxo-core": "^1.1.0", "@bitgo/unspents": "^0.47.17", "@bitgo/utxo-lib": "^11.2.1", - "@bitgo/wasm-miniscript": "^2.0.0-beta.3" + "@bitgo/wasm-miniscript": "^2.0.0-beta.4" }, "devDependencies": { "bitcoinjs-lib": "^6.1.7", diff --git a/modules/utxo-staking/src/babylon/descriptor.ts b/modules/utxo-staking/src/babylon/descriptor.ts index 942f7c3dbe..28699f6544 100644 --- a/modules/utxo-staking/src/babylon/descriptor.ts +++ b/modules/utxo-staking/src/babylon/descriptor.ts @@ -1,5 +1,4 @@ import { Descriptor, ast } from '@bitgo/wasm-miniscript'; -import { MiniscriptNode } from '@bitgo/wasm-miniscript/dist/node/js/ast'; export function getUnspendableKey(): string { // https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/constants/internalPubkey.ts @@ -7,7 +6,7 @@ export function getUnspendableKey(): string { } // Helper functions for creating miniscript nodes -function pk(b: Buffer): MiniscriptNode { +function pk(b: Buffer): ast.MiniscriptNode { return { 'v:pk': b.toString('hex') }; } @@ -15,11 +14,11 @@ function sortedKeys(keys: Buffer[]): Buffer[] { return keys.sort((a, b) => a.compare(b)); } -function multi_a(threshold: number, keys: Buffer[]): { multi_a: [number, ...string[]] } { - return { multi_a: [threshold, ...sortedKeys(keys).map((k) => k.toString('hex'))] }; +function multiArgs(threshold: number, keys: Buffer[]): [number, ...string[]] { + return [threshold, ...sortedKeys(keys).map((k) => k.toString('hex'))]; } -function taprootScriptOnlyFromAst(n: ast.MiniscriptNode): Descriptor { +function taprootScriptOnlyFromAst(n: ast.TapTreeNode): Descriptor { return Descriptor.fromString(ast.formatNode({ tr: [getUnspendableKey(), n] }), 'definite'); } @@ -33,35 +32,34 @@ export class BabylonDescriptorBuilder { public unbondingTimeLock: number ) {} - getTimelockMiniscript(): MiniscriptNode { + getTimelockMiniscript(): ast.MiniscriptNode { return { and_v: [pk(this.stakerKey), { older: this.stakingTimeLock }] }; } - getUnbondingMiniscript(): MiniscriptNode { - return { and_v: [pk(this.stakerKey), multi_a(this.covenantThreshold, this.covenantKeys)] }; + getUnbondingMiniscript(): ast.MiniscriptNode { + return { and_v: [pk(this.stakerKey), { multi_a: multiArgs(this.covenantThreshold, this.covenantKeys) }] }; } - getSlashingMiniscript(): MiniscriptNode { + getSlashingMiniscript(): ast.MiniscriptNode { return { and_v: [ { - and_v: [pk(this.stakerKey), { 'v:multi_a': multi_a(1, this.finalityProviderKeys).multi_a }], + and_v: [pk(this.stakerKey), { 'v:multi_a': multiArgs(1, this.finalityProviderKeys) }], }, - multi_a(this.covenantThreshold, this.covenantKeys), + { multi_a: multiArgs(this.covenantThreshold, this.covenantKeys) }, ], }; } - getUnbondingTimelockMiniscript(): MiniscriptNode { + getUnbondingTimelockMiniscript(): ast.MiniscriptNode { return { and_v: [pk(this.stakerKey), { older: this.unbondingTimeLock }] }; } getStakingDescriptor(): Descriptor { - const a = ast.formatNode(this.getSlashingMiniscript()); - const b = ast.formatNode(this.getUnbondingMiniscript()); - const c = ast.formatNode(this.getTimelockMiniscript()); - const desc = `tr(${getUnspendableKey()},{${a},{${b},${c}}})`; - return Descriptor.fromString(desc, 'definite'); + return taprootScriptOnlyFromAst([ + this.getSlashingMiniscript(), + [this.getUnbondingMiniscript(), this.getTimelockMiniscript()], + ]); } getSlashingDescriptor(): Descriptor { @@ -69,9 +67,6 @@ export class BabylonDescriptorBuilder { } getUnbondingDescriptor(): Descriptor { - const a = ast.formatNode(this.getSlashingMiniscript()); - const b = ast.formatNode(this.getUnbondingTimelockMiniscript()); - const desc = `tr(${getUnspendableKey()},{${a},${b}})`; - return Descriptor.fromString(desc, 'definite'); + return taprootScriptOnlyFromAst([this.getSlashingMiniscript(), this.getUnbondingTimelockMiniscript()]); } } From d29e0dcb09352f2ba9910d224ac3ac9c92cc9e81 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 14 Feb 2025 12:24:30 +0100 Subject: [PATCH 3/3] feat(utxo-core): update descriptor test util to use AST types Refactor test descriptor builder to use AST nodes and types instead of string manipulation. Issue: BTC-1829 --- modules/abstract-utxo/package.json | 2 +- modules/utxo-bin/package.json | 2 +- modules/utxo-core/package.json | 2 +- .../src/testutil/descriptor/descriptors.ts | 55 ++++++++++--------- yarn.lock | 8 +-- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index feb5b18dbb..d5622d72c8 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -48,7 +48,7 @@ "@bitgo/unspents": "^0.47.17", "@bitgo/utxo-core": "^1.1.0", "@bitgo/utxo-lib": "^11.2.1", - "@bitgo/wasm-miniscript": "^2.0.0-beta.3", + "@bitgo/wasm-miniscript": "^2.0.0-beta.4", "@types/bluebird": "^3.5.25", "@types/lodash": "^4.14.121", "@types/superagent": "4.1.15", diff --git a/modules/utxo-bin/package.json b/modules/utxo-bin/package.json index 1cee7ebdbe..bd92ff73b3 100644 --- a/modules/utxo-bin/package.json +++ b/modules/utxo-bin/package.json @@ -30,7 +30,7 @@ "@bitgo/statics": "^50.24.0", "@bitgo/unspents": "^0.47.17", "@bitgo/utxo-lib": "^11.2.1", - "@bitgo/wasm-miniscript": "^2.0.0-beta.3", + "@bitgo/wasm-miniscript": "^2.0.0-beta.4", "archy": "^1.0.0", "bech32": "^2.0.0", "bitcoinjs-lib": "npm:@bitgo-forks/bitcoinjs-lib@7.1.0-master.2", diff --git a/modules/utxo-core/package.json b/modules/utxo-core/package.json index 48c7663c6d..2d8b3a295c 100644 --- a/modules/utxo-core/package.json +++ b/modules/utxo-core/package.json @@ -53,7 +53,7 @@ "dependencies": { "@bitgo/unspents": "^0.47.17", "@bitgo/utxo-lib": "^11.2.1", - "@bitgo/wasm-miniscript": "^2.0.0-beta.3", + "@bitgo/wasm-miniscript": "^2.0.0-beta.4", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4" }, "gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c" diff --git a/modules/utxo-core/src/testutil/descriptor/descriptors.ts b/modules/utxo-core/src/testutil/descriptor/descriptors.ts index 19cf816284..035490b83c 100644 --- a/modules/utxo-core/src/testutil/descriptor/descriptors.ts +++ b/modules/utxo-core/src/testutil/descriptor/descriptors.ts @@ -1,4 +1,6 @@ -import { Descriptor } from '@bitgo/wasm-miniscript'; +import assert from 'assert'; + +import { Descriptor, ast } from '@bitgo/wasm-miniscript'; import { BIP32Interface } from '@bitgo/utxo-lib'; import { DescriptorMap, PsbtParams } from '../../descriptor'; @@ -47,20 +49,14 @@ export type DescriptorTemplate = */ | 'ShWsh2Of3CltvDrop'; -function toXPub(k: BIP32Interface | string): string { +function toXPub(k: BIP32Interface | string, path: string): string { if (typeof k === 'string') { - return k; + return k + '/' + path; } - return k.neutered().toBase58(); + return k.neutered().toBase58() + '/' + path; } -function multi( - prefix: 'multi' | 'multi_a', - m: number, - n: number, - keys: BIP32Interface[] | string[], - path: string -): string { +function multiArgs(m: number, n: number, keys: BIP32Interface[] | string[], path: string): [number, ...string[]] { if (n < m) { throw new Error(`Cannot create ${m} of ${n} multisig`); } @@ -68,15 +64,7 @@ function multi( throw new Error(`Not enough keys for ${m} of ${n} multisig: keys.length=${keys.length}`); } keys = keys.slice(0, n); - return prefix + `(${m},${keys.map((k) => `${toXPub(k)}/${path}`).join(',')})`; -} - -function multiWsh(m: number, n: number, keys: BIP32Interface[] | string[], path: string): string { - return multi('multi', m, n, keys, path); -} - -function multiTap(m: number, n: number, keys: BIP32Interface[] | string[], path: string): string { - return multi('multi_a', m, n, keys, path); + return [m, ...keys.map((k) => toXPub(k, path))]; } export function getPsbtParams(t: DescriptorTemplate): Partial { @@ -90,21 +78,34 @@ export function getPsbtParams(t: DescriptorTemplate): Partial { } } -export function getDescriptorString( +function getDescriptorNode( template: DescriptorTemplate, keys: KeyTriple | string[] = getDefaultXPubs(), path = '0/*' -): string { +): ast.DescriptorNode { switch (template) { case 'Wsh2Of3': - return `wsh(${multiWsh(2, 3, keys, path)})`; + return { + wsh: { multi: multiArgs(2, 3, keys, path) }, + }; case 'ShWsh2Of3CltvDrop': const { locktime } = getPsbtParams(template); - return `sh(wsh(and_v(r:after(${locktime}),${multiWsh(2, 3, keys, path)})))`; + assert(locktime); + return { + sh: { + wsh: { + and_v: [{ 'r:after': locktime }, { multi: multiArgs(2, 3, keys, path) }], + }, + }, + }; case 'Wsh2Of2': - return `wsh(${multiWsh(2, 2, keys, path)})`; + return { + wsh: { multi: multiArgs(2, 2, keys, path) }, + }; case 'Tr2Of3-NoKeyPath': - return `tr(${getUnspendableKey()},${multiTap(2, 3, keys, path)})`; + return { + tr: [getUnspendableKey(), { multi_a: multiArgs(2, 3, keys, path) }], + }; } throw new Error(`Unknown descriptor template: ${template}`); } @@ -114,7 +115,7 @@ export function getDescriptor( keys: KeyTriple | string[] = getDefaultXPubs(), path = '0/*' ): Descriptor { - return Descriptor.fromString(getDescriptorString(template, keys, path), 'derivable'); + return Descriptor.fromString(ast.formatNode(getDescriptorNode(template, keys, path)), 'derivable'); } export function getDescriptorMap( diff --git a/yarn.lock b/yarn.lock index e160ed97fb..995cdbfdf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -886,10 +886,10 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" -"@bitgo/wasm-miniscript@^2.0.0-beta.3": - version "2.0.0-beta.3" - resolved "https://registry.npmjs.org/@bitgo/wasm-miniscript/-/wasm-miniscript-2.0.0-beta.3.tgz#f52f9fa411f4f13527c960842f81729133aa6810" - integrity sha512-9JWfizfdpSExQ5qDkMlZAR0UbonKILPwDXM+iqiBKIRGwlYak071lX9sfGPWoRuo7g72gU//s7AiZc6kZqgetw== +"@bitgo/wasm-miniscript@^2.0.0-beta.4": + version "2.0.0-beta.4" + resolved "https://registry.npmjs.org/@bitgo/wasm-miniscript/-/wasm-miniscript-2.0.0-beta.4.tgz#6897327a0e6007cfa02081a02cba8b442e318dc5" + integrity sha512-lZCSo3NY/vb1hagU3J7fIdv7GhCGYb/INCdAi6ogDXBzWDl3tloviCZ9DXCNPXc7lbj8mVr3GR8zAFkPTQ0xEw== "@brandonblack/musig@^0.0.1-alpha.0": version "0.0.1-alpha.1"