Skip to content

Commit cf87147

Browse files
Merge pull request #6751 from BitGo/BTC-2390.bip322-explain-transaction
feat(abstract-utxo): add BIP322 message extraction to explainTransaction
2 parents d075e9f + f23f65f commit cf87147

File tree

4 files changed

+194
-14
lines changed

4 files changed

+194
-14
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ export interface FixedScriptWalletOutput<TAmount = string | number> extends Base
143143

144144
export type Output<TAmount = string | number> = BaseOutput<TAmount> | FixedScriptWalletOutput<TAmount>;
145145

146+
export type Bip322Message = {
147+
address: string;
148+
message: string;
149+
};
150+
146151
export function isWalletOutput(output: Output): output is FixedScriptWalletOutput {
147152
return (
148153
(output as FixedScriptWalletOutput).chain !== undefined && (output as FixedScriptWalletOutput).index !== undefined
@@ -164,6 +169,12 @@ export interface TransactionExplanation extends BaseTransactionExplanation<strin
164169
* Highest input signature count for the transaction
165170
*/
166171
signatures: number;
172+
173+
/**
174+
* BIP322 messages extracted from the transaction inputs.
175+
* These messages are used for verifying the transaction against the BIP322 standard.
176+
*/
177+
messages?: Bip322Message[];
167178
}
168179

169180
export interface TransactionInfo<TNumber extends number | bigint = number> {

modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2+
import { bip322 } from '@bitgo/utxo-core';
23
import { bip32, BIP32Interface, bitgo } from '@bitgo/utxo-lib';
34
import { Triple } from '@bitgo/sdk-core';
45
import * as utxocore from '@bitgo/utxo-core';
56

6-
import { Output, TransactionExplanation, FixedScriptWalletOutput } from '../../abstractUtxoCoin';
7+
import { Output, TransactionExplanation, Bip322Message, FixedScriptWalletOutput } from '../../abstractUtxoCoin';
78
import { toExtendedAddressFormat } from '../recipient';
89
import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey';
910

10-
export type ChangeAddressInfo = { address: string; chain: number; index: number };
11+
export type ChangeAddressInfo = {
12+
address: string;
13+
chain: number;
14+
index: number;
15+
};
1116

1217
function explainCommon<TNumber extends number | bigint>(
1318
tx: bitgo.UtxoTransaction<TNumber>,
@@ -150,6 +155,7 @@ export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.Ut
150155
{ strict = false }: { strict?: boolean } = {}
151156
): TransactionExplanation {
152157
const txOutputs = psbt.txOutputs;
158+
const txInputs = psbt.txInputs;
153159

154160
function getChainAndIndexFromBip32Derivations(output: bitgo.PsbtOutput) {
155161
const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined;
@@ -223,6 +229,73 @@ export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.Ut
223229
return { outputIndex, verificationPubkey };
224230
}
225231

232+
/**
233+
* Extract the BIP322 messages and addresses from the PSBT inputs and perform
234+
* verification on the transaction to ensure that it meets the BIP322 requirements.
235+
* @returns An array of objects containing the message and address for each input,
236+
* or undefined if no BIP322 messages are found.
237+
*/
238+
function getBip322MessageInfoAndVerify(): Bip322Message[] | undefined {
239+
const bip322Messages: { message: string; address: string }[] = [];
240+
for (let i = 0; i < psbt.data.inputs.length; i++) {
241+
const message = bip322.getBip322ProofMessageAtIndex(psbt, i);
242+
if (message) {
243+
const input = psbt.data.inputs[i];
244+
if (!input.witnessUtxo) {
245+
throw new Error(`Missing witnessUtxo for input index ${i}`);
246+
}
247+
if (!input.nonWitnessUtxo) {
248+
throw new Error(`Missing nonWitnessUtxo for input index ${i}`);
249+
}
250+
const scriptPubKey = input.witnessUtxo.script;
251+
252+
// Verify that the toSpend transaction can be recreated in the PSBT and is encoded correctly in the nonWitnessUtxo
253+
const toSpend = bip322.buildToSpendTransaction(scriptPubKey, message);
254+
const toSpendB64 = toSpend.toBuffer().toString('base64');
255+
if (input.nonWitnessUtxo.toString('base64') !== toSpendB64) {
256+
throw new Error(`Non-witness UTXO does not match the expected toSpend transaction at input index ${i}`);
257+
}
258+
259+
// Verify that the toSpend transaction ID matches the input's referenced transaction ID
260+
if (toSpend.getId() !== utxolib.bitgo.getOutputIdForInput(txInputs[i]).txid) {
261+
throw new Error(`ToSpend transaction ID does not match the input at index ${i}`);
262+
}
263+
264+
// Verify the input specifics
265+
if (txInputs[i].sequence !== 0) {
266+
throw new Error(`Unexpected sequence number at input index ${i}: ${txInputs[i].sequence}. Expected 0.`);
267+
}
268+
if (txInputs[i].index !== 0) {
269+
throw new Error(`Unexpected input index at position ${i}: ${txInputs[i].index}. Expected 0.`);
270+
}
271+
272+
bip322Messages.push({
273+
message: message.toString('utf8'),
274+
address: utxolib.address.fromOutputScript(scriptPubKey, network),
275+
});
276+
}
277+
}
278+
279+
if (bip322Messages.length > 0) {
280+
// If there is a BIP322 message in any input, all inputs must have one.
281+
if (bip322Messages.length !== psbt.data.inputs.length) {
282+
throw new Error('Inconsistent BIP322 messages across inputs.');
283+
}
284+
285+
// Verify the transaction specifics for BIP322
286+
if (psbt.version !== 0 && psbt.version !== 2) {
287+
throw new Error(`Unsupported PSBT version for BIP322: ${psbt.version}. Expected 0 `);
288+
}
289+
if (psbt.data.outputs.length !== 1 || txOutputs[0].script.toString('hex') !== '6a' || txOutputs[0].value !== 0n) {
290+
throw new Error(`Invalid PSBT outputs for BIP322. Expected exactly one OP_RETURN output with zero value.`);
291+
}
292+
293+
return bip322Messages;
294+
}
295+
296+
return undefined;
297+
}
298+
226299
const payGoVerificationInfo = getPayGoVerificationInfo();
227300
if (payGoVerificationInfo) {
228301
try {
@@ -239,6 +312,7 @@ export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.Ut
239312
}
240313
}
241314

315+
const messages = getBip322MessageInfoAndVerify();
242316
const changeInfo = getChangeInfo();
243317
const tx = psbt.getUnsignedTx() as bitgo.UtxoTransaction<TNumber>;
244318
const common = explainCommon(tx, { ...params, changeInfo }, network);
@@ -263,6 +337,7 @@ export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.Ut
263337
fee: (inputAmount - outputAmount).toString(),
264338
inputSignatures: inputSignaturesCount,
265339
signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0),
340+
messages,
266341
} as TransactionExplanation;
267342
}
268343

0 commit comments

Comments
 (0)