@scure/btc-signer 2.3.0 → 2.4.1
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 +49 -10
- package/index.d.ts +4 -3
- package/index.js +3 -3
- package/musig2.d.ts +17 -3
- package/musig2.js +18 -6
- package/net.js +7 -2
- package/package.json +12 -11
- package/payment.d.ts +16 -5
- package/payment.js +118 -27
- package/psbt.d.ts +680 -9
- package/psbt.js +187 -38
- package/src/_type_test.ts +14 -0
- package/src/index.ts +4 -2
- package/src/musig2.ts +32 -7
- package/src/net.ts +7 -2
- package/src/payment.ts +145 -32
- package/src/psbt.ts +210 -35
- package/src/transaction.ts +790 -136
- package/src/utils.ts +24 -2
- package/src/utxo.ts +185 -71
- package/transaction.d.ts +40 -6
- package/transaction.js +667 -123
- package/utils.d.ts +15 -1
- package/utils.js +21 -2
- package/utxo.d.ts +192 -1
- package/utxo.js +166 -69
package/src/transaction.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
2
|
import { anumber } from '@noble/hashes/utils.js';
|
|
3
3
|
import * as P from 'micro-packed';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Address,
|
|
6
|
+
type CustomScript,
|
|
7
|
+
OutScript,
|
|
8
|
+
_WitnessOutScript,
|
|
9
|
+
checkScript,
|
|
10
|
+
tapLeafHash,
|
|
11
|
+
} from './payment.ts';
|
|
5
12
|
import * as psbt from './psbt.ts';
|
|
6
13
|
import {
|
|
7
14
|
CompactSizeLen,
|
|
@@ -10,6 +17,7 @@ import {
|
|
|
10
17
|
RawInput,
|
|
11
18
|
RawOutput,
|
|
12
19
|
RawTx,
|
|
20
|
+
RawWitness,
|
|
13
21
|
Script,
|
|
14
22
|
scriptPushLen,
|
|
15
23
|
VarBytes,
|
|
@@ -54,7 +62,7 @@ const stripCodeSeparator = (script: TArg<Bytes>): TRet<Bytes> => {
|
|
|
54
62
|
// byte, because semantic decode/re-encode would change the signed digest.
|
|
55
63
|
let start = 0;
|
|
56
64
|
const out: Uint8Array[] = [];
|
|
57
|
-
for (let i = 0; i < script.length;
|
|
65
|
+
for (let i = 0; i < script.length;) {
|
|
58
66
|
const pos = i;
|
|
59
67
|
const op = script[i++];
|
|
60
68
|
if (op === OP.CODESEPARATOR) {
|
|
@@ -164,6 +172,8 @@ export function cloneDeep<T>(obj: T): T {
|
|
|
164
172
|
|
|
165
173
|
// Mostly security features, hardened defaults;
|
|
166
174
|
// but you still can parse other people tx with unspendable outputs and stuff if you want
|
|
175
|
+
/** PSBT unknown/proprietary-field handling policy. */
|
|
176
|
+
export type Unknowns = psbt.Unknowns;
|
|
167
177
|
/** Transaction construction and parsing options. */
|
|
168
178
|
export interface TxOpts {
|
|
169
179
|
/** Transaction version to place into new transactions and imported PSBTs. */
|
|
@@ -193,7 +203,7 @@ export interface TxOpts {
|
|
|
193
203
|
/** Allow signing and finalizing inputs with unknown script shapes. */
|
|
194
204
|
allowUnknownInputs?: boolean;
|
|
195
205
|
// Check input/output scripts for sanity
|
|
196
|
-
/** Skip redeem-script and
|
|
206
|
+
/** Skip redeem/witness-script and Taproot commitment consistency checks. */
|
|
197
207
|
disableScriptCheck?: boolean;
|
|
198
208
|
// There is strange behaviour where tx without outputs encoded with empty output in the end,
|
|
199
209
|
// tx without outputs in BIP174 doesn't have itb
|
|
@@ -203,12 +213,30 @@ export interface TxOpts {
|
|
|
203
213
|
// result paying higher mining fee
|
|
204
214
|
/** Permit legacy inputs that only provide witness UTXO data. */
|
|
205
215
|
allowLegacyWitnessUtxo?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Before signing, require every input to provide a full previous transaction whose txid and
|
|
218
|
+
* selected output match the input. Use this for untrusted or multi-party PSBTs to prevent forged
|
|
219
|
+
* witness-UTXO amounts from hiding an excessive transaction fee.
|
|
220
|
+
*/
|
|
221
|
+
strictPrevoutValidation?: boolean;
|
|
206
222
|
/** Grind ECDSA signatures until they use a low-R encoding. */
|
|
207
223
|
lowR?: boolean;
|
|
208
224
|
/** UNSAFE: additional custom payment-script codecs and finalizers. */
|
|
209
225
|
customScripts?: CustomScript[];
|
|
210
|
-
|
|
211
|
-
|
|
226
|
+
/** Unknown PSBT field policy. Defaults to `strip`. */
|
|
227
|
+
unknown?: Unknowns;
|
|
228
|
+
/** Proprietary PSBT field policy. Defaults to the resolved {@link unknown} policy. */
|
|
229
|
+
proprietary?: Unknowns;
|
|
230
|
+
/**
|
|
231
|
+
* Treat an absent PSBTv2 transaction-modifiable field as allowing input/output changes. Older
|
|
232
|
+
* scure versions emitted PSBTv2 without this field, so this opts out of strict BIP370 behavior
|
|
233
|
+
* when upgrading and editing their PSBTs.
|
|
234
|
+
*/
|
|
235
|
+
allowMissingTxModifiable?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Deprecated alias for {@link unknown}: true selects `ignore`, false selects `strip`.
|
|
238
|
+
* @deprecated Use `unknown`.
|
|
239
|
+
*/
|
|
212
240
|
allowUnknown?: boolean;
|
|
213
241
|
}
|
|
214
242
|
|
|
@@ -321,13 +349,65 @@ export function inputBeforeSign(i: TArg<psbt.TransactionInput>): TRet<Transactio
|
|
|
321
349
|
RawInput.encode(res);
|
|
322
350
|
return res as TRet<TransactionInputRequired>;
|
|
323
351
|
}
|
|
324
|
-
|
|
352
|
+
type ExtensionMap = { unknown?: unknown[]; proprietary?: unknown[] };
|
|
353
|
+
const cleanExtensions = <T extends ExtensionMap>(
|
|
354
|
+
map: T,
|
|
355
|
+
unknownMode: Unknowns,
|
|
356
|
+
proprietaryMode: Unknowns,
|
|
357
|
+
rejectStrip = false
|
|
358
|
+
): T => {
|
|
359
|
+
const out = { ...map } as T & Record<string, unknown>;
|
|
360
|
+
for (const [name, mode] of [
|
|
361
|
+
['unknown', unknownMode],
|
|
362
|
+
['proprietary', proprietaryMode],
|
|
363
|
+
] as const) {
|
|
364
|
+
const value = out[name];
|
|
365
|
+
// Policy cleanup must not reinterpret malformed caller metadata as an empty keyed map.
|
|
366
|
+
if (value !== undefined) u.aarray(value, `${name} PSBT field`);
|
|
367
|
+
const rows = value as unknown[] | undefined;
|
|
368
|
+
if (!rows?.length) {
|
|
369
|
+
delete out[name];
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (mode === 'strict')
|
|
373
|
+
throw new Error(`PSBT: ${name} PSBT field is not allowed in strict mode`);
|
|
374
|
+
if (mode === 'strip') {
|
|
375
|
+
// Silent stripping is appropriate at relay/cleanup boundaries. On direct mutation it would
|
|
376
|
+
// hide a caller bug by accepting metadata that can never become transaction state.
|
|
377
|
+
if (rejectStrip)
|
|
378
|
+
throw new Error(`PSBT: ${name} PSBT field cannot be supplied when policy is strip`);
|
|
379
|
+
delete out[name];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const cleanTxModifiable = (value: number | undefined, mode: Unknowns): number | undefined => {
|
|
386
|
+
if (value === undefined || !(value & ~0b111)) return value;
|
|
387
|
+
if (mode === 'strict') throw new Error('PSBT: unknown txModifiable bits in strict mode');
|
|
388
|
+
return mode === 'strip' ? value & 0b111 : value;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
function cleanFinalInput(
|
|
392
|
+
i: TArg<PSBTInputs>,
|
|
393
|
+
unknownMode: Unknowns = 'strip',
|
|
394
|
+
proprietaryMode: Unknowns = 'strip'
|
|
395
|
+
) {
|
|
325
396
|
const _i = i as PSBTInputs;
|
|
397
|
+
const extensions = cleanExtensions(_i as ExtensionMap, unknownMode, proprietaryMode);
|
|
398
|
+
if (extensions.unknown) _i.unknown = extensions.unknown as PSBTInputs['unknown'];
|
|
399
|
+
else delete _i.unknown;
|
|
400
|
+
if (extensions.proprietary) _i.proprietary = extensions.proprietary as PSBTInputs['proprietary'];
|
|
401
|
+
else delete _i.proprietary;
|
|
326
402
|
// BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
|
|
327
403
|
// That intentionally drops sighashType here, so post-finalize mutation becomes conservative
|
|
328
|
-
// until callers explicitly
|
|
404
|
+
// until callers explicitly clear satisfaction by removing finalScriptSig/finalScriptWitness.
|
|
329
405
|
for (const _k in _i) {
|
|
330
406
|
const k = _k as keyof PSBTInputs;
|
|
407
|
+
// Proprietary records are cleanup metadata too, but callers may need their opaque protocol
|
|
408
|
+
// state after finalization for PSBT coordination outside transaction extraction. An empty
|
|
409
|
+
// keyed list encodes no records, so canonicalize it to absence like its serialized clone.
|
|
410
|
+
if (proprietaryMode === 'ignore' && k === 'proprietary' && _i.proprietary?.length) continue;
|
|
331
411
|
if (!psbt.PSBTInputFinalKeys.includes(k)) delete _i[k];
|
|
332
412
|
}
|
|
333
413
|
}
|
|
@@ -350,6 +430,25 @@ function unpackSighash(hashType: number) {
|
|
|
350
430
|
};
|
|
351
431
|
}
|
|
352
432
|
|
|
433
|
+
const sighashScope = (sighash: number) => ({
|
|
434
|
+
sigInputs: sighash & SignatureHash.ANYONECANPAY,
|
|
435
|
+
sigOutputs: sighash === SignatureHash.DEFAULT ? SignatureHash.ALL : sighash & 0b11,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const normalizeUnknowns = (
|
|
439
|
+
name: 'unknown' | 'proprietary',
|
|
440
|
+
mode: Unknowns | undefined,
|
|
441
|
+
legacy: boolean | undefined,
|
|
442
|
+
fallback: Unknowns = 'strip'
|
|
443
|
+
): Unknowns => {
|
|
444
|
+
const alias = legacy === undefined ? undefined : legacy ? 'ignore' : 'strip';
|
|
445
|
+
if (mode !== undefined && mode !== 'ignore' && mode !== 'strip' && mode !== 'strict')
|
|
446
|
+
throw new Error(`Transaction options wrong value: ${name}=${mode}`);
|
|
447
|
+
if (mode !== undefined && alias !== undefined && mode !== alias)
|
|
448
|
+
throw new Error(`Transaction options: conflicting ${name} options`);
|
|
449
|
+
return mode || alias || fallback;
|
|
450
|
+
};
|
|
451
|
+
|
|
353
452
|
function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
354
453
|
if (opts !== undefined) validateObject(opts as Record<string, any>, {}, {}, 'opts');
|
|
355
454
|
|
|
@@ -366,6 +465,7 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
366
465
|
_opts.allowUnknownInputs = _opts.allowUnknowInput;
|
|
367
466
|
if (typeof _opts.allowUnknowOutput !== 'undefined')
|
|
368
467
|
_opts.allowUnknownOutputs = _opts.allowUnknowOutput;
|
|
468
|
+
if (_opts.allowMissingTxModifiable === undefined) _opts.allowMissingTxModifiable = true;
|
|
369
469
|
if (typeof _opts.lockTime !== 'number') throw new Error('Transaction lock time should be number');
|
|
370
470
|
P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
|
|
371
471
|
// There is no PSBT v1, and any new version will probably have fields which we don't know how to parse, which
|
|
@@ -380,13 +480,18 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
380
480
|
'disableScriptCheck',
|
|
381
481
|
'bip174jsCompat',
|
|
382
482
|
'allowLegacyWitnessUtxo',
|
|
483
|
+
'strictPrevoutValidation',
|
|
383
484
|
'lowR',
|
|
485
|
+
'allowUnknown',
|
|
486
|
+
'allowMissingTxModifiable',
|
|
384
487
|
] as const) {
|
|
385
488
|
const v = _opts[k];
|
|
386
489
|
if (v === undefined) continue; // optional
|
|
387
490
|
if (typeof v !== 'boolean')
|
|
388
491
|
throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
|
|
389
492
|
}
|
|
493
|
+
_opts.unknown = normalizeUnknowns('unknown', _opts.unknown, _opts.allowUnknown);
|
|
494
|
+
_opts.proprietary = normalizeUnknowns('proprietary', _opts.proprietary, undefined, _opts.unknown);
|
|
390
495
|
// 0 and -1 happens in tests
|
|
391
496
|
// With allowUnknownVersion any numeric version is fine; the ternary was inverted
|
|
392
497
|
// before 2026-07 (audit), which made the option throw for every numeric version.
|
|
@@ -414,14 +519,117 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
414
519
|
return Object.freeze(_opts) as TRet<Readonly<TxOpts>>;
|
|
415
520
|
}
|
|
416
521
|
|
|
522
|
+
function checkTaprootInputCommitments(input: TArg<PSBTInputs>, prevScript: TArg<Bytes>): void {
|
|
523
|
+
const output = _WitnessOutScript.decode(prevScript);
|
|
524
|
+
const hasTaprootCommitments =
|
|
525
|
+
input.tapInternalKey !== undefined ||
|
|
526
|
+
input.tapMerkleRoot !== undefined ||
|
|
527
|
+
// Repeated keyed fields only exist on the PSBT wire when at least one entry is encoded.
|
|
528
|
+
!!input.tapLeafScript?.length;
|
|
529
|
+
if (output.type !== 'tr') {
|
|
530
|
+
if (hasTaprootCommitments)
|
|
531
|
+
throw new Error('validateInput: Taproot metadata without P2TR previous output');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const checkOutputKey = (
|
|
536
|
+
internalKey: TArg<Bytes>,
|
|
537
|
+
merkleRoot: TArg<Bytes>,
|
|
538
|
+
parity?: number
|
|
539
|
+
): void => {
|
|
540
|
+
const [outputKey, outputParity] = u.taprootTweakPubkey(internalKey, merkleRoot);
|
|
541
|
+
if (!equalBytes(outputKey, output.pubkey))
|
|
542
|
+
throw new Error('validateInput: Taproot commitment does not match previous output');
|
|
543
|
+
if (parity !== undefined && outputParity !== parity)
|
|
544
|
+
throw new Error('validateInput: Taproot control-block parity does not match previous output');
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
if (input.tapLeafScript) {
|
|
548
|
+
for (const [controlBlock, scriptWithVersion] of input.tapLeafScript) {
|
|
549
|
+
const leafVersion = scriptWithVersion[scriptWithVersion.length - 1];
|
|
550
|
+
const script = scriptWithVersion.subarray(0, -1);
|
|
551
|
+
let merkleRoot = tapLeafHash(script, leafVersion);
|
|
552
|
+
for (const sibling of controlBlock.merklePath) {
|
|
553
|
+
merkleRoot =
|
|
554
|
+
u.compareBytes(sibling, merkleRoot) === -1
|
|
555
|
+
? u.tagSchnorr('TapBranch', sibling, merkleRoot)
|
|
556
|
+
: u.tagSchnorr('TapBranch', merkleRoot, sibling);
|
|
557
|
+
}
|
|
558
|
+
checkOutputKey(controlBlock.internalKey, merkleRoot, controlBlock.version & 1);
|
|
559
|
+
if (input.tapInternalKey && !equalBytes(input.tapInternalKey, controlBlock.internalKey))
|
|
560
|
+
throw new Error('validateInput: tapInternalKey does not match Taproot control block');
|
|
561
|
+
if (input.tapMerkleRoot && !equalBytes(input.tapMerkleRoot, merkleRoot))
|
|
562
|
+
throw new Error('validateInput: tapMerkleRoot does not match Taproot control block');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// A tree-bearing input can omit the aggregate root while still providing independently
|
|
567
|
+
// verifiable control blocks. With no leaves, an internal key without a root describes the
|
|
568
|
+
// standard key-only (empty-root) commitment.
|
|
569
|
+
if (input.tapInternalKey && (input.tapMerkleRoot || !input.tapLeafScript?.length))
|
|
570
|
+
checkOutputKey(input.tapInternalKey, input.tapMerkleRoot || P.EMPTY);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const LOCKTIME_THRESHOLD = 500_000_000;
|
|
574
|
+
function validateRequiredLocktimes(input: TArg<PSBTInputs>): void {
|
|
575
|
+
const height = input.requiredHeightLocktime;
|
|
576
|
+
if (height !== undefined) {
|
|
577
|
+
anumber(height, 'requiredHeightLocktime');
|
|
578
|
+
if (height === 0 || height >= LOCKTIME_THRESHOLD)
|
|
579
|
+
throw new RangeError(
|
|
580
|
+
`requiredHeightLocktime must be between 1 and ${LOCKTIME_THRESHOLD - 1}, got ${height}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const time = input.requiredTimeLocktime;
|
|
584
|
+
if (time !== undefined) {
|
|
585
|
+
anumber(time, 'requiredTimeLocktime');
|
|
586
|
+
if (time < LOCKTIME_THRESHOLD || time > 0xffffffff)
|
|
587
|
+
throw new RangeError(
|
|
588
|
+
`requiredTimeLocktime must be between ${LOCKTIME_THRESHOLD} and 4294967295, got ${time}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function resolvePSBTLocktime(
|
|
594
|
+
inputs: TArg<readonly PSBTInputs[]>,
|
|
595
|
+
fallback = DEFAULT_LOCKTIME
|
|
596
|
+
): number {
|
|
597
|
+
let height = DEFAULT_LOCKTIME;
|
|
598
|
+
let time = DEFAULT_LOCKTIME;
|
|
599
|
+
let hasRequirements = false;
|
|
600
|
+
let heightSupported = true;
|
|
601
|
+
let timeSupported = true;
|
|
602
|
+
for (const input of inputs) {
|
|
603
|
+
validateRequiredLocktimes(input);
|
|
604
|
+
const hasHeight = input.requiredHeightLocktime !== undefined;
|
|
605
|
+
const hasTime = input.requiredTimeLocktime !== undefined;
|
|
606
|
+
if (!hasHeight && !hasTime) continue;
|
|
607
|
+
hasRequirements = true;
|
|
608
|
+
if (hasHeight) height = Math.max(height, input.requiredHeightLocktime!);
|
|
609
|
+
else heightSupported = false;
|
|
610
|
+
if (hasTime) time = Math.max(time, input.requiredTimeLocktime!);
|
|
611
|
+
else timeSupported = false;
|
|
612
|
+
}
|
|
613
|
+
if (!hasRequirements) return fallback;
|
|
614
|
+
// BIP370 requires height when every relevant input supports both domains.
|
|
615
|
+
if (heightSupported) return height;
|
|
616
|
+
if (timeSupported) return time;
|
|
617
|
+
throw new Error('PSBTv2: incompatible height-based and time-based locktime requirements');
|
|
618
|
+
}
|
|
619
|
+
|
|
417
620
|
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
418
|
-
function validateInput(
|
|
621
|
+
function validateInput(
|
|
622
|
+
i: TArg<psbt.TransactionInput>,
|
|
623
|
+
disableScriptCheck = false
|
|
624
|
+
): TRet<PSBTInputs> {
|
|
419
625
|
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
420
626
|
const _i = i as PSBTInputs;
|
|
627
|
+
validateRequiredLocktimes(_i);
|
|
628
|
+
let prevOut: P.UnwrapCoder<typeof RawOutput> | undefined;
|
|
421
629
|
if (_i.nonWitnessUtxo && _i.index !== undefined) {
|
|
422
630
|
const last = _i.nonWitnessUtxo.outputs.length - 1;
|
|
423
631
|
if (_i.index > last) throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
|
|
424
|
-
|
|
632
|
+
prevOut = _i.nonWitnessUtxo.outputs[_i.index];
|
|
425
633
|
if (
|
|
426
634
|
_i.witnessUtxo &&
|
|
427
635
|
(!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
|
|
@@ -455,6 +663,10 @@ function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
|
|
|
455
663
|
// for any byte-order conversions required by their wire formats.
|
|
456
664
|
if (tx.id !== txid) throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
457
665
|
}
|
|
666
|
+
} else if (_i.witnessUtxo) prevOut = _i.witnessUtxo;
|
|
667
|
+
if (prevOut && !disableScriptCheck) {
|
|
668
|
+
checkScript(prevOut.script, _i.redeemScript, _i.witnessScript);
|
|
669
|
+
checkTaprootInputCommitments(_i, prevOut.script);
|
|
458
670
|
}
|
|
459
671
|
return _i as TRet<PSBTInputs>;
|
|
460
672
|
}
|
|
@@ -465,6 +677,35 @@ export type PSBTInputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTInput>;
|
|
|
465
677
|
/** Canonical PSBT output shape used by the coder layer. */
|
|
466
678
|
export type PSBTOutputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput>;
|
|
467
679
|
|
|
680
|
+
type InputSignature = { sighash: number; taproot: boolean; scriptPath: boolean };
|
|
681
|
+
|
|
682
|
+
const inputSignatures = (input: TArg<PSBTInputs>): InputSignature[] => {
|
|
683
|
+
const _input = input as PSBTInputs;
|
|
684
|
+
const res: InputSignature[] = [];
|
|
685
|
+
const add = (signature: TArg<Bytes>, taproot: boolean, scriptPath = false) => {
|
|
686
|
+
const sig = signature as Bytes;
|
|
687
|
+
if (!sig.length) return;
|
|
688
|
+
// Taproot's 64-byte encoding omits the SIGHASH_DEFAULT byte; every other PSBT signature
|
|
689
|
+
// carries its sighash in the final byte, including signatures imported from another signer.
|
|
690
|
+
const sighash = taproot && sig.length === 64 ? SignatureHash.DEFAULT : sig[sig.length - 1];
|
|
691
|
+
res.push({ sighash, taproot, scriptPath });
|
|
692
|
+
};
|
|
693
|
+
for (const [, signature] of _input.partialSig || []) add(signature, false);
|
|
694
|
+
if (_input.tapKeySig) add(_input.tapKeySig, true);
|
|
695
|
+
for (const [, signature] of _input.tapScriptSig || []) add(signature, true, true);
|
|
696
|
+
return res;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const inputSignedKeys = {
|
|
700
|
+
// sighashType is signer policy rather than a digest byte, but changing it after one signature
|
|
701
|
+
// exists would make later signers interpret the same input under a different policy.
|
|
702
|
+
self: ['txid', 'index', 'sequence', 'nonWitnessUtxo', 'witnessUtxo', 'sighashType'],
|
|
703
|
+
ecdsa: ['redeemScript', 'witnessScript'],
|
|
704
|
+
tapscript: ['tapLeafScript'],
|
|
705
|
+
cross: ['txid', 'index'],
|
|
706
|
+
prevout: ['nonWitnessUtxo', 'witnessUtxo'],
|
|
707
|
+
} as const satisfies Record<string, readonly (keyof PSBTInputs)[]>;
|
|
708
|
+
|
|
468
709
|
// Normalizes input
|
|
469
710
|
/**
|
|
470
711
|
* Extracts the previous output referenced by an input.
|
|
@@ -510,9 +751,12 @@ export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<ty
|
|
|
510
751
|
* @param i - input update to normalize
|
|
511
752
|
* @param cur - existing input value to merge with
|
|
512
753
|
* @param allowedFields - fields that may still change on signed inputs
|
|
513
|
-
* @param disableScriptCheck - whether to skip
|
|
514
|
-
* @param
|
|
754
|
+
* @param disableScriptCheck - whether to skip wrapper and Taproot commitment sanity checks
|
|
755
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
756
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
515
757
|
* @returns Normalized PSBT input.
|
|
758
|
+
* @throws If the update conflicts with the existing input or its signatures. {@link Error}
|
|
759
|
+
* @throws If a numeric input field is outside its wire or protocol range. {@link RangeError}
|
|
516
760
|
* @example
|
|
517
761
|
* Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
|
|
518
762
|
* normalize them into the repo's internal `TransactionInput` shape.
|
|
@@ -531,7 +775,8 @@ export function normalizeInput(
|
|
|
531
775
|
cur?: TArg<PSBTInputs>,
|
|
532
776
|
allowedFields?: TArg<readonly (keyof PSBTInputs)[]>,
|
|
533
777
|
disableScriptCheck = false,
|
|
534
|
-
|
|
778
|
+
unknown: Unknowns | boolean = 'strip',
|
|
779
|
+
proprietary: Unknowns | boolean = 'strip'
|
|
535
780
|
): TRet<PSBTInputs> {
|
|
536
781
|
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
537
782
|
if (cur !== undefined) validateObject(cur as Record<string, any>, {}, {}, 'cur');
|
|
@@ -555,18 +800,26 @@ export function normalizeInput(
|
|
|
555
800
|
if (!('nonWitnessUtxo' in _i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
|
|
556
801
|
if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
|
|
557
802
|
if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
|
|
558
|
-
res = psbt.mergeKeyMap(
|
|
803
|
+
res = psbt.mergeKeyMap(
|
|
804
|
+
psbt.PSBTInput,
|
|
805
|
+
res,
|
|
806
|
+
_cur,
|
|
807
|
+
_allowedFields,
|
|
808
|
+
unknown,
|
|
809
|
+
proprietary
|
|
810
|
+
) as PSBTInputs;
|
|
811
|
+
// An actual empty repeated field emits no PSBT keypairs. Canonicalize only arrays so malformed
|
|
812
|
+
// falsy values still reach the PSBT coder's validation instead of becoming valid absence.
|
|
813
|
+
if (Array.isArray(res.tapLeafScript) && !res.tapLeafScript.length) delete res.tapLeafScript;
|
|
814
|
+
validateRequiredLocktimes(res);
|
|
559
815
|
// Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
|
|
560
816
|
// the repo's historical raw internal shape and casts only at the validation boundary here.
|
|
561
817
|
psbt.PSBTInputCoder.encode(res as Parameters<typeof psbt.PSBTInputCoder.encode>[0]); // Validates that everything is correct at this point
|
|
562
818
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (prevOut && !disableScriptCheck)
|
|
568
|
-
checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
|
|
569
|
-
return res as TRet<PSBTInputs>;
|
|
819
|
+
// Direct construction and UTXO selection consume nonWitnessUtxo amounts without crossing a
|
|
820
|
+
// PSBT serialization boundary. Enforce the same outpoint binding here so a mismatched previous
|
|
821
|
+
// transaction cannot understate a legacy input amount and turn the difference into mining fees.
|
|
822
|
+
return validateInput(res as TArg<psbt.TransactionInput>, disableScriptCheck);
|
|
570
823
|
}
|
|
571
824
|
|
|
572
825
|
/**
|
|
@@ -598,10 +851,13 @@ export function getInputType(input: TArg<psbt.TransactionInput>, allowLegacyWitn
|
|
|
598
851
|
let txType = 'legacy';
|
|
599
852
|
let defaultSighash: number = SignatureHash.ALL;
|
|
600
853
|
const prevOut = getPrevOut(_input as TArg<psbt.TransactionInput>);
|
|
601
|
-
const first =
|
|
854
|
+
const first = _WitnessOutScript.decode(prevOut.script);
|
|
602
855
|
let type = first.type;
|
|
603
856
|
let cur = first;
|
|
604
857
|
const stack = [first];
|
|
858
|
+
// Classification is semantic, but legacy/BIP143 scriptCode and finalization must retain the
|
|
859
|
+
// exact committed spelling (including consensus-valid non-minimal pushes).
|
|
860
|
+
let lastScript = prevOut.script;
|
|
605
861
|
if (first.type === 'tr') {
|
|
606
862
|
// Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
|
|
607
863
|
// legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
|
|
@@ -618,10 +874,11 @@ export function getInputType(input: TArg<psbt.TransactionInput>, allowLegacyWitn
|
|
|
618
874
|
if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
|
|
619
875
|
if (first.type === 'sh') {
|
|
620
876
|
if (!_input.redeemScript) throw new Error('inputType: sh without redeemScript');
|
|
621
|
-
let child =
|
|
877
|
+
let child = _WitnessOutScript.decode(_input.redeemScript);
|
|
622
878
|
if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
|
|
623
879
|
stack.push(child);
|
|
624
880
|
cur = child;
|
|
881
|
+
lastScript = _input.redeemScript;
|
|
625
882
|
type += `-${child.type}`;
|
|
626
883
|
}
|
|
627
884
|
// wsh can be inside sh
|
|
@@ -631,12 +888,12 @@ export function getInputType(input: TArg<psbt.TransactionInput>, allowLegacyWitn
|
|
|
631
888
|
if (child.type === 'wsh') txType = 'segwit';
|
|
632
889
|
stack.push(child);
|
|
633
890
|
cur = child;
|
|
891
|
+
lastScript = _input.witnessScript;
|
|
634
892
|
type += `-${child.type}`;
|
|
635
893
|
}
|
|
636
894
|
const last = stack[stack.length - 1];
|
|
637
895
|
if (last.type === 'sh' || last.type === 'wsh')
|
|
638
896
|
throw new Error('inputType: sh/wsh cannot be terminal type');
|
|
639
|
-
const lastScript = OutScript.encode(last);
|
|
640
897
|
const res = {
|
|
641
898
|
type,
|
|
642
899
|
txType,
|
|
@@ -683,8 +940,62 @@ export class Transaction {
|
|
|
683
940
|
constructor(opts: TxOpts = {}) {
|
|
684
941
|
const _opts = (this.opts = validateOpts(opts));
|
|
685
942
|
// Merge with global structure of PSBTv2
|
|
686
|
-
|
|
943
|
+
// Bitcoin Core sets fallback even when it is zero. Matching its common encoding reduces the
|
|
944
|
+
// fingerprint of locally created PSBTv2s; imported PSBTs replace this map and retain omission.
|
|
945
|
+
this.global.fallbackLocktime = _opts.lockTime;
|
|
687
946
|
this.global.txVersion = _opts.version;
|
|
947
|
+
// A locally-created PSBTv2 is still under construction. Imported PSBTs replace this global
|
|
948
|
+
// map below, so an omitted field there retains BIP370's immutable meaning.
|
|
949
|
+
if (_opts.PSBTVersion === 2) this.global.txModifiable = 0b011;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
private isPSBTv2(): boolean {
|
|
953
|
+
return (this.global.version ?? this.opts.PSBTVersion) === 2;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private requireTxModifiable(bit: number, kind: 'inputs' | 'outputs'): void {
|
|
957
|
+
if (!this.isPSBTv2()) return;
|
|
958
|
+
if (!(this.txModifiablePolicy() & bit)) throw new Error(`PSBTv2: ${kind} are not modifiable`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private txModifiablePolicy(
|
|
962
|
+
allowMissing = this.opts.allowMissingTxModifiable,
|
|
963
|
+
unknownMode = this.opts.unknown!
|
|
964
|
+
): number {
|
|
965
|
+
if (!this.isPSBTv2()) return 0b011;
|
|
966
|
+
if (this.global.txModifiable !== undefined)
|
|
967
|
+
return cleanTxModifiable(this.global.txModifiable, unknownMode)!;
|
|
968
|
+
return allowMissing ? 0b011 : 0;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private modifiable(
|
|
972
|
+
allowMissing = this.opts.allowMissingTxModifiable,
|
|
973
|
+
unknownMode = this.opts.unknown!
|
|
974
|
+
): number {
|
|
975
|
+
let flags = this.txModifiablePolicy(allowMissing, unknownMode);
|
|
976
|
+
let hasOpaqueFinal = false;
|
|
977
|
+
let hasSingle = false;
|
|
978
|
+
for (let idx = 0; idx < this.inputs.length; idx++) {
|
|
979
|
+
const signatures = inputSignatures(this.inputs[idx]);
|
|
980
|
+
if (!signatures.length && this.inputStatus(idx) === 'finalized') hasOpaqueFinal = true;
|
|
981
|
+
for (const { sighash } of signatures) {
|
|
982
|
+
const { isAny, isNone, isSingle } = unpackSighash(sighash);
|
|
983
|
+
if (!isAny) flags &= ~0b001;
|
|
984
|
+
if (!isNone) flags &= ~0b010;
|
|
985
|
+
if (isSingle) hasSingle = true;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// Bit 2 summarizes signatures rather than granting policy. Preserve an imported summary for
|
|
989
|
+
// opaque or externally managed state, and union in every signature visible to this object.
|
|
990
|
+
if (hasSingle) flags |= 0b100;
|
|
991
|
+
// PSBTv0 and legacy field-less PSBTv2 cannot describe an opaque finalized sighash. Promotion
|
|
992
|
+
// must therefore deny both mutations instead of manufacturing permissions from absence.
|
|
993
|
+
if (hasOpaqueFinal && this.global.txModifiable === undefined) flags &= ~0b011;
|
|
994
|
+
return flags;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private get txModifiable(): number {
|
|
998
|
+
return this.modifiable();
|
|
688
999
|
}
|
|
689
1000
|
|
|
690
1001
|
// Import
|
|
@@ -722,21 +1033,54 @@ export class Transaction {
|
|
|
722
1033
|
const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion });
|
|
723
1034
|
// We need slice here, because otherwise
|
|
724
1035
|
const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount;
|
|
725
|
-
tx.inputs = parsed.inputs.slice(0, inputCount).map(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1036
|
+
tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) => {
|
|
1037
|
+
const input = {
|
|
1038
|
+
...parsed.global.unsignedTx?.inputs[j],
|
|
1039
|
+
...i,
|
|
1040
|
+
};
|
|
1041
|
+
// The unsigned transaction's empty scriptSig is framing, not a PSBT_IN_FINAL_SCRIPTSIG
|
|
1042
|
+
// record. Keeping it makes combination conflict with an otherwise identical finalized PSBT.
|
|
1043
|
+
if (!i.finalScriptSig?.length) delete input.finalScriptSig;
|
|
1044
|
+
return validateInput(input, tx.opts.disableScriptCheck) as PSBTInputs;
|
|
1045
|
+
});
|
|
733
1046
|
const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount;
|
|
734
|
-
|
|
1047
|
+
// bip174js writes a phantom empty input map when a PSBTv0 transaction has zero inputs. Raw v0
|
|
1048
|
+
// framing necessarily reads it as the first output map, so skip it before pairing real maps
|
|
1049
|
+
// with the unsigned transaction's declared outputs.
|
|
1050
|
+
const hasBip174InputMap =
|
|
1051
|
+
PSBTVersion === 0 &&
|
|
1052
|
+
inputCount === 0 &&
|
|
1053
|
+
Object.keys(parsed.outputs[0] || {}).length === 0 &&
|
|
1054
|
+
((outputCount! > 0 && parsed.outputs.length === outputCount! + 1) ||
|
|
1055
|
+
(outputCount === 0 &&
|
|
1056
|
+
parsed.outputs.length === 2 &&
|
|
1057
|
+
Object.keys(parsed.outputs[1]).length === 0));
|
|
1058
|
+
const outputStart = hasBip174InputMap ? 1 : 0;
|
|
1059
|
+
tx.outputs = parsed.outputs.slice(outputStart, outputStart + outputCount!).map((i, j) => ({
|
|
735
1060
|
...i,
|
|
736
1061
|
...parsed.global.unsignedTx?.outputs[j],
|
|
737
1062
|
}));
|
|
738
|
-
|
|
739
|
-
|
|
1063
|
+
const unknownMode = tx.opts.unknown!;
|
|
1064
|
+
const proprietaryMode = tx.opts.proprietary!;
|
|
1065
|
+
// Unknown PSBT rows can carry opaque metadata between participants. The documented default is
|
|
1066
|
+
// to strip them; callers that need forward compatibility must opt in explicitly. Proprietary
|
|
1067
|
+
// (0xfc) rows follow the same explicit policy, which defaults to the resolved unknown mode.
|
|
1068
|
+
tx.global = cleanExtensions(
|
|
1069
|
+
{ ...parsed.global, txVersion: version },
|
|
1070
|
+
unknownMode,
|
|
1071
|
+
proprietaryMode
|
|
1072
|
+
);
|
|
1073
|
+
tx.inputs = tx.inputs.map((input) => cleanExtensions(input, unknownMode, proprietaryMode));
|
|
1074
|
+
tx.outputs = tx.outputs.map((output) => cleanExtensions(output, unknownMode, proprietaryMode));
|
|
1075
|
+
if (tx.global.txModifiable !== undefined)
|
|
1076
|
+
tx.global.txModifiable = cleanTxModifiable(tx.global.txModifiable, unknownMode);
|
|
1077
|
+
// A high-level Transaction must have a determinable nLockTime. Raw PSBT coders can still be
|
|
1078
|
+
// used by callers that need to inspect or relay a structurally valid but incompatible PSBT.
|
|
1079
|
+
resolvePSBTLocktime(tx.inputs, tx.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
1080
|
+
// PSBTv0 always provides nLockTime in its unsigned transaction. Retain zero internally too so
|
|
1081
|
+
// promotion to v2 matches fresh construction and Bitcoin Core rather than gaining a
|
|
1082
|
+
// fingerprint.
|
|
1083
|
+
if (PSBTVersion === 0) tx.global.fallbackLocktime = def(lockTime, DEFAULT_LOCKTIME);
|
|
740
1084
|
return tx;
|
|
741
1085
|
}
|
|
742
1086
|
// Prefer `global.version` when present so cross-version combiners can serialize at the highest
|
|
@@ -753,9 +1097,17 @@ export class Transaction {
|
|
|
753
1097
|
// );
|
|
754
1098
|
// }
|
|
755
1099
|
const inputs = this.inputs.map((i) =>
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1100
|
+
cleanExtensions(
|
|
1101
|
+
// For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
|
|
1102
|
+
// validate the full transaction input before version filtering drops those fields.
|
|
1103
|
+
psbt.cleanPSBTFields(
|
|
1104
|
+
PSBTVersion,
|
|
1105
|
+
psbt.PSBTInput,
|
|
1106
|
+
validateInput(i, this.opts.disableScriptCheck) as TArg<PSBTInputs>
|
|
1107
|
+
),
|
|
1108
|
+
this.opts.unknown!,
|
|
1109
|
+
this.opts.proprietary!
|
|
1110
|
+
)
|
|
759
1111
|
);
|
|
760
1112
|
for (const inp of inputs) {
|
|
761
1113
|
// Don't serialize empty fields
|
|
@@ -763,8 +1115,16 @@ export class Transaction {
|
|
|
763
1115
|
if (inp.finalScriptSig && !inp.finalScriptSig.length) delete inp.finalScriptSig;
|
|
764
1116
|
if (inp.finalScriptWitness && !inp.finalScriptWitness.length) delete inp.finalScriptWitness;
|
|
765
1117
|
}
|
|
766
|
-
const outputs = this.outputs.map((i) =>
|
|
767
|
-
|
|
1118
|
+
const outputs = this.outputs.map((i) =>
|
|
1119
|
+
cleanExtensions(
|
|
1120
|
+
psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i),
|
|
1121
|
+
this.opts.unknown!,
|
|
1122
|
+
this.opts.proprietary!
|
|
1123
|
+
)
|
|
1124
|
+
);
|
|
1125
|
+
const global = cleanExtensions({ ...this.global }, this.opts.unknown!, this.opts.proprietary!);
|
|
1126
|
+
if (global.txModifiable !== undefined)
|
|
1127
|
+
global.txModifiable = cleanTxModifiable(global.txModifiable, this.opts.unknown!);
|
|
768
1128
|
if (PSBTVersion === 0) {
|
|
769
1129
|
/*
|
|
770
1130
|
- Bitcoin raw transaction expects to have at least 1 input because it uses case with zero inputs as marker for SegWit
|
|
@@ -789,10 +1149,12 @@ export class Transaction {
|
|
|
789
1149
|
delete global.txVersion;
|
|
790
1150
|
// PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
|
|
791
1151
|
// removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
|
|
792
|
-
// PSBTv2 and reject the required `unsignedTx` field.
|
|
1152
|
+
// PSBTv2 and reject the required `unsignedTx` field. Transaction-modifiable is also v2-only;
|
|
1153
|
+
// its restrictions remain represented by the signatures when explicitly converting to v0.
|
|
793
1154
|
delete global.inputCount;
|
|
794
1155
|
delete global.outputCount;
|
|
795
1156
|
delete global.version;
|
|
1157
|
+
delete global.txModifiable;
|
|
796
1158
|
} else {
|
|
797
1159
|
// Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
|
|
798
1160
|
// `this.global`, but PSBTv2 serializes the transaction through split global/input/output
|
|
@@ -802,13 +1164,19 @@ export class Transaction {
|
|
|
802
1164
|
global.txVersion = this.version;
|
|
803
1165
|
global.inputCount = this.inputs.length;
|
|
804
1166
|
global.outputCount = this.outputs.length;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1167
|
+
// Core serializes this optional field exactly as stored. Preserve no-op v2 round-trips;
|
|
1168
|
+
// only v0 promotion and the explicit legacy-omission compatibility mode materialize policy.
|
|
1169
|
+
if (
|
|
1170
|
+
!this.isPSBTv2() ||
|
|
1171
|
+
(global.txModifiable === undefined && this.opts.allowMissingTxModifiable)
|
|
1172
|
+
)
|
|
1173
|
+
global.txModifiable = this.txModifiable;
|
|
811
1174
|
}
|
|
1175
|
+
// bip174js historically emits one empty output map for a PSBTv0 transaction with no outputs.
|
|
1176
|
+
// Input maps are count-framed by the unsigned transaction, so a phantom input map cannot be
|
|
1177
|
+
// represented: with zero inputs it would be decoded as an output map instead. PSBTv2 has
|
|
1178
|
+
// explicit counts for both map arrays and does not use this compatibility encoding.
|
|
1179
|
+
if (this.opts.bip174jsCompat && PSBTVersion === 0 && !outputs.length) outputs.push({});
|
|
812
1180
|
const raw = { global, inputs, outputs };
|
|
813
1181
|
return PSBTVersion === 0
|
|
814
1182
|
? psbt.RawPSBTV0.encode(raw as Parameters<typeof psbt.RawPSBTV0.encode>[0])
|
|
@@ -817,23 +1185,7 @@ export class Transaction {
|
|
|
817
1185
|
|
|
818
1186
|
// BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
|
|
819
1187
|
get lockTime(): number {
|
|
820
|
-
|
|
821
|
-
let heightCnt = 0;
|
|
822
|
-
let time = DEFAULT_LOCKTIME;
|
|
823
|
-
let timeCnt = 0;
|
|
824
|
-
for (const i of this.inputs) {
|
|
825
|
-
if (i.requiredHeightLocktime) {
|
|
826
|
-
height = Math.max(height, i.requiredHeightLocktime);
|
|
827
|
-
heightCnt++;
|
|
828
|
-
}
|
|
829
|
-
if (i.requiredTimeLocktime) {
|
|
830
|
-
time = Math.max(time, i.requiredTimeLocktime);
|
|
831
|
-
timeCnt++;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
if (heightCnt && heightCnt >= timeCnt) return height;
|
|
835
|
-
if (time !== DEFAULT_LOCKTIME) return time;
|
|
836
|
-
return this.global.fallbackLocktime || DEFAULT_LOCKTIME;
|
|
1188
|
+
return resolvePSBTLocktime(this.inputs, this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
837
1189
|
}
|
|
838
1190
|
|
|
839
1191
|
get version(): number {
|
|
@@ -855,6 +1207,13 @@ export class Transaction {
|
|
|
855
1207
|
if (input.partialSig && input.partialSig.length) return 'signed';
|
|
856
1208
|
return 'unsigned';
|
|
857
1209
|
}
|
|
1210
|
+
private cleanFinalInput(input: PSBTInputs): void {
|
|
1211
|
+
// Core preserves producer policy during finalization. Once cleanup makes signatures opaque,
|
|
1212
|
+
// signStatus conservatively locks transaction mutation until the input is explicitly reopened.
|
|
1213
|
+
cleanFinalInput(input as TArg<PSBTInputs>, this.opts.unknown!, this.opts.proprietary!);
|
|
1214
|
+
if (this.global.txModifiable !== undefined)
|
|
1215
|
+
this.global.txModifiable = cleanTxModifiable(this.global.txModifiable, this.opts.unknown!);
|
|
1216
|
+
}
|
|
858
1217
|
// Cannot replace unpackSighash, tests rely on very generic implemenetation with signing inputs outside of range
|
|
859
1218
|
// We will lose some vectors -> smaller test coverage of preimages (very important!)
|
|
860
1219
|
private inputSighash(idx: number) {
|
|
@@ -867,30 +1226,70 @@ export class Transaction {
|
|
|
867
1226
|
// ALL + ANYONE -- specific input + all outputs
|
|
868
1227
|
// NONE + ANYONE -- specific input + no outputs
|
|
869
1228
|
// SINGLE -- specific inputs + output with same index
|
|
870
|
-
|
|
871
|
-
const sigInputs = sighash & SignatureHash.ANYONECANPAY;
|
|
872
|
-
return { sigInputs, sigOutputs };
|
|
1229
|
+
return sighashScope(sighash);
|
|
873
1230
|
}
|
|
874
1231
|
// Very nice for debug purposes, but slow. If there is too much inputs/outputs to add, will be quadratic.
|
|
875
1232
|
// Some cache will be nice, but there chance to have bugs with cache invalidation
|
|
876
|
-
private
|
|
1233
|
+
private signatures() {
|
|
1234
|
+
const res: (InputSignature & { idx: number })[] = [];
|
|
1235
|
+
for (let idx = 0; idx < this.inputs.length; idx++) {
|
|
1236
|
+
const actual = inputSignatures(this.inputs[idx]);
|
|
1237
|
+
for (const signature of actual) res.push({ idx, ...signature });
|
|
1238
|
+
if (actual.length || this.inputStatus(idx) !== 'finalized') continue;
|
|
1239
|
+
let taproot = true;
|
|
1240
|
+
try {
|
|
1241
|
+
taproot = _WitnessOutScript.decode(getPrevOut(this.inputs[idx]).script).type === 'tr';
|
|
1242
|
+
} catch {
|
|
1243
|
+
// Finalization may remove the data needed to classify an imported opaque signature.
|
|
1244
|
+
// Treating it as Taproot conservatively protects the larger all-prevout commitment.
|
|
1245
|
+
}
|
|
1246
|
+
const declared = this.inputs[idx].sighashType;
|
|
1247
|
+
const sighash = declared === undefined ? SignatureHash.DEFAULT : declared;
|
|
1248
|
+
res.push({ idx, sighash, taproot, scriptPath: taproot });
|
|
1249
|
+
}
|
|
1250
|
+
return res;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private signedInputKeys(idx: number, signatures = this.signatures()): (keyof PSBTInputs)[] {
|
|
1254
|
+
const res = new Set<keyof PSBTInputs>();
|
|
1255
|
+
const add = (keys: readonly (keyof PSBTInputs)[]) => {
|
|
1256
|
+
for (const key of keys) res.add(key);
|
|
1257
|
+
};
|
|
1258
|
+
for (const signature of signatures) {
|
|
1259
|
+
const { isAny, isNone, isSingle } = unpackSighash(signature.sighash);
|
|
1260
|
+
if (signature.idx === idx) {
|
|
1261
|
+
add(inputSignedKeys.self);
|
|
1262
|
+
if (signature.taproot) {
|
|
1263
|
+
if (signature.scriptPath) add(inputSignedKeys.tapscript);
|
|
1264
|
+
} else add(inputSignedKeys.ecdsa);
|
|
1265
|
+
} else if (!isAny) {
|
|
1266
|
+
add(inputSignedKeys.cross);
|
|
1267
|
+
// Legacy and BIP143 omit other sequences for NONE/SINGLE. BIP341 commits every sequence
|
|
1268
|
+
// whenever ANYONECANPAY is absent, independently of the output sighash mode.
|
|
1269
|
+
if (signature.taproot || (!isNone && !isSingle)) res.add('sequence');
|
|
1270
|
+
if (signature.taproot) add(inputSignedKeys.prevout);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return [...res];
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private signStatus(signatures = this.signatures()) {
|
|
877
1277
|
// if addInput or addOutput is not possible, then all inputs or outputs are signed
|
|
878
1278
|
let addInput = true,
|
|
879
1279
|
addOutput = true;
|
|
880
|
-
let inputs = [],
|
|
881
|
-
outputs = [];
|
|
882
|
-
for (
|
|
883
|
-
const
|
|
884
|
-
// Unsigned input doesn't affect anything
|
|
885
|
-
if (status === 'unsigned') continue;
|
|
886
|
-
const { sigInputs, sigOutputs } = this.inputSighash(idx);
|
|
1280
|
+
let inputs: number[] = [],
|
|
1281
|
+
outputs: number[] = [];
|
|
1282
|
+
for (const { idx, sighash } of signatures) {
|
|
1283
|
+
const { sigInputs, sigOutputs } = sighashScope(sighash);
|
|
887
1284
|
// Input type
|
|
888
|
-
if (sigInputs === SignatureHash.ANYONECANPAY)
|
|
889
|
-
|
|
1285
|
+
if (sigInputs === SignatureHash.ANYONECANPAY) {
|
|
1286
|
+
if (!inputs.includes(idx)) inputs.push(idx);
|
|
1287
|
+
} else addInput = false;
|
|
890
1288
|
// Output type
|
|
891
1289
|
if (sigOutputs === SignatureHash.ALL) addOutput = false;
|
|
892
|
-
else if (sigOutputs === SignatureHash.SINGLE)
|
|
893
|
-
|
|
1290
|
+
else if (sigOutputs === SignatureHash.SINGLE) {
|
|
1291
|
+
if (!outputs.includes(idx)) outputs.push(idx);
|
|
1292
|
+
} else if (sigOutputs === SignatureHash.NONE) {
|
|
894
1293
|
// Doesn't affect any outputs at all
|
|
895
1294
|
} else throw new Error(`Wrong signature hash output type: ${sigOutputs}`);
|
|
896
1295
|
}
|
|
@@ -969,6 +1368,26 @@ export class Transaction {
|
|
|
969
1368
|
anumber(idx, 'idx');
|
|
970
1369
|
if (idx >= this.inputs.length) throw new Error(`Wrong input index=${idx}`);
|
|
971
1370
|
}
|
|
1371
|
+
private validatePrevoutsForSigning(): void {
|
|
1372
|
+
if (!this.opts.strictPrevoutValidation) return;
|
|
1373
|
+
for (let i = 0; i < this.inputs.length; i++) {
|
|
1374
|
+
const input = this.inputs[i];
|
|
1375
|
+
if (!input.nonWitnessUtxo) {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
`Transaction/sign: strictPrevoutValidation requires nonWitnessUtxo for input=${i}`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
if (input.txid === undefined || input.index === undefined) {
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
`Transaction/sign: strictPrevoutValidation requires an outpoint for input=${i}`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
// A full previous transaction is only a trusted amount/script commitment after its txid and
|
|
1386
|
+
// selected output have been checked against the unsigned transaction. Also cross-check a
|
|
1387
|
+
// redundant witnessUtxo when one is present.
|
|
1388
|
+
validateInput(input as TArg<psbt.TransactionInput>, this.opts.disableScriptCheck);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
972
1391
|
getInput(idx: number): psbt.TransactionInput {
|
|
973
1392
|
this.checkInputIdx(idx);
|
|
974
1393
|
return cloneDeep(this.inputs[idx]) as psbt.TransactionInput;
|
|
@@ -979,15 +1398,31 @@ export class Transaction {
|
|
|
979
1398
|
// Modification
|
|
980
1399
|
addInput(input: TArg<psbt.TransactionInputUpdate>, _ignoreSignStatus = false): number {
|
|
981
1400
|
validateObject(input as Record<string, any>, {}, {}, 'input');
|
|
982
|
-
|
|
983
|
-
|
|
1401
|
+
cleanExtensions(input as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1402
|
+
this.requireTxModifiable(0b001, 'inputs');
|
|
1403
|
+
const signatures = _ignoreSignStatus ? undefined : this.signatures();
|
|
1404
|
+
const status = signatures && this.signStatus(signatures);
|
|
1405
|
+
if (status && !status.addInput) throw new Error('Tx has signed inputs, cannot add new one');
|
|
984
1406
|
// normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
|
|
985
1407
|
// new input becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1408
|
+
const normalized = cloneDeep(
|
|
1409
|
+
normalizeInput(
|
|
1410
|
+
input,
|
|
1411
|
+
undefined,
|
|
1412
|
+
undefined,
|
|
1413
|
+
this.opts.disableScriptCheck,
|
|
1414
|
+
this.opts.unknown!,
|
|
1415
|
+
this.opts.proprietary!
|
|
1416
|
+
)
|
|
1417
|
+
) as PSBTInputs;
|
|
1418
|
+
const nextLockTime = resolvePSBTLocktime(
|
|
1419
|
+
[...this.inputs, normalized],
|
|
1420
|
+
this.global.fallbackLocktime ?? DEFAULT_LOCKTIME
|
|
990
1421
|
);
|
|
1422
|
+
// ANYONECANPAY permits adding an outpoint, but every signature still commits to nLockTime.
|
|
1423
|
+
if (signatures?.length && nextLockTime !== this.lockTime)
|
|
1424
|
+
throw new Error('Tx has signed inputs, cannot change lockTime');
|
|
1425
|
+
this.inputs.push(normalized);
|
|
991
1426
|
return this.inputs.length - 1;
|
|
992
1427
|
}
|
|
993
1428
|
updateInput(
|
|
@@ -996,23 +1431,54 @@ export class Transaction {
|
|
|
996
1431
|
_ignoreSignStatus = false
|
|
997
1432
|
): void {
|
|
998
1433
|
this.checkInputIdx(idx);
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1434
|
+
cleanExtensions(input as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1435
|
+
let allowedFields: (keyof PSBTInputs)[] | undefined;
|
|
1436
|
+
const signatures = _ignoreSignStatus ? undefined : this.signatures();
|
|
1437
|
+
if (signatures?.length) {
|
|
1438
|
+
if (this.inputStatus(idx) === 'finalized') {
|
|
1439
|
+
// Once finalized, only already-present signature/final fields may be repeated or removed.
|
|
1440
|
+
// In particular, do not let a native-SegWit final witness gain a stray finalScriptSig.
|
|
1441
|
+
allowedFields = psbt.PSBTInputSignatureKeys.filter(
|
|
1442
|
+
(key) => this.inputs[idx][key] !== undefined
|
|
1443
|
+
);
|
|
1444
|
+
} else {
|
|
1445
|
+
const signed = new Set(this.signedInputKeys(idx, signatures));
|
|
1446
|
+
if (signed.size)
|
|
1447
|
+
allowedFields = (Object.keys(psbt.PSBTInput) as (keyof PSBTInputs)[]).filter(
|
|
1448
|
+
(key) => !signed.has(key)
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1004
1451
|
}
|
|
1005
1452
|
// normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
|
|
1006
1453
|
// before the updated input becomes transaction state and later caller mutation can rewrite it.
|
|
1007
|
-
|
|
1454
|
+
const normalized = cloneDeep(
|
|
1008
1455
|
normalizeInput(
|
|
1009
1456
|
input,
|
|
1010
1457
|
this.inputs[idx],
|
|
1011
1458
|
allowedFields,
|
|
1012
1459
|
this.opts.disableScriptCheck,
|
|
1013
|
-
this.opts.
|
|
1460
|
+
this.opts.unknown!,
|
|
1461
|
+
this.opts.proprietary!
|
|
1014
1462
|
)
|
|
1015
1463
|
) as PSBTInputs;
|
|
1464
|
+
const inputs = this.inputs.slice();
|
|
1465
|
+
inputs[idx] = normalized;
|
|
1466
|
+
const nextLockTime = resolvePSBTLocktime(
|
|
1467
|
+
inputs,
|
|
1468
|
+
this.global.fallbackLocktime ?? DEFAULT_LOCKTIME
|
|
1469
|
+
);
|
|
1470
|
+
if (signatures?.length && nextLockTime !== this.lockTime)
|
|
1471
|
+
throw new Error('Tx has signed inputs, cannot change lockTime');
|
|
1472
|
+
const current = this.inputs[idx];
|
|
1473
|
+
const transactionChanged =
|
|
1474
|
+
current.index !== normalized.index ||
|
|
1475
|
+
def(current.sequence, DEFAULT_SEQUENCE) !== def(normalized.sequence, DEFAULT_SEQUENCE) ||
|
|
1476
|
+
(current.txid === undefined
|
|
1477
|
+
? normalized.txid !== undefined
|
|
1478
|
+
: normalized.txid === undefined || !equalBytes(current.txid, normalized.txid)) ||
|
|
1479
|
+
nextLockTime !== this.lockTime;
|
|
1480
|
+
if (transactionChanged) this.requireTxModifiable(0b001, 'inputs');
|
|
1481
|
+
this.inputs[idx] = normalized;
|
|
1016
1482
|
}
|
|
1017
1483
|
// Output stuff
|
|
1018
1484
|
private checkOutputIdx(idx: number) {
|
|
@@ -1027,7 +1493,7 @@ export class Transaction {
|
|
|
1027
1493
|
const out = this.getOutput(idx);
|
|
1028
1494
|
if (!out.script) return;
|
|
1029
1495
|
return Address(network).encode(
|
|
1030
|
-
|
|
1496
|
+
_WitnessOutScript.decode(out.script) as Parameters<ReturnType<typeof Address>['encode']>[0]
|
|
1031
1497
|
);
|
|
1032
1498
|
}
|
|
1033
1499
|
|
|
@@ -1047,12 +1513,19 @@ export class Transaction {
|
|
|
1047
1513
|
if (script === undefined) script = cur?.script;
|
|
1048
1514
|
let res: PSBTOutputs = { ...cur, ...(o as PSBTOutputs & { script?: string }), amount, script };
|
|
1049
1515
|
if (res.amount === undefined) delete res.amount;
|
|
1050
|
-
res = psbt.mergeKeyMap(
|
|
1516
|
+
res = psbt.mergeKeyMap(
|
|
1517
|
+
psbt.PSBTOutput,
|
|
1518
|
+
res,
|
|
1519
|
+
cur,
|
|
1520
|
+
allowedFields,
|
|
1521
|
+
this.opts.unknown!,
|
|
1522
|
+
this.opts.proprietary!
|
|
1523
|
+
);
|
|
1051
1524
|
psbt.PSBTOutputCoder.encode(res as Parameters<typeof psbt.PSBTOutputCoder.encode>[0]);
|
|
1052
1525
|
if (
|
|
1053
1526
|
res.script &&
|
|
1054
1527
|
!this.opts.allowUnknownOutputs &&
|
|
1055
|
-
|
|
1528
|
+
_WitnessOutScript.decode(res.script).type === 'unknown'
|
|
1056
1529
|
) {
|
|
1057
1530
|
throw new Error(
|
|
1058
1531
|
'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
|
|
@@ -1062,7 +1535,11 @@ export class Transaction {
|
|
|
1062
1535
|
return res;
|
|
1063
1536
|
}
|
|
1064
1537
|
addOutput(o: TArg<psbt.TransactionOutputUpdate>, _ignoreSignStatus = false): number {
|
|
1065
|
-
|
|
1538
|
+
cleanExtensions(o as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1539
|
+
this.requireTxModifiable(0b010, 'outputs');
|
|
1540
|
+
const status = _ignoreSignStatus ? undefined : this.signStatus();
|
|
1541
|
+
// Appending the previously missing same-index output changes a SIGHASH_SINGLE digest.
|
|
1542
|
+
if (status && (!status.addOutput || status.outputs.includes(this.outputs.length)))
|
|
1066
1543
|
throw new Error('Tx has signed outputs, cannot add new one');
|
|
1067
1544
|
// normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
|
|
1068
1545
|
// new output becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
@@ -1075,6 +1552,7 @@ export class Transaction {
|
|
|
1075
1552
|
_ignoreSignStatus = false
|
|
1076
1553
|
): void {
|
|
1077
1554
|
this.checkOutputIdx(idx);
|
|
1555
|
+
cleanExtensions(output as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1078
1556
|
let allowedFields = undefined;
|
|
1079
1557
|
if (!_ignoreSignStatus) {
|
|
1080
1558
|
const status = this.signStatus();
|
|
@@ -1083,7 +1561,15 @@ export class Transaction {
|
|
|
1083
1561
|
}
|
|
1084
1562
|
// updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
|
|
1085
1563
|
// before storing it or later caller mutation of `output.script` will rewrite transaction state.
|
|
1086
|
-
|
|
1564
|
+
const current = this.outputs[idx];
|
|
1565
|
+
const normalized = cloneDeep(this.normalizeOutput(output, current, allowedFields));
|
|
1566
|
+
const transactionChanged =
|
|
1567
|
+
current.amount !== normalized.amount ||
|
|
1568
|
+
(current.script === undefined
|
|
1569
|
+
? normalized.script !== undefined
|
|
1570
|
+
: normalized.script === undefined || !equalBytes(current.script, normalized.script));
|
|
1571
|
+
if (transactionChanged) this.requireTxModifiable(0b010, 'outputs');
|
|
1572
|
+
this.outputs[idx] = normalized;
|
|
1087
1573
|
}
|
|
1088
1574
|
addOutputAddress(address: string, amount: bigint, network: u.BTC_NETWORK = NETWORK): number {
|
|
1089
1575
|
return this.addOutput({
|
|
@@ -1116,7 +1602,8 @@ export class Transaction {
|
|
|
1116
1602
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
1117
1603
|
if (idx < 0 || !Number.isSafeInteger(idx)) throw new Error(`Invalid input idx=${idx}`);
|
|
1118
1604
|
if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
|
|
1119
|
-
|
|
1605
|
+
// Bitcoin Core passes uint256::ONE's internal little-endian bytes directly to ECDSA.
|
|
1606
|
+
return P.U256LE.encode(_1n);
|
|
1120
1607
|
prevOutScript = stripCodeSeparator(prevOutScript);
|
|
1121
1608
|
let inputs: TransactionInputRequired[] = this.inputs
|
|
1122
1609
|
.map(inputBeforeSign)
|
|
@@ -1216,6 +1703,14 @@ export class Transaction {
|
|
|
1216
1703
|
const inType = hashType & SignatureHash.ANYONECANPAY;
|
|
1217
1704
|
const inputs = this.inputs.map(inputBeforeSign);
|
|
1218
1705
|
const outputs = this.outputs.map(outputBeforeSign);
|
|
1706
|
+
// Unlike legacy and segwit v0, BIP341 defines no digest for SINGLE when the
|
|
1707
|
+
// corresponding output does not exist. Returning a digest here would produce
|
|
1708
|
+
// signatures that consensus can never accept.
|
|
1709
|
+
if (outType === SignatureHash.SINGLE && idx >= outputs.length) {
|
|
1710
|
+
throw new Error(
|
|
1711
|
+
`Input with sighash SINGLE, but there is no output with corresponding index=${idx}`
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1219
1714
|
if (inType !== SignatureHash.ANYONECANPAY) {
|
|
1220
1715
|
out.push(
|
|
1221
1716
|
...[
|
|
@@ -1241,8 +1736,7 @@ export class Transaction {
|
|
|
1241
1736
|
);
|
|
1242
1737
|
} else out.push(P.U32LE.encode(idx));
|
|
1243
1738
|
if (spendType & 1) out.push(u.sha256(VarBytes.encode(annex || P.EMPTY)));
|
|
1244
|
-
if (outType === SignatureHash.SINGLE)
|
|
1245
|
-
out.push(idx < outputs.length ? u.sha256(RawOutput.encode(outputs[idx])) : EMPTY32);
|
|
1739
|
+
if (outType === SignatureHash.SINGLE) out.push(u.sha256(RawOutput.encode(outputs[idx])));
|
|
1246
1740
|
if (leafScript)
|
|
1247
1741
|
out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
|
|
1248
1742
|
return u.tagSchnorr('TapSighash', ...out);
|
|
@@ -1262,7 +1756,11 @@ export class Transaction {
|
|
|
1262
1756
|
);
|
|
1263
1757
|
}
|
|
1264
1758
|
this.checkInputIdx(idx);
|
|
1265
|
-
|
|
1759
|
+
this.validatePrevoutsForSigning();
|
|
1760
|
+
const input = validateInput(
|
|
1761
|
+
this.inputs[idx] as TArg<psbt.TransactionInput>,
|
|
1762
|
+
this.opts.disableScriptCheck
|
|
1763
|
+
);
|
|
1266
1764
|
const inputType = getInputType(
|
|
1267
1765
|
input as TArg<psbt.TransactionInput>,
|
|
1268
1766
|
this.opts.allowLegacyWitnessUtxo
|
|
@@ -1471,6 +1969,9 @@ export class Transaction {
|
|
|
1471
1969
|
// Even worse: another user can add bip32 derivation, and spend money from different address.
|
|
1472
1970
|
// Better api: signIdx
|
|
1473
1971
|
sign(privateKey: Signer, allowedSighash?: number[], _auxRand?: Bytes): number {
|
|
1972
|
+
// Check transaction-wide strict requirements outside the per-input catch below so callers get
|
|
1973
|
+
// the actionable validation error instead of the generic "No inputs signed" result.
|
|
1974
|
+
this.validatePrevoutsForSigning();
|
|
1474
1975
|
let num = 0;
|
|
1475
1976
|
for (let i = 0; i < this.inputs.length; i++) {
|
|
1476
1977
|
try {
|
|
@@ -1485,17 +1986,25 @@ export class Transaction {
|
|
|
1485
1986
|
this.checkInputIdx(idx);
|
|
1486
1987
|
if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
|
|
1487
1988
|
const input = this.inputs[idx];
|
|
1989
|
+
// Validate strict extension policy before constructing satisfaction so a rejection is atomic.
|
|
1990
|
+
cleanExtensions(input, this.opts.unknown!, this.opts.proprietary!);
|
|
1991
|
+
cleanTxModifiable(this.global.txModifiable, this.opts.unknown!);
|
|
1488
1992
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
1489
1993
|
// Taproot finalize
|
|
1490
1994
|
if (inputType.txType === 'taproot') {
|
|
1491
1995
|
if (input.tapKeySig) input.finalScriptWitness = [input.tapKeySig];
|
|
1492
1996
|
else if (input.tapLeafScript && input.tapScriptSig) {
|
|
1493
|
-
//
|
|
1494
|
-
const leafs = input.tapLeafScript
|
|
1495
|
-
(
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1997
|
+
// Preserve the old shallowest-path tie-break without mutating caller-visible leaf order.
|
|
1998
|
+
const leafs = input.tapLeafScript
|
|
1999
|
+
.slice()
|
|
2000
|
+
.sort(
|
|
2001
|
+
(a, b) =>
|
|
2002
|
+
psbt.TaprootControlBlock.encode(a[0]).length -
|
|
2003
|
+
psbt.TaprootControlBlock.encode(b[0]).length
|
|
2004
|
+
);
|
|
2005
|
+
let smallest: Bytes[] | undefined;
|
|
2006
|
+
let smallestSize = Number.POSITIVE_INFINITY;
|
|
2007
|
+
let unsupported = false;
|
|
1499
2008
|
for (const [cb, _script] of leafs) {
|
|
1500
2009
|
// Last byte is version
|
|
1501
2010
|
const script = _script.slice(0, -1);
|
|
@@ -1504,6 +2013,7 @@ export class Transaction {
|
|
|
1504
2013
|
const hash = tapLeafHash(script, ver);
|
|
1505
2014
|
const scriptSig = input.tapScriptSig.filter((i) => equalBytes(i[0].leafHash, hash));
|
|
1506
2015
|
let signatures: Bytes[] = [];
|
|
2016
|
+
let witness: Bytes[] | undefined;
|
|
1507
2017
|
if (outScript.type === 'tr_ms') {
|
|
1508
2018
|
const m = outScript.m;
|
|
1509
2019
|
const pubkeys = outScript.pubkeys;
|
|
@@ -1543,33 +2053,43 @@ export class Transaction {
|
|
|
1543
2053
|
if (!signatures.length) continue;
|
|
1544
2054
|
} else {
|
|
1545
2055
|
const custom = this.opts.customScripts;
|
|
2056
|
+
let recognized = false;
|
|
1546
2057
|
if (custom) {
|
|
1547
2058
|
for (const c of custom) {
|
|
1548
2059
|
if (!c.finalizeTaproot) continue;
|
|
1549
2060
|
const scriptDecoded = Script.decode(script);
|
|
1550
2061
|
const csEncoded = c.encode(scriptDecoded);
|
|
1551
2062
|
if (csEncoded === undefined) continue;
|
|
2063
|
+
recognized = true;
|
|
2064
|
+
// Do not catch hook errors here. `undefined` means no satisfaction, while a throw
|
|
2065
|
+
// from a matching custom finalizer reports broken leaf/signature data and must
|
|
2066
|
+
// abort even when another leaf has already produced a valid witness candidate.
|
|
1552
2067
|
const finalized = c.finalizeTaproot(script, csEncoded, scriptSig);
|
|
1553
2068
|
if (!finalized) continue;
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
cleanFinalInput(input as TArg<PSBTInputs>);
|
|
1557
|
-
return;
|
|
2069
|
+
witness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
|
|
2070
|
+
break;
|
|
1558
2071
|
}
|
|
1559
2072
|
}
|
|
1560
|
-
|
|
2073
|
+
// Minimum search inspects every leaf, so an unsupported path cannot block an already
|
|
2074
|
+
// complete known path merely because it appears later. Retain the old error when no
|
|
2075
|
+
// supported satisfaction exists at all.
|
|
2076
|
+
if (!witness && !recognized && scriptSig.length) unsupported = true;
|
|
2077
|
+
if (!witness) continue;
|
|
1561
2078
|
}
|
|
1562
2079
|
// Witness is stack, so last element will be used first
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
2080
|
+
witness ||= signatures.reverse().concat([script, psbt.TaprootControlBlock.encode(cb)]);
|
|
2081
|
+
const size = RawWitness.encode(witness).length;
|
|
2082
|
+
if (size >= smallestSize) continue;
|
|
2083
|
+
smallest = witness;
|
|
2084
|
+
smallestSize = size;
|
|
1567
2085
|
}
|
|
1568
|
-
if (!
|
|
2086
|
+
if (!smallest && unsupported) throw new Error('Finalize: Unknown tapLeafScript');
|
|
2087
|
+
if (!smallest) throw new Error('finalize/taproot: empty witness');
|
|
2088
|
+
input.finalScriptWitness = smallest;
|
|
1569
2089
|
} else throw new Error('finalize/taproot: unknown input');
|
|
1570
2090
|
// BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
|
|
1571
2091
|
delete input.finalScriptSig;
|
|
1572
|
-
cleanFinalInput(input
|
|
2092
|
+
this.cleanFinalInput(input);
|
|
1573
2093
|
return;
|
|
1574
2094
|
}
|
|
1575
2095
|
if (!input.partialSig || !input.partialSig.length) throw new Error('Not enough partial sign');
|
|
@@ -1629,7 +2149,7 @@ export class Transaction {
|
|
|
1629
2149
|
if (!finalScriptSig && !finalScriptWitness) throw new Error('Unknown error finalizing input');
|
|
1630
2150
|
if (finalScriptSig) input.finalScriptSig = finalScriptSig;
|
|
1631
2151
|
if (finalScriptWitness) input.finalScriptWitness = finalScriptWitness;
|
|
1632
|
-
cleanFinalInput(input
|
|
2152
|
+
this.cleanFinalInput(input);
|
|
1633
2153
|
}
|
|
1634
2154
|
finalize(): void {
|
|
1635
2155
|
for (let i = 0; i < this.inputs.length; i++) this.finalizeIdx(i);
|
|
@@ -1643,15 +2163,25 @@ export class Transaction {
|
|
|
1643
2163
|
combine(other: Transaction): this {
|
|
1644
2164
|
if (!(other instanceof Transaction))
|
|
1645
2165
|
throw new TypeError('"other" expected Transaction, got type=' + typeof other);
|
|
2166
|
+
// Match main's accumulator model: operation policy belongs to the receiver that is mutated.
|
|
2167
|
+
const opts = this.opts;
|
|
1646
2168
|
// BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
|
|
1647
2169
|
// version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
|
|
1648
2170
|
const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
|
|
1649
|
-
|
|
1650
|
-
|
|
2171
|
+
if (this.opts.version !== other.opts.version)
|
|
2172
|
+
throw new Error(
|
|
2173
|
+
`Transaction/combine: different version this=${this.opts.version} ` +
|
|
2174
|
+
`other=${other.opts.version}`
|
|
2175
|
+
);
|
|
2176
|
+
const thisV2 = this.isPSBTv2();
|
|
2177
|
+
const otherV2 = other.isPSBTv2();
|
|
2178
|
+
if (!thisV2 || !otherV2) {
|
|
2179
|
+
const thisLockTime = this.lockTime;
|
|
2180
|
+
const otherLockTime = other.lockTime;
|
|
2181
|
+
if (thisLockTime !== otherLockTime)
|
|
1651
2182
|
throw new Error(
|
|
1652
|
-
`Transaction/combine: different
|
|
2183
|
+
`Transaction/combine: different lockTime this=${thisLockTime} other=${otherLockTime}`
|
|
1653
2184
|
);
|
|
1654
|
-
}
|
|
1655
2185
|
}
|
|
1656
2186
|
for (const k of ['inputs', 'outputs'] as const) {
|
|
1657
2187
|
if (this[k].length !== other[k].length) {
|
|
@@ -1662,18 +2192,139 @@ export class Transaction {
|
|
|
1662
2192
|
}
|
|
1663
2193
|
// Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
|
|
1664
2194
|
// `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
|
|
1665
|
-
|
|
2195
|
+
const unsignedTx = this.unsignedTx;
|
|
2196
|
+
if (!equalBytes(unsignedTx, other.unsignedTx))
|
|
1666
2197
|
throw new Error(`Transaction/combine: different unsigned tx`);
|
|
1667
|
-
|
|
2198
|
+
let txModifiable: number | undefined;
|
|
2199
|
+
if (thisV2 && otherV2) {
|
|
2200
|
+
// Core combines the stored optional bytes without deriving replacements from signatures.
|
|
2201
|
+
// Only explicit legacy-omission compatibility gives an absent field an effective value.
|
|
2202
|
+
const policy = (tx: Transaction) => {
|
|
2203
|
+
if (tx.global.txModifiable !== undefined)
|
|
2204
|
+
return cleanTxModifiable(tx.global.txModifiable, opts.unknown!)!;
|
|
2205
|
+
return opts.allowMissingTxModifiable ? tx.modifiable(true, opts.unknown!) : 0;
|
|
2206
|
+
};
|
|
2207
|
+
const a = policy(this);
|
|
2208
|
+
const b = policy(other);
|
|
2209
|
+
// Known mutability permissions use intersection and SIGHASH_SINGLE presence uses union.
|
|
2210
|
+
// Future flag bits must agree because this implementation does not know how to merge them.
|
|
2211
|
+
if ((a & ~0b111) !== (b & ~0b111))
|
|
2212
|
+
throw new Error('Transaction/combine: conflicting unknown txModifiable flags');
|
|
2213
|
+
txModifiable = (a & ~0b111) | (a & b & 0b011) | ((a | b) & 0b100);
|
|
2214
|
+
// Preserve Core's optional-field semantics when neither participant supplied policy.
|
|
2215
|
+
if (
|
|
2216
|
+
txModifiable === 0 &&
|
|
2217
|
+
this.global.txModifiable === undefined &&
|
|
2218
|
+
other.global.txModifiable === undefined &&
|
|
2219
|
+
!opts.allowMissingTxModifiable
|
|
2220
|
+
)
|
|
2221
|
+
txModifiable = undefined;
|
|
2222
|
+
} else if (thisV2) txModifiable = this.modifiable(opts.allowMissingTxModifiable, opts.unknown!);
|
|
2223
|
+
else if (otherV2) txModifiable = other.modifiable(opts.allowMissingTxModifiable, opts.unknown!);
|
|
2224
|
+
const thisGlobal = { ...this.global };
|
|
2225
|
+
const otherGlobal = { ...other.global };
|
|
2226
|
+
// PSBTv0 has no fallback-locktime field: fromPSBT caches unsignedTx.nLockTime there only for
|
|
2227
|
+
// effective-locktime resolution and v2 promotion. Do not merge that cache as a v2 wire value.
|
|
2228
|
+
if (thisV2 !== otherV2) {
|
|
2229
|
+
if (!thisV2) delete thisGlobal.fallbackLocktime;
|
|
2230
|
+
if (!otherV2) delete otherGlobal.fallbackLocktime;
|
|
2231
|
+
// BIP174 permits v0 to encode version zero explicitly or omit it. Remove only that v0
|
|
2232
|
+
// spelling before scalar conflicts; retaining the v2 field anchors repeated promotion when
|
|
2233
|
+
// the accumulator's original options still target v0.
|
|
2234
|
+
if (!thisV2) delete thisGlobal.version;
|
|
2235
|
+
if (!otherV2) delete otherGlobal.version;
|
|
2236
|
+
}
|
|
2237
|
+
// Transaction-modifiable has dedicated bitwise merge rules above. Fallback locktime is only
|
|
2238
|
+
// one input to the effective locktime resolved from the combined input maps, so retain receiver
|
|
2239
|
+
// precedence and validate the resulting unsigned transaction after those maps merge below.
|
|
2240
|
+
const fallbackLocktime =
|
|
2241
|
+
thisGlobal.fallbackLocktime !== undefined
|
|
2242
|
+
? thisGlobal.fallbackLocktime
|
|
2243
|
+
: otherGlobal.fallbackLocktime;
|
|
2244
|
+
delete thisGlobal.txModifiable;
|
|
2245
|
+
delete otherGlobal.txModifiable;
|
|
2246
|
+
delete thisGlobal.fallbackLocktime;
|
|
2247
|
+
delete otherGlobal.fallbackLocktime;
|
|
2248
|
+
// Every ordinary global scalar must agree when both participants provide it; silently choosing
|
|
2249
|
+
// either value can detach extension metadata such as a BIP322 message from its signatures.
|
|
2250
|
+
const global = psbt.combineKeyMap(
|
|
1668
2251
|
psbt.PSBTGlobal,
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2252
|
+
thisGlobal,
|
|
2253
|
+
otherGlobal,
|
|
2254
|
+
opts.unknown!,
|
|
2255
|
+
opts.proprietary!
|
|
1673
2256
|
);
|
|
1674
|
-
if (
|
|
1675
|
-
|
|
1676
|
-
|
|
2257
|
+
if (fallbackLocktime !== undefined) global.fallbackLocktime = fallbackLocktime;
|
|
2258
|
+
if (PSBTVersion) global.version = PSBTVersion;
|
|
2259
|
+
if (txModifiable === undefined) delete global.txModifiable;
|
|
2260
|
+
else global.txModifiable = txModifiable;
|
|
2261
|
+
let hasOpaqueFinalizedV0 = false;
|
|
2262
|
+
const inputs = this.inputs.map((current, i) => {
|
|
2263
|
+
const currentFinal = this.inputStatus(i) === 'finalized';
|
|
2264
|
+
const otherFinal = other.inputStatus(i) === 'finalized';
|
|
2265
|
+
// Finalized v0 maps no longer contain the partial signatures needed to derive v2 flags.
|
|
2266
|
+
if ((!thisV2 && currentFinal) || (!otherV2 && otherFinal)) hasOpaqueFinalizedV0 = true;
|
|
2267
|
+
if (currentFinal && otherFinal) {
|
|
2268
|
+
// Two finalized PSBTs must describe the same complete satisfaction. Requiring matching
|
|
2269
|
+
// presence as well as matching values prevents combining witness-only and scriptSig-only
|
|
2270
|
+
// final states into a third, unreviewed satisfaction.
|
|
2271
|
+
for (const k of ['finalScriptSig', 'finalScriptWitness'] as const) {
|
|
2272
|
+
const currentHas = !!this.inputs[i][k]?.length;
|
|
2273
|
+
const otherHas = !!other.inputs[i][k]?.length;
|
|
2274
|
+
if (currentHas !== otherHas)
|
|
2275
|
+
throw new Error(`Transaction/combine: different finalized field=${k} input=${i}`);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const combined = psbt.combineKeyMap(
|
|
2279
|
+
psbt.PSBTInput,
|
|
2280
|
+
current,
|
|
2281
|
+
other.inputs[i],
|
|
2282
|
+
opts.unknown!,
|
|
2283
|
+
opts.proprietary!
|
|
2284
|
+
) as PSBTInputs;
|
|
2285
|
+
// A final satisfaction supersedes partial signatures and transient signing metadata. This
|
|
2286
|
+
// also avoids manufacturing a contradictory final+partial input from two valid PSBTs.
|
|
2287
|
+
if (currentFinal || otherFinal)
|
|
2288
|
+
cleanFinalInput(combined as TArg<PSBTInputs>, opts.unknown!, opts.proprietary!);
|
|
2289
|
+
return cloneDeep(
|
|
2290
|
+
normalizeInput(
|
|
2291
|
+
combined,
|
|
2292
|
+
undefined,
|
|
2293
|
+
undefined,
|
|
2294
|
+
opts.disableScriptCheck,
|
|
2295
|
+
opts.unknown!,
|
|
2296
|
+
opts.proprietary!
|
|
2297
|
+
)
|
|
2298
|
+
) as PSBTInputs;
|
|
2299
|
+
});
|
|
2300
|
+
// A promoted opaque satisfaction may commit to both transaction halves. Clear only known
|
|
2301
|
+
// permissions; retain a v2 participant's SIGHASH_SINGLE indicator and any future flag bits.
|
|
2302
|
+
if (hasOpaqueFinalizedV0 && global.txModifiable !== undefined) global.txModifiable &= ~0b011;
|
|
2303
|
+
const candidate = new Transaction({ ...opts, PSBTVersion });
|
|
2304
|
+
const outputs = this.outputs.map((current, i) => {
|
|
2305
|
+
const combined = psbt.combineKeyMap(
|
|
2306
|
+
psbt.PSBTOutput,
|
|
2307
|
+
current,
|
|
2308
|
+
other.outputs[i],
|
|
2309
|
+
opts.unknown!,
|
|
2310
|
+
opts.proprietary!
|
|
2311
|
+
);
|
|
2312
|
+
return cloneDeep(candidate.normalizeOutput(combined));
|
|
2313
|
+
});
|
|
2314
|
+
// Build and validate a detached candidate before touching the receiver. Combining
|
|
2315
|
+
// complementary v2 locktime fields can otherwise create a different unsigned transaction.
|
|
2316
|
+
candidate.global = global;
|
|
2317
|
+
candidate.inputs = inputs;
|
|
2318
|
+
candidate.outputs = outputs;
|
|
2319
|
+
// A v0 input map can contribute signatures while the combined transaction is promoted to v2.
|
|
2320
|
+
// All-v2 restrictions were already intersected above, preserving mutual field omission.
|
|
2321
|
+
if ((!thisV2 || !otherV2) && candidate.isPSBTv2())
|
|
2322
|
+
candidate.global.txModifiable = candidate.txModifiable;
|
|
2323
|
+
if (!equalBytes(candidate.unsignedTx, unsignedTx))
|
|
2324
|
+
throw new Error('Transaction/combine: combined unsigned tx differs');
|
|
2325
|
+
this.global = candidate.global;
|
|
2326
|
+
this.inputs = candidate.inputs;
|
|
2327
|
+
this.outputs = candidate.outputs;
|
|
1677
2328
|
return this;
|
|
1678
2329
|
}
|
|
1679
2330
|
clone(): Transaction {
|
|
@@ -1685,6 +2336,7 @@ export class Transaction {
|
|
|
1685
2336
|
/**
|
|
1686
2337
|
* Merges multiple PSBT blobs into one.
|
|
1687
2338
|
* @param psbts - PSBT byte arrays to combine
|
|
2339
|
+
* @param opts - Transaction parsing, combination, and serialization options. See {@link TxOpts}.
|
|
1688
2340
|
* @returns Combined PSBT bytes.
|
|
1689
2341
|
* @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
|
|
1690
2342
|
* @example
|
|
@@ -1695,12 +2347,14 @@ export class Transaction {
|
|
|
1695
2347
|
* PSBTCombine([psbt, psbt]);
|
|
1696
2348
|
* ```
|
|
1697
2349
|
*/
|
|
1698
|
-
export function PSBTCombine(psbts: TArg<Bytes[]>): TRet<Bytes> {
|
|
2350
|
+
export function PSBTCombine(psbts: TArg<Bytes[]>, opts: TArg<TxOpts> = {}): TRet<Bytes> {
|
|
1699
2351
|
if (!psbts || !Array.isArray(psbts) || !psbts.length)
|
|
1700
2352
|
throw new Error('PSBTCombine: wrong PSBT list');
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2353
|
+
// Options affect both map cleanup during combination and the encoding of the returned PSBT.
|
|
2354
|
+
const combineOpts = opts as TxOpts;
|
|
2355
|
+
const tx = Transaction.fromPSBT(psbts[0], combineOpts);
|
|
2356
|
+
for (let i = 1; i < psbts.length; i++) tx.combine(Transaction.fromPSBT(psbts[i], tx.opts));
|
|
2357
|
+
return tx.toPSBT(combineOpts.PSBTVersion) as TRet<Bytes>;
|
|
1704
2358
|
}
|
|
1705
2359
|
|
|
1706
2360
|
// Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
|