Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ COPY --from=builder /tmp/bitgo/modules/argon2 /var/modules/argon2/
COPY --from=builder /tmp/bitgo/modules/sdk-hmac /var/modules/sdk-hmac/
COPY --from=builder /tmp/bitgo/modules/unspents /var/modules/unspents/
COPY --from=builder /tmp/bitgo/modules/utxo-core /var/modules/utxo-core/
COPY --from=builder /tmp/bitgo/modules/utxo-descriptors /var/modules/utxo-descriptors/
COPY --from=builder /tmp/bitgo/modules/utxo-ord /var/modules/utxo-ord/
COPY --from=builder /tmp/bitgo/modules/account-lib /var/modules/account-lib/
COPY --from=builder /tmp/bitgo/modules/sdk-coin-ada /var/modules/sdk-coin-ada/
Expand Down Expand Up @@ -163,6 +164,7 @@ cd /var/modules/argon2 && yarn link && \
cd /var/modules/sdk-hmac && yarn link && \
cd /var/modules/unspents && yarn link && \
cd /var/modules/utxo-core && yarn link && \
cd /var/modules/utxo-descriptors && yarn link && \
cd /var/modules/utxo-ord && yarn link && \
cd /var/modules/account-lib && yarn link && \
cd /var/modules/sdk-coin-ada && yarn link && \
Expand Down Expand Up @@ -269,6 +271,7 @@ RUN cd /var/bitgo-express && \
yarn link @bitgo/sdk-hmac && \
yarn link @bitgo/unspents && \
yarn link @bitgo/utxo-core && \
yarn link @bitgo/utxo-descriptors && \
yarn link @bitgo/utxo-ord && \
yarn link @bitgo/account-lib && \
yarn link @bitgo/sdk-coin-ada && \
Expand Down
3 changes: 2 additions & 1 deletion modules/abstract-utxo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@
"@bitgo/sdk-api": "^1.81.0",
"@bitgo/sdk-core": "^37.2.0",
"@bitgo/utxo-core": "^1.38.0",
"@bitgo/utxo-descriptors": "^1.2.0",
"@bitgo/utxo-lib": "^11.22.1",
"@bitgo/utxo-ord": "^1.31.0",
"@bitgo/wasm-utxo": "^4.13.0",
"@bitgo/wasm-utxo": "^4.14.1",
"@types/lodash": "^4.14.121",
"@types/superagent": "4.1.15",
"bignumber.js": "^9.0.2",
Expand Down
36 changes: 35 additions & 1 deletion modules/abstract-utxo/src/descriptor/builder/builder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sbtc } from '@bitgo/utxo-descriptors';
import { bip32, Descriptor } from '@bitgo/wasm-utxo';

