diff --git a/modules/utxo-core/src/bip322/toSpend.ts b/modules/utxo-core/src/bip322/toSpend.ts index d0aab1af77..255b4aced7 100644 --- a/modules/utxo-core/src/bip322/toSpend.ts +++ b/modules/utxo-core/src/bip322/toSpend.ts @@ -1,4 +1,5 @@ import { Hash } from 'fast-sha256'; +import { Psbt } from '@bitgo/utxo-lib'; export const BIP322_TAG = 'BIP0322-signed-message'; @@ -22,3 +23,42 @@ export function hashMessageWithTag(message: string | Buffer, tag = BIP322_TAG): const messageHash = messageHasher.digest(); return Buffer.from(messageHash); } + +/** + * Build a BIP322 "to spend" transaction + * Source: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full + * + * @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 + */ +export function buildToSpendTransaction(scriptPubKey: Buffer, message: string | Buffer, tag = BIP322_TAG): string { + // 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 + // Compute the message hash - SHA256(SHA256(tag) || SHA256(tag) || message) + const messageHash = hashMessageWithTag(message, tag); + // Construct the scriptSig - OP_0 PUSH32[ message_hash ] + const scriptSigPartOne = new Uint8Array([0x00, 0x20]); // OP_0 PUSH32 + const scriptSig = new Uint8Array(scriptSigPartOne.length + messageHash.length); + scriptSig.set(scriptSigPartOne); + scriptSig.set(messageHash, scriptSigPartOne.length); + // Set the input + psbt.addInput({ + hash: '0'.repeat(64), // vin[0].prevout.hash = 0000...000 + index: 0xffffffff, // vin[0].prevout.n = 0xFFFFFFFF + sequence: 0, // vin[0].nSequence = 0 + finalScriptSig: Buffer.from(scriptSig), // vin[0].scriptSig = OP_0 PUSH32[ message_hash ] + witnessScript: Buffer.from([]), // vin[0].scriptWitness = [] + }); + // Set the output + psbt.addOutput({ + value: BigInt(0), // vout[0].nValue = 0 + script: scriptPubKey, // vout[0].scriptPubKey = message_challenge + }); + // Return transaction + return psbt.extractTransaction().toHex(); +} diff --git a/modules/utxo-core/test/bip322/toSpend.ts b/modules/utxo-core/test/bip322/toSpend.ts index d279d83687..09ea990b13 100644 --- a/modules/utxo-core/test/bip322/toSpend.ts +++ b/modules/utxo-core/test/bip322/toSpend.ts @@ -1,6 +1,8 @@ import assert from 'assert'; -import { hashMessageWithTag } from '../../src/bip322'; +import { payments, Transaction } from '@bitgo/utxo-lib'; + +import { buildToSpendTransaction, hashMessageWithTag } from '../../src/bip322'; describe('to_spend', function () { describe('Message hashing', function () { @@ -27,4 +29,30 @@ describe('to_spend', function () { }); }); }); + + describe('build to_spend transaction', function () { + 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: 'c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7', + }, + { + message: 'Hello World', + txid: 'b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b', + }, + ]; + + 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(); + assert.strictEqual(computedTxid, txid, `Transaction ID for message "${message}" does not match expected value`); + }); + }); + }); });