Skip to content

Commit c4004ab

Browse files
refactor(utxo-core): enhance BIP322 with support for multiple inputs
Refactor BIP322 implementation to support multiple inputs in one PSBT. This change allows for a more flexible approach where we can batch multiple message signature requests into a single transaction. - Create a base PSBT for to_sign transactions - Add method to add inputs incrementally - Add limit of maximum inputs (200) - Update tests to use the new API TICKET: BTC-2385
1 parent 6813e6d commit c4004ab

File tree

3 files changed

+50
-27
lines changed

3 files changed

+50
-27
lines changed

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

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,35 @@ export type AddressDetails = {
88
witnessScript?: Buffer;
99
scriptPubKey: Buffer;
1010
};
11+
export const MAX_NUM_BIP322_INPUTS = 200;
1112

1213
/**
13-
* Construct the toSign PSBT for a BIP322 verification.
14+
* Create the base PSBT for the to_sign transaction for BIP322 signing.
15+
* There will be ever 1 output.
16+
*/
17+
export function createBaseToSignPsbt(rootWalletKeys?: bitgo.RootWalletKeys): Psbt {
18+
// Create PSBT object for constructing the transaction
19+
const psbt = new Psbt();
20+
// Set default value for nVersion and nLockTime
21+
psbt.setVersion(0); // nVersion = 0
22+
psbt.setLocktime(0); // nLockTime = 0
23+
24+
// Set the output
25+
psbt.addOutput({
26+
value: BigInt(0), // vout[0].nValue = 0
27+
script: Buffer.from([0x6a]), // vout[0].scriptPubKey = OP_RETURN
28+
});
29+
30+
// If rootWalletKeys are provided, add them to the PSBT as global xpubs
31+
if (rootWalletKeys) {
32+
bitgo.addXpubsToPsbt(psbt, rootWalletKeys);
33+
}
34+
35+
return psbt;
36+
}
37+
38+
/**
39+
* Add a BIP322 input to the PSBT.
1440
* Source implementation:
1541
* https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full
1642
*
@@ -19,44 +45,38 @@ export type AddressDetails = {
1945
* @param {string} [tag=BIP322_TAG] - The tag to use for hashing, defaults to BIP322_TAG.
2046
* @returns {Psbt} - The hex representation of the constructed PSBT.
2147
*/
22-
export function buildToSignPsbt(message: string, addressDetails: AddressDetails, tag = BIP322_TAG): Psbt {
48+
export function addBip322Input(psbt: Psbt, message: string, addressDetails: AddressDetails, tag = BIP322_TAG): void {
2349
const toSpendTx = buildToSpendTransaction(addressDetails.scriptPubKey, message, tag);
50+
if (psbt.data.inputs.length >= MAX_NUM_BIP322_INPUTS) {
51+
throw new Error(`Exceeded maximum number of BIP322 inputs (${MAX_NUM_BIP322_INPUTS})`);
52+
}
2453

25-
// Create PSBT object for constructing the transaction
26-
const psbt = new Psbt();
27-
// Set default value for nVersion and nLockTime
28-
psbt.setVersion(0); // nVersion = 0
29-
psbt.setLocktime(0); // nLockTime = 0
30-
// Set the input
3154
psbt.addInput({
3255
hash: toSpendTx.getId(), // vin[0].prevout.hash = to_spend.txid
3356
index: 0, // vin[0].prevout.n = 0
3457
sequence: 0, // vin[0].nSequence = 0
3558
nonWitnessUtxo: toSpendTx.toBuffer(), // previous transaction for us to rebuild later to verify
3659
});
37-
psbt.updateInput(0, {
60+
const inputIndex = psbt.data.inputs.length - 1;
61+
psbt.updateInput(inputIndex, {
3862
witnessUtxo: { value: BigInt(0), script: addressDetails.scriptPubKey },
3963
});
4064

4165
if (addressDetails.redeemScript) {
42-
psbt.updateInput(0, { redeemScript: addressDetails.redeemScript });
66+
psbt.updateInput(inputIndex, { redeemScript: addressDetails.redeemScript });
4367
}
4468
if (addressDetails.witnessScript) {
45-
psbt.updateInput(0, { witnessScript: addressDetails.witnessScript });
69+
psbt.updateInput(inputIndex, {
70+
witnessScript: addressDetails.witnessScript,
71+
});
4672
}
4773

4874
// Add the message as a proprietary key value to the PSBT so we can verify it later
49-
addBip322ProofMessage(psbt, 0, Buffer.from(message));
50-
51-
// Set the output
52-
psbt.addOutput({
53-
value: BigInt(0), // vout[0].nValue = 0
54-
script: Buffer.from([0x6a]), // vout[0].scriptPubKey = OP_RETURN
55-
});
56-
return psbt;
75+
addBip322ProofMessage(psbt, inputIndex, Buffer.from(message));
5776
}
5877

59-
export function buildToSignPsbtForChainAndIndex(
78+
export function addBip322InputWithChainAndIndex(
79+
psbt: Psbt,
6080
message: string,
6181
rootWalletKeys: bitgo.RootWalletKeys,
6282
chain: bitgo.ChainCode,
@@ -68,17 +88,17 @@ export function buildToSignPsbtForChainAndIndex(
6888
const walletKeys = rootWalletKeys.deriveForChainAndIndex(chain, index);
6989
const output = bitgo.outputScripts.createOutputScript2of3(walletKeys.publicKeys, bitgo.scriptTypeForChain(chain));
7090

71-
const psbt = buildToSignPsbt(message, {
91+
addBip322Input(psbt, message, {
7292
scriptPubKey: output.scriptPubKey,
7393
redeemScript: output.redeemScript,
7494
witnessScript: output.witnessScript,
7595
});
7696

97+
const inputIndex = psbt.data.inputs.length - 1;
7798
psbt.updateInput(
78-
0,
99+
inputIndex,
79100
bitgo.getPsbtBip32DerivationOutputUpdate(rootWalletKeys, walletKeys, bitgo.scriptTypeForChain(chain))
80101
);
81-
bitgo.addXpubsToPsbt(psbt, rootWalletKeys);
82102

83103
return psbt;
84104
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { payments, ECPair } from '@bitgo/utxo-lib';
22

3-
import { buildToSignPsbt, buildToSpendTransaction } from '../../src/bip322';
3+
import { addBip322Input, createBaseToSignPsbt, buildToSpendTransaction } from '../../src/bip322';
44

55
export const BIP322_WIF_FIXTURE = 'L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k';
66
export const BIP322_PRV_FIXTURE = ECPair.fromWIF(BIP322_WIF_FIXTURE);
@@ -13,6 +13,7 @@ export const BIP322_FIXTURE_HELLO_WORLD_TOSPEND_TX = buildToSpendTransaction(
1313
Buffer.from('Hello World')
1414
);
1515

16-
export const BIP322_FIXTURE_HELLO_WORLD_TOSIGN_PSBT = buildToSignPsbt('Hello World', {
16+
export const BIP322_FIXTURE_HELLO_WORLD_TOSIGN_PSBT = createBaseToSignPsbt();
17+
addBip322Input(BIP322_FIXTURE_HELLO_WORLD_TOSIGN_PSBT, 'Hello World', {
1718
scriptPubKey: BIP322_PAYMENT_P2WPKH_FIXTURE.output as Buffer,
1819
});

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ describe('BIP322 toSign', function () {
2323

2424
fixtures.forEach(({ message, txid }) => {
2525
it(`should build a to_sign PSBT for message "${message}"`, function () {
26-
const result = bip322.buildToSignPsbt(message, {
26+
const result = bip322.createBaseToSignPsbt();
27+
bip322.addBip322Input(result, message, {
2728
scriptPubKey,
2829
});
2930
const computedTxid = result
@@ -51,7 +52,8 @@ describe('BIP322 toSign', function () {
5152
return;
5253
}
5354
const toSpendTx = bip322.buildToSpendTransactionFromChainAndIndex(rootWalletKeys, chain, index, message);
54-
const toSignPsbt = bip322.buildToSignPsbtForChainAndIndex(message, rootWalletKeys, chain, index);
55+
const toSignPsbt = bip322.createBaseToSignPsbt(rootWalletKeys);
56+
bip322.addBip322InputWithChainAndIndex(toSignPsbt, message, rootWalletKeys, chain, index);
5557

5658
// Can sign the PSBT with the keys
5759
// Should be able to use HD because we have the bip32Derivation information

0 commit comments

Comments
 (0)