type DescriptorWithKeys<TName extends string> = {
Expand All @@ -14,7 +15,24 @@ export type DescriptorBuilder =
* relative locktime with an OP_DROP (requiring a miniscript extension).
* It is basically what is used in CoreDao staking transactions.
*/
| (DescriptorWithKeys<'ShWsh2Of3CltvDrop' | 'Wsh2Of3CltvDrop'> & { locktime: number });
| (DescriptorWithKeys<'ShWsh2Of3CltvDrop' | 'Wsh2Of3CltvDrop'> & { locktime: number })
/*
* sBTC peg-in deposit Taproot descriptor:
* tr(<UNSPENDABLE>,
* {
* c:and_v(payload_drop(<feeBE||recipient>), pk_k(<signersKey>)),
* and_v(r:older(<lockTime>), multi_a(2, k1/*, k2/*, k3/*))
* }
* )
*
* `keys` are the three reclaim keys used in the reclaim-leaf multi_a.
*/
| (DescriptorWithKeys<'SbtcDeposit'> & {
lockTime: number;
maxFee: bigint;
stacksRecipient: Buffer;
signersAggregateKey: Buffer;
});

function toXPub(k: bip32.BIP32Interface | string): string {
if (typeof k === 'string') {
Expand Down Expand Up @@ -44,6 +62,22 @@ function getDescriptorString(builder: DescriptorBuilder): string {
return `wsh(and_v(r:after(${builder.locktime}),${multi(2, 3, builder.keys, builder.path)}))`;
case 'ShWsh2Of3CltvDrop':
return `sh(${getDescriptorString({ ...builder, name: 'Wsh2Of3CltvDrop' })})`;
case 'SbtcDeposit':
// The reclaim leaf always uses `/*` wildcard derivation; `createSbtcDepositDescriptor`
// hardcodes that suffix when given BIP32 keys, so reject any other path here.
if (builder.path !== '*') {
throw new Error(`SbtcDeposit path must be '*', got '${builder.path}'`);
}
if (builder.keys.length !== 3) {
throw new Error(`SbtcDeposit needs exactly 3 reclaim keys, got ${builder.keys.length}`);
}
return sbtc.createSbtcDepositDescriptor({
walletKeys: [builder.keys[0], builder.keys[1], builder.keys[2]],
lockTime: builder.lockTime,
maxFee: builder.maxFee,
stacksRecipient: builder.stacksRecipient,
signersAggregateKey: builder.signersAggregateKey,
});
}
throw new Error(`Unknown descriptor template: ${builder}`);
}
Expand Down
39 changes: 37 additions & 2 deletions modules/abstract-utxo/src/descriptor/builder/parse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sbtc } from '@bitgo/utxo-descriptors';
import { BIP32, bip32, Descriptor } from '@bitgo/wasm-utxo';

import { DescriptorBuilder, getDescriptorFromBuilder } from './builder';
Expand Down Expand Up @@ -62,7 +63,7 @@ function parseWshMulti(node: unknown): DescriptorBuilder | undefined {
const wshMsMulti = unwrapNode(node, ['Wsh', 'Ms', 'Multi']);
if (wshMsMulti) {
const { threshold, keys, path } = parseMulti(wshMsMulti);
let name;
let name: 'Wsh2Of2' | 'Wsh2Of3';
if (threshold === 2 && keys.length === 2) {
name = 'Wsh2Of2';
} else if (threshold === 2 && keys.length === 3) {
Expand All @@ -78,6 +79,38 @@ function parseWshMulti(node: unknown): DescriptorBuilder | undefined {
}
}

function parseSbtcDeposit(descriptor: Descriptor): DescriptorBuilder | undefined {
const parsed = sbtc.parseSbtcDepositDescriptor(descriptor);
if (!parsed) {
return;
}

// For wallet descriptors the reclaim leaf is a derivable `multi_a` with
// `xpub.../*` entries; split each into xpub + path. Definite descriptors
// produce raw hex strings here, which `BIP32.fromBase58` rejects — that
// matches our intent, since a definite descriptor isn't a wallet descriptor.
const keyWithPath = parsed.reclaimKeyStrings.map((k) => {
const [xpub, ...rest] = k.split('/');
return { xpub, path: rest.join('/') };
});
const paths = keyWithPath.map((k) => k.path);
paths.forEach((p) => {
if (p !== paths[0]) {
throw new Error(`Expected all multi_a paths to be the same: ${p} !== ${paths[0]}`);
}
});

return {
name: 'SbtcDeposit',
keys: keyWithPath.map((k) => BIP32.fromBase58(k.xpub)),
path: paths[0],
lockTime: parsed.lockTime,
maxFee: parsed.maxFee,
stacksRecipient: parsed.stacksRecipient,
signersAggregateKey: parsed.signersAggregateKey,
};
}

function parseCltvDrop(
node: unknown,
name: 'Wsh2Of3CltvDrop' | 'ShWsh2Of3CltvDrop',
Expand Down Expand Up @@ -120,7 +153,9 @@ export function parseDescriptorNode(node: unknown): DescriptorBuilder {
}

export function parseDescriptor(descriptor: Descriptor): DescriptorBuilder {
const builder = parseDescriptorNode(descriptor.node());
// sBTC peg-in uses a Taproot `tr(...)` descriptor that the raw-node parsers
// don't recognize; match it against the ast form first.
const builder = parseSbtcDeposit(descriptor) ?? parseDescriptorNode(descriptor.node());
if (getDescriptorFromBuilder(builder).toString() !== descriptor.toString()) {
throw new Error('Failed to parse descriptor');
}
Expand Down
2 changes: 2 additions & 0 deletions modules/abstract-utxo/src/descriptor/validatePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolic
getValidatorDescriptorTemplate('Wsh2Of3'),
// allow descriptor groups where all keys match the wallet keys plus OP_DROP (coredao staking)
getValidatorDescriptorTemplate('Wsh2Of3CltvDrop'),
// allow sBTC peg-in deposit descriptors where the reclaim keys match the wallet keys
getValidatorDescriptorTemplate('SbtcDeposit'),
// allow all descriptors signed by the user key
getValidatorSignedByUserKey(),
]);
Expand Down
18 changes: 18 additions & 0 deletions modules/abstract-utxo/test/unit/descriptor/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import * as testutils from '@bitgo/wasm-utxo/testutils';

import { parseDescriptor, DescriptorBuilder, getDescriptorFromBuilder } from '../../../src/descriptor/builder';

const SBTC_SIGNERS_AGGREGATE_KEY = Buffer.from(
'c9c2312ca406dcb8eed50b829b5292f5fb3e846db0a556af61cc53834ce75421',
'hex'
);
const SBTC_STACKS_RECIPIENT = Buffer.from('05' + '16' + '6d'.repeat(20), 'hex');

function getDescriptorBuilderForType(name: DescriptorBuilder['name']): DescriptorBuilder {
const keys = testutils.getKeyTriple('default').map((k) => k.neutered());
switch (name) {
Expand All @@ -22,6 +28,17 @@ function getDescriptorBuilderForType(name: DescriptorBuilder['name']): Descripto
path: '0/*',
locktime: 1,
};
case 'SbtcDeposit':
return {
name,
keys,
// sBTC reclaim leaf uses bare `/*` wildcard (no chain prefix).
path: '*',
lockTime: 950,
maxFee: 80000n,
stacksRecipient: SBTC_STACKS_RECIPIENT,
signersAggregateKey: SBTC_SIGNERS_AGGREGATE_KEY,
};
}
}

Expand All @@ -46,3 +63,4 @@ describeForName('Wsh2Of2');
describeForName('Wsh2Of3');
describeForName('Wsh2Of3CltvDrop');
describeForName('ShWsh2Of3CltvDrop');
describeForName('SbtcDeposit');
3 changes: 3 additions & 0 deletions modules/abstract-utxo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
{
"path": "../utxo-core"
},
{
"path": "../utxo-descriptors"
},
{
"path": "../utxo-ord"
}
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 @@ -31,7 +31,7 @@
"@bitgo/unspents": "^0.51.4",
"@bitgo/utxo-core": "^1.38.0",
"@bitgo/utxo-lib": "^11.22.1",
"@bitgo/wasm-utxo": "^4.13.0",
"@bitgo/wasm-utxo": "^4.14.1",
"@noble/curves": "1.8.1",
"archy": "^1.0.0",
"bech32": "^2.0.0",
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 @@ -81,7 +81,7 @@
"@bitgo/secp256k1": "^1.11.0",
"@bitgo/unspents": "^0.51.4",
"@bitgo/utxo-lib": "^11.22.1",
"@bitgo/wasm-utxo": "^4.13.0",
"@bitgo/wasm-utxo": "^4.14.1",
"bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4",
"fast-sha256": "^1.3.0"
},
Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-descriptors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@
},
"dependencies": {
"@bitgo/utxo-core": "^1.38.0",
"@bitgo/wasm-utxo": "^4.13.0"
"@bitgo/wasm-utxo": "^4.14.1"
}
}
14 changes: 14 additions & 0 deletions modules/utxo-descriptors/src/sbtc/parseDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export type ParsedSbtcDepositDescriptor = {
depositMiniscriptNode: ast.MiniscriptNode;
/** Raw miniscript AST for the reclaim leaf (second tap-tree leaf). */
reclaimMiniscriptNode: ast.MiniscriptNode;
/**
* The three raw `multi_a` key entries from the reclaim leaf, exactly as
* they appear in the descriptor — `xpub.../*` for derivable descriptors and
* 64-hex-char x-only keys for definite descriptors. Useful for callers that
* need the original xpub form (e.g. to round-trip back to a descriptor
* builder) and don't want the resolved `reclaimKeys` buffers.
*/
reclaimKeyStrings: [string, string, string];
};

