Skip to content
Closed
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
1 change: 1 addition & 0 deletions modules/abstract-utxo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@bitgo/utxo-core": "^1.41.5",
"@bitgo/utxo-descriptors": "^2.1.0",
"@bitgo/utxo-ord": "^1.34.5",
"@bitgo/utxo-staking": "^2.0.1",
"@bitgo/wasm-utxo": "^5.5.1",
"@types/lodash": "^4.14.121",
"@types/superagent": "4.1.15",
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';
}
}
16 changes: 13 additions & 3 deletions modules/abstract-utxo/src/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,20 @@ export type UtxoCoinNameTestnet =
export type UtxoCoinName = UtxoCoinNameMainnet | UtxoCoinNameTestnet;

export function toWasmUtxoCoinName(coinName: UtxoCoinName | WasmUtxoCoinName): WasmUtxoCoinName {
if (!isCoinName(coinName)) {
throw new Error(`coin ${coinName} is not supported by wasm-utxo`);
if (isCoinName(coinName)) {
return coinName;
}
// Stacks PoX coins share a wasm coin with their base chain for script and
// address codecs (see the `wasmName` of the coin classes)
switch (coinName) {
case 'tbtcstx':
case 'tbtcstxprivate1':
return 'tbtcreg';
case 'tbtcstxsignet':
return 'tbtcsig';
default:
throw new Error(`coin ${coinName} is not supported by wasm-utxo`);
}
return coinName;
}

export function isUtxoCoinNameMainnet(coinName: string): coinName is UtxoCoinNameMainnet {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface TransactionExplanation<TFee> {
payGoFeeString: string | number | undefined;
payGoFeeAddress: string | undefined;
};
/* PSBT nLockTime — the operator must verify it against the PoX-5 unlock height before co-signing a CLTV spend */
locktime: number;
}

export function getTransactionExplanation(coinName: UtxoCoinName, tx: unknown): TransactionExplanation<string> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as t from 'io-ts';
import { pox5 } from '@bitgo/utxo-descriptors';
import { pox5 as pox5Staking } from '@bitgo/utxo-staking';
import { bip32, Psbt } from '@bitgo/wasm-utxo';

import { DescriptorMap, NamedDescriptor } from '../../descriptor';
import { OfflineVaultSignable, toKeyTriple } from '../OfflineVaultSignable';
import {
getValidatorOneOfTemplates,
getValidatorPox5Lockup,
getValidatorSignedByUserKey,
getValidatorSome,
toDescriptorMapValidate,
Expand All @@ -20,21 +23,58 @@ export const DescriptorTransaction = t.intersection(

export type DescriptorTransaction = t.TypeOf<typeof DescriptorTransaction>;

const descriptorTemplates = ['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop'];

export function getDescriptorsFromDescriptorTransaction(tx: DescriptorTransaction): DescriptorMap {
const { descriptors, xpubsWithDerivationPath } = tx;
const pubkeys = toKeyTriple(xpubsWithDerivationPath);
const policy = getValidatorSome([
// allow all 2-of-3-ish descriptors where the keys match the wallet keys
getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']),
getValidatorOneOfTemplates(descriptorTemplates),
// allow PoX-5 staking groups: canonical lockup descriptors with wallet staker
// keys alongside the wallet's external/internal descriptors
getValidatorPox5Lockup(descriptorTemplates),
// allow all descriptors signed by the user key
getValidatorSignedByUserKey(),
]);
return toDescriptorMapValidate(descriptors, pubkeys, policy);
}

function getPox5InputMatches(psbt: Psbt, descriptorMap: DescriptorMap): pox5.Pox5InputMatch[] {
const matches: pox5.Pox5InputMatch[] = [];
for (let vin = 0; vin < psbt.inputCount(); vin++) {
const match = pox5.matchPox5Input(psbt, vin, descriptorMap);
if (match) {
matches.push(match);
}
}
return matches;
}

/**
* Apply the BitGoJS PoX-5 staking policy (WAL-2022) before half-signing a late
* exit: the spend must use the post-CLTV branch over exactly one lockup input.
* Early exits (principal preimage branch) are out of scope for the offline vault.
*/
function assertPox5LateExitSpend(psbt: Psbt, descriptorMap: DescriptorMap): void {
const matches = getPox5InputMatches(psbt, descriptorMap);
if (matches.length === 0) {
return;
}
if (matches.length > 1) {
throw new Error(`PoX-5 late exit must spend exactly one lockup input, found ${matches.length}`);
}
const [match] = matches;
if (pox5Staking.classifyPox5Spend(psbt, match) !== 'locktime') {
throw new Error('PoX-5 early exit spends are not supported by the offline vault');
}
pox5Staking.assertPox5LocktimeSpend(psbt, matches);
}

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);
assertPox5LateExitSpend(psbt, descriptorMap);
signPsbt(psbt, descriptorMap, prv, { onUnknownInput: 'throw' });
return psbt;
}
Expand All @@ -56,5 +96,6 @@ export function getTransactionExplanationFromPsbt(
payGoFeeString: undefined,
payGoFeeAddress: undefined,
},
locktime: psbt.lockTime(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
],
"fee": {
"fee": "4421"
}
},
"locktime": 0
}
Loading
Loading