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, + })); +} 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' })) + ) + ); + }); + }); +});