Skip to content

Commit 30e144d

Browse files
Merge pull request #8335 from BitGo/BTC-0.deprecate-legacy-tx-signing
feat(abstract-utxo): deprecate legacy transaction signing
2 parents f13a816 + eb91975 commit 30e144d

6 files changed

Lines changed: 48 additions & 35 deletions

File tree

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ type UtxoBaseSignTransactionOptions<TNumber extends number | bigint = number> =
338338
*/
339339
returnLegacyFormat?: boolean;
340340
wallet?: UtxoWallet;
341+
/**
342+
* When true (default), extract finalized PSBT to legacy transaction format.
343+
* When false, return finalized PSBT. Useful for testing to keep transactions in PSBT format.
344+
*/
345+
extractTransaction?: boolean;
341346
};
342347

343348
export type SignTransactionOptions<TNumber extends number | bigint = number> = UtxoBaseSignTransactionOptions<TNumber> &
@@ -406,6 +411,11 @@ export abstract class AbstractUtxoCoin
406411

407412
public readonly amountType: 'number' | 'bigint';
408413

414+
protected readonly supportedTxFormats: { readonly psbt: boolean; readonly legacy: boolean } = {
415+
psbt: true,
416+
legacy: false,
417+
};
418+
409419
protected constructor(bitgo: BitGoBase, amountType: 'number' | 'bigint' = 'number') {
410420
super(bitgo);
411421
this.amountType = amountType;
@@ -582,8 +592,15 @@ export abstract class AbstractUtxoCoin
582592
}
583593

584594
if (utxolib.bitgo.isPsbt(input)) {
595+
if (!this.supportedTxFormats.psbt) {
596+
throw new ErrorDeprecatedTxFormat('psbt');
597+
}
585598
return decodePsbtWith(input, this.name, decodeWith);
586599
} else {
600+
// Legacy format transactions are deprecated. This will be an unconditional error in the future.
601+
if (!this.supportedTxFormats.legacy) {
602+
throw new ErrorDeprecatedTxFormat('legacy');
603+
}
587604
if (decodeWith !== 'utxolib') {
588605
console.error('received decodeWith hint %s, ignoring for legacy transaction', decodeWith);
589606
}

modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export async function signTransaction<
6767
allowNonSegwitSigningWithoutPrevTx: boolean;
6868
pubs: string[] | undefined;
6969
cosignerPub: string | undefined;
70+
/** When true (default), extract finalized PSBT to legacy transaction format. When false, return finalized PSBT. */
71+
extractTransaction?: boolean;
7072
}
7173
): Promise<
7274
utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction<bigint | number> | fixedScriptWallet.BitGoPsbt | Buffer
@@ -77,6 +79,8 @@ export async function signTransaction<
7779
isLastSignature = params.isLastSignature;
7880
}
7981

82+
const { extractTransaction = true } = params;
83+
8084
if (tx instanceof bitgo.UtxoPsbt) {
8185
const signedPsbt = await signPsbtWithMusig2ParticipantUtxolib(
8286
coin as Musig2Participant<utxolib.bitgo.UtxoPsbt>,
@@ -88,8 +92,12 @@ export async function signTransaction<
8892
}
8993
);
9094
if (isLastSignature) {
91-
signedPsbt.finalizeAllInputs();
92-
return signedPsbt.extractTransaction();
95+
if (extractTransaction) {
96+
signedPsbt.finalizeAllInputs();
97+
return signedPsbt.extractTransaction();
98+
}
99+
// Return signed PSBT without finalizing to preserve derivation info
100+
return signedPsbt;
93101
}
94102
return signedPsbt;
95103
} else if (tx instanceof fixedScriptWallet.BitGoPsbt) {
@@ -110,8 +118,12 @@ export async function signTransaction<
110118
}
111119
);
112120
if (isLastSignature) {
113-
signedPsbt.finalizeAllInputs();
114-
return Buffer.from(signedPsbt.extractTransaction().toBytes());
121+
if (extractTransaction) {
122+
signedPsbt.finalizeAllInputs();
123+
return Buffer.from(signedPsbt.extractTransaction().toBytes());
124+
}
125+
// Return finalized PSBT without extracting to legacy format
126+
return signedPsbt;
115127
}
116128
return signedPsbt;
117129
}

modules/abstract-utxo/src/transaction/signTransaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export async function signTransaction<TNumber extends number | bigint>(
7878
allowNonSegwitSigningWithoutPrevTx: params.allowNonSegwitSigningWithoutPrevTx ?? false,
7979
pubs: params.pubs,
8080
cosignerPub: params.cosignerPub,
81+
extractTransaction: params.extractTransaction,
8182
});
8283

8384
// Convert half-signed PSBT to legacy format when the caller explicitly requested txFormat: 'legacy'

modules/abstract-utxo/test/unit/customSigner.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,4 @@ describe('UTXO Custom Signer Function', function () {
8383
sinon.assert.calledOnce(customSigningFunction as sinon.SinonStub);
8484
scope.done();
8585
});
86-
87-
it('should use a custom signing function if provided for Tx without taprootKeyPathSpend input', async function () {
88-
const tx = utxoLib.testutil.constructTxnBuilder(
89-
[{ scriptType: 'p2wsh', value: BigInt(1000) }],
90-
[{ scriptType: 'p2sh', value: BigInt(900) }],
91-
basecoin.network,
92-
rootWalletKey,
93-
'unsigned'
94-
);
95-
const scope = nocks({ txHex: tx.buildIncomplete().toHex() });
96-
const result = await wallet.sendMany({ recipients, customSigningFunction });
97-
98-
assertHasProperty(result, 'ok', true);
99-
sinon.assert.calledOnce(customSigningFunction as sinon.SinonStub);
100-
scope.done();
101-
});
10286
});