function asString(v: unknown, field: string): string {
Expand Down Expand Up @@ -218,6 +226,11 @@ export function parseSbtcDepositDescriptor(

const { signersAggregateKey, maxFee, stacksRecipient } = parseDepositLeaf(depositMiniscriptNode, matcher);
const { lockTime, reclaimKeyStrings } = parseReclaimLeaf(reclaimMiniscriptNode, matcher);
const reclaimKeyStringsTuple: [string, string, string] = [
reclaimKeyStrings[0],
reclaimKeyStrings[1],
reclaimKeyStrings[2],
];

const reclaimKeys = reclaimKeysFromStrings(reclaimKeyStrings);
if (reclaimKeys) {
Expand All @@ -237,6 +250,7 @@ export function parseSbtcDepositDescriptor(
stacksRecipient,
lockTime,
reclaimKeys,
reclaimKeyStrings: reclaimKeyStringsTuple,
depositMiniscriptNode,
reclaimMiniscriptNode,
};
Expand Down
15 changes: 15 additions & 0 deletions modules/utxo-descriptors/test/unit/sbtc/parseDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ describe('parseSbtcDepositDescriptor', function () {
assert.deepStrictEqual(parsed.reclaimKeys, sortedInput);
});

it('exposes raw hex reclaimKeyStrings for definite inputs', function () {
const parsed = parseSbtcDepositDescriptor(descriptor);
assert.ok(parsed);
assert.deepStrictEqual(parsed.reclaimKeyStrings, [...reclaimKeyHex].sort());
});

it('returns miniscript AST nodes for both leaves', function () {
const parsed = parseSbtcDepositDescriptor(descriptor);
assert.ok(parsed);
Expand Down Expand Up @@ -110,6 +116,15 @@ describe('parseSbtcDepositDescriptor', function () {
assert.strictEqual(parsed.reclaimKeys, undefined);
});

it('exposes the raw xpub.../* reclaimKeyStrings for derivable inputs', function () {
const parsed = parseSbtcDepositDescriptor(descriptor);
assert.ok(parsed);
assert.strictEqual(parsed.reclaimKeyStrings.length, 3);
for (const k of parsed.reclaimKeyStrings) {
assert.match(k, /^xpub[1-9A-HJ-NP-Za-km-z]+\/\*$/);
}
});

it('resolves to concrete reclaim keys after atDerivationIndex(0) and compiles to a tap Miniscript', function () {
assert.strictEqual(descriptor.hasWildcard(), true);
const derivedZero = descriptor.atDerivationIndex(0);
Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-ord/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"directory": "modules/utxo-ord"
},
"dependencies": {
"@bitgo/wasm-utxo": "^4.13.0"
"@bitgo/wasm-utxo": "^4.14.1"
},
"devDependencies": {
"@bitgo/utxo-lib": "^11.22.1"
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 @@ -63,7 +63,7 @@
"@bitgo/babylonlabs-io-btc-staking-ts": "^3.5.0",
"@bitgo/utxo-core": "^1.38.0",
"@bitgo/utxo-lib": "^11.22.1",
"@bitgo/wasm-utxo": "^4.13.0",
"@bitgo/wasm-utxo": "^4.14.1",
"bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4",
"bip322-js": "^2.0.0",
"bitcoinjs-lib": "^6.1.7",
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1094,10 +1094,10 @@
resolved "https://registry.npmjs.org/@bitgo/wasm-ton/-/wasm-ton-1.1.1.tgz"
integrity sha512-Y4x2V2ZcYWlmx42v7dlrKDtT2DuUt8smk8E98mh7RhpiifJhLk2v5RmXDwBl0A3v9TzUOU6qMOnSS/iZ8Pq52w==

"@bitgo/wasm-utxo@^4.13.0":
version "4.13.0"
resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-4.13.0.tgz#95efb853e40b30c6525cbf99a05f1a4db4161609"
integrity sha512-/jejFe7o/KJ08sivfWNKNWoNt1iBJ7tL7MZ7IbhVGeC2TLWo7ZF6UuE1A/E3rNyAM7egJ6BrtGuTumKVXDeibQ==
"@bitgo/wasm-utxo@^4.14.1":
version "4.14.1"
resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-4.14.1.tgz#bf709a98078f10f9c52900bcba24a6c6a9d0256e"
integrity sha512-cHGJzCNlPMpVXp/ydFIHQ0/GSRJLYG+cqzVYa6u7x2bbnKDzIqgn34hpWX1w/FfZ626U0S47HGo8ujmoWoUn2Q==

"@brandonblack/musig@^0.0.1-alpha.0":
version "0.0.1-alpha.1"
Expand Down
Loading