From bf5cb2765acf38a4df5ae08cfcd299fb52538b87 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 30 Apr 2026 10:27:30 +0200 Subject: [PATCH 1/3] test(abstract-utxo): simplify prebuildAndSign test setup Remove encrypted keychains and webauthn fixtures from prebuildAndSign tests. Use plain private keys directly instead of testing passphrase decryption logic. Move webauthn passphrase fallback tests to dedicated test file where they belong. Issue: BTC-0 Co-authored-by: llm-git --- .../test/unit/prebuildAndSign.ts | 86 +++++-------------- modules/abstract-utxo/test/unit/webauthn.ts | 46 ++++++++++ 2 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 modules/abstract-utxo/test/unit/webauthn.ts diff --git a/modules/abstract-utxo/test/unit/prebuildAndSign.ts b/modules/abstract-utxo/test/unit/prebuildAndSign.ts index 289a1d9950..623305b902 100644 --- a/modules/abstract-utxo/test/unit/prebuildAndSign.ts +++ b/modules/abstract-utxo/test/unit/prebuildAndSign.ts @@ -10,7 +10,6 @@ import { AbstractUtxoCoin } from '../../src'; import { defaultBitGo, - encryptKeychain, getDefaultWalletKeys, getMinUtxoCoins, getUtxoWallet, @@ -25,13 +24,9 @@ type KeyDoc = { id: string; pub: string; source: string; - encryptedPrv: string; coinSpecific: any; }; -const walletPassphrase = 'gabagool'; -const webauthnWalletPassPhrase = 'just the gabagool'; - type Input = { scriptType: ScriptType; value: bigint; @@ -59,19 +54,6 @@ const keyDocumentObjects = rootWalletKeys.triple.map((bip32, keyIdx) => { id: getSeed(keychainsBase58[keyIdx].pub).toString('hex'), pub: bip32.neutered().toBase58(), source: ['user', 'backup', 'bitgo'][keyIdx], - encryptedPrv: encryptKeychain(walletPassphrase, keychainsBase58[keyIdx]), - webauthnDevices: [ - { - otpDeviceId: '123', - authenticatorInfo: { - credID: 'credID', - fmt: 'packed', - publicKey: 'some value', - }, - prfSalt: '456', - encryptedPrv: encryptKeychain(webauthnWalletPassPhrase, keychainsBase58[keyIdx]), - }, - ], coinSpecific: {}, }; }); @@ -176,9 +158,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor return nocks; } - describe(`${coin.getFullName()}-prebuildAndSign-txFormat=${txFormat}-inputScripts=${inputScripts.join( - ',' - )}`, function () { + describe(`${coin.name} prebuildAndSign inputScripts=${inputScripts.join(',')}`, function () { const wallet = getUtxoWallet(coin, { coinSpecific: { addressVersion: 'base58' }, keys: keyDocumentObjects.map((k) => k.id), @@ -219,51 +199,29 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor afterEach(nock.cleanAll); - [true, false].forEach((useWebauthn) => { - it(`should succeed with ${useWebauthn ? 'webauthn encryptedPrv' : 'encryptedPrv'}`, async function () { - // Check if this wallet/coin combination defaults to psbt - const defaultTxFormat = coin.getDefaultTxFormat(wallet); - const nocks = createNocks({ - bgUrl, - wallet, - keyDocuments: keyDocumentObjects, - prebuild, - recipient, - addressInfo, - nockOutputAddresses: txFormat !== 'psbt', - txFormat: defaultTxFormat, - }); - - // call prebuild and sign, nocks should be consumed - const res = (await wallet.prebuildAndSignTransaction({ - recipients: [recipient], - walletPassphrase: useWebauthn ? webauthnWalletPassPhrase : walletPassphrase, - })) as HalfSignedUtxoTransaction; - - nocks.forEach((nock) => assert.ok(nock.isDone())); - - assertSignable(res.txHex, inputScripts, coin.network); + it('should succeed', async function () { + // Check if this wallet/coin combination defaults to psbt + const defaultTxFormat = coin.getDefaultTxFormat(wallet); + const nocks = createNocks({ + bgUrl, + wallet, + keyDocuments: keyDocumentObjects, + prebuild, + recipient, + addressInfo, + nockOutputAddresses: txFormat !== 'psbt', + txFormat: defaultTxFormat, }); - it('should fail if the wallet passphrase is incorrect', async function () { - createNocks({ - bgUrl, - wallet, - keyDocuments: keyDocumentObjects, - prebuild, - recipient, - addressInfo, - nockOutputAddresses: txFormat !== 'psbt', - }); + // call prebuild and sign, nocks should be consumed + const res = (await wallet.prebuildAndSignTransaction({ + recipients: [recipient], + prv: keychainsBase58[0].prv, + })) as HalfSignedUtxoTransaction; - await assert.rejects( - wallet.prebuildAndSignTransaction({ - recipients: [recipient], - walletPassphrase: Math.random().toString(), - }), - { message: 'unable to decrypt keychain with the given wallet passphrase' } - ); - }); + nocks.forEach((nock) => assert.ok(nock.isDone())); + + assertSignable(res.txHex, inputScripts, coin.network); }); [true, false].forEach((selfSend) => { @@ -289,7 +247,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor // call prebuild and sign, nocks should be consumed const res = (await wallet.prebuildAndSignTransaction({ recipients: [recipient], - walletPassphrase, + prv: keychainsBase58[0].prv, rbfTxIds, feeMultiplier, })) as HalfSignedUtxoTransaction; diff --git a/modules/abstract-utxo/test/unit/webauthn.ts b/modules/abstract-utxo/test/unit/webauthn.ts new file mode 100644 index 0000000000..ec6b47c581 --- /dev/null +++ b/modules/abstract-utxo/test/unit/webauthn.ts @@ -0,0 +1,46 @@ +import * as assert from 'assert'; + +import { Keychain } from '@bitgo/sdk-core'; +import { getSeed } from '@bitgo/sdk-test'; + +import { getMinUtxoCoins, getUtxoWallet, encryptKeychain, keychainsBase58 } from './util'; + +const regularPassphrase = 'passphrase-a'; +const webauthnPassphrase = 'passphrase-b'; + +describe('webauthn passphrase decryption', function () { + const [coin] = getMinUtxoCoins(); + const wallet = getUtxoWallet(coin); + + // keychain encrypted with regularPassphrase; webauthn device encrypted with webauthnPassphrase + const keychain: Keychain = { + id: getSeed(keychainsBase58[0].pub).toString('hex'), + pub: keychainsBase58[0].pub, + type: 'independent', + encryptedPrv: encryptKeychain(regularPassphrase, keychainsBase58[0]), + webauthnDevices: [ + { + otpDeviceId: '123', + authenticatorInfo: { credID: 'credID', fmt: 'packed', publicKey: 'some value' }, + prfSalt: '456', + encryptedPrv: encryptKeychain(webauthnPassphrase, keychainsBase58[0]), + }, + ], + }; + + it('should decrypt with the regular passphrase', function () { + const prv = wallet.getUserPrv({ keychain, walletPassphrase: regularPassphrase }); + assert.strictEqual(prv, keychainsBase58[0].prv); + }); + + it('should fall back to webauthn device when the regular passphrase fails', function () { + const prv = wallet.getUserPrv({ keychain, walletPassphrase: webauthnPassphrase }); + assert.strictEqual(prv, keychainsBase58[0].prv); + }); + + it('should throw when all passphrases are wrong', function () { + assert.throws(() => wallet.getUserPrv({ keychain, walletPassphrase: 'wrong' }), { + message: 'failed to decrypt user keychain', + }); + }); +}); From 247850b0c574c05346912c1868573392fb65601b Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 30 Apr 2026 10:45:16 +0200 Subject: [PATCH 2/3] test(abstract-utxo): remove unused TxFormat parameter Remove the `txFormat` parameter from the `run` function and hardcode `nockOutputAddresses` to `false` since all tests now use PSBT format. Issue: BTC-0 Co-authored-by: llm-git --- modules/abstract-utxo/test/unit/prebuildAndSign.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/abstract-utxo/test/unit/prebuildAndSign.ts b/modules/abstract-utxo/test/unit/prebuildAndSign.ts index 623305b902..b1d54d1be7 100644 --- a/modules/abstract-utxo/test/unit/prebuildAndSign.ts +++ b/modules/abstract-utxo/test/unit/prebuildAndSign.ts @@ -15,7 +15,6 @@ import { getUtxoWallet, keychainsBase58, getScriptTypes, - TxFormat, } from './util'; type ScriptType = testutil.InputScriptType; @@ -58,7 +57,7 @@ const keyDocumentObjects = rootWalletKeys.triple.map((bip32, keyIdx) => { }; }); -function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFormat): void { +function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { function createPrebuildPsbt(inputs: Input[], outputs: { scriptType: 'p2sh'; value: bigint }[]) { const psbt = utxolib.testutil.constructPsbt( inputs as utxolib.testutil.Input[], @@ -200,7 +199,6 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor afterEach(nock.cleanAll); it('should succeed', async function () { - // Check if this wallet/coin combination defaults to psbt const defaultTxFormat = coin.getDefaultTxFormat(wallet); const nocks = createNocks({ bgUrl, @@ -209,7 +207,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor prebuild, recipient, addressInfo, - nockOutputAddresses: txFormat !== 'psbt', + nockOutputAddresses: false, txFormat: defaultTxFormat, }); @@ -228,7 +226,6 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor it(`should be able to build, sign, & verify a replacement transaction with selfSend: ${selfSend}`, async function () { const rbfTxIds = ['tx-to-be-replaced'], feeMultiplier = 1.5; - // Check if this wallet/coin combination defaults to psbt const defaultTxFormat = coin.getDefaultTxFormat(wallet); const nocks = createNocks({ bgUrl, @@ -240,7 +237,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor rbfTxIds, feeMultiplier, selfSend, - nockOutputAddresses: txFormat !== 'psbt', + nockOutputAddresses: false, txFormat: defaultTxFormat, }); @@ -261,5 +258,5 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor } getMinUtxoCoins().forEach((coin) => { - run(coin, getScriptTypes(coin, 'psbt'), 'psbt'); + run(coin, getScriptTypes(coin, 'psbt')); }); From 2b087149da7575036fa0acc6932982818b1f179a Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 30 Apr 2026 10:58:46 +0200 Subject: [PATCH 3/3] fix(abstract-utxo): add replay protection to prebuildAndSign test Update test to include replay protection pubkey configuration when creating transaction. Uses getReplayProtectionPubkeys helper to fetch appropriate key for coin network. Issue: BTC-0 Co-authored-by: llm-git --- modules/abstract-utxo/test/unit/prebuildAndSign.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/abstract-utxo/test/unit/prebuildAndSign.ts b/modules/abstract-utxo/test/unit/prebuildAndSign.ts index b1d54d1be7..e2ae0dd0ec 100644 --- a/modules/abstract-utxo/test/unit/prebuildAndSign.ts +++ b/modules/abstract-utxo/test/unit/prebuildAndSign.ts @@ -6,7 +6,7 @@ import nock = require('nock'); import { common, HalfSignedUtxoTransaction, Wallet } from '@bitgo/sdk-core'; import { getSeed } from '@bitgo/sdk-test'; -import { AbstractUtxoCoin } from '../../src'; +import { AbstractUtxoCoin, getReplayProtectionPubkeys } from '../../src'; import { defaultBitGo, @@ -64,7 +64,10 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { outputs, coin.network, rootWalletKeys, - 'unsigned' + 'unsigned', + { + p2shP2pkKey: getReplayProtectionPubkeys(coin.name)[0], + } ); utxolib.bitgo.addXpubsToPsbt(psbt, rootWalletKeys); return psbt;