Skip to content

Commit 80839d5

Browse files
Merge pull request #6611 from BitGo/BTC-2362
feat(utxo-core): implement buildToSignPsbt for BIP322 message signing
2 parents b373bd3 + 16c29d1 commit 80839d5

File tree

5 files changed

+103
-6
lines changed

5 files changed

+103
-6
lines changed

modules/utxo-core/src/bip322/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './toSpend';
2+
export * from './toSign';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Psbt, Transaction } from '@bitgo/utxo-lib';
2+
3+
export type AddressDetails = {
4+
redeemScript?: Buffer;
5+
witnessScript?: Buffer;
6+
};
7+
8+
/**
9+
* Construct the toSign PSBT for a BIP322 verification.
10+
* Source implementation:
11+
* https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full
12+
*
13+
* @param {string} toSpendTxHex - The hex representation of the `toSpend` transaction.
14+
* @param {AddressDetails} addressDetails - The details of the address, including redeemScript and/or witnessScript.
15+
* @returns {string} - The hex representation of the constructed PSBT.
16+
*/
17+
export function buildToSignPsbt(toSpendTx: Transaction<bigint>, addressDetails: AddressDetails): Psbt {
18+
if (!addressDetails.redeemScript && !addressDetails.witnessScript) {
19+
throw new Error('redeemScript and/or witnessScript must be provided');
20+
}
21+
22+
// Create PSBT object for constructing the transaction
23+
const psbt = new Psbt();
24+
// Set default value for nVersion and nLockTime
25+
psbt.setVersion(0); // nVersion = 0
26+
psbt.setLocktime(0); // nLockTime = 0
27+
// Set the input
28+
psbt.addInput({
29+
hash: toSpendTx.getId(), // vin[0].prevout.hash = to_spend.txid
30+
index: 0, // vin[0].prevout.n = 0
31+
sequence: 0, // vin[0].nSequence = 0
32+
nonWitnessUtxo: toSpendTx.toBuffer(), // previous transaction for us to rebuild later to verify
33+
});
34+
if (addressDetails.redeemScript) {
35+
psbt.updateInput(0, { redeemScript: addressDetails.redeemScript });
36+
}
37+
if (addressDetails.witnessScript) {
38+
psbt.updateInput(0, {
39+
witnessUtxo: { value: BigInt(0), script: addressDetails.witnessScript },
40+
});
41+
}
42+
43+
// Set the output
44+
psbt.addOutput({
45+
value: BigInt(0), // vout[0].nValue = 0
46+
script: Buffer.from([0x6a]), // vout[0].scriptPubKey = OP_RETURN
47+
});
48+
return psbt;
49+
}

modules/utxo-core/src/bip322/toSpend.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Hash } from 'fast-sha256';
2-
import { Psbt } from '@bitgo/utxo-lib';
2+
import { Psbt, Transaction } from '@bitgo/utxo-lib';
33

44
export const BIP322_TAG = 'BIP0322-signed-message';
55

@@ -31,9 +31,13 @@ export function hashMessageWithTag(message: string | Buffer, tag = BIP322_TAG):
3131
* @param {Buffer} scriptPubKey - The scriptPubKey to use for the output
3232
* @param {string | Buffer} message - The message to include in the transaction
3333
* @param {Buffer} [tag=BIP322_TAG] - The tag to use for hashing, defaults to BIP322_TAG.
34-
* @returns {string} - The hex representation of the constructed transaction
34+
* @returns {Transaction} - The constructed transaction
3535
*/
36-
export function buildToSpendTransaction(scriptPubKey: Buffer, message: string | Buffer, tag = BIP322_TAG): string {
36+
export function buildToSpendTransaction(
37+
scriptPubKey: Buffer,
38+
message: string | Buffer,
39+
tag = BIP322_TAG
40+
): Transaction<bigint> {
3741
// Create PSBT object for constructing the transaction
3842
const psbt = new Psbt();
3943
// Set default value for nVersion and nLockTime
@@ -60,5 +64,5 @@ export function buildToSpendTransaction(scriptPubKey: Buffer, message: string |
6064
script: scriptPubKey, // vout[0].scriptPubKey = message_challenge
6165
});
6266
// Return transaction
63-
return psbt.extractTransaction().toHex();
67+
return psbt.extractTransaction();
6468
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import assert from 'assert';
2+
3+
import { payments, ECPair, Transaction } from '@bitgo/utxo-lib';
4+
5+
import * as bip322 from '../../src/bip322';
6+
7+
describe('BIP322 toSign', function () {
8+
describe('buildToSignPsbt', function () {
9+
const WIF = 'L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k';
10+
const prv = ECPair.fromWIF(WIF);
11+
const scriptPubKey = payments.p2wpkh({
12+
address: 'bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l',
13+
}).output as Buffer;
14+
15+
// Source: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#transaction-hashes
16+
const fixtures = [
17+
{
18+
message: '',
19+
txid: '1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6',
20+
},
21+
{
22+
message: 'Hello World',
23+
txid: '88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf',
24+
},
25+
];
26+
27+
fixtures.forEach(({ message, txid }) => {
28+
it(`should build a to_sign PSBT for message "${message}"`, function () {
29+
const toSpendTx = bip322.buildToSpendTransaction(scriptPubKey, Buffer.from(message));
30+
const addressDetails = {
31+
witnessScript: scriptPubKey,
32+
};
33+
const result = bip322.buildToSignPsbt(toSpendTx, addressDetails);
34+
const computedTxid = result
35+
.signAllInputs(prv, [Transaction.SIGHASH_ALL])
36+
.finalizeAllInputs()
37+
.extractTransaction()
38+
.getId();
39+
assert.strictEqual(computedTxid, txid, `Transaction ID for message "${message}" does not match expected value`);
40+
});
41+
});
42+
});
43+
});

modules/utxo-core/test/bip322/toSpend.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'assert';
22

3-
import { payments, Transaction } from '@bitgo/utxo-lib';
3+
import { payments } from '@bitgo/utxo-lib';
44

55
import { buildToSpendTransaction, hashMessageWithTag } from '../../src/bip322';
66

@@ -50,7 +50,7 @@ describe('to_spend', function () {
5050
fixtures.forEach(({ message, txid }) => {
5151
it(`should build a to_spend transaction for message "${message}"`, function () {
5252
const result = buildToSpendTransaction(scriptPubKey, Buffer.from(message));
53-
const computedTxid = Transaction.fromHex(result).getId();
53+
const computedTxid = result.getId();
5454
assert.strictEqual(computedTxid, txid, `Transaction ID for message "${message}" does not match expected value`);
5555
});
5656
});

0 commit comments

Comments
 (0)