Skip to content

Commit df559db

Browse files
refactor(abstract-utxo): reduce test boilerplate
- util/address.ts: Extract getWalletAddress helper from psbt.ts for generating addresses from RootWalletKeys, enabling dynamic address generation in tests instead of hardcoded values - util/nockBitGo.ts: Add nockWalletKeys helper to encapsulate the repeated pattern of mocking wallet key fetch endpoints, reducing 10+ lines of boilerplate per test to a single function call - util/index.ts: Export nockBitGo and address utilities so they're accessible from the central util import - postProcessPrebuild.ts: Use getUtxoCoin('tbtc') instead of manual TestBitGo setup, and generate output address dynamically instead of hardcoding - testSpoofTransaction.ts: Replace hardcoded addresses with dynamically generated ones using wallet keys vs attacker keys, making the test intent clearer. Use nockBitGo() and nockWalletKeys() to eliminate URL extraction and key mocking boilerplate Co-authored-by: Cursor <cursoragent@cursor.com> TICKET: BTC-2650
1 parent 2ca3417 commit df559db

5 files changed

Lines changed: 66 additions & 100 deletions

File tree

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

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,23 @@
11
import 'should';
22

3-
import { type TestBitGoAPI, TestBitGo } from '@bitgo/sdk-test';
4-
import { BitGoAPI } from '@bitgo/sdk-api';
53
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
64
import * as testutils from '@bitgo/wasm-utxo/testutils';
75

8-
import { Tbtc } from '../../src/impl/btc';
9-
10-
import { constructPsbt } from './util';
6+
import { constructPsbt, getWalletAddress, getUtxoCoin } from './util';
117

128
const { BitGoPsbt } = fixedScriptWallet;
139

