From f71aa51edf0d90038f894a57f62fdeb44d2fbdb5 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Wed, 3 Sep 2025 11:19:48 -0400 Subject: [PATCH 1/2] feat(utxo-core): add BIP322 signature verification Add functions to verify BIP322 message signatures. This includes verification of both fully signed transactions and PSBTs. The verification checks that: 1. The transaction follows BIP322 format requirements 2. Each input correctly references the expected message 3. Signatures are valid for the corresponding public keys BTC-2375 Co-authored-by: llm-git TICKET: BTC-2375 --- modules/utxo-core/src/bip322/index.ts | 1 + modules/utxo-core/src/bip322/toSign.ts | 7 +- modules/utxo-core/src/bip322/verify.ts | 140 +++++++++ modules/utxo-core/test/bip322/verify.ts | 392 ++++++++++++++++++++++++ 4 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 modules/utxo-core/src/bip322/verify.ts create mode 100644 modules/utxo-core/test/bip322/verify.ts diff --git a/modules/utxo-core/src/bip322/index.ts b/modules/utxo-core/src/bip322/index.ts index 2f0b915bb3..afa5675751 100644 --- a/modules/utxo-core/src/bip322/index.ts +++ b/modules/utxo-core/src/bip322/index.ts @@ -1,3 +1,4 @@ export * from './toSpend'; export * from './toSign'; export * from './utils'; +export * from './verify'; diff --git a/modules/utxo-core/src/bip322/toSign.ts b/modules/utxo-core/src/bip322/toSign.ts index 16d000d4a7..97a7bbe580 100644 --- a/modules/utxo-core/src/bip322/toSign.ts +++ b/modules/utxo-core/src/bip322/toSign.ts @@ -15,9 +15,12 @@ export const MAX_NUM_BIP322_INPUTS = 200; * Create the base PSBT for the to_sign transaction for BIP322 signing. * There will be ever 1 output. */ -export function createBaseToSignPsbt(rootWalletKeys?: bitgo.RootWalletKeys): bitgo.UtxoPsbt { +export function createBaseToSignPsbt( + rootWalletKeys?: bitgo.RootWalletKeys, + network = networks.bitcoin +): bitgo.UtxoPsbt { // Create PSBT object for constructing the transaction - const psbt = bitgo.createPsbtForNetwork({ network: networks.bitcoin }); + const psbt = bitgo.createPsbtForNetwork({ network }); // Set default value for nVersion and nLockTime psbt.setVersion(0); // nVersion = 0 psbt.setLocktime(0); // nLockTime = 0 diff --git a/modules/utxo-core/src/bip322/verify.ts b/modules/utxo-core/src/bip322/verify.ts new file mode 100644 index 0000000000..94b5123370 --- /dev/null +++ b/modules/utxo-core/src/bip322/verify.ts @@ -0,0 +1,140 @@ +import * as assert from 'assert'; + +import * as utxolib from '@bitgo/utxo-lib'; + +import { buildToSpendTransaction } from './toSpend'; + +export type MessageInfo = { + address: string; + message: string; + // Hex encoded pubkeys + pubkeys: string[]; + scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3; +}; + +export function assertBaseTx(tx: utxolib.bitgo.UtxoTransaction): void { + assert.deepStrictEqual(tx.version, 0, 'Transaction version must be 0.'); + assert.deepStrictEqual(tx.locktime, 0, 'Transaction locktime must be 0.'); + assert.deepStrictEqual(tx.outs.length, 1, 'Transaction must have exactly 1 output.'); + assert.deepStrictEqual(tx.outs[0].value, BigInt(0), 'Transaction output value must be 0.'); + assert.deepStrictEqual(tx.outs[0].script.toString('hex'), '6a', 'Transaction output script must be OP_RETURN.'); +} + +export function assertTxInput( + tx: utxolib.bitgo.UtxoTransaction, + inputIndex: number, + prevOuts: utxolib.TxOutput[], + info: MessageInfo, + checkSignature: boolean +): void { + assert.ok( + inputIndex < tx.ins.length, + `inputIndex ${inputIndex} is out of range for tx with ${tx.ins.length} inputs.` + ); + const input = tx.ins[inputIndex]; + assert.deepStrictEqual(input.index, 0, `transaction input ${inputIndex} must have index=0.`); + assert.deepStrictEqual(input.sequence, 0, `transaction input ${inputIndex} sequence must be 0.`); + + // Make sure that the message is correctly encoded into the input of the transaction and + // verify that the message info corresponds + const scriptPubKey = utxolib.bitgo.outputScripts.createOutputScript2of3( + info.pubkeys.map((pubkey) => Buffer.from(pubkey, 'hex')), + info.scriptType, + tx.network + ).scriptPubKey; + assert.deepStrictEqual( + info.address, + utxolib.address.fromOutputScript(scriptPubKey, tx.network).toString(), + `Address does not match derived scriptPubKey for input ${inputIndex}.` + ); + + const txid = utxolib.bitgo.getOutputIdForInput(input).txid; + const toSpendTx = buildToSpendTransaction(scriptPubKey, info.message); + assert.deepStrictEqual( + txid, + toSpendTx.getId(), + `Input ${inputIndex} derived to_spend transaction is not encoded in the input.` + ); + + if (checkSignature) { + const signatureScript = utxolib.bitgo.parseSignatureScript2Of3(input); + const scriptType = + signatureScript.scriptType === 'taprootKeyPathSpend' + ? 'p2trMusig2' + : signatureScript.scriptType === 'taprootScriptPathSpend' + ? 'p2tr' + : signatureScript.scriptType; + assert.deepStrictEqual(scriptType, info.scriptType, 'Script type does not match.'); + utxolib.bitgo.verifySignatureWithPublicKeys( + tx, + inputIndex, + prevOuts, + info.pubkeys.map((pubkey) => Buffer.from(pubkey, 'hex')) + ); + } +} + +export function assertBip322TxProof(tx: utxolib.bitgo.UtxoTransaction, messageInfo: MessageInfo[]): void { + assertBaseTx(tx); + assert.deepStrictEqual( + tx.ins.length, + messageInfo.length, + 'Transaction must have the same number of inputs as messageInfo entries.' + ); + const prevOuts = messageInfo.map((info) => { + return { + value: 0n, + script: utxolib.bitgo.outputScripts.createOutputScript2of3( + info.pubkeys.map((pubkey) => Buffer.from(pubkey, 'hex')), + info.scriptType, + tx.network + ).scriptPubKey, + }; + }); + tx.ins.forEach((input, inputIndex) => assertTxInput(tx, inputIndex, prevOuts, messageInfo[inputIndex], true)); +} + +export function assertBip322PsbtProof(psbt: utxolib.bitgo.UtxoPsbt, messageInfo: MessageInfo[]): void { + const unsignedTx = psbt.getUnsignedTx(); + + assertBaseTx(unsignedTx); + assert.deepStrictEqual( + psbt.data.inputs.length, + messageInfo.length, + 'PSBT must have the same number of inputs as messageInfo entries.' + ); + + const prevOuts = psbt.data.inputs.map((input, inputIndex) => { + assert.ok(input.witnessUtxo, `PSBT input ${inputIndex} is missing witnessUtxo`); + return input.witnessUtxo; + }); + + psbt.data.inputs.forEach((input, inputIndex) => { + // Check that the metadata in the PSBT matches the messageInfo, then check the input data + const info = messageInfo[inputIndex]; + + // Check that the to_spend transaction is encoded in the nonWitnessUtxo + assert.ok(input.nonWitnessUtxo, `PSBT input ${inputIndex} is missing nonWitnessUtxo`); + const toSpendTx = buildToSpendTransaction(prevOuts[inputIndex].script, info.message); + assert.deepStrictEqual(input.nonWitnessUtxo.toString('hex'), toSpendTx.toHex()); + + if (input.bip32Derivation) { + input.bip32Derivation.forEach((b) => { + const pubkey = b.pubkey.toString('hex'); + assert.ok( + info.pubkeys.includes(pubkey), + `PSBT input ${inputIndex} has a pubkey in (tap)bip32Derivation that is not in messageInfo` + ); + }); + } else if (!input.tapBip32Derivation) { + throw new Error(`PSBT input ${inputIndex} is missing (tap)bip32Derivation when it should have it.`); + } + + // Verify the signature on the input + assert.ok(psbt.validateSignaturesOfInputCommon(inputIndex), `PSBT input ${inputIndex} has an invalid signature.`); + + // Do not check the signature when using the PSBT, the signature is not there. We are going + // to signatures in the PSBT. + assertTxInput(unsignedTx, inputIndex, prevOuts, info, false); + }); +} diff --git a/modules/utxo-core/test/bip322/verify.ts b/modules/utxo-core/test/bip322/verify.ts new file mode 100644 index 0000000000..35eaf28daf --- /dev/null +++ b/modules/utxo-core/test/bip322/verify.ts @@ -0,0 +1,392 @@ +import * as assert from 'assert'; + +import * as utxolib from '@bitgo/utxo-lib'; + +import * as bip322 from '../../src/bip322'; + +import * as testutils from './bip322.utils'; + +const opReturnOutput = { value: 0n, script: Buffer.from('6a', 'hex') }; + +describe('Verify BIP322 proofs', function () { + describe('assertBaseTx', function () { + it('should pass for a valid bip322 transaction', function () { + assert.doesNotThrow(() => bip322.assertBaseTx(testutils.BIP322_FIXTURE_HELLO_WORLD_TOSIGN_PSBT.getUnsignedTx())); + }); + + it('should reject if the outputs are not conformed correctly', function () { + let psbt = utxolib.bitgo.createPsbtForNetwork({ + network: utxolib.networks.bitcoin, + }); + psbt.addOutput(opReturnOutput); + psbt.addOutput({ value: 1n, script: Buffer.from('6a', 'hex') }); + psbt.setVersion(0); + psbt.setLocktime(0); + assert.throws(() => bip322.assertBaseTx(psbt.getUnsignedTx()), /Transaction must have exactly 1 output./); + + psbt = utxolib.bitgo.createPsbtForNetwork({ + network: utxolib.networks.bitcoin, + }); + psbt.setVersion(0); + psbt.setLocktime(0); + psbt.addOutput({ value: 1n, script: Buffer.from('6a', 'hex') }); + assert.throws(() => bip322.assertBaseTx(psbt.getUnsignedTx()), /Transaction output value must be 0./); + + psbt = utxolib.bitgo.createPsbtForNetwork({ + network: utxolib.networks.bitcoin, + }); + psbt.setVersion(0); + psbt.setLocktime(0); + psbt.addOutput({ + value: 0n, + script: utxolib.bitgo.outputScripts.createOutputScript2of3( + utxolib.testutil.getDefaultWalletKeys().deriveForChainAndIndex(0, 0).publicKeys, + 'p2wsh', + utxolib.networks.bitcoin + ).scriptPubKey, + }); + assert.throws(() => bip322.assertBaseTx(psbt.getUnsignedTx()), /Transaction output script must be OP_RETURN./); + }); + + it('should reject if version is not 0', function () { + const psbt = utxolib.bitgo.createPsbtForNetwork({ + network: utxolib.networks.bitcoin, + }); + psbt.addOutput({ value: 1n, script: Buffer.from('6a', 'hex') }); + psbt.setVersion(1); + assert.throws(() => bip322.assertBaseTx(psbt.getUnsignedTx()), /Transaction version must be 0./); + }); + + it('should reject if locktime is not 0', function () { + const psbt = utxolib.bitgo.createPsbtForNetwork({ + network: utxolib.networks.bitcoin, + }); + psbt.setVersion(0); + psbt.addOutput({ value: 1n, script: Buffer.from('6a', 'hex') }); + psbt.setLocktime(1); + assert.throws(() => bip322.assertBaseTx(psbt.getUnsignedTx()), /Transaction locktime must be 0./); + }); + }); + + describe('assertTxInput', function () { + it('should fail if input index is out of range', function () { + const tx = utxolib.bitgo.createPsbtForNetwork({ network: utxolib.networks.bitcoin }).getUnsignedTx(); + assert.throws( + () => bip322.assertTxInput(tx, 1, [], { address: '', message: '', pubkeys: [], scriptType: 'p2wsh' }, false), + /inputIndex 1 is out of range for tx with 0 inputs./ + ); + }); + + it('should fail if txInput index is not 0', function () { + const psbt = utxolib.bitgo.createPsbtForNetwork({ + network: utxolib.networks.bitcoin, + }); + psbt.setVersion(0); + psbt.setLocktime(0); + psbt.addOutput(opReturnOutput); + psbt.addInput({ + index: 1, + hash: Buffer.alloc(32), + }); + assert.throws( + () => + bip322.assertTxInput( + psbt.getUnsignedTx(), + 0, + [], + { address: '', message: '', pubkeys: [], scriptType: 'p2wsh' }, + false + ), + /transaction input 0 must have index=0./ + ); + }); + + it('should fail if txInput sequence is not 0', function () { + const psbt = utxolib.bitgo.createPsbtForNetwork({ + network: utxolib.networks.bitcoin, + }); + psbt.setVersion(0); + psbt.setLocktime(0); + psbt.addOutput(opReturnOutput); + psbt.addInput({ + index: 0, + hash: Buffer.alloc(32), + sequence: 1, + }); + assert.throws( + () => + bip322.assertTxInput( + psbt.getUnsignedTx(), + 0, + [], + { address: '', message: '', pubkeys: [], scriptType: 'p2wsh' }, + false + ), + /transaction input 0 sequence must be 0./ + ); + }); + + it('should fail if the scriptPubKey created by the public keys does not match the address provided in the messageInfo', function () { + const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = bip322.createBaseToSignPsbt(rootWalletKeys); + const message = 'to be or not to be'; + bip322.addBip322InputWithChainAndIndex(psbt, message, rootWalletKeys, { + chain: 20, + index: 0, + }); + assert.ok(psbt.data.inputs[0]?.witnessUtxo?.script); + const address = utxolib.address.fromOutputScript( + psbt.data.inputs[0].witnessUtxo.script, + utxolib.networks.bitcoin + ); + const wrongPublicKeys = rootWalletKeys.deriveForChainAndIndex(20, 1).publicKeys.map((p) => p.toString('hex')); + assert.throws( + () => + bip322.assertTxInput( + psbt.getUnsignedTx(), + 0, + [], + { address, message, pubkeys: wrongPublicKeys, scriptType: 'p2wsh' }, + false + ), + /Address does not match derived scriptPubKey for input 0./ + ); + }); + + it('should fail if the txid of the input does not match the derived to_spend transaction', function () { + const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = bip322.createBaseToSignPsbt(rootWalletKeys); + const message = 'to be or not to be'; + + const output = utxolib.bitgo.outputScripts.createOutputScript2of3( + rootWalletKeys.deriveForChainAndIndex(20, 0).publicKeys, + 'p2wsh', + psbt.network + ); + bip322.addBip322Input(psbt, message, output); + assert.ok(psbt.data.inputs[0]?.witnessUtxo?.script); + const address = utxolib.address.fromOutputScript(output.scriptPubKey, utxolib.networks.bitcoin); + const pubkeys = rootWalletKeys.deriveForChainAndIndex(20, 0).publicKeys.map((p) => p.toString('hex')); + assert.throws(() => + bip322.assertTxInput( + psbt.getUnsignedTx(), + 0, + [], + { + address, + message: 'that is the question', + pubkeys, + scriptType: 'p2wsh', + }, + false + ) + ); + }); + + describe('checkSignature=true', function () { + it('should fail if the scriptType is not a 2of3 script', function () { + const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = bip322.createBaseToSignPsbt(rootWalletKeys); + const message = 'to be or not to be'; + const derivedWalletKeys = rootWalletKeys.deriveForChainAndIndex(20, 0).publicKeys; + + const output = utxolib.payments.p2pkh({ + pubkey: rootWalletKeys.publicKeys[0], + network: psbt.network, + }).output; + assert.ok(output); + bip322.addBip322Input(psbt, message, { + scriptPubKey: Buffer.from(output), + }); + psbt.signAllInputs(rootWalletKeys.user); + assert.throws(() => + bip322.assertTxInput( + psbt.getUnsignedTx(), + 0, + [], + { + // Make the messageInfo self consistent, but not match the non-2of3 address in the PSBT + address: utxolib.address.fromOutputScript( + utxolib.bitgo.outputScripts.createOutputScript2of3(derivedWalletKeys, 'p2wsh', psbt.network) + .scriptPubKey, + psbt.network + ), + message, + pubkeys: derivedWalletKeys.map((k) => k.toString('hex')), + scriptType: 'p2wsh', + }, + false + ) + ); + }); + + utxolib.bitgo.outputScripts.scriptTypes2Of3.forEach((scriptType) => { + describe(scriptType + ' address', function () { + it('should pass with a full signed tx', function () { + const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = bip322.createBaseToSignPsbt(rootWalletKeys); + const message = 'to be or not to be'; + const chainCode = utxolib.bitgo.getExternalChainCode(scriptType); + const derivedWalletKeys = rootWalletKeys.deriveForChainAndIndex(chainCode, 0).publicKeys; + const address = utxolib.address.fromOutputScript( + utxolib.bitgo.outputScripts.createOutputScript2of3(derivedWalletKeys, scriptType, psbt.network) + .scriptPubKey, + psbt.network + ); + + bip322.addBip322InputWithChainAndIndex(psbt, message, rootWalletKeys, { chain: chainCode, index: 0 }); + if (scriptType === 'p2trMusig2') { + psbt.setAllInputsMusig2NonceHD(rootWalletKeys.user); + psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo); + } + psbt.signAllInputsHD(rootWalletKeys.user); + psbt.signAllInputsHD(rootWalletKeys.bitgo); + + psbt.validateSignaturesOfAllInputs(); + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + + assert.doesNotThrow(() => + bip322.assertTxInput( + tx, + 0, + [ + { + value: 0n, + script: utxolib.address.toOutputScript(address, psbt.network), + }, + ], + { + // Make the messageInfo self consistent, but not match the non-2of3 address in the PSBT + address, + message, + pubkeys: derivedWalletKeys.map((k) => k.toString('hex')), + scriptType, + }, + true + ) + ); + }); + }); + }); + }); + + describe('checkSignature=false', function () { + it('should not throw if the input is not signed', function () { + const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = bip322.createBaseToSignPsbt(rootWalletKeys); + const message = 'to be or not to be'; + + const output = utxolib.bitgo.outputScripts.createOutputScript2of3( + rootWalletKeys.deriveForChainAndIndex(20, 0).publicKeys, + 'p2wsh', + psbt.network + ); + bip322.addBip322Input(psbt, message, output); + const address = utxolib.address.fromOutputScript(output.scriptPubKey, utxolib.networks.bitcoin); + assert.doesNotThrow(() => + bip322.assertTxInput( + psbt.getUnsignedTx(), + 0, + [], + { + address, + message, + pubkeys: rootWalletKeys.deriveForChainAndIndex(20, 0).publicKeys.map((p) => p.toString('hex')), + scriptType: 'p2wsh', + }, + false + ) + ); + }); + }); + }); + + describe('assertBip322TxProof', function () { + const messageInfo: bip322.MessageInfo[] = []; + let tx: utxolib.bitgo.UtxoTransaction; + before(function () { + const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = bip322.createBaseToSignPsbt(rootWalletKeys); + utxolib.bitgo.outputScripts.scriptTypes2Of3.forEach((scriptType, index) => { + const chain = utxolib.bitgo.getExternalChainCode(scriptType); + const message = `message for ${scriptType}`; + const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chain, index).publicKeys; + messageInfo.push({ + address: utxolib.address.fromOutputScript( + utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys, scriptType, psbt.network).scriptPubKey, + psbt.network + ), + message, + pubkeys: derivedKeys.map((p) => p.toString('hex')), + scriptType, + }); + + bip322.addBip322InputWithChainAndIndex(psbt, message, rootWalletKeys, { chain, index }); + }); + + psbt.setAllInputsMusig2NonceHD(rootWalletKeys.user); + psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo); + psbt.signAllInputsHD(rootWalletKeys.user); + psbt.signAllInputsHD(rootWalletKeys.bitgo); + assert.ok(psbt.validateSignaturesOfAllInputs(), `All signatures on the inputs should be valid`); + psbt.finalizeAllInputs(); + tx = psbt.extractTransaction(); + }); + it('should pass if the messageInfo matches the transaction', function () { + assert.doesNotThrow(() => bip322.assertBip322TxProof(tx, messageInfo)); + }); + + it('should fail if the messageInfo does not match the transaction', function () { + assert.throws(() => + bip322.assertBip322TxProof( + tx, + messageInfo.map((m) => ({ ...m, message: m.message + ' altered' })) + ) + ); + }); + }); + + describe('assertBip322PsbtProof', function () { + const messageInfo: bip322.MessageInfo[] = []; + const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = bip322.createBaseToSignPsbt(rootWalletKeys); + before(function () { + utxolib.bitgo.outputScripts.scriptTypes2Of3.forEach((scriptType, index) => { + const chain = utxolib.bitgo.getExternalChainCode(scriptType); + const message = `message for ${scriptType}`; + const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chain, index).publicKeys; + messageInfo.push({ + address: utxolib.address.fromOutputScript( + utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys, scriptType, psbt.network).scriptPubKey, + psbt.network + ), + message, + pubkeys: derivedKeys.map((p) => p.toString('hex')), + scriptType, + }); + + bip322.addBip322InputWithChainAndIndex(psbt, message, rootWalletKeys, { chain, index }); + }); + + psbt.setAllInputsMusig2NonceHD(rootWalletKeys.user); + psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo); + psbt.signAllInputsHD(rootWalletKeys.user); + psbt.signAllInputsHD(rootWalletKeys.bitgo); + assert.ok(psbt.validateSignaturesOfAllInputs(), `All signatures on the inputs should be valid`); + }); + + it('should work when the message info matches what is in the PSBT', function () { + assert.doesNotThrow(() => bip322.assertBip322PsbtProof(psbt, messageInfo)); + }); + + it('should fail when the message info does not match what is in the PSBT', function () { + assert.throws(() => + bip322.assertBip322PsbtProof( + psbt, + messageInfo.map((m) => ({ ...m, message: m.message + ' altered' })) + ) + ); + }); + }); +}); From 531a83c16d9b187506068d57a27bab1bc5b3b865 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Wed, 3 Sep 2025 11:36:08 -0400 Subject: [PATCH 2/2] feat(abstract-utxo): add BIP322 message verification functionality Add functions to verify BIP322 message signatures in transaction proofs. Implement support for both PSBT and regular transaction formats, with verification against provided message information. Co-authored-by: llm-git TICKET: BTC-2375 --- .../abstract-utxo/src/transaction/bip322.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/modules/abstract-utxo/src/transaction/bip322.ts b/modules/abstract-utxo/src/transaction/bip322.ts index de96e2a2ba..faae500ce6 100644 --- a/modules/abstract-utxo/src/transaction/bip322.ts +++ b/modules/abstract-utxo/src/transaction/bip322.ts @@ -1,4 +1,6 @@ import { decodeOrElse } from '@bitgo/sdk-core'; +import { bip322 } from '@bitgo/utxo-core'; +import { bitgo, networks, Network } from '@bitgo/utxo-lib'; import * as t from 'io-ts'; const BIP322MessageInfo = t.type({ @@ -33,3 +35,63 @@ export function deserializeBIP322BroadcastableMessage(hex: string): BIP322Messag throw new Error(`Failed to decode ${BIP322MessageBroadcastable.name}: ${error}`); }); } + +export function verifyTransactionFromBroadcastableMessage( + message: BIP322MessageBroadcastable, + coinName: string +): boolean { + let network: Network = networks.bitcoin; + if (coinName === 'tbtc4') { + network = networks.bitcoinTestnet4; + } else if (coinName !== 'btc') { + throw new Error('Only tbtc4 or btc coinNames are supported.'); + } + if (bitgo.isPsbt(message.txHex)) { + const psbt = bitgo.createPsbtFromBuffer(Buffer.from(message.txHex, 'hex'), network); + try { + bip322.assertBip322PsbtProof(psbt, message.messageInfo); + return true; + } catch (error) { + return false; + } + } else { + const tx = bitgo.createTransactionFromBuffer(Buffer.from(message.txHex, 'hex'), network, { amountType: 'bigint' }); + try { + bip322.assertBip322TxProof(tx, message.messageInfo); + return true; + } catch (error) { + return false; + } + } +} + +export function generateBIP322MessageListAndVerifyFromMessageBroadcastable( + messageBroadcastables: BIP322MessageBroadcastable[], + coinName: string +): { address: string; message: string }[] { + // Map from the address to the message. If there are duplicates of the address, make sure that the + // message is the same. If there are duplicate addresses and the messages are not the same, throw an error. + const addressMap = new Map(); + + messageBroadcastables.forEach((message, index) => { + if (verifyTransactionFromBroadcastableMessage(message, coinName)) { + message.messageInfo.forEach((info) => { + const { address, message: msg } = info; + if (addressMap.has(address)) { + if (addressMap.get(address) !== msg) { + throw new Error(`Duplicate address ${address} has different messages`); + } + } else { + addressMap.set(address, msg); + } + }); + } else { + throw new Error(`Message Broadcastable ${index} did not have a successful validation`); + } + }); + + return Array.from(addressMap.entries()).map(([address, message]) => ({ + address, + message, + })); +}