diff --git a/modules/utxo-core/src/bip322/utils.ts b/modules/utxo-core/src/bip322/utils.ts index 6368811dce..e864ce93cc 100644 --- a/modules/utxo-core/src/bip322/utils.ts +++ b/modules/utxo-core/src/bip322/utils.ts @@ -33,3 +33,47 @@ export function getBip322ProofMessageAtIndex(psbt: utxolib.Psbt, inputIndex: num } return Buffer.from(proprietaryKeyVals[0].value); } + +/** + * checks if the transaction contains a BIP322 proof + * does not check if the proof is valid + * Source: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#user-content-Full + * @params tx The transaction or the PSBT to check + * @returns boolean + */ +export function isBip322ProofCheck(tx: utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction): boolean { + if (tx instanceof utxolib.bitgo.UtxoPsbt) { + if (tx.version !== 0 || tx.locktime !== 0 || tx.data.outputs.length !== 1) { + return false; + } + const output = tx.txOutputs[0]; + if (output.script.toString('hex') !== '6a' || output.value !== 0n) { + return false; + } + + // Return true if there is a message encoded in every input in the proprietary field + return tx.data.inputs.every((input, index) => getBip322ProofMessageAtIndex(tx, index) !== undefined); + } else if (tx instanceof utxolib.bitgo.UtxoTransaction) { + if (tx.version !== 0 || tx.locktime !== 0 || tx.outs.length !== 1) { + return false; + } + if (tx.outs.length !== 1) { + return false; + } + const output = tx.outs[0]; + // check that the only output is an OP_RETURN with 0 value + if (output.script.toString('hex') !== '6a' || output.value !== 0n) { + return false; + } + + for (const input of tx.ins) { + if (input.index !== 0 || input.sequence !== 0) { + return false; + } + } + + return true; + } else { + throw new Error('Unsupported transaction type for BIP322 proof check'); + } +} diff --git a/modules/utxo-core/test/bip322/utils.ts b/modules/utxo-core/test/bip322/utils.ts index 1f464e3a16..e9b2e5ba9e 100644 --- a/modules/utxo-core/test/bip322/utils.ts +++ b/modules/utxo-core/test/bip322/utils.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import * as utxolib from '@bitgo/utxo-lib'; -import { getBip322ProofMessageAtIndex } from '../../src/bip322'; +import { getBip322ProofMessageAtIndex, isBip322ProofCheck } from '../../src/bip322'; import { BIP322_FIXTURE_HELLO_WORLD_TOSIGN_PSBT } from './bip322.utils'; @@ -35,4 +35,50 @@ describe('BIP322 Proof utils', function () { assert.ok(messageBuffer, 'Message buffer should not be undefined'); assert.deepStrictEqual(messageBuffer.toString('utf-8'), 'Hello World', 'Message does not match expected value'); }); + + describe('isBip322ProofCheck', function () { + it('should work for PSBTs', function () { + const psbt = utxolib.bitgo.createPsbtFromBuffer( + BIP322_FIXTURE_HELLO_WORLD_TOSIGN_PSBT.toBuffer(), + utxolib.networks.bitcoin + ); + assert.ok(isBip322ProofCheck(psbt), 'Expected PSBT to be a valid BIP322 proof'); + assert.deepEqual( + isBip322ProofCheck( + utxolib.testutil.constructPsbt( + [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }], + [{ scriptType: 'p2sh', value: BigInt(900) }], + utxolib.networks.bitcoin, + utxolib.testutil.getDefaultWalletKeys(), + 'unsigned' + ) + ), + false + ); + }); + + it('should work for Transactions', function () { + const psbt = utxolib.bitgo.createPsbtFromBuffer( + BIP322_FIXTURE_HELLO_WORLD_TOSIGN_PSBT.toBuffer(), + utxolib.networks.bitcoin + ); + // Cannot extract the transaction because it has no signatures + const tx = psbt.getUnsignedTx(); + assert.ok(isBip322ProofCheck(tx), 'Expected Transaction to be a valid BIP322 proof'); + assert.deepEqual( + isBip322ProofCheck( + utxolib.testutil + .constructPsbt( + [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }], + [{ scriptType: 'p2sh', value: BigInt(900) }], + utxolib.networks.bitcoin, + utxolib.testutil.getDefaultWalletKeys(), + 'unsigned' + ) + .getUnsignedTx() + ), + false + ); + }); + }); });