modules/abstract-utxo/test/unit/signTransaction.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import nock = require('nock');
66
import { testutil } from '@bitgo/utxo-lib';
77
import { common, Triple } from '@bitgo/sdk-core';
88

9-
import { getReplayProtectionPubkeys } from '../../src';
9+
import { getReplayProtectionPubkeys, ErrorDeprecatedTxFormat } from '../../src';
1010
import type { Unspent } from '../../src/unspent';
1111

1212
import { getUtxoWallet, getDefaultWalletKeys, getUtxoCoin, keychainsBase58, defaultBitGo } from './util';
@@ -170,7 +170,7 @@ describe('signTransaction', function () {
170170
}
171171
});
172172

173-
it('customSigningFunction flow - Network Tx', async function () {
173+
it('customSigningFunction flow - Network Tx should reject legacy format', async function () {
174174
const inputs: testutil.TxnInput<bigint>[] = testutil.txnInputScriptTypes
175175
.filter((v) => v !== 'p2shP2pk')
176176
.map((scriptType) => ({
@@ -182,9 +182,10 @@ describe('signTransaction', function () {
182182
const txBuilder = testutil.constructTxnBuilder(inputs, outputs, coin.network, rootWalletKeys, 'unsigned');
183183
const unspents = inputs.map((v, i) => testutil.toTxnUnspent(v, i, coin.network, rootWalletKeys));
184184

185-
for (const v of [false, true]) {
186-
await signTransaction(txBuilder.buildIncomplete(), v, unspents);
187-
}
185+
// Legacy format transactions are now deprecated and should throw ErrorDeprecatedTxFormat
186+
await assert.rejects(async () => {
187+
await signTransaction(txBuilder.buildIncomplete(), false, unspents);
188+
}, ErrorDeprecatedTxFormat);
188189
});
189190

190191
it('fails on PSBT cache miss', async function () {

modules/abstract-utxo/test/unit/transaction.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as _ from 'lodash';
55
import * as utxolib from '@bitgo/utxo-lib';
66
import nock = require('nock');
77
import { BIP32Interface, bitgo, testutil } from '@bitgo/utxo-lib';
8-
import { address as wasmAddress } from '@bitgo/wasm-utxo';
8+
import { address as wasmAddress, fixedScriptWallet } from '@bitgo/wasm-utxo';
99
import {
1010
common,
1111
FullySignedTransaction,
@@ -108,6 +108,7 @@ function run<TNumber extends number | bigint = number>(
108108
prv: signer.toBase58(),
109109
pubs: walletKeys.triple.map((k) => k.neutered().toBase58()),
110110
cosignerPub: cosigner.neutered().toBase58(),
111+
extractTransaction: false,
111112
} as WalletSignTransactionOptions;
112113
}
113114

@@ -223,7 +224,7 @@ function run<TNumber extends number | bigint = number>(
223224

224225
function toTransactionStagesObj(stages: TransactionStages): TransactionObjStages {
225226
return _.mapValues(stages, (v) =>
226-
v === undefined || v instanceof utxolib.bitgo.UtxoPsbt
227+
v === undefined || v instanceof utxolib.bitgo.UtxoPsbt || v instanceof fixedScriptWallet.BitGoPsbt
227228
? undefined
228229
: v instanceof utxolib.bitgo.UtxoTransaction
229230
? transactionToObj<TNumber>(v)
@@ -266,14 +267,11 @@ function run<TNumber extends number | bigint = number>(
266267
signedBy: BIP32Interface[],
267268
sign: 'halfsigned' | 'fullsigned'
268269
) {
269-
if (txFormat === 'psbt' && sign === 'halfsigned') {
270+
if (txFormat === 'psbt') {
270271
testPsbtValidSignatures(tx, signedBy);
271272
return;
272273
}
273-
const unspents =
274-
txFormat === 'psbt'
275-
? getUnspentsForPsbt().map((u) => ({ ...u, value: bitgo.toTNumber(u.value, amountType) as TNumber }))
276-
: getUnspents();
274+
const unspents = getUnspents();
277275
const prevOutputs = unspents.map(
278276
(u): utxolib.TxOutput<TNumber> => ({
279277
script: Buffer.from(wasmAddress.toOutputScriptWithCoin(u.address, coin.name)),
@@ -398,6 +396,8 @@ function run<TNumber extends number | bigint = number>(
398396
const txHex =
399397
stageTx instanceof utxolib.bitgo.UtxoPsbt || stageTx instanceof utxolib.bitgo.UtxoTransaction
400398
? stageTx.toBuffer().toString('hex')
399+
: stageTx instanceof fixedScriptWallet.BitGoPsbt
400+
? Buffer.from(stageTx.serialize()).toString('hex')
401401
: stageTx.txHex;
402402

403403
const pubs = walletKeys.triple.map((k) => k.neutered().toBase58()) as Triple<string>;
@@ -415,9 +415,7 @@ function run<TNumber extends number | bigint = number>(
415415
}
416416

417417
function runTestForCoin(coin: AbstractUtxoCoin) {
418-
(['legacy', 'psbt'] as const).forEach((txFormat) => {
419-
run(coin, getScriptTypes(coin, txFormat), txFormat, { decodeWith: 'wasm-utxo' });
420-
});
418+
run(coin, getScriptTypes(coin, 'psbt'), 'psbt', { decodeWith: 'wasm-utxo' });
421419
}
422420

423421
describe('Transaction Suite', function () {

0 commit comments

Comments
 (0)