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
75 changes: 72 additions & 3 deletions modules/abstract-utxo/src/descriptor/validatePolicy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { EnvironmentName, Triple } from '@bitgo/sdk-core';
import { bip32, descriptorWallet } from '@bitgo/wasm-utxo';
import { pox5 } from '@bitgo/utxo-descriptors';
import { bip32, Descriptor, descriptorWallet } from '@bitgo/wasm-utxo';

import { parseDescriptor } from './builder';
import { DescriptorBuilder, parseDescriptor } from './builder';
import { hasValidSignature, NamedDescriptor, NamedDescriptorNative, toNamedDescriptorNative } from './NamedDescriptor';

import type { DescriptorMap } from './index';
Expand All @@ -19,13 +20,23 @@ export const policyAllowAll: DescriptorValidationPolicy = {
validate: () => true,
};

function tryParseDescriptor(descriptor: Descriptor): DescriptorBuilder | undefined {
try {
return parseDescriptor(descriptor);
} catch {
// not a descriptor we know how to parse: it cannot match any template
return undefined;
}
}

export function getValidatorDescriptorTemplate(name: string): DescriptorValidationPolicy {
return {
name: 'descriptorTemplate(' + name + ')',
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean {
return arr.every((d) => {
const parsed = parseDescriptor(d.value);
const parsed = tryParseDescriptor(d.value);
return (
parsed !== undefined &&
parsed.name === name &&
parsed.keys.length === walletKeys.length &&
parsed.keys.every((k, i) => k.toBase58() === walletKeys[i].neutered().toBase58())
Expand Down Expand Up @@ -67,6 +78,62 @@ export function getValidatorSignedByUserKey(): DescriptorValidationPolicy {
};
}

function isCanonicalPox5Lockup(descriptor: Descriptor, walletKeys: KeyTriple): boolean {
try {
const info = pox5.parsePox5LockupDescriptor(descriptor);
if (!info) {
return false;
}
// Rebuilding the canonical descriptor from the wallet keys checks both that the
// staker keys are the wallet keys (with the `/*` derivation) and that the
// descriptor has no non-canonical encoding.
const canonical = pox5.createPox5LockupDescriptor({
unlockHeight: info.unlockHeight,
stakerCommitment: info.stakerCommitment,
earlyExitKey: info.earlyExitKey,
stakerKeys: [walletKeys[0], walletKeys[1], walletKeys[2]],
});
return canonical.toString() === descriptor.toString();
} catch {
// malformed lockup parameters
return false;
}
}

/**
* Accepts descriptors that are canonical PoX-5 lockup descriptors whose staker keys
* are the wallet keys.
*/
export function getValidatorPox5Lockup(): DescriptorValidationPolicy {
return {
name: 'pox5Lockup',
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean {
return arr.every((d) => isCanonicalPox5Lockup(d.value, walletKeys));
},
};
}

/**
* Accepts descriptor groups where each descriptor individually satisfies at least one of
* the validators. Unlike `getValidatorSome`, different descriptors may match different validators.
*/
export function getValidatorEachOneOf(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
return {
name: 'eachOneOf(' + validators.map((v) => v.name).join(',') + ')',
validate(arr: NamedDescriptorNative[], walletKeys: KeyTriple): boolean {
return arr.every((d) => validators.some((v) => v.validate([d], walletKeys)));
},
};
}

/**
* Accepts the descriptors of a PoX-5 staking wallet: the regular 2-of-3 wallet descriptors
* together with PoX-5 lockup descriptors, all using the wallet keys.
*/
export function getValidatorPox5Wallet(): DescriptorValidationPolicy {
return getValidatorEachOneOf([getValidatorDescriptorTemplate('Wsh2Of3'), getValidatorPox5Lockup()]);
}

export class DescriptorPolicyValidationError extends Error {
constructor(ds: NamedDescriptorNative[], policy: DescriptorValidationPolicy) {
super(`Descriptors ${ds.map((d) => d.value.toString())} does not match policy ${policy.name}`);
Expand Down Expand Up @@ -106,6 +173,8 @@ export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolic
getValidatorDescriptorTemplate('Wsh2Of3CltvDrop'),
// allow sBTC peg-in deposit descriptors where the reclaim keys match the wallet keys
getValidatorDescriptorTemplate('SbtcDeposit'),
// allow 2-of-3 wallet descriptors together with PoX-5 lockup descriptors (stacks staking)
getValidatorPox5Wallet(),
// allow all descriptors signed by the user key
getValidatorSignedByUserKey(),
]);
Expand Down
6 changes: 1 addition & 5 deletions modules/abstract-utxo/src/impl/btc/tbtcstx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BitGoBase } from '@bitgo/sdk-core';

import { UtxoCoinName, WasmUtxoCoinName } from '../../names';
import { UtxoCoinName } from '../../names';

import { Tbtc } from './tbtc';

Expand All @@ -14,8 +14,4 @@ export class Tbtcstx extends Tbtc {
static createInstance(bitgo: BitGoBase): Tbtcstx {
return new Tbtcstx(bitgo);
}

override get wasmName(): WasmUtxoCoinName {
return 'tbtcreg';
}
}
6 changes: 1 addition & 5 deletions modules/abstract-utxo/src/impl/btc/tbtcstxsignet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BitGoBase } from '@bitgo/sdk-core';

import { UtxoCoinName, WasmUtxoCoinName } from '../../names';
import { UtxoCoinName } from '../../names';

import { Tbtc } from './tbtc';

Expand All @@ -14,8 +14,4 @@ export class Tbtcstxsignet extends Tbtc {
static createInstance(bitgo: BitGoBase): Tbtcstxsignet {
return new Tbtcstxsignet(bitgo);
}

override get wasmName(): WasmUtxoCoinName {
return 'tbtcsig';
}
}
8 changes: 8 additions & 0 deletions modules/abstract-utxo/src/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ export type UtxoCoinNameTestnet =
| 'tbtcstxsignet';
export type UtxoCoinName = UtxoCoinNameMainnet | UtxoCoinNameTestnet;

/** Coin name used by wasm-utxo. Private Bitcoin networks map to a shared codec. */
export function toWasmUtxoCoinName(coinName: UtxoCoinName | WasmUtxoCoinName): WasmUtxoCoinName {
switch (coinName) {
case 'tbtcstx':
case 'tbtcstxprivate1':
return 'tbtcreg';
case 'tbtcstxsignet':
return 'tbtcsig';
}
if (!isCoinName(coinName)) {
throw new Error(`coin ${coinName} is not supported by wasm-utxo`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface ExplanationOutput {
export interface TransactionExplanation<TFee> {
outputs: ExplanationOutput[];
changeOutputs: ExplanationOutput[];
/* nLockTime of the transaction, to be verified by the signer for time-locked spends */
locktime: number;
fee: {
/* network fee */
fee: TFee;
Expand Down
30 changes: 30 additions & 0 deletions modules/abstract-utxo/src/offlineVault/descriptor/pox5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { pox5 } from '@bitgo/utxo-descriptors';
import { descriptorWallet, Psbt } from '@bitgo/wasm-utxo';

function getPox5InputMatches(psbt: Psbt, descriptorMap: descriptorWallet.DescriptorMap): pox5.Pox5InputMatch[] {
return Array.from({ length: psbt.inputCount() }, (_, i) => pox5.matchPox5Input(psbt, i, descriptorMap)).filter(
(match): match is pox5.Pox5InputMatch => match !== undefined
);
}

/**
* Apply the PoX-5 staking spend policy to a PSBT before it is signed.
*
* PSBTs without PoX-5 lockup inputs are not affected. Otherwise, the PSBT must spend exactly one lockup
* input through the post-CLTV (late exit) branch, with nLockTime at or above the unlock height and a non-final sequence.
*/
export function assertPox5SpendPolicy(psbt: Psbt, descriptorMap: descriptorWallet.DescriptorMap): void {
const matches = getPox5InputMatches(psbt, descriptorMap);
if (matches.length === 0) {
return;
}
if (matches.length !== 1) {
throw new Error(`expected exactly one PoX-5 lockup input, found ${matches.length}`);
}
const [match] = matches;
const branch = pox5.classifyPox5Spend(psbt, match);
if (branch !== 'locktime') {
throw new Error(`unsupported PoX-5 spend branch: ${branch}`);
}
pox5.assertPox5LocktimeSpend(psbt, matches);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DescriptorMap, NamedDescriptor } from '../../descriptor';
import { OfflineVaultSignable, toKeyTriple } from '../OfflineVaultSignable';
import {
getValidatorOneOfTemplates,
getValidatorPox5Wallet,
getValidatorSignedByUserKey,
getValidatorSome,
toDescriptorMapValidate,
Expand All @@ -13,6 +14,8 @@ import { explainPsbt, signPsbt } from '../../transaction/descriptor';
import { TransactionExplanation } from '../TransactionExplanation';
import { UtxoCoinName } from '../../names';

import { assertPox5SpendPolicy } from './pox5';

export const DescriptorTransaction = t.intersection(
[OfflineVaultSignable, t.type({ descriptors: t.array(NamedDescriptor) })],
'DescriptorTransaction'
Expand All @@ -26,6 +29,8 @@ export function getDescriptorsFromDescriptorTransaction(tx: DescriptorTransactio
const policy = getValidatorSome([
// allow all 2-of-3-ish descriptors where the keys match the wallet keys
getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']),
// allow 2-of-3 wallet descriptors together with PoX-5 lockup descriptors where the keys match the wallet keys
getValidatorPox5Wallet(),
// allow all descriptors signed by the user key
getValidatorSignedByUserKey(),
]);
Expand All @@ -35,6 +40,7 @@ export function getDescriptorsFromDescriptorTransaction(tx: DescriptorTransactio
export function getHalfSignedPsbt(tx: DescriptorTransaction, prv: bip32.BIP32Interface, coinName: UtxoCoinName): Psbt {
const psbt = Psbt.deserialize(Buffer.from(tx.coinSpecific.txHex, 'hex'));
const descriptorMap = getDescriptorsFromDescriptorTransaction(tx);
assertPox5SpendPolicy(psbt, descriptorMap);
signPsbt(psbt, descriptorMap, prv, { onUnknownInput: 'throw' });
return psbt;
}
Expand All @@ -49,6 +55,7 @@ export function getTransactionExplanationFromPsbt(
return {
outputs,
changeOutputs,
locktime: psbt.lockTime(),
fee: {
/* network fee */
fee,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"amount": "196995579"
}
],
"locktime": 0,
"fee": {
"fee": "4421"
}
Expand Down
Loading
Loading