Skip to content

Commit a7228c5

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): optimize test suite with min coin selection
Use a minimal subset of test coins (BTC, BCH, ZEC) that provides full feature coverage while significantly reducing test runtime. Refactor test utilities to support this optimization: - Add `getMinUtxoCoins()` helper function - Move shared code like `getScriptTypes()` and `TxFormat` to utilities - Improve validation logic for different script types Issue: BTC-2866 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 4c68dbf commit a7228c5

3 files changed

Lines changed: 66 additions & 62 deletions

File tree

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

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import * as assert from 'assert';
22

33
import * as utxolib from '@bitgo/utxo-lib';
4+
import { testutil } from '@bitgo/utxo-lib';
45
import nock = require('nock');
56
import { common, HalfSignedUtxoTransaction, Wallet } from '@bitgo/sdk-core';
67
import { getSeed } from '@bitgo/sdk-test';
78

8-
import { AbstractUtxoCoin, getReplayProtectionAddresses } from '../../src';
9-
import { getMainnetCoinName } from '../../src/names';
9+
import { AbstractUtxoCoin } from '../../src';
1010

11-
import { defaultBitGo, encryptKeychain, getDefaultWalletKeys, getUtxoWallet, keychainsBase58, utxoCoins } from './util';
11+
import {
12+
defaultBitGo,
13+
encryptKeychain,
14+
getDefaultWalletKeys,
15+
getMinUtxoCoins,
16+
getUtxoWallet,
17+
keychainsBase58,
18+
getScriptTypes,
19+
TxFormat,
20+
} from './util';
1221

13-
const txFormats = ['legacy', 'psbt'] as const;
14-
export type TxFormat = (typeof txFormats)[number];
22+
type ScriptType = testutil.InputScriptType;
1523

