Skip to content

Commit 11993ae

Browse files
davidkaplanbitgollm-git
andcommitted
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 <[email protected]>
1 parent c9bf219 commit 11993ae

File tree

3 files changed

+93
-0
lines changed

3 files changed

+93
-0
lines changed
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(toSpendTxHex: string, addressDetails: AddressDetails): string {
18+
if (!addressDetails.redeemScript && !addressDetails.witnessScript) {
19+
throw new Error('redeemScript and/or witnessScript must be provided');
20+
}
21+
22+
const toSpendTx = Transaction.fromHex(toSpendTxHex);
23+
24+
// Create PSBT object for constructing the transaction
25+
const psbt = new Psbt();
26+
// Set default value for nVersion and nLockTime
27+
psbt.setVersion(0); // nVersion = 0
28+
psbt.setLocktime(0); // nLockTime = 0
29+
// Set the input
30+
psbt.addInput({
31+
hash: toSpendTx.getId(), // vin[0].prevout.hash = to_spend.txid
32+
index: 0, // vin[0].prevout.n = 0
33+
sequence: 0, // vin[0].nSequence = 0
34+
nonWitnessUtxo: toSpendTx.toBuffer(), // previous transaction for us to rebuild later to verify
35+
});
36+
if (addressDetails.redeemScript) {
37+
psbt.updateInput(0, { redeemScript: addressDetails.redeemScript });
38+
}
39+
if (addressDetails.witnessScript) {
40+
psbt.updateInput(0, { witnessUtxo: { value: BigInt(0), script: addressDetails.witnessScript } });
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.toHex();
49+
}
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, Psbt, 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 toSpendTxHex = bip322.buildToSpendTransaction(scriptPubKey, Buffer.from(message));
30+
const addressDetails = {
31+
witnessScript: scriptPubKey,
32+
};
33+
const result = bip322.buildToSignPsbt(toSpendTxHex, addressDetails);
34+
const computedTxid = Psbt.fromHex(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+
});

0 commit comments

Comments
 (0)