From 11993aed2da9b8052076da42928f9e0972b7405d Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Fri, 1 Aug 2025 12:21:44 -0400 Subject: [PATCH 1/2] feat(utxo-core): implement buildToSignPsbt for BIP322 message signing Add functionality to construct the toSign PSBT for BIP322 message verification with support for redeem scripts and witness scripts. Includes tests to verify correctness against BIP322 test vectors. Supports v0 segwit and non-segwit addresses. Taproot to be added later. Issue: BTC-2360 Co-authored-by: llm-git --- modules/utxo-core/src/bip322/index.ts | 1 + modules/utxo-core/src/bip322/toSign.ts | 49 +++++++++++++++++++++++++ modules/utxo-core/test/bip322/toSign.ts | 43 ++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 modules/utxo-core/src/bip322/toSign.ts create mode 100644 modules/utxo-core/test/bip322/toSign.ts diff --git a/modules/utxo-core/src/bip322/index.ts b/modules/utxo-core/src/bip322/index.ts index 6cd3479af0..111eb7bf12 100644 --- a/modules/utxo-core/src/bip322/index.ts +++ b/modules/utxo-core/src/bip322/index.ts @@ -1 +1,2 @@ export * from './toSpend'; +export * from './toSign'; diff --git a/modules/utxo-core/src/bip322/toSign.ts b/modules/utxo-core/src/bip322/toSign.ts new file mode 100644 index 0000000000..fd563a82b6 --- /dev/null +++ b/modules/utxo-core/src/bip322/toSign.ts @@ -0,0 +1,49 @@ +import { Psbt, Transaction } from '@bitgo/utxo-lib'; + +export type AddressDetails = { + redeemScript?: Buffer; + witnessScript?: Buffer; +}; + +/** + * Construct the toSign PSBT for a BIP322 verification. + * Source implementation: + * https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full + * + * @param {string} toSpendTxHex - The hex representation of the `toSpend` transaction. + * @param {AddressDetails} addressDetails - The details of the address, including redeemScript and/or witnessScript. + * @returns {string} - The hex representation of the constructed PSBT. + */ +export function buildToSignPsbt(toSpendTxHex: string, addressDetails: AddressDetails): string { + if (!addressDetails.redeemScript && !addressDetails.witnessScript) { + throw new Error('redeemScript and/or witnessScript must be provided'); + } + + const toSpendTx = Transaction.fromHex(toSpendTxHex); + + // Create PSBT object for constructing the transaction + const psbt = new Psbt(); + // Set default value for nVersion and nLockTime + psbt.setVersion(0); // nVersion = 0 + psbt.setLocktime(0); // nLockTime = 0 + // Set the input + psbt.addInput({ + hash: toSpendTx.getId(), // vin[0].prevout.hash = to_spend.txid + index: 0, // vin[0].prevout.n = 0 + sequence: 0, // vin[0].nSequence = 0 + nonWitnessUtxo: toSpendTx.toBuffer(), // previous transaction for us to rebuild later to verify + }); + if (addressDetails.redeemScript) { + psbt.updateInput(0, { redeemScript: addressDetails.redeemScript }); + } + if (addressDetails.witnessScript) { + psbt.updateInput(0, { witnessUtxo: { value: BigInt(0), script: addressDetails.witnessScript } }); + } + + // Set the output + psbt.addOutput({ + value: BigInt(0), // vout[0].nValue = 0 + script: Buffer.from([0x6a]), // vout[0].scriptPubKey = OP_RETURN + }); + return psbt.toHex(); +} diff --git a/modules/utxo-core/test/bip322/toSign.ts b/modules/utxo-core/test/bip322/toSign.ts new file mode 100644 index 0000000000..4eaa9a2611 --- /dev/null +++ b/modules/utxo-core/test/bip322/toSign.ts @@ -0,0 +1,43 @@ +import assert from 'assert'; + +import { payments, Psbt, ECPair, Transaction } from '@bitgo/utxo-lib'; + +import * as bip322 from '../../src/bip322'; + +describe('BIP322 toSign', function () { + describe('buildToSignPsbt', function () { + const WIF = 'L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k'; + const prv = ECPair.fromWIF(WIF); + const scriptPubKey = payments.p2wpkh({ + address: 'bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l', + }).output as Buffer; + + // Source: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#transaction-hashes + const fixtures = [ + { + message: '', + txid: '1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6', + }, + { + message: 'Hello World', + txid: '88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf', + }, + ]; + + fixtures.forEach(({ message, txid }) => { + it(`should build a to_sign PSBT for message "${message}"`, function () { + const toSpendTxHex = bip322.buildToSpendTransaction(scriptPubKey, Buffer.from(message)); + const addressDetails = { + witnessScript: scriptPubKey, + }; + const result = bip322.buildToSignPsbt(toSpendTxHex, addressDetails); + const computedTxid = Psbt.fromHex(result) + .signAllInputs(prv, [Transaction.SIGHASH_ALL]) + .finalizeAllInputs() + .extractTransaction() + .getId(); + assert.strictEqual(computedTxid, txid, `Transaction ID for message "${message}" does not match expected value`); + }); + }); + }); +}); From 16c29d104fe83107f7b1eea845c6d3a36c5566de Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Fri, 1 Aug 2025 13:59:08 -0400 Subject: [PATCH 2/2] feat(utxo-core): improve bip322 transaction handling Change buildToSignPsbt to directly accept a Transaction object instead of hex string, and return a Psbt object instead of hex. Similarly update buildToSpendTransaction to return a Transaction object. Issue: BTC-2362 Co-authored-by: llm-git --- modules/utxo-core/src/bip322/toSign.ts | 10 +++++----- modules/utxo-core/src/bip322/toSpend.ts | 12 ++++++++---- modules/utxo-core/test/bip322/toSign.ts | 8 ++++---- modules/utxo-core/test/bip322/toSpend.ts | 4 ++-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/modules/utxo-core/src/bip322/toSign.ts b/modules/utxo-core/src/bip322/toSign.ts index fd563a82b6..88ba4dd259 100644 --- a/modules/utxo-core/src/bip322/toSign.ts +++ b/modules/utxo-core/src/bip322/toSign.ts @@ -14,13 +14,11 @@ export type AddressDetails = { * @param {AddressDetails} addressDetails - The details of the address, including redeemScript and/or witnessScript. * @returns {string} - The hex representation of the constructed PSBT. */ -export function buildToSignPsbt(toSpendTxHex: string, addressDetails: AddressDetails): string { +export function buildToSignPsbt(toSpendTx: Transaction, addressDetails: AddressDetails): Psbt { if (!addressDetails.redeemScript && !addressDetails.witnessScript) { throw new Error('redeemScript and/or witnessScript must be provided'); } - const toSpendTx = Transaction.fromHex(toSpendTxHex); - // Create PSBT object for constructing the transaction const psbt = new Psbt(); // Set default value for nVersion and nLockTime @@ -37,7 +35,9 @@ export function buildToSignPsbt(toSpendTxHex: string, addressDetails: AddressDet psbt.updateInput(0, { redeemScript: addressDetails.redeemScript }); } if (addressDetails.witnessScript) { - psbt.updateInput(0, { witnessUtxo: { value: BigInt(0), script: addressDetails.witnessScript } }); + psbt.updateInput(0, { + witnessUtxo: { value: BigInt(0), script: addressDetails.witnessScript }, + }); } // Set the output @@ -45,5 +45,5 @@ export function buildToSignPsbt(toSpendTxHex: string, addressDetails: AddressDet value: BigInt(0), // vout[0].nValue = 0 script: Buffer.from([0x6a]), // vout[0].scriptPubKey = OP_RETURN }); - return psbt.toHex(); + return psbt; } diff --git a/modules/utxo-core/src/bip322/toSpend.ts b/modules/utxo-core/src/bip322/toSpend.ts index 255b4aced7..33c68c3676 100644 --- a/modules/utxo-core/src/bip322/toSpend.ts +++ b/modules/utxo-core/src/bip322/toSpend.ts @@ -1,5 +1,5 @@ import { Hash } from 'fast-sha256'; -import { Psbt } from '@bitgo/utxo-lib'; +import { Psbt, Transaction } from '@bitgo/utxo-lib'; export const BIP322_TAG = 'BIP0322-signed-message'; @@ -31,9 +31,13 @@ export function hashMessageWithTag(message: string | Buffer, tag = BIP322_TAG): * @param {Buffer} scriptPubKey - The scriptPubKey to use for the output * @param {string | Buffer} message - The message to include in the transaction * @param {Buffer} [tag=BIP322_TAG] - The tag to use for hashing, defaults to BIP322_TAG. - * @returns {string} - The hex representation of the constructed transaction + * @returns {Transaction} - The constructed transaction */ -export function buildToSpendTransaction(scriptPubKey: Buffer, message: string | Buffer, tag = BIP322_TAG): string { +export function buildToSpendTransaction( + scriptPubKey: Buffer, + message: string | Buffer, + tag = BIP322_TAG +): Transaction { // Create PSBT object for constructing the transaction const psbt = new Psbt(); // Set default value for nVersion and nLockTime @@ -60,5 +64,5 @@ export function buildToSpendTransaction(scriptPubKey: Buffer, message: string | script: scriptPubKey, // vout[0].scriptPubKey = message_challenge }); // Return transaction - return psbt.extractTransaction().toHex(); + return psbt.extractTransaction(); } diff --git a/modules/utxo-core/test/bip322/toSign.ts b/modules/utxo-core/test/bip322/toSign.ts index 4eaa9a2611..10d13fbf15 100644 --- a/modules/utxo-core/test/bip322/toSign.ts +++ b/modules/utxo-core/test/bip322/toSign.ts @@ -1,6 +1,6 @@ import assert from 'assert'; -import { payments, Psbt, ECPair, Transaction } from '@bitgo/utxo-lib'; +import { payments, ECPair, Transaction } from '@bitgo/utxo-lib'; import * as bip322 from '../../src/bip322'; @@ -26,12 +26,12 @@ describe('BIP322 toSign', function () { fixtures.forEach(({ message, txid }) => { it(`should build a to_sign PSBT for message "${message}"`, function () { - const toSpendTxHex = bip322.buildToSpendTransaction(scriptPubKey, Buffer.from(message)); + const toSpendTx = bip322.buildToSpendTransaction(scriptPubKey, Buffer.from(message)); const addressDetails = { witnessScript: scriptPubKey, }; - const result = bip322.buildToSignPsbt(toSpendTxHex, addressDetails); - const computedTxid = Psbt.fromHex(result) + const result = bip322.buildToSignPsbt(toSpendTx, addressDetails); + const computedTxid = result .signAllInputs(prv, [Transaction.SIGHASH_ALL]) .finalizeAllInputs() .extractTransaction() diff --git a/modules/utxo-core/test/bip322/toSpend.ts b/modules/utxo-core/test/bip322/toSpend.ts index 09ea990b13..f6fc030458 100644 --- a/modules/utxo-core/test/bip322/toSpend.ts +++ b/modules/utxo-core/test/bip322/toSpend.ts @@ -1,6 +1,6 @@ import assert from 'assert'; -import { payments, Transaction } from '@bitgo/utxo-lib'; +import { payments } from '@bitgo/utxo-lib'; import { buildToSpendTransaction, hashMessageWithTag } from '../../src/bip322'; @@ -50,7 +50,7 @@ describe('to_spend', function () { fixtures.forEach(({ message, txid }) => { it(`should build a to_spend transaction for message "${message}"`, function () { const result = buildToSpendTransaction(scriptPubKey, Buffer.from(message)); - const computedTxid = Transaction.fromHex(result).getId(); + const computedTxid = result.getId(); assert.strictEqual(computedTxid, txid, `Transaction ID for message "${message}" does not match expected value`); }); });