@caravan/psbt 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/psbt.ts ADDED
@@ -0,0 +1,440 @@
1
+ import { Psbt, Transaction } from "bitcoinjs-lib";
2
+ import { reverseBuffer } from "bitcoinjs-lib/src/bufferutils.js";
3
+ import { toHexString } from "@caravan/bitcoin";
4
+ import {
5
+ generateMultisigFromHex,
6
+ multisigAddressType,
7
+ multisigBraidDetails,
8
+ multisigRedeemScript,
9
+ multisigWitnessScript,
10
+ bip32PathToSequence,
11
+ P2SH,
12
+ P2WSH,
13
+ P2SH_P2WSH,
14
+ generateBip32DerivationByIndex,
15
+ generateBraid,
16
+ networkData
17
+ } from "@caravan/bitcoin";
18
+ import BigNumber from "bignumber.js";
19
+
20
+ /**
21
+ * This module provides functions for interacting with PSBTs, see BIP174
22
+ * https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
23
+ */
24
+
25
+ /**
26
+ * Represents a transaction PSBT input.
27
+ *
28
+ * The [`Multisig`]{@link module:multisig.MULTISIG} object represents
29
+ * the address the corresponding UTXO belongs to.
30
+ */
31
+
32
+ /**
33
+ * Represents an output in a PSBT transaction.
34
+ */
35
+
36
+ export const PSBT_MAGIC_HEX = "70736274ff";
37
+ export const PSBT_MAGIC_B64 = "cHNidP8";
38
+ export const PSBT_MAGIC_BYTES = Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff]);
39
+
40
+ /**
41
+ * Given a string, try to create a Psbt object based on MAGIC (hex or Base64)
42
+ */
43
+ export function autoLoadPSBT(psbtFromFile, options?: any) {
44
+ if (typeof psbtFromFile !== "string") return null;
45
+ // Auto-detect and decode Base64 and Hex.
46
+ if (psbtFromFile.substring(0, 10) === PSBT_MAGIC_HEX) {
47
+ return Psbt.fromHex(psbtFromFile, options);
48
+ } else if (psbtFromFile.substring(0, 7) === PSBT_MAGIC_B64) {
49
+ return Psbt.fromBase64(psbtFromFile, options);
50
+ } else {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Return the getBip32Derivation (if known) for a given `Multisig` object.
57
+ */
58
+ function getBip32Derivation(multisig, index = 0) {
59
+ // Already have one, return it
60
+ if (multisig.bip32Derivation) {
61
+ return multisig.bip32Derivation;
62
+ }
63
+ // Otherwise generate it
64
+ const config = JSON.parse(multisigBraidDetails(multisig));
65
+ const braid = generateBraid(
66
+ config.network,
67
+ config.addressType,
68
+ config.extendedPublicKeys,
69
+ config.requiredSigners,
70
+ config.index
71
+ );
72
+ return generateBip32DerivationByIndex(braid, index);
73
+ }
74
+
75
+ /**
76
+ * Grabs appropriate bip32Derivation based on the input's last index
77
+ */
78
+ function psbtInputDerivation(input) {
79
+ // Multi-address inputs will have different bip32Derivations per address (index/path),
80
+ // so specify the index ... If the input is missing a path, assume you want index = 0.
81
+ const index = input.bip32Path
82
+ ? bip32PathToSequence(input.bip32Path).slice(-1)[0]
83
+ : 0;
84
+ return getBip32Derivation(input.multisig, index);
85
+ }
86
+
87
+ /**
88
+ * Grabs appropriate bip32Derivation for a change output
89
+ */
90
+ function psbtOutputDerivation(output) {
91
+ return getBip32Derivation(output.multisig);
92
+ }
93
+
94
+ /**
95
+ * Gets the Witness script from the ouput that generated the input
96
+ */
97
+ function getWitnessOutputScriptFromInput(input) {
98
+ // We have the transactionHex - use bitcoinjs to pluck out the witness script
99
+ // return format is:
100
+ // {
101
+ // script: Buffer.from(out.script, 'hex'),
102
+ // amount: out.value,
103
+ // }
104
+ // See https://github.com/bitcoinjs/bitcoinjs-lib/issues/1282
105
+ const tx = Transaction.fromHex(input.transactionHex);
106
+ return tx.outs[input.index];
107
+ }
108
+
109
+ /**
110
+ * Return the locking script for the given `Multisig` object in a PSBT consumable format
111
+ */
112
+ function psbtMultisigLock(multisig) {
113
+ const multisigLock: any = {};
114
+
115
+ // eslint-disable-next-line default-case
116
+ switch (multisigAddressType(multisig)) {
117
+ case P2SH:
118
+ multisigLock.redeemScript = multisigRedeemScript(multisig).output;
119
+ break;
120
+ case P2WSH:
121
+ multisigLock.witnessScript = multisigWitnessScript(multisig).output;
122
+ break;
123
+ case P2SH_P2WSH: // need both
124
+ multisigLock.witnessScript = multisigWitnessScript(multisig).output;
125
+ multisigLock.redeemScript = multisigRedeemScript(multisig).output;
126
+ break;
127
+ }
128
+
129
+ return multisigLock;
130
+ }
131
+
132
+ /**
133
+ * Take a MultisigTransactionInput and turn it into a MultisigTransactionPSBTInput
134
+ */
135
+ export function psbtInputFormatter(input) {
136
+ // In this function we're decorating the MultisigTransactionInput appropriately based
137
+ // on its address type.
138
+ //
139
+ // Essentially we need to define a couple parameters to make the whole thing work.
140
+ // 1) Either a Witness UTXO or Non-Witness UTXO pointing to where this input originated
141
+ // 2) multisigScript (spending lock) which can be either a redeemScript, a witnessScript, or both.
142
+ //
143
+ // For more info see https://github.com/bitcoinjs/bitcoinjs-lib/blob/v5.1.10/test/integration/transactions.spec.ts#L680
144
+
145
+ // For SegWit inputs, you need an object with the output script buffer and output value
146
+ const witnessUtxo = getWitnessOutputScriptFromInput(input);
147
+ // For non-SegWit inputs, you must pass the full transaction buffer
148
+ const nonWitnessUtxo = Buffer.from(input.transactionHex, "hex");
149
+
150
+ // FIXME - this makes the assumption that the funding transaction used the same transaction type as the current input
151
+ // we dont have isSegWit info on our inputs at the moment, so we don't know for sure.
152
+ // This assumption holds in our fixtures, but it may need to be remedied in the future.
153
+ const isSegWit = multisigWitnessScript(input.multisig) !== null;
154
+ const utxoToVerify = isSegWit ? { witnessUtxo } : { nonWitnessUtxo };
155
+ const multisigScripts = psbtMultisigLock(input.multisig);
156
+
157
+ const bip32Derivation = psbtInputDerivation(input);
158
+ return {
159
+ hash: input.txid,
160
+ index: input.index,
161
+ ...utxoToVerify,
162
+ ...multisigScripts,
163
+ bip32Derivation,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Take a MultisigTransactionOutput and turn it into a MultisigTransactionPSBTOutput
169
+ */
170
+ export function psbtOutputFormatter(output) {
171
+ let multisigScripts = {};
172
+ let bip32Derivation = [];
173
+
174
+ if (output.multisig) {
175
+ // This indicates that this output is a *change* output, so we include additional information:
176
+ // Change address bip32Derivation (rootFingerprints && pubkeys && bip32paths)
177
+ // Change address multisig locking script (redeem || witness || both)
178
+ // With the above information, the device (e.g. Coldcard) can validate that the change address
179
+ // can be signed with the same device. The display will show the output as "Change" instead of
180
+ // a normal external output.
181
+ multisigScripts = psbtMultisigLock(output.multisig);
182
+ bip32Derivation = psbtOutputDerivation(output);
183
+ return {
184
+ address: output.address,
185
+ value: new BigNumber(output.amountSats).toNumber(),
186
+ ...multisigScripts,
187
+ bip32Derivation,
188
+ };
189
+ }
190
+
191
+ return {
192
+ address: output.address,
193
+ value: new BigNumber(output.amountSats).toNumber(),
194
+ ...output, // the output may have come in already decorated with bip32Derivation/multisigScripts
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Create @caravan/wallets style transaction input objects from a PSBT
200
+ */
201
+ function getUnchainedInputsFromPSBT(network, addressType, psbt) {
202
+ return psbt.txInputs.map((input, index) => {
203
+ const dataInput = psbt.data.inputs[index];
204
+
205
+ // FIXME - this is where we're currently only handling P2SH correctly
206
+ const fundingTxHex = dataInput.nonWitnessUtxo.toString("hex");
207
+ const fundingTx = Transaction.fromHex(fundingTxHex);
208
+ const multisig = generateMultisigFromHex(
209
+ network,
210
+ addressType,
211
+ dataInput.redeemScript.toString("hex")
212
+ );
213
+
214
+ return {
215
+ amountSats: fundingTx.outs[input.index].value,
216
+ index: input.index,
217
+ transactionHex: fundingTxHex,
218
+ txid: reverseBuffer(input.hash).toString("hex"),
219
+ multisig,
220
+ };
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Create @caravan/wallets style transaction output objects from a PSBT
226
+ */
227
+ function getUnchainedOutputsFromPSBT(psbt) {
228
+ return psbt.txOutputs.map((output) => ({
229
+ address: output.address,
230
+ amountSats: output.value,
231
+ }));
232
+ }
233
+
234
+ /**
235
+ * Create @caravan/wallets style transaction input objects
236
+ *
237
+ * @param {Object} psbt - Psbt bitcoinjs-lib object
238
+ * @param {Object} signingKeyDetails - Object containing signing key details (Fingerprint + bip32path prefix)
239
+ * @return {Object[]} bip32Derivations - array of signing bip32Derivation objects
240
+ */
241
+ function filterRelevantBip32Derivations(psbt, signingKeyDetails) {
242
+ return psbt.data.inputs.map((input) => {
243
+ const bip32Derivation = input.bip32Derivation.filter(
244
+ (b32d) =>
245
+ b32d.path.startsWith(signingKeyDetails.path) &&
246
+ b32d.masterFingerprint.toString("hex") === signingKeyDetails.xfp
247
+ );
248
+
249
+ if (!bip32Derivation.length) {
250
+ throw new Error("Signing key details not included in PSBT");
251
+ }
252
+ return bip32Derivation[0];
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Translates a PSBT into inputs/outputs consumable by supported non-PSBT devices in the
258
+ * `@caravan/wallets` library.
259
+ *
260
+ * FIXME - Have only confirmed this is working for P2SH addresses on Ledger on regtest
261
+ */
262
+ export function translatePSBT(network, addressType, psbt, signingKeyDetails) {
263
+ if (addressType !== P2SH) {
264
+ throw new Error(
265
+ "Unsupported addressType -- only P2SH is supported right now"
266
+ );
267
+ }
268
+ let localPSBT = autoLoadPSBT(psbt, { network: networkData(network) });
269
+ if (localPSBT === null) return null;
270
+
271
+ // The information we need to provide proper @caravan/wallets style objects to the supported
272
+ // non-PSBT devices, we need to grab info from different places from within the PSBT.
273
+ // 1. the "data inputs"
274
+ // 2. the "transaction inputs"
275
+ //
276
+ // We'll do that in the functions below.
277
+
278
+ // First, we check that we actually do have any inputs to sign:
279
+ const bip32Derivations = filterRelevantBip32Derivations(
280
+ localPSBT,
281
+ signingKeyDetails
282
+ );
283
+
284
+ // The shape of these return objects are specific to existing code
285
+ // in @caravan/wallets for signing with Trezor and Ledger devices.
286
+ const unchainedInputs = getUnchainedInputsFromPSBT(
287
+ network,
288
+ addressType,
289
+ localPSBT
290
+ );
291
+ const unchainedOutputs = getUnchainedOutputsFromPSBT(localPSBT);
292
+
293
+ return {
294
+ unchainedInputs,
295
+ unchainedOutputs,
296
+ bip32Derivations,
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Given a PSBT, an input index, a pubkey, and a signature,
302
+ * update the input inside the PSBT with a partial signature object.
303
+ *
304
+ * Make sure it validates, and then return the PSBT with the partial
305
+ * signature inside.
306
+ */
307
+ function addSignatureToPSBT(psbt, inputIndex, pubkey, signature) {
308
+ const partialSig = [
309
+ {
310
+ pubkey,
311
+ signature,
312
+ },
313
+ ];
314
+ psbt.data.updateInput(inputIndex, { partialSig });
315
+ if (!psbt.validateSignaturesOfInput(inputIndex, pubkey)) {
316
+ throw new Error("One or more invalid signatures.");
317
+ }
318
+ return psbt;
319
+ }
320
+
321
+ /**
322
+ * Given an unsigned PSBT, an array of signing public key(s) (one per input),
323
+ * an array of signature(s) (one per input) in the same order as the pubkey(s),
324
+ * adds partial signature object(s) to each input and returns the PSBT with
325
+ * partial signature(s) included.
326
+ *
327
+ * FIXME - maybe we add functionality of sending in a single pubkey as well,
328
+ * which would assume all of the signature(s) are for that pubkey.
329
+ */
330
+ export function addSignaturesToPSBT(network, psbt, pubkeys, signatures) {
331
+ let psbtWithSignatures = autoLoadPSBT(psbt, {
332
+ network: networkData(network),
333
+ });
334
+ if (psbtWithSignatures === null) return null;
335
+
336
+ signatures.forEach((sig, idx) => {
337
+ const pubkey = pubkeys[idx];
338
+ psbtWithSignatures = addSignatureToPSBT(
339
+ psbtWithSignatures,
340
+ idx,
341
+ pubkey,
342
+ sig
343
+ );
344
+ });
345
+ return psbtWithSignatures.toBase64();
346
+ }
347
+
348
+ /**
349
+ * Get number of signers in the PSBT
350
+ */
351
+
352
+ function getNumSigners(psbt) {
353
+ const partialSignatures =
354
+ psbt && psbt.data && psbt.data.inputs && psbt.data.inputs[0]
355
+ ? psbt.data.inputs[0].partialSig
356
+ : undefined;
357
+ return partialSignatures === undefined ? 0 : partialSignatures.length;
358
+ }
359
+
360
+ /**
361
+ * Extracts the signature(s) from a PSBT.
362
+ * NOTE: there should be one signature per input, per signer.
363
+ *
364
+ * ADDITIONAL NOTE: because of the restrictions we place on braids to march their
365
+ * multisig addresses (slices) forward at the *same* index across each chain of the
366
+ * braid, we do not run into a possible collision with this data structure.
367
+ * BUT - to have this method accommodate the *most* general form of signature parsing,
368
+ * it would be wise to wrap this one level deeper like:
369
+ *
370
+ * address: [pubkey : [signature(s)]]
371
+ *
372
+ * that way if your braid only advanced one chain's (member's) index so that a pubkey
373
+ * could be used in more than one address, everything would still function properly.
374
+ */
375
+ export function parseSignaturesFromPSBT(psbtFromFile) {
376
+ let psbt = autoLoadPSBT(psbtFromFile, {});
377
+ if (psbt === null) {
378
+ return null;
379
+ }
380
+
381
+ const numSigners = getNumSigners(psbt);
382
+
383
+ const signatureSet = {};
384
+ let pubKey = "";
385
+ const inputs = psbt.data.inputs;
386
+ // Find signatures in the PSBT
387
+ if (numSigners >= 1) {
388
+ // return array of arrays of signatures
389
+ for (let i = 0; i < inputs.length; i++) {
390
+ for (let j = 0; j < numSigners; j++) {
391
+ pubKey = toHexString(
392
+ Array.prototype.slice.call(inputs?.[i]?.partialSig?.[j].pubkey)
393
+ );
394
+ if (pubKey in signatureSet) {
395
+ signatureSet[pubKey].push(
396
+ inputs?.[i]?.partialSig?.[j].signature.toString("hex")
397
+ );
398
+ } else {
399
+ signatureSet[pubKey] = [
400
+ inputs?.[i]?.partialSig?.[j].signature.toString("hex"),
401
+ ];
402
+ }
403
+ }
404
+ }
405
+ } else {
406
+ return null;
407
+ }
408
+ return signatureSet;
409
+ }
410
+
411
+ /**
412
+ * Extracts signatures in order of inputs and returns as array (or array of arrays if multiple signature sets)
413
+ */
414
+ export function parseSignatureArrayFromPSBT(psbtFromFile) {
415
+ let psbt = autoLoadPSBT(psbtFromFile);
416
+ if (psbt === null) return null;
417
+
418
+ const numSigners = getNumSigners(psbt);
419
+
420
+ const signatureArrays: string[][] = Array.from(
421
+ { length: numSigners },
422
+ () => []
423
+ );
424
+
425
+ const { inputs } = psbt.data;
426
+
427
+ if (numSigners >= 1) {
428
+ for (let i = 0; i < inputs.length; i += 1) {
429
+ for (let j = 0; j < numSigners; j += 1) {
430
+ let signature = inputs?.[i]?.partialSig?.[j].signature.toString("hex");
431
+ if (signature) {
432
+ signatureArrays[j].push(signature);
433
+ }
434
+ }
435
+ }
436
+ } else {
437
+ return null;
438
+ }
439
+ return numSigners === 1 ? signatureArrays[0] : signatureArrays;
440
+ }