Skip to content

feat(abstract-utxo): verify paygo #6445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };

Expand Down Expand Up @@ -144,7 +146,8 @@ export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.Ut
pubs?: string[];
txInfo?: { unspents?: bitgo.Unspent<TNumber>[] };
},
network: utxolib.Network
network: utxolib.Network,
{ strict = false }: { strict?: boolean } = {}
): TransactionExplanation {
const txOutputs = psbt.txOutputs;

Expand Down Expand Up @@ -191,6 +194,51 @@ export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.Ut
throw e;
}
}

/**
* Extract PayGo address proof information from the PSBT if present
* @returns Information about the PayGo proof, including the output index and address
*/
function getPayGoVerificationInfo(): { outputIndex: number; verificationPubkey: string } | undefined {
let outputIndex: number | undefined = undefined;
let address: string | undefined = undefined;
// Check if this PSBT has any PayGo address proofs
if (!utxocore.paygo.psbtOutputIncludesPaygoAddressProof(psbt)) {
return undefined;
}

// This pulls the pubkey depending on given network
const verificationPubkey = getPayGoVerificationPubkey(network);
// find which output index that contains the PayGo proof
outputIndex = utxocore.paygo.getPayGoAddressProofOutputIndex(psbt);
if (outputIndex === undefined || !verificationPubkey) {
return undefined;
}
const output = txOutputs[outputIndex];
address = utxolib.address.fromOutputScript(output.script, network);
if (!address) {
throw new Error(`Can not derive address ${address} Pay Go Attestation.`);
}

return { outputIndex, verificationPubkey };
}

const payGoVerificationInfo = getPayGoVerificationInfo();
if (payGoVerificationInfo) {
try {
utxocore.paygo.verifyPayGoAddressProof(
psbt,
payGoVerificationInfo.outputIndex,
utxolib.bip32.fromBase58(payGoVerificationInfo.verificationPubkey, utxolib.networks.bitcoin).publicKey
);
} catch (e) {
if (strict) {
throw e;
}
console.error(e);
}
}

const changeInfo = getChangeInfo();
const tx = psbt.getUnsignedTx() as bitgo.UtxoTransaction<TNumber>;
const common = explainCommon(tx, { ...params, changeInfo }, network);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
15 changes: 15 additions & 0 deletions modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:', () => {
Expand Down Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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',
};
6 changes: 6 additions & 0 deletions modules/utxo-lib/src/testutil/psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
UtxoPsbt,
UtxoTransaction,
verifySignatureWithUnspent,
addXpubsToPsbt,
} from '../bitgo';
import { Network } from '../networks';
import { mockReplayProtectionUnspent, mockWalletUnspent } from './mock';
Expand Down Expand Up @@ -167,6 +168,7 @@ export function constructPsbt(
signers?: { signerName: KeyName; cosignerName?: KeyName };
deterministic?: boolean;
skipNonWitnessUtxo?: boolean;
addGlobalXPubs?: boolean;
}
): UtxoPsbt {
const { signers, deterministic, skipNonWitnessUtxo } = params ?? {};
Expand Down Expand Up @@ -219,6 +221,10 @@ export function constructPsbt(
signAllPsbtInputs(psbt, inputs, rootWalletKeys, sign, { signers, deterministic, skipNonWitnessUtxo });
}

if (params?.addGlobalXPubs) {
addXpubsToPsbt(psbt, rootWalletKeys);
}

return psbt;
}

Expand Down
Loading