Skip to content

Commit

Permalink
Merge pull request #5555 from BitGo/BTC-1829.add-trdecay
Browse files Browse the repository at this point in the history
feat(utxo-staking): use miniscript AST for descriptor creation
  • Loading branch information
OttoAllmendinger authored Feb 14, 2025
2 parents a6f97f9 + d29e0dc commit fc8361c
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 60 deletions.
2 changes: 1 addition & 1 deletion modules/abstract-utxo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-bin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]",
Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]"
},
"gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c"
Expand Down
55 changes: 28 additions & 27 deletions modules/utxo-core/src/testutil/descriptor/descriptors.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -47,36 +49,22 @@ 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`);
}
if (keys.length < n) {
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<PsbtParams> {
Expand All @@ -90,21 +78,34 @@ export function getPsbtParams(t: DescriptorTemplate): Partial<PsbtParams> {
}
}

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}`);
}
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-staking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 16 additions & 21 deletions modules/utxo-staking/src/babylon/descriptor.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
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
return '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0';
}

// Helper functions for creating miniscript nodes
function pk(b: Buffer): MiniscriptNode {
function pk(b: Buffer): ast.MiniscriptNode {
return { 'v:pk': b.toString('hex') };
}

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');
}

Expand All @@ -33,45 +32,41 @@ 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 {
return taprootScriptOnlyFromAst(this.getUnbondingTimelockMiniscript());
}

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()]);
}
}
9 changes: 5 additions & 4 deletions modules/utxo-staking/src/coreDao/descriptor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BIP32Interface } from '@bitgo/utxo-lib';
import { ast } from '@bitgo/wasm-miniscript';

/**
* Script type for a descriptor.
Expand Down Expand Up @@ -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 });
}
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit fc8361c

Please sign in to comment.