Skip to content

Commit 64aabd4

Browse files
OttoAllmendingerllm-git
andcommitted
feat(utxo-lib): add AcidTest utility for comprehensive PSBT testing
Creates a utility class for testing that generates PSBTs with all supported input and output types for a given network. The AcidTest class can create PSBTs with: - All wallet script types supported by the network - p2shP2pk inputs (for replay protection) - Various output types including those from another wallet - OP_RETURN outputs Refactors existing tests to use this utility, making them more comprehensive and maintainable. Issue: BTC-2732 Co-authored-by: llm-git <[email protected]>
1 parent fb8f402 commit 64aabd4

File tree

3 files changed

+160
-129
lines changed

3 files changed

+160
-129
lines changed

modules/utxo-lib/src/testutil/psbt.ts

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ok as assert } from 'assert';
33

44
import {
55
createOutputScriptP2shP2pk,
6+
isSupportedScriptType,
67
ScriptType,
78
ScriptType2Of3,
89
scriptTypeP2shP2pk,
@@ -26,10 +27,12 @@ import {
2627
UtxoTransaction,
2728
verifySignatureWithUnspent,
2829
addXpubsToPsbt,
30+
clonePsbtWithoutNonWitnessUtxo,
2931
} from '../bitgo';
3032
import { Network } from '../networks';
3133
import { mockReplayProtectionUnspent, mockWalletUnspent } from './mock';
3234
import { toOutputScript } from '../address';
35+
import { getDefaultWalletKeys, getWalletKeysForSeed } from './keys';
3336

3437
/**
3538
* This is a bit of a misnomer, as it actually specifies the spend type of the input.
@@ -48,6 +51,8 @@ export type Input = {
4851
value: bigint;
4952
};
5053

54+
export type SignStage = 'unsigned' | 'halfsigned' | 'fullsigned';
55+
5156
/**
5257
* Set isInternalAddress=true for internal output address
5358
*/
@@ -169,7 +174,7 @@ export function constructPsbt(
169174
outputs: Output[],
170175
network: Network,
171176
rootWalletKeys: RootWalletKeys,
172-
sign: 'unsigned' | 'halfsigned' | 'fullsigned',
177+
signStage: SignStage,
173178
params?: {
174179
signers?: { signerName: KeyName; cosignerName?: KeyName };
175180
deterministic?: boolean;
@@ -183,6 +188,11 @@ export function constructPsbt(
183188
assert(totalInputAmount >= outputInputAmount, 'total output can not exceed total input');
184189

185190
const psbt = createPsbtForNetwork({ network });
191+
192+
if (params?.addGlobalXPubs) {
193+
addXpubsToPsbt(psbt, rootWalletKeys);
194+
}
195+
186196
const unspents = inputs.map((input, i) => toUnspent(input, i, network, rootWalletKeys));
187197

188198
unspents.forEach((u, i) => {
@@ -226,7 +236,7 @@ export function constructPsbt(
226236
throw new Error('invalid output');
227237
});
228238

229-
if (sign === 'unsigned') {
239+
if (signStage === 'unsigned') {
230240
return psbt;
231241
}
232242

@@ -237,15 +247,90 @@ export function constructPsbt(
237247

238248
signAllPsbtInputs(psbt, inputs, rootWalletKeys, 'halfsigned', { signers, skipNonWitnessUtxo });
239249

240-
if (sign === 'fullsigned') {
241-
signAllPsbtInputs(psbt, inputs, rootWalletKeys, sign, { signers, deterministic, skipNonWitnessUtxo });
250+
if (signStage === 'fullsigned') {
251+
signAllPsbtInputs(psbt, inputs, rootWalletKeys, signStage, { signers, deterministic, skipNonWitnessUtxo });
242252
}
243253

244-
if (params?.addGlobalXPubs) {
245-
addXpubsToPsbt(psbt, rootWalletKeys);
254+
return psbt;
255+
}
256+
257+
export type TxFormat = 'psbt' | 'psbt-lite';
258+
259+
/**
260+
* Creates a valid PSBT with as many features as possible.
261+
*
262+
* - Inputs:
263+
* - All wallet script types that are supported by the network.
264+
* - A p2shP2pk input (for replay protection)
265+
* - Outputs:
266+
* - All wallet script types that are supported by the network.
267+
* - A p2sh output with derivation info of a different wallet (not in the global psbt xpubs)
268+
* - A p2sh output with no derivation info (external output)
269+
* - An OP_RETURN output
270+
*/
271+
export class AcidTest {
272+
public readonly network: Network;
273+
public readonly signStage: SignStage;
274+
public readonly txFormat: TxFormat;
275+
public readonly rootWalletKeys: RootWalletKeys;
276+
public readonly otherWalletKeys: RootWalletKeys;
277+
public readonly inputs: Input[];
278+
public readonly outputs: Output[];
279+
280+
constructor(
281+
network: Network,
282+
signStage: SignStage,
283+
txFormat: TxFormat,
284+
rootWalletKeys: RootWalletKeys,
285+
otherWalletKeys: RootWalletKeys,
286+
inputs: Input[],
287+
outputs: Output[]
288+
) {
289+
this.network = network;
290+
this.signStage = signStage;
291+
this.txFormat = txFormat;
292+
this.rootWalletKeys = rootWalletKeys;
293+
this.otherWalletKeys = otherWalletKeys;
294+
this.inputs = inputs;
295+
this.outputs = outputs;
246296
}
247297

248-
return psbt;
298+
static withDefaults(network: Network, signStage: SignStage, txFormat: TxFormat): AcidTest {
299+
const rootWalletKeys = getDefaultWalletKeys();
300+
301+
const otherWalletKeys = getWalletKeysForSeed('too many secrets');
302+
const inputs: Input[] = inputScriptTypes
303+
.filter((scriptType) =>
304+
scriptType === 'taprootKeyPathSpend'
305+
? isSupportedScriptType(network, 'p2trMusig2')
306+
: isSupportedScriptType(network, scriptType)
307+
)
308+
.map((scriptType) => ({ scriptType, value: BigInt(2000) }));
309+
310+
const outputs: Output[] = outputScriptTypes
311+
.filter((scriptType) => isSupportedScriptType(network, scriptType))
312+
.map((scriptType) => ({ scriptType, value: BigInt(900) }));
313+
314+
// Test other wallet output (with derivation info)
315+
outputs.push({ scriptType: 'p2sh', value: BigInt(900), walletKeys: otherWalletKeys });
316+
// Tes non-wallet output
317+
outputs.push({ scriptType: 'p2sh', value: BigInt(900), walletKeys: null });
318+
// Test OP_RETURN output
319+
outputs.push({ opReturn: 'setec astronomy', value: BigInt(900) });
320+
321+
return new AcidTest(network, signStage, txFormat, rootWalletKeys, otherWalletKeys, inputs, outputs);
322+
}
323+
324+
createPsbt(): UtxoPsbt {
325+
const psbt = constructPsbt(this.inputs, this.outputs, this.network, this.rootWalletKeys, this.signStage, {
326+
deterministic: true,
327+
addGlobalXPubs: true,
328+
});
329+
if (this.txFormat === 'psbt-lite') {
330+
return clonePsbtWithoutNonWitnessUtxo(psbt);
331+
}
332+
return psbt;
333+
}
249334
}
250335

251336
/**

modules/utxo-lib/src/testutil/transaction.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,12 @@ export function constructTxnBuilder<TNumber extends number | bigint>(
154154
const outputInputAmount = outputs.reduce((sum, output) => sum + BigInt(output.value), BigInt(0));
155155
assert(totalInputAmount >= outputInputAmount, 'total output can not exceed total input');
156156
assert(
157-
!outputs.some((o) => (o.scriptType && o.address) || (!o.scriptType && !o.address)),
158-
'only either output script type or address should be provided'
157+
outputs.every((o) => o.scriptType || o.address),
158+
'must provide either scriptType or address for each output'
159+
);
160+
assert(
161+
outputs.every((o) => !(o.scriptType && o.address)),
162+
'cannot provide both scriptType and address for the same output'
159163
);
160164

161165
const txb = createTransactionBuilderForNetwork<TNumber>(network);

0 commit comments

Comments
 (0)