Skip to content

Commit f6509fb

Browse files
committed
feat: add wasm-utxo backend for cross-chain recovery
Add wasm-utxo implementation for PSBT creation in cross-chain recovery flows, alongside the existing utxolib implementation. The wasm-utxo backend is enabled for testnet coins only, while mainnet continues to use utxolib for compatibility. Key changes: - Add support for taproot script types in cross-chain recovery - Use sequence number feffffff instead of ffffffff - Add helper functions to detect taproot chains and convert networks Co-authored-by: llm-git <llm-git@ttll.de> Ticket: BTC-2892 TICKET: BTC-2892
1 parent 9423f1d commit f6509fb

100 files changed

Lines changed: 271 additions & 127 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/abstract-utxo/src/recovery/crossChainRecovery.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import { decrypt } from '@bitgo/sdk-api';
77
import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin';
88
import { signAndVerifyPsbt } from '../transaction/fixedScript/signPsbt';
99

10+
import {
11+
PsbtBackend,
12+
createEmptyWasmPsbt,
13+
addWalletInputsToWasmPsbt,
14+
addOutputToWasmPsbt,
15+
wasmPsbtToUtxolibPsbt,
16+
} from './psbt';
17+
1018
const { unspentSum } = utxolib.bitgo;
1119
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
1220
type Unspent<TNumber extends number | bigint = number> = utxolib.bitgo.Unspent<TNumber>;
@@ -325,15 +333,15 @@ async function getPrv(xprv?: string, passphrase?: string, wallet?: IWallet | Wal
325333
}
326334

