11import * as utxolib from '@bitgo/utxo-lib' ;
2+ import { bip322 } from '@bitgo/utxo-core' ;
23import { bip32 , BIP32Interface , bitgo } from '@bitgo/utxo-lib' ;
34import { Triple } from '@bitgo/sdk-core' ;
45import * as utxocore from '@bitgo/utxo-core' ;
56
6- import { Output , TransactionExplanation , FixedScriptWalletOutput } from '../../abstractUtxoCoin' ;
7+ import { Output , TransactionExplanation , Bip322Message , FixedScriptWalletOutput } from '../../abstractUtxoCoin' ;
78import { toExtendedAddressFormat } from '../recipient' ;
89import { 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
1217function 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