From a3203bc59a04bf5ae7b573eb4b911a3425df21b7 Mon Sep 17 00:00:00 2001 From: Christopher Fong Date: Fri, 11 Jul 2025 16:25:49 -0400 Subject: [PATCH] feat(abstract-utxo): verify paygo TICKET: BTC-2118 --- CODEOWNERS | 2 + .../fixedScript/explainTransaction.ts | 50 ++++++++++++++++++- .../transaction/getPayGoVerificationPubkey.ts | 18 +++++++ .../test/v2/unit/coins/abstractUtxoCoin.ts | 15 ++++++ .../coins/payGoPSBTHexFixture/psbtHexProof.ts | 6 +++ modules/utxo-lib/src/testutil/psbt.ts | 6 +++ 6 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 modules/abstract-utxo/src/transaction/getPayGoVerificationPubkey.ts create mode 100644 modules/bitgo/test/v2/unit/coins/payGoPSBTHexFixture/psbtHexProof.ts diff --git a/CODEOWNERS b/CODEOWNERS index f20b389edf..40fb8d5fb6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,8 @@ /modules/utxo-ord/ @BitGo/btc-team /modules/utxo-staking/ @BitGo/btc-team /modules/babylonlabs-io-btc-staking-ts @BitGo/btc-team +/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts @BitGo/btc-team +/modules/bitgo/test/v2/unit/coins/payGoPSBTHexFixture/psbtHexProof.ts @BitGo/btc-team # Lightning coin modules /modules/abstract-lightning/ @BitGo/btc-team diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts index 84d8cf2abc..0e46828825 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts @@ -1,9 +1,11 @@ import * as utxolib from '@bitgo/utxo-lib'; import { bip32, BIP32Interface, bitgo } from '@bitgo/utxo-lib'; import { Triple } from '@bitgo/sdk-core'; +import * as utxocore from '@bitgo/utxo-core'; import { Output, TransactionExplanation, FixedScriptWalletOutput } from '../../abstractUtxoCoin'; import { toExtendedAddressFormat } from '../recipient'; +import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey'; export type ChangeAddressInfo = { address: string; chain: number; index: number }; @@ -144,7 +146,8 @@ export function explainPsbt[] }; }, - network: utxolib.Network + network: utxolib.Network, + { strict = false }: { strict?: boolean } = {} ): TransactionExplanation { const txOutputs = psbt.txOutputs; @@ -191,6 +194,51 @@ export function explainPsbt; const common = explainCommon(tx, { ...params, changeInfo }, network); diff --git a/modules/abstract-utxo/src/transaction/getPayGoVerificationPubkey.ts b/modules/abstract-utxo/src/transaction/getPayGoVerificationPubkey.ts new file mode 100644 index 0000000000..6712093768 --- /dev/null +++ b/modules/abstract-utxo/src/transaction/getPayGoVerificationPubkey.ts @@ -0,0 +1,18 @@ +import * as utxolib from '@bitgo/utxo-lib'; + +const BITGOPAYGOATTESTATIONPUBKEY = + 'xpub6BKRgmCPX5oQiJgJ6Vq6BF8tDvZhwQki5dVVQohckK2ZJXtxj8K6M9pavLwt9piW33hZz17SWmG8QWsjJ1tHdde2Fs5UA3DFbApCtbdaGKn'; + +/** + * We want to return the verification pubkey from our statics that has our + * verification pubkey. + * @param network + * @returns + */ +export function getPayGoVerificationPubkey(network: utxolib.Network): string | undefined { + if (utxolib.isTestnet(network)) { + return BITGOPAYGOATTESTATIONPUBKEY; + } else if (utxolib.isMainnet(network)) { + return undefined; + } +} diff --git a/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts b/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts index 002d251cec..e2d977f43f 100644 --- a/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts +++ b/modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts @@ -4,6 +4,7 @@ import * as sinon from 'sinon'; import { Wallet, UnexpectedAddressError, VerificationOptions } from '@bitgo/sdk-core'; import { TestBitGo } from '@bitgo/sdk-test'; import { BitGo } from '../../../../src/bitgo'; +import { psbtTxHex } from './payGoPSBTHexFixture/psbtHexProof'; import { AbstractUtxoCoin, UtxoWallet, Output, TransactionExplanation, TransactionParams } from '@bitgo/abstract-utxo'; describe('Abstract UTXO Coin:', () => { @@ -583,4 +584,18 @@ describe('Abstract UTXO Coin:', () => { bitcoinMock.restore(); }); }); + + describe('Verify paygo output when explaining psbt transaction', function () { + const bitgo: BitGo = TestBitGo.decorate(BitGo, { env: 'mock' }); + let coin: AbstractUtxoCoin; + + beforeEach(() => { + coin = bitgo.coin('tbtc4') as AbstractUtxoCoin; + }); + + it('should detect and verify paygo address proof in PSBT', async function () { + // Call explainTransaction + await coin.explainTransaction(psbtTxHex); + }); + }); }); diff --git a/modules/bitgo/test/v2/unit/coins/payGoPSBTHexFixture/psbtHexProof.ts b/modules/bitgo/test/v2/unit/coins/payGoPSBTHexFixture/psbtHexProof.ts new file mode 100644 index 0000000000..fb584e04a9 --- /dev/null +++ b/modules/bitgo/test/v2/unit/coins/payGoPSBTHexFixture/psbtHexProof.ts @@ -0,0 +1,6 @@ +// We got this psbt tx hex by getting a test wallet id in test env on wp, +// called the build-transaction.js in examples/ with the wallet id +export const psbtTxHex = { + txHex: + '70736274ff0100b4010000000176e65d40a648da1a3b9412d5b49cc309a37a6a7f3c9cd8b8f1f41a2fbbfd479f0100000000fdffffff03204e0000000000002251206f6b69b20d569b8b14500e3e2e6ba10573d1ce4ddcbc2d0b056219fe27b3ed3beb0d1e00000000002251205f4f20e046cca9808a211ee6e217ca3f9b4023b56e5b5875bccebe414746a2a600127a000000000022002037f34405b83128e4cdecb64f44bff64d0d1b6188d2d656782b136e3488eab1f2000000004f010488b21e00000000000000000067b8325a2303718652d085e2bb65e4c84e8a51eaa4f4ea56e11542290bd27875037cb4cea965ae9d7846addc2a534318a377436864f90a38e2619ca51513cf7a7b0488f255714f010488b21e000000000000000000a5d5ff1d68596719baad5a209925b5edfdc7899ca460a071b3c84bd5bc76b99303f9dc3f6f974f2c90c6d0b7b7c50bc4de49bb8105bfa27223babc909720c6191a043d4b2b964f010488b21e000000000000000000dd32781f8dff65567d76ba1c9485014f7eb07d9527a502cbe0dafb64c20961cd028a55ce3f0c98964dfc6814b1b55b2b77cc31d33a5ae1a994476ce8b20b1618530435eabe8d0001012bd36e980000000000225120866e64ab95028afde6e42cd0dcbd6b38f8dec74d3f3a7648e11259f64075321b0103040000000021163024116333a45124af6141080383eea1b756c1983a07d71d50a2c62d774b7b64150088f25571000000000000000029000000000000002116e5a56ea96f692990ef584ebecaa55acf8d7d4e186db4c489e5498210cc2162df15003d4b2b96000000000000000029000000000000000117201263360542e6b356eaf5c4f03b4af4e0693e169ca7c44207209d3bcc2a3b8f620118208dca1a18f7db9002face5d00d8ebe62ecd0b557a6590b6a2f5686890926bdde548fc05424954474f01866e64ab95028afde6e42cd0dcbd6b38f8dec74d3f3a7648e11259f64075321b1263360542e6b356eaf5c4f03b4af4e0693e169ca7c44207209d3bcc2a3b8f6242033024116333a45124af6141080383eea1b756c1983a07d71d50a2c62d774b7b6402e5a56ea96f692990ef584ebecaa55acf8d7d4e186db4c489e5498210cc2162df0048fc05424954474f04576367473770677a4c556d7036587263684a7169483374326337654a5663546b573959503336586d703258573474344262424336657538504242676a66563934411fd7799767369cfdab6d24bea3a10f3fe18639aba182a1ce6d04242c2795802ff70b60a55f761144622565dcb30317018095af2fa9bf20ff71fd49f8a272ef433b00010520751e929f9b2701a7ae083395f3ccdc26a295bf4b06348d54a330a461ad713f2301068e01c04420165e9a0bcba3fe78e403086052286e24d1a96e04c942f23fd3b407086541d55aad20a7106400ab4a303e360251ac4c0d1ebf73d9f42df2411b383c9675a4899926c0ac01c04420a7106400ab4a303e360251ac4c0d1ebf73d9f42df2411b383c9675a4899926c0ad203abe647510bb3b08087f3025763d8d34b8bdc85d36a91ceaf6289a3466169ba3ac2107165e9a0bcba3fe78e403086052286e24d1a96e04c942f23fd3b407086541d55a3501905217dd2aa5707a9c5ee0476ae4cd0fa56c3a4c01cd832564e4f649a9dea8e888f255710000000000000000290000000f00000021073abe647510bb3b08087f3025763d8d34b8bdc85d36a91ceaf6289a3466169ba335019b06176d0562c9e8007d34dad4b159042cce1a1d7327c42185c050dcf4e6aa963d4b2b960000000000000000290000000f0000002107a7106400ab4a303e360251ac4c0d1ebf73d9f42df2411b383c9675a4899926c05502905217dd2aa5707a9c5ee0476ae4cd0fa56c3a4c01cd832564e4f649a9dea8e89b06176d0562c9e8007d34dad4b159042cce1a1d7327c42185c050dcf4e6aa9635eabe8d0000000000000000290000000f0000000000', +}; diff --git a/modules/utxo-lib/src/testutil/psbt.ts b/modules/utxo-lib/src/testutil/psbt.ts index 6d7316efd8..ea3a63a644 100644 --- a/modules/utxo-lib/src/testutil/psbt.ts +++ b/modules/utxo-lib/src/testutil/psbt.ts @@ -24,6 +24,7 @@ import { UtxoPsbt, UtxoTransaction, verifySignatureWithUnspent, + addXpubsToPsbt, } from '../bitgo'; import { Network } from '../networks'; import { mockReplayProtectionUnspent, mockWalletUnspent } from './mock'; @@ -167,6 +168,7 @@ export function constructPsbt( signers?: { signerName: KeyName; cosignerName?: KeyName }; deterministic?: boolean; skipNonWitnessUtxo?: boolean; + addGlobalXPubs?: boolean; } ): UtxoPsbt { const { signers, deterministic, skipNonWitnessUtxo } = params ?? {}; @@ -219,6 +221,10 @@ export function constructPsbt( signAllPsbtInputs(psbt, inputs, rootWalletKeys, sign, { signers, deterministic, skipNonWitnessUtxo }); } + if (params?.addGlobalXPubs) { + addXpubsToPsbt(psbt, rootWalletKeys); + } + return psbt; }