327335
/**
328-
* Create a sweep transaction for cross-chain recovery using PSBT
336+
* Create a sweep transaction for cross-chain recovery using PSBT (utxolib implementation)
329337
* @param network
330338
* @param walletKeys
331339
* @param unspents
332340
* @param targetAddress
333341
* @param feeRateSatVB
334342
* @return unsigned PSBT
335343
*/
336-
function createSweepTransaction<TNumber extends number | bigint = number>(
344+
function createSweepTransactionUtxolib<TNumber extends number | bigint = number>(
337345
network: utxolib.Network,
338346
walletKeys: RootWalletKeys,
339347
unspents: WalletUnspent<TNumber>[],
@@ -372,6 +380,71 @@ function createSweepTransaction<TNumber extends number | bigint = number>(
372380
return psbt;
373381
}
374382

383+
/**
384+
* Create a sweep transaction for cross-chain recovery using wasm-utxo
385+
* @param network
386+
* @param walletKeys
387+
* @param unspents
388+
* @param targetAddress
389+
* @param feeRateSatVB
390+
* @return unsigned PSBT
391+
*/
392+
function createSweepTransactionWasm<TNumber extends number | bigint = number>(
393+
network: utxolib.Network,
394+
walletKeys: RootWalletKeys,
395+
unspents: WalletUnspent<TNumber>[],
396+
targetAddress: string,
397+
feeRateSatVB: number
398+
): utxolib.bitgo.UtxoPsbt {
399+
const inputValue = unspentSum<bigint>(
400+
unspents.map((u) => ({ ...u, value: BigInt(u.value) })),
401+
'bigint'
402+
);
403+
404+
// Create PSBT with wasm-utxo and add wallet inputs using shared utilities
405+
const unspentsBigint = unspents.map((u) => ({ ...u, value: BigInt(u.value) }));
406+
const wasmPsbt = createEmptyWasmPsbt(network, walletKeys);
407+
addWalletInputsToWasmPsbt(wasmPsbt, unspentsBigint, walletKeys);
408+
409+
// Convert to utxolib PSBT temporarily for dimension calculation
410+
const tempPsbt = wasmPsbtToUtxolibPsbt(wasmPsbt, network);
411+
const vsize = Dimensions.fromPsbt(tempPsbt)
412+
.plus(Dimensions.fromOutput({ script: utxolib.address.toOutputScript(targetAddress, network) }))
413+
.getVSize();
414+
const fee = BigInt(Math.round(vsize * feeRateSatVB));
415+
416+
// Add output to wasm PSBT
417+
addOutputToWasmPsbt(wasmPsbt, targetAddress, inputValue - fee, network);
418+
419+
// Convert to utxolib PSBT for signing and return
420+
return wasmPsbtToUtxolibPsbt(wasmPsbt, network);
421+
}
422+
423+
/**
424+
* Create a sweep transaction for cross-chain recovery using PSBT
425+
* @param network
426+
* @param walletKeys
427+
* @param unspents
428+
* @param targetAddress
429+
* @param feeRateSatVB
430+
* @param backend - Which backend to use for PSBT creation (default: 'wasm-utxo')
431+
* @return unsigned PSBT
432+
*/
433+
function createSweepTransaction<TNumber extends number | bigint = number>(
434+
network: utxolib.Network,
435+
walletKeys: RootWalletKeys,
436+
unspents: WalletUnspent<TNumber>[],
437+
targetAddress: string,
438+
feeRateSatVB: number,
439+
backend: PsbtBackend = 'wasm-utxo'
440+
): utxolib.bitgo.UtxoPsbt {
441+
if (backend === 'wasm-utxo') {
442+
return createSweepTransactionWasm(network, walletKeys, unspents, targetAddress, feeRateSatVB);
443+
} else {
444+
return createSweepTransactionUtxolib(network, walletKeys, unspents, targetAddress, feeRateSatVB);
445+
}
446+
}
447+
375448
type RecoverParams = {
376449
/** Wallet ID (can be v1 wallet or v2 wallet) */
377450
walletId: string;
@@ -420,12 +493,15 @@ export async function recoverCrossChain<TNumber extends number | bigint = number
420493
const feeRateSatVB = await getFeeRateSatVB(params.sourceCoin);
421494

422495
// Create PSBT for both signed and unsigned recovery
496+
// Use wasm-utxo for testnet coins only, utxolib for mainnet
497+
const backend: PsbtBackend = utxolib.isTestnet(params.sourceCoin.network) ? 'wasm-utxo' : 'utxolib';
423498
const psbt = createSweepTransaction<TNumber>(
424499
params.sourceCoin.network,
425500
walletKeys,
426501
walletUnspents,
427502
params.recoveryAddress,
428-
feeRateSatVB
503+
feeRateSatVB,
504+
backend
429505
);
430506

431507
// For unsigned recovery, return unsigned PSBT hex

modules/abstract-utxo/src/recovery/psbt.ts

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type PsbtBackend = 'wasm-utxo' | 'utxolib';
1919
/**
2020
* Check if a chain code is for a taproot script type
2121
*/
22-
function isTaprootChain(chain: ChainCode): boolean {
22+
export function isTaprootChain(chain: ChainCode): boolean {
2323
return (
2424
(chainCodesP2tr as readonly number[]).includes(chain) || (chainCodesP2trMusig2 as readonly number[]).includes(chain)
2525
);
@@ -28,7 +28,7 @@ function isTaprootChain(chain: ChainCode): boolean {
2828
/**
2929
* Convert utxolib Network to wasm-utxo network name
3030
*/
31-
function toNetworkName(network: utxolib.Network): utxolibCompat.UtxolibName {
31+
export function toNetworkName(network: utxolib.Network): utxolibCompat.UtxolibName {
3232
const networkName = utxolib.getNetworkName(network);
3333
if (!networkName) {
3434
throw new Error(`Invalid network`);
@@ -129,32 +129,53 @@ const ZCASH_DEFAULT_BLOCK_HEIGHTS: Record<string, number> = {
129129
};
130130

131131
/**
132-
* Create a backup key recovery PSBT using wasm-utxo
132+
* Options for creating an empty wasm-utxo PSBT
133133
*/
134-
function createBackupKeyRecoveryPsbtWasm(
134+
export interface CreateEmptyWasmPsbtOptions {
135+
/** Block height for Zcash networks (required to determine consensus branch ID) */
136+
blockHeight?: number;
137+
}
138+
139+
/**
140+
* Create an empty wasm-utxo BitGoPsbt for a given network.
141+
* Handles Zcash networks specially by using ZcashBitGoPsbt.
142+
*
143+
* @param network - The network for the PSBT
144+
* @param rootWalletKeys - The wallet keys
145+
* @param options - Optional settings (e.g., blockHeight for Zcash)
146+
* @returns A wasm-utxo BitGoPsbt instance
147+
*/
148+
export function createEmptyWasmPsbt(
135149
network: utxolib.Network,
136150
rootWalletKeys: RootWalletKeys,
137-
unspents: WalletUnspent<bigint>[],
138-
options: CreateBackupKeyRecoveryPsbtOptions
139-
): utxolib.bitgo.UtxoPsbt {
140-
const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options;
141-
151+
options?: CreateEmptyWasmPsbtOptions
152+
): fixedScriptWallet.BitGoPsbt {
142153
const networkName = toNetworkName(network);
143154

144-
// Create PSBT with wasm-utxo and add wallet inputs
145-
// wasm-utxo's RootWalletKeys.from() accepts utxolib's RootWalletKeys format (IWalletKeys interface)
146-
let wasmPsbt: fixedScriptWallet.BitGoPsbt;
147-
148155
if (isZcashNetwork(networkName)) {
149156
// For Zcash, use ZcashBitGoPsbt which requires block height to determine consensus branch ID
150-
const blockHeight = options.blockHeight ?? ZCASH_DEFAULT_BLOCK_HEIGHTS[networkName];
151-
wasmPsbt = fixedScriptWallet.ZcashBitGoPsbt.createEmpty(networkName as 'zcash' | 'zcashTest', rootWalletKeys, {
157+
const blockHeight = options?.blockHeight ?? ZCASH_DEFAULT_BLOCK_HEIGHTS[networkName];
158+
return fixedScriptWallet.ZcashBitGoPsbt.createEmpty(networkName as 'zcash' | 'zcashTest', rootWalletKeys, {
152159
blockHeight,
153160
});
154-
} else {
155-
wasmPsbt = fixedScriptWallet.BitGoPsbt.createEmpty(networkName, rootWalletKeys);
156161
}
157162

163+
return fixedScriptWallet.BitGoPsbt.createEmpty(networkName, rootWalletKeys);
164+
}
165+
166+
/**
167+
* Add wallet inputs from unspents to a wasm-utxo BitGoPsbt.
168+
* Handles taproot inputs by setting the appropriate signPath.
169+
*
170+
* @param wasmPsbt - The wasm-utxo BitGoPsbt to add inputs to
171+
* @param unspents - The wallet unspents to add as inputs
172+
* @param rootWalletKeys - The wallet keys
173+
*/
174+
export function addWalletInputsToWasmPsbt(
175+
wasmPsbt: fixedScriptWallet.BitGoPsbt,
176+
unspents: WalletUnspent<bigint>[],
177+
rootWalletKeys: RootWalletKeys
178+
): void {
158179
unspents.forEach((unspent) => {
159180
const { txid, vout } = utxolib.bitgo.parseOutputId(unspent.id);
160181
const signPath: fixedScriptWallet.SignPath | undefined = isTaprootChain(unspent.chain)
@@ -178,11 +199,59 @@ function createBackupKeyRecoveryPsbtWasm(
178199
}
179200
);
180201
});
202+
}
181203

182-
// Convert wasm-utxo PSBT to utxolib PSBT for dimension calculation and output addition
183-
const psbt = utxolib.bitgo.createPsbtFromBuffer(Buffer.from(wasmPsbt.serialize()), network);
204+
/**
205+
* Add an output to a wasm-utxo BitGoPsbt.
206+
*
207+
* @param wasmPsbt - The wasm-utxo BitGoPsbt to add the output to
208+
* @param address - The destination address
209+
* @param value - The output value in satoshis
210+
* @param network - The network (used to convert address to script)
211+
* @returns The output index
212+
*/
213+
export function addOutputToWasmPsbt(
214+
wasmPsbt: fixedScriptWallet.BitGoPsbt,
215+
address: string,
216+
value: bigint,
217+
network: utxolib.Network
218+
): number {
219+
const script = utxolib.address.toOutputScript(address, network);
220+
return wasmPsbt.addOutput({ script: new Uint8Array(script), value });
221+
}
184222

185-
let dimensions = Dimensions.fromPsbt(psbt).plus(
223+
/**
224+
* Convert a wasm-utxo BitGoPsbt to a utxolib UtxoPsbt.
225+
*
226+
* @param wasmPsbt - The wasm-utxo BitGoPsbt to convert
227+
* @param network - The network
228+
* @returns A utxolib UtxoPsbt
229+
*/
230+
export function wasmPsbtToUtxolibPsbt(
231+
wasmPsbt: fixedScriptWallet.BitGoPsbt,
232+
network: utxolib.Network
233+
): utxolib.bitgo.UtxoPsbt {
234+
return utxolib.bitgo.createPsbtFromBuffer(Buffer.from(wasmPsbt.serialize()), network);
235+
}
236+
237+
/**
238+
* Create a backup key recovery PSBT using wasm-utxo
239+
*/
240+
function createBackupKeyRecoveryPsbtWasm(
241+
network: utxolib.Network,
242+
rootWalletKeys: RootWalletKeys,
243+
unspents: WalletUnspent<bigint>[],
244+
options: CreateBackupKeyRecoveryPsbtOptions
245+
): utxolib.bitgo.UtxoPsbt {
246+
const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options;
247+
248+
// Create PSBT with wasm-utxo and add wallet inputs using shared utilities
249+
const wasmPsbt = createEmptyWasmPsbt(network, rootWalletKeys, { blockHeight: options.blockHeight });
250+
addWalletInputsToWasmPsbt(wasmPsbt, unspents, rootWalletKeys);
251+
252+
// Convert to utxolib PSBT temporarily for dimension calculation
253+
const tempPsbt = wasmPsbtToUtxolibPsbt(wasmPsbt, network);
254+
let dimensions = Dimensions.fromPsbt(tempPsbt).plus(
186255
Dimensions.fromOutput({ script: utxolib.address.toOutputScript(recoveryDestination, network) })
187256
);
188257

@@ -202,16 +271,15 @@ function createBackupKeyRecoveryPsbtWasm(
202271
throw new InsufficientFundsError(totalInputAmount, approximateFee, keyRecoveryServiceFee, recoveryAmount);
203272
}
204273

205-
psbt.addOutput({ script: utxolib.address.toOutputScript(recoveryDestination, network), value: recoveryAmount });
274+
// Add outputs to wasm PSBT
275+
addOutputToWasmPsbt(wasmPsbt, recoveryDestination, recoveryAmount, network);
206276

207277
if (keyRecoveryServiceFeeAddress) {
208-
psbt.addOutput({
209-
script: utxolib.address.toOutputScript(keyRecoveryServiceFeeAddress, network),
210-
value: keyRecoveryServiceFee,
211-
});
278+
addOutputToWasmPsbt(wasmPsbt, keyRecoveryServiceFeeAddress, keyRecoveryServiceFee, network);
212279
}
213280

214-
return psbt;
281+
// Convert to utxolib PSBT for signing and return
282+
return wasmPsbtToUtxolibPsbt(wasmPsbt, network);
215283
}
216284

217285
/**

modules/abstract-utxo/test/unit/fixtures/tbch/recovery/crossChainRecovery-tbsv-signed.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"version": 2,
33
"walletId": "5abacebe28d72fbd07e0b8cbba0ff39e",
4-
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000ffffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b6872202037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e474830450221009c29a7f6f5473fd3b2b3177f0f61de0162b339342605ecadd8f04b39bca9139802207937a1a4f66037d1d6238c5c9b2def7895af1e222bba6fab135fd83edb92feab41010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
4+
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000feffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b6872202037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e47483045022100a7d7b412b2da3064818a87f524038ab5a796b2fb24e72d2ec13d363271cdeb390220519d8330223adf28a3d25027ff572786f78dc941b9a82770793271192c555abe41010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
55
"sourceCoin": "tbch",
66
"recoveryCoin": "tbsv",
77
"recoveryAmount": 99992200

modules/abstract-utxo/test/unit/fixtures/tbch/recovery/crossChainRecovery-tbsv-unsigned.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000ffffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b687010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
2+
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000feffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b687010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
33
"walletId": "5abacebe28d72fbd07e0b8cbba0ff39e",
44
"address": "2N7VWEhmfT8CzGSW2bCVeKJ3GCwSD1nsL2V",
55
"coin": "tbch"

0 commit comments

Comments
 (0)