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/src/tx.ts CHANGED
@@ -8,8 +8,12 @@ import { writeVarSize, readVarSize } from './io/varsize.js';
8
8
  import { Writer } from './io/writer.js';
9
9
  import { WriterBytes } from './io/writerbytes.js';
10
10
  import { WriterLength } from './io/writerlength.js';
11
+ import { Ecc } from './ecc.js';
11
12
  import { Script } from './script.js';
12
13
  import { sha256d } from './hash.js';
14
+ import { flagSignature } from './signatories.js';
15
+ import { ALL_BIP143, SigHashType } from './sigHashType.js';
16
+ import { UnsignedTx } from './unsignedTx.js';
13
17
 
14
18
  /**
15
19
  * Default value for nSequence of inputs if left undefined; this opts out of
@@ -226,6 +230,281 @@ export class Tx {
226
230
  return undefined;
227
231
  }
228
232
  }
233
+
234
+ /**
235
+ * Add a signature to a partially-signed multisig input.
236
+ * Verifies the signature against the sighash for each pubkey in the
237
+ * redeem/output script and merges with existing signatures (which pubkey
238
+ * signed is inferred from verification).
239
+ */
240
+ public addMultisigSignature(params: {
241
+ inputIdx: number;
242
+ signature: Uint8Array;
243
+ signData: SignData;
244
+ ecc?: Ecc;
245
+ }): Tx {
246
+ const { inputIdx, signature, signData } = params;
247
+ const ecc = params.ecc ?? new Ecc();
248
+ const input = this.inputs[inputIdx];
249
+ if (!input.script || input.script.bytecode.length === 0) {
250
+ throw new Error(
251
+ `Input ${inputIdx} has no scriptSig to add signature to`,
252
+ );
253
+ }
254
+ const isBare =
255
+ signData.outputScript !== undefined &&
256
+ signData.redeemScript === undefined;
257
+ const parsed = isBare
258
+ ? input.script.parseBareMultisigSpend(signData.outputScript!)
259
+ : input.script.parseP2shMultisigSpend();
260
+ const txWithSignData = new Tx({
261
+ version: this.version,
262
+ inputs: this.inputs.map((inp, i) =>
263
+ i === inputIdx
264
+ ? { ...copyTxInput(inp), signData }
265
+ : copyTxInput(inp),
266
+ ),
267
+ outputs: this.outputs,
268
+ locktime: this.locktime,
269
+ });
270
+ const unsignedTx = UnsignedTx.fromTx(txWithSignData);
271
+ const inputAt = unsignedTx.inputAt(inputIdx);
272
+ const sigHashType =
273
+ SigHashType.fromInt(
274
+ (signature[signature.length - 1] ?? 0) & 0xff,
275
+ ) ?? ALL_BIP143;
276
+ const preimage = inputAt.sigHashPreimage(sigHashType);
277
+ const sighash = sha256d(preimage.bytes);
278
+ const sigWithoutFlag = signature.slice(0, -1);
279
+
280
+ let pubkeyIndex = -1;
281
+ if (parsed.isSchnorr) {
282
+ for (let i = 0; i < parsed.pubkeys.length; i++) {
283
+ try {
284
+ ecc.schnorrVerify(
285
+ sigWithoutFlag,
286
+ sighash,
287
+ parsed.pubkeys[i]!,
288
+ );
289
+ pubkeyIndex = i;
290
+ break;
291
+ } catch {
292
+ /* try next pubkey */
293
+ }
294
+ }
295
+ if (pubkeyIndex < 0) {
296
+ throw new Error(
297
+ 'Schnorr signature does not verify for any pubkey in the multisig script',
298
+ );
299
+ }
300
+ } else {
301
+ for (let i = 0; i < parsed.pubkeys.length; i++) {
302
+ try {
303
+ ecc.ecdsaVerify(
304
+ sigWithoutFlag,
305
+ sighash,
306
+ parsed.pubkeys[i]!,
307
+ );
308
+ pubkeyIndex = i;
309
+ break;
310
+ } catch {
311
+ /* try next pubkey */
312
+ }
313
+ }
314
+ if (pubkeyIndex < 0) {
315
+ throw new Error(
316
+ 'ECDSA signature does not verify for any pubkey in the multisig script',
317
+ );
318
+ }
319
+ }
320
+
321
+ const sigsByPubkey: (Uint8Array | undefined)[] = Array(
322
+ parsed.pubkeys.length,
323
+ ).fill(undefined);
324
+
325
+ if (parsed.isSchnorr) {
326
+ const indices = parsed.pubkeyIndices!;
327
+ const sortedIndices = [...indices].sort((a, b) => a - b);
328
+ for (let i = 0; i < parsed.signatures.length; i++) {
329
+ const sig = parsed.signatures[i];
330
+ if (sig !== undefined && i < sortedIndices.length) {
331
+ sigsByPubkey[sortedIndices[i]!] = sig;
332
+ }
333
+ }
334
+ } else {
335
+ for (const sig of parsed.signatures) {
336
+ if (sig === undefined) continue;
337
+ const sigNoFlag = sig.slice(0, -1);
338
+ for (let i = 0; i < parsed.pubkeys.length; i++) {
339
+ try {
340
+ ecc.ecdsaVerify(sigNoFlag, sighash, parsed.pubkeys[i]!);
341
+ sigsByPubkey[i] = sig;
342
+ break;
343
+ } catch {
344
+ /* try next pubkey */
345
+ }
346
+ }
347
+ }
348
+ }
349
+ sigsByPubkey[pubkeyIndex] = signature;
350
+
351
+ const nonNullSigs = sigsByPubkey.filter(
352
+ (s): s is Uint8Array => s !== undefined,
353
+ );
354
+ const sigsForScript =
355
+ nonNullSigs.length >= parsed.numSignatures
356
+ ? nonNullSigs.slice(0, parsed.numSignatures)
357
+ : [
358
+ ...nonNullSigs,
359
+ ...Array(parsed.numSignatures - nonNullSigs.length).fill(
360
+ undefined,
361
+ ),
362
+ ];
363
+
364
+ const redeemScript: Script | undefined =
365
+ !isBare && 'redeemScript' in parsed
366
+ ? (parsed as { redeemScript: Script }).redeemScript
367
+ : undefined;
368
+ const newScriptSig = parsed.isSchnorr
369
+ ? (() => {
370
+ const signerIndices = new Set<number>();
371
+ for (
372
+ let i = 0;
373
+ i < parsed.pubkeys.length &&
374
+ signerIndices.size < parsed.numSignatures;
375
+ i++
376
+ ) {
377
+ if (sigsByPubkey[i] !== undefined) signerIndices.add(i);
378
+ }
379
+ return isBare
380
+ ? Script.multisigSpend({
381
+ signatures: sigsForScript,
382
+ pubkeyIndices: signerIndices,
383
+ numPubkeys: parsed.numPubkeys,
384
+ })
385
+ : Script.multisigSpend({
386
+ signatures: sigsForScript,
387
+ redeemScript,
388
+ pubkeyIndices: signerIndices,
389
+ });
390
+ })()
391
+ : Script.multisigSpend({
392
+ signatures: sigsForScript,
393
+ redeemScript,
394
+ });
395
+
396
+ const newInputs = this.inputs.map((inp, i) =>
397
+ i === inputIdx
398
+ ? { ...copyTxInput(inp), script: newScriptSig }
399
+ : copyTxInput(inp),
400
+ );
401
+ return new Tx({
402
+ version: this.version,
403
+ inputs: newInputs,
404
+ outputs: this.outputs,
405
+ locktime: this.locktime,
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Like {@link addMultisigSignature}, but computes the signature from a
411
+ * secret key: BIP143 preimage (or legacy if `sigHashType` is legacy),
412
+ * Schnorr for Schnorr-format multisig spends and ECDSA otherwise.
413
+ */
414
+ public addMultisigSignatureFromKey(params: {
415
+ inputIdx: number;
416
+ sk: Uint8Array;
417
+ signData: SignData;
418
+ /** Defaults to {@link ALL_BIP143}. */
419
+ sigHashType?: SigHashType;
420
+ ecc?: Ecc;
421
+ }): Tx {
422
+ const sigHashType = params.sigHashType ?? ALL_BIP143;
423
+ const ecc = params.ecc ?? new Ecc();
424
+ const { inputIdx, sk, signData } = params;
425
+ const input = this.inputs[inputIdx];
426
+ if (!input.script || input.script.bytecode.length === 0) {
427
+ throw new Error(
428
+ `Input ${inputIdx} has no scriptSig to add signature to`,
429
+ );
430
+ }
431
+ const isBare =
432
+ signData.outputScript !== undefined &&
433
+ signData.redeemScript === undefined;
434
+ const parsed = isBare
435
+ ? input.script.parseBareMultisigSpend(signData.outputScript!)
436
+ : input.script.parseP2shMultisigSpend();
437
+ const txWithSignData = new Tx({
438
+ version: this.version,
439
+ inputs: this.inputs.map((inp, i) =>
440
+ i === inputIdx
441
+ ? { ...copyTxInput(inp), signData }
442
+ : copyTxInput(inp),
443
+ ),
444
+ outputs: this.outputs,
445
+ locktime: this.locktime,
446
+ });
447
+ const unsignedTx = UnsignedTx.fromTx(txWithSignData);
448
+ const preimage = unsignedTx
449
+ .inputAt(inputIdx)
450
+ .sigHashPreimage(sigHashType);
451
+ const sighash = sha256d(preimage.bytes);
452
+ const sig = parsed.isSchnorr
453
+ ? ecc.schnorrSign(sk, sighash)
454
+ : ecc.ecdsaSign(sk, sighash);
455
+ const signature = flagSignature(sig, sigHashType);
456
+ return this.addMultisigSignature({
457
+ inputIdx,
458
+ signature,
459
+ signData,
460
+ ecc,
461
+ });
462
+ }
463
+
464
+ /**
465
+ * Whether every **multisig** input (identified from `signData`) has enough
466
+ * signatures in its scriptSig. Non-multisig inputs are ignored.
467
+ *
468
+ * If the transaction has **no** multisig inputs, this returns `true` (there
469
+ * is nothing multisig-specific left to satisfy). That can look surprising on
470
+ * a non-multisig or otherwise incomplete tx; this helper is **not** a
471
+ * broadcast-readiness check. Call sites are expected to use it only in
472
+ * multisig / PSBT flows where the question is specifically whether multisig
473
+ * inputs still need more signatures (including mixed txs: non-multisig
474
+ * inputs are finalized elsewhere).
475
+ */
476
+ public isFullySignedMultisig(): boolean {
477
+ for (let i = 0; i < this.inputs.length; i++) {
478
+ const input = this.inputs[i];
479
+ const multisigScript =
480
+ input.signData?.redeemScript !== undefined
481
+ ? input.signData.redeemScript
482
+ : input.signData?.outputScript?.isMultisig()
483
+ ? input.signData!.outputScript
484
+ : undefined;
485
+ if (multisigScript === undefined) {
486
+ continue;
487
+ }
488
+ if (!input.script || input.script.bytecode.length === 0) {
489
+ return false;
490
+ }
491
+ try {
492
+ const parsed =
493
+ input.signData?.redeemScript === undefined
494
+ ? input.script.parseBareMultisigSpend(multisigScript)
495
+ : input.script.parseP2shMultisigSpend();
496
+ const sigCount = parsed.signatures.filter(
497
+ s => s !== undefined,
498
+ ).length;
499
+ if (sigCount < parsed.numSignatures) {
500
+ return false;
501
+ }
502
+ } catch {
503
+ return false;
504
+ }
505
+ }
506
+ return true;
507
+ }
229
508
  }
230
509
 
231
510
  export function readTxOutput(bytes: Bytes): TxOutput {