ecash-lib 4.11.0 → 4.12.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/README.md +1 -0
- package/dist/consts.d.ts +11 -0
- package/dist/consts.d.ts.map +1 -1
- package/dist/consts.js +12 -1
- package/dist/consts.js.map +1 -1
- package/dist/ffi/ecash_lib_wasm_bg_browser.js +20489 -20489
- package/dist/ffi/ecash_lib_wasm_bg_browser.wasm +0 -0
- package/dist/ffi/ecash_lib_wasm_bg_nodejs.wasm +0 -0
- package/dist/psbt.d.ts +54 -4
- package/dist/psbt.d.ts.map +1 -1
- package/dist/psbt.js +256 -7
- package/dist/psbt.js.map +1 -1
- package/dist/script.d.ts +11 -1
- package/dist/script.d.ts.map +1 -1
- package/dist/script.js +24 -0
- package/dist/script.js.map +1 -1
- package/dist/signatories.d.ts +10 -2
- package/dist/signatories.d.ts.map +1 -1
- package/dist/signatories.js +156 -1
- package/dist/signatories.js.map +1 -1
- package/dist/tx.d.ts +40 -0
- package/dist/tx.d.ts.map +1 -1
- package/dist/tx.js +223 -0
- package/dist/tx.js.map +1 -1
- package/package.json +1 -1
- package/src/consts.ts +13 -0
- package/src/ffi/ecash_lib_wasm_bg_browser.js +20489 -20489
- package/src/ffi/ecash_lib_wasm_bg_browser.wasm +0 -0
- package/src/ffi/ecash_lib_wasm_bg_nodejs.wasm +0 -0
- package/src/psbt.ts +328 -10
- package/src/script.ts +25 -1
- package/src/signatories.ts +214 -3
- package/src/tx.ts +279 -0
|
Binary file
|
|
Binary file
|
package/src/psbt.ts
CHANGED
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
* Partially Signed Bitcoin Transaction (PSBT) per **BIP 174**:
|
|
7
7
|
* - Spec: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* `{@link Psbt.toBytes}`)
|
|
11
|
-
*
|
|
12
|
-
* preservation of unknown pairs (BIP 174).
|
|
9
|
+
* Decode/encode aligned with Bitcoin ABC (`{@link Psbt.fromBytes}`,
|
|
10
|
+
* `{@link Psbt.toBytes}`), plus multisig workflows: per-input `PSBT_IN_UTXO`
|
|
11
|
+
* (`0x00`), redeem script, partial signatures; unknown pairs preserved (BIP 174).
|
|
13
12
|
*
|
|
14
13
|
* **Input key `0x00` (`PSBT_IN_UTXO`):** BIP 174 also defines type `0x01` (“witness
|
|
15
14
|
* UTXO”) for the same *value* shape. **Bitcoin ABC only implements `0x00`:** value
|
|
16
15
|
* is `CTxOut` or full previous tx (non-witness UTXO). We match the node; `0x01`
|
|
17
16
|
* entries are preserved as unknown keys on round-trip.
|
|
17
|
+
*
|
|
18
|
+
* High-level merge/sign helpers: {@link Tx.addMultisigSignature},
|
|
19
|
+
* {@link Tx.addMultisigSignatureFromKey}; {@link Psbt.fromTx} builds a PSBT from a
|
|
20
|
+
* partially signed {@link Tx}; {@link Psbt.toTx} rebuilds scriptSigs from partial sigs.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import { Bytes } from './io/bytes.js';
|
|
@@ -23,9 +26,19 @@ import { readVarSize, writeVarSize } from './io/varsize.js';
|
|
|
23
26
|
import { WriterBytes } from './io/writerbytes.js';
|
|
24
27
|
import { WriterLength } from './io/writerlength.js';
|
|
25
28
|
import { Writer } from './io/writer.js';
|
|
26
|
-
import {
|
|
29
|
+
import { Ecc } from './ecc.js';
|
|
30
|
+
import { shaRmd160, sha256d } from './hash.js';
|
|
27
31
|
import { Script } from './script.js';
|
|
28
|
-
import {
|
|
32
|
+
import { ALL_BIP143, type SigHashType } from './sigHashType.js';
|
|
33
|
+
import {
|
|
34
|
+
copyTxInput,
|
|
35
|
+
copyTxOutput,
|
|
36
|
+
OutPoint,
|
|
37
|
+
SignData,
|
|
38
|
+
Tx,
|
|
39
|
+
TxInput,
|
|
40
|
+
} from './tx.js';
|
|
41
|
+
import { UnsignedTx } from './unsignedTx.js';
|
|
29
42
|
|
|
30
43
|
/**
|
|
31
44
|
* BIP 174 **magic bytes** for PSBT version 0: ASCII `psbt` + `0xff`.
|
|
@@ -206,6 +219,20 @@ function validateBip32DerivationKeyValue(
|
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
|
|
222
|
+
/** Strip input scripts for PSBT global unsigned transaction (BIP 174). */
|
|
223
|
+
export function txToUnsigned(tx: Tx): Tx {
|
|
224
|
+
return new Tx({
|
|
225
|
+
version: tx.version,
|
|
226
|
+
inputs: tx.inputs.map(inp => ({
|
|
227
|
+
prevOut: inp.prevOut,
|
|
228
|
+
script: new Script(),
|
|
229
|
+
sequence: inp.sequence,
|
|
230
|
+
})),
|
|
231
|
+
outputs: tx.outputs.map(o => copyTxOutput(o)),
|
|
232
|
+
locktime: tx.locktime,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
209
236
|
/** `CTxOut` bytes for a {@link PSBT_IN_UTXO} map value (same layout BIP 174 labels “witness UTXO”). */
|
|
210
237
|
function encodeWitnessUtxo(sats: bigint, scriptPubKey: Uint8Array): Uint8Array {
|
|
211
238
|
const wl = new WriterLength();
|
|
@@ -279,6 +306,178 @@ function scriptPubKeyFromSignData(signData: SignData): Uint8Array {
|
|
|
279
306
|
throw new Error('SignData needs redeemScript or outputScript for PSBT');
|
|
280
307
|
}
|
|
281
308
|
|
|
309
|
+
/**
|
|
310
|
+
* The multisig **redeem** script used to interpret partial sigs: P2SH `redeemScript`,
|
|
311
|
+
* or bare `outputScript` (the locking script is the multisig template itself).
|
|
312
|
+
*/
|
|
313
|
+
function multisigLockingScript(signData: SignData): Script {
|
|
314
|
+
if (signData.redeemScript !== undefined) {
|
|
315
|
+
return signData.redeemScript;
|
|
316
|
+
}
|
|
317
|
+
if (signData.outputScript !== undefined) {
|
|
318
|
+
return signData.outputScript;
|
|
319
|
+
}
|
|
320
|
+
throw new Error('SignData needs redeemScript or outputScript');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Build the input `scriptSig` for a multisig spend from the PSBT partial-signature
|
|
325
|
+
* map (pubkey hex → signature with sighash byte). Used by {@link Psbt.toTx} only.
|
|
326
|
+
*
|
|
327
|
+
* Matches pubkey order from {@link multisigLockingScript}'s `parseMultisigRedeemScript`,
|
|
328
|
+
* then dispatches:
|
|
329
|
+
* - **Schnorr** if any partial sig has length 65 (Schnorr + sighash): uses
|
|
330
|
+
* {@link Script.multisigSpend} with `pubkeyIndices` derived from which pubkeys
|
|
331
|
+
* have sigs (bare passes `numPubkeys`; P2SH passes `redeemScript`).
|
|
332
|
+
* - **ECDSA** otherwise: fills `m` slots left-to-right from present sigs (same
|
|
333
|
+
* convention as {@link BareMultisigSignatory} / {@link P2SHMultisigSignatory}).
|
|
334
|
+
*/
|
|
335
|
+
function buildScriptSigFromPartialSigs(
|
|
336
|
+
signData: SignData,
|
|
337
|
+
partialSigs: Map<string, Uint8Array>,
|
|
338
|
+
): Script {
|
|
339
|
+
const lock = multisigLockingScript(signData);
|
|
340
|
+
const { numSignatures, numPubkeys, pubkeys } =
|
|
341
|
+
lock.parseMultisigRedeemScript();
|
|
342
|
+
const perPk = pubkeys.map(pk => partialSigs.get(pubkeyHex(pk)));
|
|
343
|
+
const signatures = perPk.filter((s): s is Uint8Array => s !== undefined);
|
|
344
|
+
|
|
345
|
+
const anySchnorr = [...partialSigs.values()].some(s => s.length === 65);
|
|
346
|
+
if (anySchnorr) {
|
|
347
|
+
const pubkeyIndices = new Set(
|
|
348
|
+
perPk.flatMap((s, i) => (s !== undefined ? [i] : [])),
|
|
349
|
+
);
|
|
350
|
+
if (signData.redeemScript !== undefined) {
|
|
351
|
+
return Script.multisigSpend({
|
|
352
|
+
signatures,
|
|
353
|
+
redeemScript: signData.redeemScript,
|
|
354
|
+
pubkeyIndices,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return Script.multisigSpend({
|
|
358
|
+
signatures,
|
|
359
|
+
pubkeyIndices,
|
|
360
|
+
numPubkeys,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const sigsForScript: (Uint8Array | undefined)[] =
|
|
365
|
+
signatures.length >= numSignatures
|
|
366
|
+
? signatures.slice(0, numSignatures)
|
|
367
|
+
: [
|
|
368
|
+
...signatures,
|
|
369
|
+
...Array(numSignatures - signatures.length).fill(undefined),
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
return Script.multisigSpend({
|
|
373
|
+
signatures: sigsForScript,
|
|
374
|
+
redeemScript: signData.redeemScript,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Extract pubkey → signature (with sighash byte) from a multisig scriptSig.
|
|
380
|
+
*/
|
|
381
|
+
function extractPartialSigsFromInput(
|
|
382
|
+
script: Script,
|
|
383
|
+
signData: SignData,
|
|
384
|
+
unsignedTx: Tx,
|
|
385
|
+
inputIdx: number,
|
|
386
|
+
ecc: Ecc,
|
|
387
|
+
): Map<string, Uint8Array> {
|
|
388
|
+
const map = new Map<string, Uint8Array>();
|
|
389
|
+
if (!script.bytecode.length) return map;
|
|
390
|
+
|
|
391
|
+
const inputs: TxInput[] = unsignedTx.inputs.map((inp, i) => ({
|
|
392
|
+
...copyTxInput(inp),
|
|
393
|
+
signData: i === inputIdx ? signData : undefined,
|
|
394
|
+
}));
|
|
395
|
+
inputs[inputIdx] = {
|
|
396
|
+
...copyTxInput(unsignedTx.inputs[inputIdx]!),
|
|
397
|
+
script,
|
|
398
|
+
signData,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const txForPreimage = new Tx({
|
|
402
|
+
version: unsignedTx.version,
|
|
403
|
+
inputs,
|
|
404
|
+
outputs: unsignedTx.outputs.map(copyTxOutput),
|
|
405
|
+
locktime: unsignedTx.locktime,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const msgHash = sha256d(
|
|
409
|
+
UnsignedTx.fromTx(txForPreimage)
|
|
410
|
+
.inputAt(inputIdx)
|
|
411
|
+
.sigHashPreimage(ALL_BIP143).bytes,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (signData.redeemScript !== undefined) {
|
|
415
|
+
const parsed = script.parseP2shMultisigSpend();
|
|
416
|
+
if (parsed.isSchnorr) {
|
|
417
|
+
const sorted = [...(parsed.pubkeyIndices ?? [])].sort(
|
|
418
|
+
(a, b) => a - b,
|
|
419
|
+
);
|
|
420
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
421
|
+
const pkIdx = sorted[i]!;
|
|
422
|
+
const pk = parsed.pubkeys[pkIdx]!;
|
|
423
|
+
const sig = parsed.signatures[i];
|
|
424
|
+
if (sig !== undefined) map.set(pubkeyHex(pk), sig);
|
|
425
|
+
}
|
|
426
|
+
return map;
|
|
427
|
+
}
|
|
428
|
+
for (const sig of parsed.signatures) {
|
|
429
|
+
if (sig === undefined) continue;
|
|
430
|
+
const sigNo = sig.slice(0, -1);
|
|
431
|
+
for (const pk of parsed.pubkeys) {
|
|
432
|
+
try {
|
|
433
|
+
ecc.ecdsaVerify(sigNo, msgHash, pk);
|
|
434
|
+
map.set(pubkeyHex(pk), sig);
|
|
435
|
+
break;
|
|
436
|
+
} catch {
|
|
437
|
+
/* next */
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return map;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (signData.outputScript !== undefined) {
|
|
445
|
+
const parsed = script.parseBareMultisigSpend(signData.outputScript);
|
|
446
|
+
if (parsed.isSchnorr) {
|
|
447
|
+
const sorted = [...(parsed.pubkeyIndices ?? [])].sort(
|
|
448
|
+
(a, b) => a - b,
|
|
449
|
+
);
|
|
450
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
451
|
+
const pkIdx = sorted[i]!;
|
|
452
|
+
const pk = parsed.pubkeys[pkIdx]!;
|
|
453
|
+
const sig = parsed.signatures[i];
|
|
454
|
+
if (sig !== undefined) map.set(pubkeyHex(pk), sig);
|
|
455
|
+
}
|
|
456
|
+
return map;
|
|
457
|
+
}
|
|
458
|
+
for (const sig of parsed.signatures) {
|
|
459
|
+
if (sig === undefined) continue;
|
|
460
|
+
const sigNo = sig.slice(0, -1);
|
|
461
|
+
for (const pk of parsed.pubkeys) {
|
|
462
|
+
try {
|
|
463
|
+
ecc.ecdsaVerify(sigNo, msgHash, pk);
|
|
464
|
+
map.set(pubkeyHex(pk), sig);
|
|
465
|
+
break;
|
|
466
|
+
} catch {
|
|
467
|
+
/* next */
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return map;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface PsbtInputMaps {
|
|
476
|
+
witnessUtxo: { sats: bigint; scriptPubKey: Uint8Array };
|
|
477
|
+
redeemScript?: Script;
|
|
478
|
+
partialSigs: Map<string, Uint8Array>;
|
|
479
|
+
}
|
|
480
|
+
|
|
282
481
|
/** One PSBT key-value pair (BIP 174). */
|
|
283
482
|
export type PsbtKeyValue = { key: Uint8Array; value: Uint8Array };
|
|
284
483
|
|
|
@@ -361,16 +560,17 @@ function parseInputMapPairs(
|
|
|
361
560
|
}
|
|
362
561
|
|
|
363
562
|
/**
|
|
364
|
-
* BIP 174 PSBT
|
|
563
|
+
* BIP 174 PSBT for eCash multisig and ABC-aligned decode/encode.
|
|
365
564
|
* See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#serialization
|
|
366
565
|
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
566
|
+
* Typical flow: {@link Psbt.fromTx} from a partially signed {@link Tx} plus
|
|
567
|
+
* {@link SignData} per input → {@link Psbt.toBytes} → share → {@link Psbt.fromBytes}.
|
|
568
|
+
* Unknown key-value pairs are preserved (BIP 174).
|
|
369
569
|
*/
|
|
370
570
|
export class Psbt {
|
|
371
571
|
/** Unsigned transaction (empty scriptSigs). */
|
|
372
572
|
public readonly unsignedTx: Tx;
|
|
373
|
-
/** Per-input data
|
|
573
|
+
/** Per-input signing data (amount, scripts, partial sig maps). */
|
|
374
574
|
public readonly signDataPerInput: SignData[];
|
|
375
575
|
/** Per-input partial signatures: hex(pubkey) → signature incl. sighash byte. */
|
|
376
576
|
public readonly inputPartialSigs: Map<string, Uint8Array>[];
|
|
@@ -434,6 +634,35 @@ export class Psbt {
|
|
|
434
634
|
}
|
|
435
635
|
}
|
|
436
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Build a PSBT from a transaction that may already include partial scriptSigs.
|
|
639
|
+
* Populates `PSBT_IN_UTXO` (`0x00`) + redeem script + partial signatures.
|
|
640
|
+
*/
|
|
641
|
+
public static fromTx(tx: Tx, signDataPerInput: SignData[], ecc: Ecc): Psbt {
|
|
642
|
+
const unsignedTx = txToUnsigned(tx);
|
|
643
|
+
const inputPartialSigs: Map<string, Uint8Array>[] = [];
|
|
644
|
+
for (let i = 0; i < tx.inputs.length; i++) {
|
|
645
|
+
const script = tx.inputs[i]?.script ?? new Script();
|
|
646
|
+
const sigs = extractPartialSigsFromInput(
|
|
647
|
+
script,
|
|
648
|
+
signDataPerInput[i]!,
|
|
649
|
+
unsignedTx,
|
|
650
|
+
i,
|
|
651
|
+
ecc,
|
|
652
|
+
);
|
|
653
|
+
inputPartialSigs.push(sigs);
|
|
654
|
+
}
|
|
655
|
+
return new Psbt({
|
|
656
|
+
unsignedTx,
|
|
657
|
+
signDataPerInput,
|
|
658
|
+
inputPartialSigs,
|
|
659
|
+
unknownGlobalPairs: [],
|
|
660
|
+
unknownInputPairs: unsignedTx.inputs.map(() => []),
|
|
661
|
+
unknownOutputPairs: unsignedTx.outputs.map(() => []),
|
|
662
|
+
inputWitnessIncomplete: unsignedTx.inputs.map(() => false),
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
437
666
|
/** Deserialize PSBT bytes (BIP 174). */
|
|
438
667
|
public static fromBytes(data: Uint8Array): Psbt {
|
|
439
668
|
const bytes = new Bytes(data);
|
|
@@ -587,4 +816,93 @@ export class Psbt {
|
|
|
587
816
|
}
|
|
588
817
|
return w.data;
|
|
589
818
|
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Current transaction with scriptSigs built from partial signatures.
|
|
822
|
+
* Attach each input's `signData` for signing and validation helpers.
|
|
823
|
+
*/
|
|
824
|
+
public toTx(): Tx {
|
|
825
|
+
return new Tx({
|
|
826
|
+
version: this.unsignedTx.version,
|
|
827
|
+
inputs: this.unsignedTx.inputs.map((inp, i) => {
|
|
828
|
+
const sd = this.signDataPerInput[i]!;
|
|
829
|
+
const script = buildScriptSigFromPartialSigs(
|
|
830
|
+
sd,
|
|
831
|
+
this.inputPartialSigs[i]!,
|
|
832
|
+
);
|
|
833
|
+
return {
|
|
834
|
+
...copyTxInput(inp),
|
|
835
|
+
script,
|
|
836
|
+
signData: sd,
|
|
837
|
+
};
|
|
838
|
+
}),
|
|
839
|
+
outputs: this.unsignedTx.outputs.map(copyTxOutput),
|
|
840
|
+
locktime: this.unsignedTx.locktime,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Add or merge a multisig signature on an input (same semantics as
|
|
846
|
+
* {@link Tx.addMultisigSignature}).
|
|
847
|
+
*/
|
|
848
|
+
public addMultisigSignature(params: {
|
|
849
|
+
inputIdx: number;
|
|
850
|
+
signature: Uint8Array;
|
|
851
|
+
signData: SignData;
|
|
852
|
+
ecc?: Ecc;
|
|
853
|
+
}): Psbt {
|
|
854
|
+
const tx = this.toTx();
|
|
855
|
+
const nextTx = tx.addMultisigSignature(params);
|
|
856
|
+
const next = Psbt.fromTx(
|
|
857
|
+
nextTx,
|
|
858
|
+
this.signDataPerInput,
|
|
859
|
+
params.ecc ?? new Ecc(),
|
|
860
|
+
);
|
|
861
|
+
return new Psbt({
|
|
862
|
+
unsignedTx: next.unsignedTx,
|
|
863
|
+
signDataPerInput: next.signDataPerInput,
|
|
864
|
+
inputPartialSigs: next.inputPartialSigs,
|
|
865
|
+
unknownGlobalPairs: this.unknownGlobalPairs,
|
|
866
|
+
unknownInputPairs: this.unknownInputPairs,
|
|
867
|
+
unknownOutputPairs: this.unknownOutputPairs,
|
|
868
|
+
inputWitnessIncomplete: this.inputWitnessIncomplete,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Like {@link addMultisigSignature}, but signs with a secret key (see
|
|
874
|
+
* {@link Tx.addMultisigSignatureFromKey}).
|
|
875
|
+
*/
|
|
876
|
+
public addMultisigSignatureFromKey(params: {
|
|
877
|
+
inputIdx: number;
|
|
878
|
+
sk: Uint8Array;
|
|
879
|
+
signData: SignData;
|
|
880
|
+
sigHashType?: SigHashType;
|
|
881
|
+
ecc?: Ecc;
|
|
882
|
+
}): Psbt {
|
|
883
|
+
const tx = this.toTx();
|
|
884
|
+
const nextTx = tx.addMultisigSignatureFromKey(params);
|
|
885
|
+
const next = Psbt.fromTx(
|
|
886
|
+
nextTx,
|
|
887
|
+
this.signDataPerInput,
|
|
888
|
+
params.ecc ?? new Ecc(),
|
|
889
|
+
);
|
|
890
|
+
return new Psbt({
|
|
891
|
+
unsignedTx: next.unsignedTx,
|
|
892
|
+
signDataPerInput: next.signDataPerInput,
|
|
893
|
+
inputPartialSigs: next.inputPartialSigs,
|
|
894
|
+
unknownGlobalPairs: this.unknownGlobalPairs,
|
|
895
|
+
unknownInputPairs: this.unknownInputPairs,
|
|
896
|
+
unknownOutputPairs: this.unknownOutputPairs,
|
|
897
|
+
inputWitnessIncomplete: this.inputWitnessIncomplete,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Same as {@link Tx.isFullySignedMultisig} on {@link toTx} (including
|
|
903
|
+
* vacuous `true` when there are no multisig inputs).
|
|
904
|
+
*/
|
|
905
|
+
public isFullySignedMultisig(): boolean {
|
|
906
|
+
return this.toTx().isFullySignedMultisig();
|
|
907
|
+
}
|
|
590
908
|
}
|
package/src/script.ts
CHANGED
|
@@ -216,6 +216,29 @@ export class Script {
|
|
|
216
216
|
return Script.fromOps(ops);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Return true iff this script has the shape of a standard multisig output script
|
|
221
|
+
* `OP_m pubkey_1 ... pubkey_N OP_N OP_CHECKMULTISIG` (fixed size: `N + 3` ops).
|
|
222
|
+
*/
|
|
223
|
+
public isMultisig(): boolean {
|
|
224
|
+
const ops: Op[] = [];
|
|
225
|
+
const iter = this.ops();
|
|
226
|
+
let op: Op | undefined;
|
|
227
|
+
while ((op = iter.next()) !== undefined) {
|
|
228
|
+
ops.push(op);
|
|
229
|
+
}
|
|
230
|
+
if (ops.length < 4 || ops[ops.length - 1] !== OP_CHECKMULTISIG) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
parseNumberFromOp(ops[0]!);
|
|
235
|
+
const numPubkeys = Number(parseNumberFromOp(ops[ops.length - 2]!));
|
|
236
|
+
return ops.length === numPubkeys + 3;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
219
242
|
/**
|
|
220
243
|
* Build scriptSig for multisig: <dummy> sig1 sig2 ... sig_m [redeemScript].
|
|
221
244
|
* Omit redeemScript for bare multisig; include for P2SH-wrapped.
|
|
@@ -346,7 +369,8 @@ export class Script {
|
|
|
346
369
|
return Script.parseMultisigScriptSig(ops, outputScript);
|
|
347
370
|
}
|
|
348
371
|
|
|
349
|
-
|
|
372
|
+
/** Parse an OP_m ... OP_CHECKMULTISIG redeem script (used by multisig PSBT flows). */
|
|
373
|
+
public parseMultisigRedeemScript(): {
|
|
350
374
|
numSignatures: number;
|
|
351
375
|
numPubkeys: number;
|
|
352
376
|
pubkeys: Uint8Array[];
|
package/src/signatories.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* Signatories and signing helpers used with `TxBuilder` (`TxBuilderInput.signatory`).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { Ecc } from './ecc.js';
|
|
9
|
+
import { Ecc, EccDummy } from './ecc.js';
|
|
10
|
+
import {
|
|
11
|
+
ECDSA_SIG_ESTIMATE_BYTES,
|
|
12
|
+
SCHNORR_SIG_ESTIMATE_BYTES,
|
|
13
|
+
} from './consts.js';
|
|
10
14
|
import { sha256d } from './hash.js';
|
|
11
15
|
import { WriterBytes } from './io/writerbytes.js';
|
|
12
16
|
import { pushBytesOp } from './op.js';
|
|
@@ -65,7 +69,7 @@ export const P2PKHSignatory = (
|
|
|
65
69
|
sk: Uint8Array,
|
|
66
70
|
pk: Uint8Array,
|
|
67
71
|
sigHashType: SigHashType,
|
|
68
|
-
) => {
|
|
72
|
+
): Signatory => {
|
|
69
73
|
return (ecc: Ecc, input: UnsignedTxInput): Script => {
|
|
70
74
|
const preimage = input.sigHashPreimage(sigHashType);
|
|
71
75
|
const sighash = sha256d(preimage.bytes);
|
|
@@ -74,8 +78,215 @@ export const P2PKHSignatory = (
|
|
|
74
78
|
};
|
|
75
79
|
};
|
|
76
80
|
|
|
81
|
+
/** Signatory for bare m-of-n multisig (output script is the multisig script itself). */
|
|
82
|
+
export const BareMultisigSignatory = (
|
|
83
|
+
m: number,
|
|
84
|
+
pubkeys: Uint8Array[],
|
|
85
|
+
sk: Uint8Array,
|
|
86
|
+
myPk: Uint8Array,
|
|
87
|
+
sigHashType: SigHashType,
|
|
88
|
+
): Signatory => {
|
|
89
|
+
return (ecc: Ecc, input: UnsignedTxInput): Script => {
|
|
90
|
+
if (ecc instanceof EccDummy) {
|
|
91
|
+
const dummySig = new Uint8Array(ECDSA_SIG_ESTIMATE_BYTES);
|
|
92
|
+
const signatures = Array(m)
|
|
93
|
+
.fill(undefined)
|
|
94
|
+
.map(() => dummySig);
|
|
95
|
+
return Script.multisigSpend({ signatures });
|
|
96
|
+
}
|
|
97
|
+
const preimage = input.sigHashPreimage(sigHashType);
|
|
98
|
+
const sighash = sha256d(preimage.bytes);
|
|
99
|
+
const sig = ecc.ecdsaSign(sk, sighash);
|
|
100
|
+
const sigFlagged = flagSignature(sig, sigHashType);
|
|
101
|
+
const myIndex = pubkeys.findIndex(
|
|
102
|
+
pk =>
|
|
103
|
+
pk.length === myPk.length && pk.every((b, i) => b === myPk[i]),
|
|
104
|
+
);
|
|
105
|
+
if (myIndex < 0) {
|
|
106
|
+
throw new Error('Signer pubkey not found in multisig pubkeys');
|
|
107
|
+
}
|
|
108
|
+
const n = pubkeys.length;
|
|
109
|
+
const signatures: (Uint8Array | undefined)[] = Array(n).fill(undefined);
|
|
110
|
+
signatures[myIndex] = sigFlagged;
|
|
111
|
+
const nonNull = signatures.filter(
|
|
112
|
+
(s): s is Uint8Array => s !== undefined,
|
|
113
|
+
);
|
|
114
|
+
const sigsForScript: (Uint8Array | undefined)[] = [
|
|
115
|
+
...nonNull,
|
|
116
|
+
...Array(m - nonNull.length).fill(undefined),
|
|
117
|
+
];
|
|
118
|
+
return Script.multisigSpend({ signatures: sigsForScript });
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** Signatory for bare m-of-n multisig using Schnorr signatures (BIP143 Schnorr multisig). */
|
|
123
|
+
export const BareMultisigSignatorySchnorr = (
|
|
124
|
+
m: number,
|
|
125
|
+
pubkeys: Uint8Array[],
|
|
126
|
+
sk: Uint8Array,
|
|
127
|
+
myPk: Uint8Array,
|
|
128
|
+
signerIndices: Set<number>,
|
|
129
|
+
sigHashType: SigHashType,
|
|
130
|
+
): Signatory => {
|
|
131
|
+
return (ecc: Ecc, input: UnsignedTxInput): Script => {
|
|
132
|
+
const n = pubkeys.length;
|
|
133
|
+
if (signerIndices.size !== m) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`signerIndices must have ${m} elements for ${m}-of-${n} multisig`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (ecc instanceof EccDummy) {
|
|
139
|
+
const dummySig = new Uint8Array(SCHNORR_SIG_ESTIMATE_BYTES);
|
|
140
|
+
const signatures = Array(m)
|
|
141
|
+
.fill(undefined)
|
|
142
|
+
.map(() => dummySig);
|
|
143
|
+
return Script.multisigSpend({
|
|
144
|
+
signatures,
|
|
145
|
+
pubkeyIndices: signerIndices,
|
|
146
|
+
numPubkeys: n,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const preimage = input.sigHashPreimage(sigHashType);
|
|
150
|
+
const sighash = sha256d(preimage.bytes);
|
|
151
|
+
const sig = ecc.schnorrSign(sk, sighash);
|
|
152
|
+
const sigFlagged = flagSignature(sig, sigHashType);
|
|
153
|
+
const myIndex = pubkeys.findIndex(
|
|
154
|
+
pk =>
|
|
155
|
+
pk.length === myPk.length && pk.every((b, i) => b === myPk[i]),
|
|
156
|
+
);
|
|
157
|
+
if (myIndex < 0) {
|
|
158
|
+
throw new Error('Signer pubkey not found in multisig pubkeys');
|
|
159
|
+
}
|
|
160
|
+
if (!signerIndices.has(myIndex)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Signer index ${myIndex} not in signerIndices ${[
|
|
163
|
+
...signerIndices,
|
|
164
|
+
].join(',')}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const sortedIndices = [...signerIndices].sort((a, b) => a - b);
|
|
168
|
+
const signatures: (Uint8Array | undefined)[] = sortedIndices.map(idx =>
|
|
169
|
+
idx === myIndex ? sigFlagged : undefined,
|
|
170
|
+
);
|
|
171
|
+
return Script.multisigSpend({
|
|
172
|
+
signatures,
|
|
173
|
+
pubkeyIndices: signerIndices,
|
|
174
|
+
numPubkeys: n,
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** Signatory for P2SH m-of-n multisig. */
|
|
180
|
+
export const P2SHMultisigSignatory = (
|
|
181
|
+
m: number,
|
|
182
|
+
pubkeys: Uint8Array[],
|
|
183
|
+
sk: Uint8Array,
|
|
184
|
+
myPk: Uint8Array,
|
|
185
|
+
sigHashType: SigHashType,
|
|
186
|
+
): Signatory => {
|
|
187
|
+
return (ecc: Ecc, input: UnsignedTxInput): Script => {
|
|
188
|
+
const redeemScript = Script.multisig(m, pubkeys);
|
|
189
|
+
if (ecc instanceof EccDummy) {
|
|
190
|
+
const dummySig = new Uint8Array(ECDSA_SIG_ESTIMATE_BYTES);
|
|
191
|
+
const signatures = Array(m)
|
|
192
|
+
.fill(undefined)
|
|
193
|
+
.map(() => dummySig);
|
|
194
|
+
return Script.multisigSpend({
|
|
195
|
+
signatures,
|
|
196
|
+
redeemScript,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const preimage = input.sigHashPreimage(sigHashType);
|
|
200
|
+
const sighash = sha256d(preimage.bytes);
|
|
201
|
+
const sig = ecc.ecdsaSign(sk, sighash);
|
|
202
|
+
const sigFlagged = flagSignature(sig, sigHashType);
|
|
203
|
+
const myIndex = pubkeys.findIndex(
|
|
204
|
+
pk =>
|
|
205
|
+
pk.length === myPk.length && pk.every((b, i) => b === myPk[i]),
|
|
206
|
+
);
|
|
207
|
+
if (myIndex < 0) {
|
|
208
|
+
throw new Error('Signer pubkey not found in multisig pubkeys');
|
|
209
|
+
}
|
|
210
|
+
const n = pubkeys.length;
|
|
211
|
+
const signatures: (Uint8Array | undefined)[] = Array(n).fill(undefined);
|
|
212
|
+
signatures[myIndex] = sigFlagged;
|
|
213
|
+
const nonNull = signatures.filter(
|
|
214
|
+
(s): s is Uint8Array => s !== undefined,
|
|
215
|
+
);
|
|
216
|
+
const sigsForScript: (Uint8Array | undefined)[] = [
|
|
217
|
+
...nonNull,
|
|
218
|
+
...Array(m - nonNull.length).fill(undefined),
|
|
219
|
+
];
|
|
220
|
+
return Script.multisigSpend({
|
|
221
|
+
signatures: sigsForScript,
|
|
222
|
+
redeemScript,
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Signatory for P2SH m-of-n multisig using Schnorr signatures. */
|
|
228
|
+
export const P2SHMultisigSignatorySchnorr = (
|
|
229
|
+
m: number,
|
|
230
|
+
pubkeys: Uint8Array[],
|
|
231
|
+
sk: Uint8Array,
|
|
232
|
+
myPk: Uint8Array,
|
|
233
|
+
signerIndices: Set<number>,
|
|
234
|
+
sigHashType: SigHashType,
|
|
235
|
+
): Signatory => {
|
|
236
|
+
return (ecc: Ecc, input: UnsignedTxInput): Script => {
|
|
237
|
+
const redeemScript = Script.multisig(m, pubkeys);
|
|
238
|
+
const n = pubkeys.length;
|
|
239
|
+
if (signerIndices.size !== m) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`signerIndices must have ${m} elements for ${m}-of-${n} multisig`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (ecc instanceof EccDummy) {
|
|
245
|
+
const dummySig = new Uint8Array(SCHNORR_SIG_ESTIMATE_BYTES);
|
|
246
|
+
const signatures = Array(m)
|
|
247
|
+
.fill(undefined)
|
|
248
|
+
.map(() => dummySig);
|
|
249
|
+
return Script.multisigSpend({
|
|
250
|
+
signatures,
|
|
251
|
+
redeemScript,
|
|
252
|
+
pubkeyIndices: signerIndices,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const preimage = input.sigHashPreimage(sigHashType);
|
|
256
|
+
const sighash = sha256d(preimage.bytes);
|
|
257
|
+
const sig = ecc.schnorrSign(sk, sighash);
|
|
258
|
+
const sigFlagged = flagSignature(sig, sigHashType);
|
|
259
|
+
const myIndex = pubkeys.findIndex(
|
|
260
|
+
pk =>
|
|
261
|
+
pk.length === myPk.length && pk.every((b, i) => b === myPk[i]),
|
|
262
|
+
);
|
|
263
|
+
if (myIndex < 0) {
|
|
264
|
+
throw new Error('Signer pubkey not found in multisig pubkeys');
|
|
265
|
+
}
|
|
266
|
+
if (!signerIndices.has(myIndex)) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Signer index ${myIndex} not in signerIndices ${[
|
|
269
|
+
...signerIndices,
|
|
270
|
+
].join(',')}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const sortedIndices = [...signerIndices].sort((a, b) => a - b);
|
|
274
|
+
const signatures: (Uint8Array | undefined)[] = sortedIndices.map(idx =>
|
|
275
|
+
idx === myIndex ? sigFlagged : undefined,
|
|
276
|
+
);
|
|
277
|
+
return Script.multisigSpend({
|
|
278
|
+
signatures,
|
|
279
|
+
redeemScript,
|
|
280
|
+
pubkeyIndices: signerIndices,
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
77
285
|
/** Signatory for a P2PK input. Always uses Schnorr signatures */
|
|
78
|
-
export const P2PKSignatory = (
|
|
286
|
+
export const P2PKSignatory = (
|
|
287
|
+
sk: Uint8Array,
|
|
288
|
+
sigHashType: SigHashType,
|
|
289
|
+
): Signatory => {
|
|
79
290
|
return (ecc: Ecc, input: UnsignedTxInput): Script => {
|
|
80
291
|
const preimage = input.sigHashPreimage(sigHashType);
|
|
81
292
|
const sighash = sha256d(preimage.bytes);
|