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..88ba4dd259 --- /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(toSpendTx: Transaction, addressDetails: AddressDetails): Psbt { + if (!addressDetails.redeemScript && !addressDetails.witnessScript) { + throw new Error('redeemScript and/or witnessScript must be provided'); + } + + // 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; +} 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 new file mode 100644 index 0000000000..10d13fbf15 --- /dev/null +++ b/modules/utxo-core/test/bip322/toSign.ts @@ -0,0 +1,43 @@ +import assert from 'assert'; + +import { payments, 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 toSpendTx = bip322.buildToSpendTransaction(scriptPubKey, Buffer.from(message)); + const addressDetails = { + witnessScript: scriptPubKey, + }; + const result = bip322.buildToSignPsbt(toSpendTx, addressDetails); + const computedTxid = result + .signAllInputs(prv, [Transaction.SIGHASH_ALL]) + .finalizeAllInputs() + .extractTransaction() + .getId(); + assert.strictEqual(computedTxid, txid, `Transaction ID for message "${message}" does not match expected value`); + }); + }); + }); +}); 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`); }); });