1410
describe('Post Build Validation', function () {
15-
let bitgo: TestBitGoAPI;
16-
let coin: Tbtc;
17-
18-
before(function () {
19-
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' });
20-
bitgo.safeRegister('tbtc', Tbtc.createInstance);
21-
bitgo.initializeTestVars();
22-
coin = bitgo.coin('tbtc') as Tbtc;
23-
});
11+
const coin = getUtxoCoin('tbtc');
2412

2513
it('should not modify locktime on postProcessPrebuild', async function () {
2614
const walletKeys = testutils.getDefaultWalletKeys();
15+
const walletAddress = getWalletAddress('tbtc', walletKeys);
2716

2817
// Create a PSBT with lockTime=0 and sequence=0xffffffff
2918
const psbt = constructPsbt(
3019
[{ scriptType: 'p2wsh' as const, value: BigInt(100000) }],
31-
[{ address: 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', value: BigInt(90000) }],
20+
[{ address: walletAddress, value: BigInt(90000) }],
3221
'tbtc',
3322
walletKeys,
3423
{ lockTime: 0, sequence: 0xffffffff }

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

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

3-
import { type TestBitGoAPI, TestBitGo } from '@bitgo/sdk-test';
4-
import { BitGoAPI, encrypt } from '@bitgo/sdk-api';
53
import * as testutils from '@bitgo/wasm-utxo/testutils';
64
import { Wallet } from '@bitgo/sdk-core';
75

8-
import { Tbtc } from '../../src/impl/btc';
9-
10-
import { constructPsbt } from './util';
6+
import { constructPsbt, getWalletAddress, getUtxoCoin, defaultBitGo, nockBitGo, nockWalletKeys } from './util';
117

128
describe('Transaction Spoofability Tests', function () {
13-
describe('Unspent management spoofability - Consolidation (BUILD_SIGN_SEND)', function () {
14-
let coin: Tbtc;
15-
let bitgoTest: TestBitGoAPI;
16-
17-
before(function () {
18-
bitgoTest = TestBitGo.decorate(BitGoAPI, { env: 'test' });
19-
bitgoTest.safeRegister('tbtc', Tbtc.createInstance);
20-
bitgoTest.initializeTestVars();
21-
coin = bitgoTest.coin('tbtc') as Tbtc;
22-
});
9+
const coin = getUtxoCoin('tbtc');
2310

24-
it('should detect hex spoofing in BUILD_SIGN_SEND', async function (): Promise<void> {
11+
describe('Unspent management spoofability - Consolidation (BUILD_SIGN_SEND)', function () {
12+
it('should detect spoofed consolidation to attacker address', async function (): Promise<void> {
2513
const keyTriple = testutils.getKeyTriple('default');
26-
const rootWalletKey = testutils.getDefaultWalletKeys();
27-
const [user] = keyTriple;
14+
const walletKeys = testutils.getDefaultWalletKeys();
15+
const attackerKeys = testutils.getWalletKeysForSeed('attacker');
2816

29-
const wallet = new Wallet(bitgoTest, coin, {
17+
const wallet = new Wallet(defaultBitGo, coin, {
3018
id: '5b34252f1bf349930e34020a',
3119
coin: 'tbtc',
3220
keys: ['user', 'backup', 'bitgo'],
3321
});
3422

35-
// originalPsbt is created to show what the legitimate transaction would look like
36-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
37-
const originalPsbt = constructPsbt(
38-
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
39-
[{ address: 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', value: BigInt(9000) }],
40-
'tbtc',
41-
rootWalletKey
42-
);
23+
// The attacker replaces the legitimate wallet address with their own
24+
const attackerAddress = getWalletAddress('tbtc', attackerKeys);
4325
const spoofedPsbt = constructPsbt(
4426
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
45-
[{ address: 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn', value: BigInt(9000) }],
27+
[{ address: attackerAddress, value: BigInt(9000) }],
4628
'tbtc',
47-
rootWalletKey
29+
walletKeys // Input uses wallet keys (the funds being stolen)
4830
);
4931
const spoofedHex: string = Buffer.from(spoofedPsbt.serialize()).toString('hex');
5032

51-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52-
const bgUrl: string = (bitgoTest as any)._baseUrl;
53-
const nock = require('nock');
54-
55-
nock(bgUrl)
33+
nockBitGo()
5634
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/consolidateUnspents`)
5735
.reply(200, { txHex: spoofedHex, consolidateId: 'test' });
5836

59-
nock(bgUrl)
37+
nockBitGo()
6038
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`)
6139
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6240
.reply((requestBody: any) => {
@@ -66,15 +44,7 @@ describe('Transaction Spoofability Tests', function () {
6644
return [200, { txid: 'test-txid-123', status: 'signed' }];
6745
});
6846

69-
const pubs = keyTriple.map((k) => k.neutered().toBase58());
70-
const responses = [
71-
{ pub: pubs[0], encryptedPrv: encrypt('pass', user.toBase58()) },
72-
{ pub: pubs[1] },
73-
{ pub: pubs[2] },
74-
];
75-
wallet
76-
.keyIds()
77-
.forEach((id, i) => nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${id}`).reply(200, responses[i]));
47+
nockWalletKeys(wallet, keyTriple, 'pass');
7848

7949
await assert.rejects(
8050
wallet.consolidateUnspents({ walletPassphrase: 'pass' }),
@@ -87,53 +57,32 @@ describe('Transaction Spoofability Tests', function () {
8757
});
8858

8959
describe('Unspent management spoofability - Fanout (BUILD_SIGN_SEND)', function () {
90-
let coin: Tbtc;
91-
let bitgoTest: TestBitGoAPI;
92-
93-
before(function () {
94-
bitgoTest = TestBitGo.decorate(BitGoAPI, { env: 'test' });
95-
bitgoTest.safeRegister('tbtc', Tbtc.createInstance);
96-
bitgoTest.initializeTestVars();
97-
coin = bitgoTest.coin('tbtc') as Tbtc;
98-
});
99-
100-
it('should detect hex spoofing in fanout BUILD_SIGN_SEND', async function (): Promise<void> {
60+
it('should detect spoofed fanout to attacker address', async function (): Promise<void> {
10161
const keyTriple = testutils.getKeyTriple('default');
102-
const rootWalletKey = testutils.getDefaultWalletKeys();
103-
const [user] = keyTriple;
62+
const walletKeys = testutils.getDefaultWalletKeys();
63+
const attackerKeys = testutils.getWalletKeysForSeed('attacker');
10464

105-
const wallet = new Wallet(bitgoTest, coin, {
65+
const wallet = new Wallet(defaultBitGo, coin, {
10666
id: '5b34252f1bf349930e34020a',
10767
coin: 'tbtc',
10868
keys: ['user', 'backup', 'bitgo'],
10969
});
11070

111-
// originalPsbt is created to show what the legitimate transaction would look like
112-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
113-
const originalPsbt = constructPsbt(
114-
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
115-
[{ address: 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', value: BigInt(9000) }],
116-
'tbtc',
117-
rootWalletKey
118-
);
119-
71+
// The attacker replaces the legitimate wallet address with their own
72+
const attackerAddress = getWalletAddress('tbtc', attackerKeys);
12073
const spoofedPsbt = constructPsbt(
12174
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
122-
[{ address: 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn', value: BigInt(9000) }],
75+
[{ address: attackerAddress, value: BigInt(9000) }],
12376
'tbtc',
124-
rootWalletKey
77+
walletKeys // Input uses wallet keys (the funds being stolen)
12578
);
12679
const spoofedHex: string = Buffer.from(spoofedPsbt.serialize()).toString('hex');
12780

128-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
129-
const bgUrl: string = (bitgoTest as any)._baseUrl;
130-
const nock = require('nock');
131-
132-
nock(bgUrl)
81+
nockBitGo()
13382
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/fanoutUnspents`)
13483
.reply(200, { txHex: spoofedHex, fanoutId: 'test' });
13584

136-
nock(bgUrl)
85+
nockBitGo()
13786
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`)
13887
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13988
.reply((requestBody: any) => {
@@ -143,15 +92,7 @@ describe('Transaction Spoofability Tests', function () {
14392
return [200, { txid: 'test-txid-123', status: 'signed' }];
14493
});
14594

146-
const pubs = keyTriple.map((k) => k.neutered().toBase58());
147-
const responses = [
148-
{ pub: pubs[0], encryptedPrv: encrypt('pass', user.toBase58()) },
149-
{ pub: pubs[1] },
150-
{ pub: pubs[2] },
151-
];
152-
wallet
153-
.keyIds()
154-
.forEach((id, i) => nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${id}`).reply(200, responses[i]));
95+
nockWalletKeys(wallet, keyTriple, 'pass');
15596

15697
await assert.rejects(
15798
wallet.fanoutUnspents({ walletPassphrase: 'pass' }),
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { fixedScriptWallet, type CoinName } from '@bitgo/wasm-utxo';
2+
3+
const { ChainCode } = fixedScriptWallet;
4+
5+
/**
6+
* Generate a wallet address from RootWalletKeys.
7+
* Useful for generating both legitimate wallet addresses and attacker addresses from different keys.
8+
*/
9+
export function getWalletAddress(
10+
coinName: CoinName,
11+
walletKeys: fixedScriptWallet.RootWalletKeys,
12+
scriptType: fixedScriptWallet.OutputScriptType = 'p2wsh',
13+
index = 0
14+
): string {
15+
const chain = ChainCode.value(scriptType, 'external');
16+
return fixedScriptWallet.address(walletKeys, chain, index, coinName);
17+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export * from './wallet';
55
export * from './unspents';
66
export * from './transaction';
77
export * from './psbt';
8+
export * from './address';
9+
export * from './nockBitGo';
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import nock = require('nock');
2-
import { Environment, Environments } from '@bitgo/sdk-core';
2+
import { encrypt } from '@bitgo/sdk-api';
3+
import { Environment, Environments, Wallet } from '@bitgo/sdk-core';
4+
import { BIP32, Triple } from '@bitgo/wasm-utxo';
35

46
import { defaultBitGo } from './utxoCoins';
57

68
export function nockBitGo(bitgo = defaultBitGo): nock.Scope {
79
const env = Environments[bitgo.getEnv()] as Environment;
810
return nock(env.uri);
911
}
12+
13+
/**
14+
* Mock the key fetching endpoints for a wallet.
15+
* Sets up nock to return the key triple with the user key encrypted.
16+
*/
17+
export function nockWalletKeys(wallet: Wallet, keyTriple: Triple<BIP32>, userPassphrase: string): void {
18+
const [user] = keyTriple;
19+
const pubs = keyTriple.map((k) => k.neutered().toBase58());
20+
const responses = [
21+
{ pub: pubs[0], encryptedPrv: encrypt(userPassphrase, user.toBase58()) },
22+
{ pub: pubs[1] },
23+
{ pub: pubs[2] },
24+
];
25+
wallet.keyIds().forEach((id, i) => nockBitGo().get(`/api/v2/${wallet.coin()}/key/${id}`).reply(200, responses[i]));
26+
}

0 commit comments

Comments
 (0)