1624
type KeyDoc = {
1725
id: string;
@@ -24,9 +32,6 @@ type KeyDoc = {
2432
const walletPassphrase = 'gabagool';
2533
const webauthnWalletPassPhrase = 'just the gabagool';
2634

27-
const scriptTypes = [...utxolib.bitgo.outputScripts.scriptTypes2Of3, 'taprootKeyPathSpend', 'p2shP2pk'] as const;
28-
export type ScriptType = (typeof scriptTypes)[number];
29-
3035
type Input = {
3136
scriptType: ScriptType;
3237
value: bigint;
@@ -35,15 +40,16 @@ type Input = {
3540
function assertSignable(psbtHex: string, inputScripts: ScriptType[], network: utxolib.Network): void {
3641
const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, network);
3742
// Make sure that you can sign with bitgo key and extract the transaction
38-
// No signatures should be present if it's a p2shP2pk input
39-
if (!inputScripts.includes('p2shP2pk')) {
40-
const key = inputScripts.includes('p2trMusig2') ? rootWalletKeys.backup : rootWalletKeys.bitgo;
41-
psbt.signAllInputsHD(key, { deterministic: true });
42-
psbt.validateSignaturesOfAllInputs();
43-
psbt.finalizeAllInputs();
44-
const tx = psbt.extractTransaction();
45-
assert.ok(tx);
43+
// Skip validation for p2shP2pk (single-sig replay protection) and taprootKeyPathSpend (requires musig2 nonce exchange)
44+
if (inputScripts.includes('p2shP2pk') || inputScripts.includes('taprootKeyPathSpend')) {
45+
return;
4646
}
47+
const key = inputScripts.includes('p2trMusig2') ? rootWalletKeys.backup : rootWalletKeys.bitgo;
48+
psbt.signAllInputsHD(key, { deterministic: true });
49+
psbt.validateSignaturesOfAllInputs();
50+
psbt.finalizeAllInputs();
51+
const tx = psbt.extractTransaction();
52+
assert.ok(tx);
4753
}
4854

4955
// Build the key objects
@@ -295,27 +301,6 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor
295301
});
296302
}
297303

298-
utxoCoins
299-
.filter((coin) => getMainnetCoinName(coin.name) !== 'bsv')
300-
.forEach((coin) => {
301-
scriptTypes
302-
// Don't iterate over p2shP2pk - in no scenario would a wallet spend two p2shP2pk inputs as these
303-
// are single signature inputs that are used for replay protection and are added to the transaction
304-
// by our system from a separate wallet. We do run tests below where one of the inputs is a p2shP2pk and
305-
// the other is an input spent by the user.
306-
.filter((scriptType) => scriptType !== 'p2shP2pk')
307-
.forEach((inputScript) => {
308-
const inputScriptCleaned = (
309-
inputScript === 'taprootKeyPathSpend' ? 'p2trMusig2' : inputScript
310-
) as utxolib.bitgo.outputScripts.ScriptType2Of3;
311-
312-
if (!coin.supportsAddressType(inputScriptCleaned)) {
313-
return;
314-
}
315-
316-
run(coin, [inputScript, inputScript], 'psbt');
317-
if (getReplayProtectionAddresses(coin.name).length) {
318-
run(coin, ['p2shP2pk', inputScript], 'psbt');
319-
}
320-
});
321-
});
304+
getMinUtxoCoins().forEach((coin) => {
305+
run(coin, getScriptTypes(coin, 'psbt'), 'psbt');
306+
});

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

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ import {
1414
WalletSignTransactionOptions,
1515
} from '@bitgo/sdk-core';
1616

17-
import { AbstractUtxoCoin, getReplayProtectionAddresses, generateAddress, getReplayProtectionPubkeys } from '../../src';
17+
import { AbstractUtxoCoin, generateAddress, getReplayProtectionPubkeys } from '../../src';
1818
import { SdkBackend } from '../../src/transaction/types';
1919
import type { Unspent, WalletUnspent } from '../../src/unspent';
2020

2121
import {
22-
utxoCoins,
2322
shouldEqualJSON,
2423
getFixture,
2524
getUtxoWallet,
@@ -32,6 +31,8 @@ import {
3231
getDefaultWalletKeys,
3332
getWalletKeys,
3433
defaultBitGo,
34+
getMinUtxoCoins,
35+
getScriptTypes,
3536
} from './util';
3637

3738
function run<TNumber extends number | bigint = number>(
@@ -406,33 +407,14 @@ function run<TNumber extends number | bigint = number>(
406407
});
407408
}
408409

409-
function getScriptTypes(coin: AbstractUtxoCoin, txFormat: 'legacy' | 'psbt') {
410-
return (['p2shP2pk', 'p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2', 'taprootKeyPathSpend'] as const).filter(
411-
(t) => {
412-
if (t === 'p2shP2pk') {
413-
return getReplayProtectionAddresses(coin.name).length > 0;
414-
}
415-
if (txFormat === 'legacy') {
416-
if (t === 'p2tr' || t === 'p2trMusig2' || t === 'taprootKeyPathSpend') {
417-
return false;
418-
}
419-
}
420-
if (t === 'taprootKeyPathSpend') {
421-
return coin.supportsAddressType('p2trMusig2');
422-
}
423-
return coin.supportsAddressType(t);
424-
}
425-
);
426-
}
427-
428410
function runTestForCoin(coin: AbstractUtxoCoin) {
429411
(['legacy', 'psbt'] as const).forEach((txFormat) => {
430412
run(coin, getScriptTypes(coin, txFormat), txFormat, { decodeWith: 'wasm-utxo' });
431413
});
432414
}
433415

434416
describe('Transaction Suite', function () {
435-
utxoCoins.forEach((coin) => {
417+
getMinUtxoCoins().forEach((coin) => {
436418
describe(`${coin.getChain()}`, function () {
437419
runTestForCoin(coin);
438420
});

modules/abstract-utxo/test/unit/util/utxoCoins.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Tdoge,
2525
Zec,
2626
Tzec,
27+
getReplayProtectionAddresses,
2728
} from '../../../src';
2829

2930
export const defaultBitGo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
@@ -65,6 +66,21 @@ function getUtxoCoins(bitgo: BitGoAPI = defaultBitGo): AbstractUtxoCoin[] {
6566

6667
export const utxoCoins = getUtxoCoins();
6768

69+
/**
70+
* Minimal subset of coins for comprehensive test coverage.
71+
* - btc: Full feature set (segwit, taproot, musig2)
72+
* - bch: replay protection addresses
73+
* - zec: special transaction format (Overwinter/Sapling)
74+
*/
75+
export const minUtxoCoinNames = ['btc', 'bch', 'zec'] as const;
76+
77+
/**
78+
* Get minimal set of coins for testing. Covers ~99% of feature paths.
79+
*/
80+
export function getMinUtxoCoins(): AbstractUtxoCoin[] {
81+
return minUtxoCoinNames.map(getUtxoCoin);
82+
}
83+
6884
export function getUtxoCoin(name: string): AbstractUtxoCoin {
6985
for (const c of utxoCoins) {
7086
if (c.getChain() === name) {
@@ -82,3 +98,24 @@ export function getUtxoCoinForNetwork(n: utxolib.Network): AbstractUtxoCoin {
8298
}
8399
throw new Error(`no coin for network ${utxolib.getNetworkName(n)}`);
84100
}
101+
102+
export type TxFormat = 'legacy' | 'psbt';
103+
104+
export function getScriptTypes(coin: AbstractUtxoCoin, txFormat: TxFormat): utxolib.testutil.InputScriptType[] {
105+
return (['p2shP2pk', 'p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2', 'taprootKeyPathSpend'] as const).filter(
106+
(t) => {
107+
if (t === 'p2shP2pk') {
108+
return getReplayProtectionAddresses(coin.name).length > 0;
109+
}
110+
if (txFormat === 'legacy') {
111+
if (t === 'p2tr' || t === 'p2trMusig2' || t === 'taprootKeyPathSpend') {
112+
return false;
113+
}
114+
}
115+
if (t === 'taprootKeyPathSpend') {
116+
return coin.supportsAddressType('p2trMusig2');
117+
}
118+
return coin.supportsAddressType(t);
119+
}
120+
);
121+
}

0 commit comments

Comments
 (0)