Skip to content

Commit a1726a6

Browse files
committed
feat(sdk-coin-btc): add transaction validation for fanout unspent management
- Add test case for fanout spoofing detection in btc.ts Ticket WP-5955 TICKET: WP-5955
1 parent 5717147 commit a1726a6

File tree

1 file changed

+72
-1
lines changed
  • modules/sdk-coin-btc/test/unit

1 file changed

+72
-1
lines changed

modules/sdk-coin-btc/test/unit/btc.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ describe('BTC:', () => {
107107
});
108108
});
109109

110-
describe('Unspent management spoofability (BUILD_SIGN_SEND)', () => {
110+
describe('Unspent management spoofability - Consolidation (BUILD_SIGN_SEND)', () => {
111111
let coin: Tbtc;
112112
let bitgoTest: TestBitGoAPI;
113113
before(() => {
@@ -177,4 +177,75 @@ describe('BTC:', () => {
177177
);
178178
});
179179
});
180+
181+
describe('Unspent management spoofability - Fanout (BUILD_SIGN_SEND)', () => {
182+
let coin: Tbtc;
183+
let bitgoTest: TestBitGoAPI;
184+
before(() => {
185+
bitgoTest = TestBitGo.decorate(BitGoAPI, { env: 'test' });
186+
bitgoTest.safeRegister('tbtc', Tbtc.createInstance);
187+
bitgoTest.initializeTestVars();
188+
coin = bitgoTest.coin('tbtc') as Tbtc;
189+
});
190+
191+
it('should detect hex spoofing in fanout BUILD_SIGN_SEND', async (): Promise<void> => {
192+
const {
193+
getDefaultWalletKeys,
194+
toKeychainObjects,
195+
} = require('../../../bitgo/test/v2/unit/coins/utxo/util/keychains');
196+
const rootWalletKey = getDefaultWalletKeys();
197+
const keysObj = toKeychainObjects(rootWalletKey, 'pass');
198+
199+
const { Wallet } = await import('@bitgo/sdk-core');
200+
const wallet = new Wallet(bitgoTest, coin, {
201+
id: '5b34252f1bf349930e34020a',
202+
coin: 'tbtc',
203+
keys: keysObj.map((k) => k.id),
204+
});
205+
206+
const originalPsbt = utxolib.testutil.constructPsbt(
207+
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
208+
[{ address: 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', value: BigInt(9000) }],
209+
coin.network,
210+
rootWalletKey,
211+
'unsigned' as const
212+
);
213+
utxolib.bitgo.addXpubsToPsbt(originalPsbt, rootWalletKey);
214+
215+
const spoofedPsbt = utxolib.testutil.constructPsbt(
216+
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
217+
[{ address: 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn', value: BigInt(9000) }],
218+
coin.network,
219+
rootWalletKey,
220+
'unsigned' as const
221+
);
222+
utxolib.bitgo.addXpubsToPsbt(spoofedPsbt, rootWalletKey);
223+
const spoofedHex: string = spoofedPsbt.toHex();
224+
225+
const bgUrl: string = (bitgoTest as any)._baseUrl;
226+
const nock = require('nock');
227+
228+
nock(bgUrl)
229+
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/fanoutUnspents`)
230+
.reply(200, { txHex: spoofedHex, fanoutId: 'test' });
231+
232+
nock(bgUrl)
233+
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`)
234+
.reply((requestBody: any) => {
235+
if (requestBody?.txHex === spoofedHex) {
236+
throw new Error('Spoofed transaction was sent: spoofing protection failed');
237+
}
238+
return [200, { txid: 'test-txid-123', status: 'signed' }];
239+
});
240+
241+
keysObj.forEach((k, i) => nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${wallet.keyIds()[i]}`).reply(200, k));
242+
243+
await assert.rejects(
244+
wallet.fanoutUnspents({ walletPassphrase: 'pass' }),
245+
(e: any) =>
246+
typeof e?.message === 'string' &&
247+
e.message.includes('prebuild attempts to spend to unintended external recipients')
248+
);
249+
});
250+
});
180251
});

0 commit comments

Comments
 (0)