@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/transaction.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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 { Address, OutScript, checkScript, tapLeafHash } from "./payment.js";
|
|
4
|
+
import { Address, OutScript, _WitnessOutScript, checkScript, tapLeafHash, } from "./payment.js";
|
|
5
5
|
import * as psbt from "./psbt.js";
|
|
6
|
-
import { CompactSizeLen, OP, RawOldTx, RawInput, RawOutput, RawTx, Script, scriptPushLen, VarBytes, } from "./script.js";
|
|
6
|
+
import { CompactSizeLen, OP, RawOldTx, RawInput, RawOutput, RawTx, RawWitness, Script, scriptPushLen, VarBytes, } from "./script.js";
|
|
7
7
|
import * as u from "./utils.js";
|
|
8
8
|
import { NETWORK, abigint, concatBytes, equalBytes, isBytes, validateObject, } from "./utils.js";
|
|
9
9
|
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
@@ -214,13 +214,61 @@ export function inputBeforeSign(i) {
|
|
|
214
214
|
RawInput.encode(res);
|
|
215
215
|
return res;
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
const cleanExtensions = (map, unknownMode, proprietaryMode, rejectStrip = false) => {
|
|
218
|
+
const out = { ...map };
|
|
219
|
+
for (const [name, mode] of [
|
|
220
|
+
['unknown', unknownMode],
|
|
221
|
+
['proprietary', proprietaryMode],
|
|
222
|
+
]) {
|
|
223
|
+
const value = out[name];
|
|
224
|
+
// Policy cleanup must not reinterpret malformed caller metadata as an empty keyed map.
|
|
225
|
+
if (value !== undefined)
|
|
226
|
+
u.aarray(value, `${name} PSBT field`);
|
|
227
|
+
const rows = value;
|
|
228
|
+
if (!rows?.length) {
|
|
229
|
+
delete out[name];
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (mode === 'strict')
|
|
233
|
+
throw new Error(`PSBT: ${name} PSBT field is not allowed in strict mode`);
|
|
234
|
+
if (mode === 'strip') {
|
|
235
|
+
// Silent stripping is appropriate at relay/cleanup boundaries. On direct mutation it would
|
|
236
|
+
// hide a caller bug by accepting metadata that can never become transaction state.
|
|
237
|
+
if (rejectStrip)
|
|
238
|
+
throw new Error(`PSBT: ${name} PSBT field cannot be supplied when policy is strip`);
|
|
239
|
+
delete out[name];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
};
|
|
244
|
+
const cleanTxModifiable = (value, mode) => {
|
|
245
|
+
if (value === undefined || !(value & ~0b111))
|
|
246
|
+
return value;
|
|
247
|
+
if (mode === 'strict')
|
|
248
|
+
throw new Error('PSBT: unknown txModifiable bits in strict mode');
|
|
249
|
+
return mode === 'strip' ? value & 0b111 : value;
|
|
250
|
+
};
|
|
251
|
+
function cleanFinalInput(i, unknownMode = 'strip', proprietaryMode = 'strip') {
|
|
218
252
|
const _i = i;
|
|
253
|
+
const extensions = cleanExtensions(_i, unknownMode, proprietaryMode);
|
|
254
|
+
if (extensions.unknown)
|
|
255
|
+
_i.unknown = extensions.unknown;
|
|
256
|
+
else
|
|
257
|
+
delete _i.unknown;
|
|
258
|
+
if (extensions.proprietary)
|
|
259
|
+
_i.proprietary = extensions.proprietary;
|
|
260
|
+
else
|
|
261
|
+
delete _i.proprietary;
|
|
219
262
|
// BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
|
|
220
263
|
// That intentionally drops sighashType here, so post-finalize mutation becomes conservative
|
|
221
|
-
// until callers explicitly
|
|
264
|
+
// until callers explicitly clear satisfaction by removing finalScriptSig/finalScriptWitness.
|
|
222
265
|
for (const _k in _i) {
|
|
223
266
|
const k = _k;
|
|
267
|
+
// Proprietary records are cleanup metadata too, but callers may need their opaque protocol
|
|
268
|
+
// state after finalization for PSBT coordination outside transaction extraction. An empty
|
|
269
|
+
// keyed list encodes no records, so canonicalize it to absence like its serialized clone.
|
|
270
|
+
if (proprietaryMode === 'ignore' && k === 'proprietary' && _i.proprietary?.length)
|
|
271
|
+
continue;
|
|
224
272
|
if (!psbt.PSBTInputFinalKeys.includes(k))
|
|
225
273
|
delete _i[k];
|
|
226
274
|
}
|
|
@@ -240,6 +288,18 @@ function unpackSighash(hashType) {
|
|
|
240
288
|
isSingle: masked === SignatureHash.SINGLE,
|
|
241
289
|
};
|
|
242
290
|
}
|
|
291
|
+
const sighashScope = (sighash) => ({
|
|
292
|
+
sigInputs: sighash & SignatureHash.ANYONECANPAY,
|
|
293
|
+
sigOutputs: sighash === SignatureHash.DEFAULT ? SignatureHash.ALL : sighash & 0b11,
|
|
294
|
+
});
|
|
295
|
+
const normalizeUnknowns = (name, mode, legacy, fallback = 'strip') => {
|
|
296
|
+
const alias = legacy === undefined ? undefined : legacy ? 'ignore' : 'strip';
|
|
297
|
+
if (mode !== undefined && mode !== 'ignore' && mode !== 'strip' && mode !== 'strict')
|
|
298
|
+
throw new Error(`Transaction options wrong value: ${name}=${mode}`);
|
|
299
|
+
if (mode !== undefined && alias !== undefined && mode !== alias)
|
|
300
|
+
throw new Error(`Transaction options: conflicting ${name} options`);
|
|
301
|
+
return mode || alias || fallback;
|
|
302
|
+
};
|
|
243
303
|
function validateOpts(opts) {
|
|
244
304
|
if (opts !== undefined)
|
|
245
305
|
validateObject(opts, {}, {}, 'opts');
|
|
@@ -256,6 +316,8 @@ function validateOpts(opts) {
|
|
|
256
316
|
_opts.allowUnknownInputs = _opts.allowUnknowInput;
|
|
257
317
|
if (typeof _opts.allowUnknowOutput !== 'undefined')
|
|
258
318
|
_opts.allowUnknownOutputs = _opts.allowUnknowOutput;
|
|
319
|
+
if (_opts.allowMissingTxModifiable === undefined)
|
|
320
|
+
_opts.allowMissingTxModifiable = true;
|
|
259
321
|
if (typeof _opts.lockTime !== 'number')
|
|
260
322
|
throw new Error('Transaction lock time should be number');
|
|
261
323
|
P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
|
|
@@ -271,7 +333,10 @@ function validateOpts(opts) {
|
|
|
271
333
|
'disableScriptCheck',
|
|
272
334
|
'bip174jsCompat',
|
|
273
335
|
'allowLegacyWitnessUtxo',
|
|
336
|
+
'strictPrevoutValidation',
|
|
274
337
|
'lowR',
|
|
338
|
+
'allowUnknown',
|
|
339
|
+
'allowMissingTxModifiable',
|
|
275
340
|
]) {
|
|
276
341
|
const v = _opts[k];
|
|
277
342
|
if (v === undefined)
|
|
@@ -279,6 +344,8 @@ function validateOpts(opts) {
|
|
|
279
344
|
if (typeof v !== 'boolean')
|
|
280
345
|
throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
|
|
281
346
|
}
|
|
347
|
+
_opts.unknown = normalizeUnknowns('unknown', _opts.unknown, _opts.allowUnknown);
|
|
348
|
+
_opts.proprietary = normalizeUnknowns('proprietary', _opts.proprietary, undefined, _opts.unknown);
|
|
282
349
|
// 0 and -1 happens in tests
|
|
283
350
|
// With allowUnknownVersion any numeric version is fine; the ternary was inverted
|
|
284
351
|
// before 2026-07 (audit), which made the option throw for every numeric version.
|
|
@@ -301,15 +368,105 @@ function validateOpts(opts) {
|
|
|
301
368
|
}
|
|
302
369
|
return Object.freeze(_opts);
|
|
303
370
|
}
|
|
371
|
+
function checkTaprootInputCommitments(input, prevScript) {
|
|
372
|
+
const output = _WitnessOutScript.decode(prevScript);
|
|
373
|
+
const hasTaprootCommitments = input.tapInternalKey !== undefined ||
|
|
374
|
+
input.tapMerkleRoot !== undefined ||
|
|
375
|
+
// Repeated keyed fields only exist on the PSBT wire when at least one entry is encoded.
|
|
376
|
+
!!input.tapLeafScript?.length;
|
|
377
|
+
if (output.type !== 'tr') {
|
|
378
|
+
if (hasTaprootCommitments)
|
|
379
|
+
throw new Error('validateInput: Taproot metadata without P2TR previous output');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const checkOutputKey = (internalKey, merkleRoot, parity) => {
|
|
383
|
+
const [outputKey, outputParity] = u.taprootTweakPubkey(internalKey, merkleRoot);
|
|
384
|
+
if (!equalBytes(outputKey, output.pubkey))
|
|
385
|
+
throw new Error('validateInput: Taproot commitment does not match previous output');
|
|
386
|
+
if (parity !== undefined && outputParity !== parity)
|
|
387
|
+
throw new Error('validateInput: Taproot control-block parity does not match previous output');
|
|
388
|
+
};
|
|
389
|
+
if (input.tapLeafScript) {
|
|
390
|
+
for (const [controlBlock, scriptWithVersion] of input.tapLeafScript) {
|
|
391
|
+
const leafVersion = scriptWithVersion[scriptWithVersion.length - 1];
|
|
392
|
+
const script = scriptWithVersion.subarray(0, -1);
|
|
393
|
+
let merkleRoot = tapLeafHash(script, leafVersion);
|
|
394
|
+
for (const sibling of controlBlock.merklePath) {
|
|
395
|
+
merkleRoot =
|
|
396
|
+
u.compareBytes(sibling, merkleRoot) === -1
|
|
397
|
+
? u.tagSchnorr('TapBranch', sibling, merkleRoot)
|
|
398
|
+
: u.tagSchnorr('TapBranch', merkleRoot, sibling);
|
|
399
|
+
}
|
|
400
|
+
checkOutputKey(controlBlock.internalKey, merkleRoot, controlBlock.version & 1);
|
|
401
|
+
if (input.tapInternalKey && !equalBytes(input.tapInternalKey, controlBlock.internalKey))
|
|
402
|
+
throw new Error('validateInput: tapInternalKey does not match Taproot control block');
|
|
403
|
+
if (input.tapMerkleRoot && !equalBytes(input.tapMerkleRoot, merkleRoot))
|
|
404
|
+
throw new Error('validateInput: tapMerkleRoot does not match Taproot control block');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// A tree-bearing input can omit the aggregate root while still providing independently
|
|
408
|
+
// verifiable control blocks. With no leaves, an internal key without a root describes the
|
|
409
|
+
// standard key-only (empty-root) commitment.
|
|
410
|
+
if (input.tapInternalKey && (input.tapMerkleRoot || !input.tapLeafScript?.length))
|
|
411
|
+
checkOutputKey(input.tapInternalKey, input.tapMerkleRoot || P.EMPTY);
|
|
412
|
+
}
|
|
413
|
+
const LOCKTIME_THRESHOLD = 500_000_000;
|
|
414
|
+
function validateRequiredLocktimes(input) {
|
|
415
|
+
const height = input.requiredHeightLocktime;
|
|
416
|
+
if (height !== undefined) {
|
|
417
|
+
anumber(height, 'requiredHeightLocktime');
|
|
418
|
+
if (height === 0 || height >= LOCKTIME_THRESHOLD)
|
|
419
|
+
throw new RangeError(`requiredHeightLocktime must be between 1 and ${LOCKTIME_THRESHOLD - 1}, got ${height}`);
|
|
420
|
+
}
|
|
421
|
+
const time = input.requiredTimeLocktime;
|
|
422
|
+
if (time !== undefined) {
|
|
423
|
+
anumber(time, 'requiredTimeLocktime');
|
|
424
|
+
if (time < LOCKTIME_THRESHOLD || time > 0xffffffff)
|
|
425
|
+
throw new RangeError(`requiredTimeLocktime must be between ${LOCKTIME_THRESHOLD} and 4294967295, got ${time}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function resolvePSBTLocktime(inputs, fallback = DEFAULT_LOCKTIME) {
|
|
429
|
+
let height = DEFAULT_LOCKTIME;
|
|
430
|
+
let time = DEFAULT_LOCKTIME;
|
|
431
|
+
let hasRequirements = false;
|
|
432
|
+
let heightSupported = true;
|
|
433
|
+
let timeSupported = true;
|
|
434
|
+
for (const input of inputs) {
|
|
435
|
+
validateRequiredLocktimes(input);
|
|
436
|
+
const hasHeight = input.requiredHeightLocktime !== undefined;
|
|
437
|
+
const hasTime = input.requiredTimeLocktime !== undefined;
|
|
438
|
+
if (!hasHeight && !hasTime)
|
|
439
|
+
continue;
|
|
440
|
+
hasRequirements = true;
|
|
441
|
+
if (hasHeight)
|
|
442
|
+
height = Math.max(height, input.requiredHeightLocktime);
|
|
443
|
+
else
|
|
444
|
+
heightSupported = false;
|
|
445
|
+
if (hasTime)
|
|
446
|
+
time = Math.max(time, input.requiredTimeLocktime);
|
|
447
|
+
else
|
|
448
|
+
timeSupported = false;
|
|
449
|
+
}
|
|
450
|
+
if (!hasRequirements)
|
|
451
|
+
return fallback;
|
|
452
|
+
// BIP370 requires height when every relevant input supports both domains.
|
|
453
|
+
if (heightSupported)
|
|
454
|
+
return height;
|
|
455
|
+
if (timeSupported)
|
|
456
|
+
return time;
|
|
457
|
+
throw new Error('PSBTv2: incompatible height-based and time-based locktime requirements');
|
|
458
|
+
}
|
|
304
459
|
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
305
|
-
function validateInput(i) {
|
|
460
|
+
function validateInput(i, disableScriptCheck = false) {
|
|
306
461
|
validateObject(i, {}, {}, 'i');
|
|
307
462
|
const _i = i;
|
|
463
|
+
validateRequiredLocktimes(_i);
|
|
464
|
+
let prevOut;
|
|
308
465
|
if (_i.nonWitnessUtxo && _i.index !== undefined) {
|
|
309
466
|
const last = _i.nonWitnessUtxo.outputs.length - 1;
|
|
310
467
|
if (_i.index > last)
|
|
311
468
|
throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
|
|
312
|
-
|
|
469
|
+
prevOut = _i.nonWitnessUtxo.outputs[_i.index];
|
|
313
470
|
if (_i.witnessUtxo &&
|
|
314
471
|
(!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
|
|
315
472
|
_i.witnessUtxo.amount !== prevOut.amount))
|
|
@@ -344,8 +501,43 @@ function validateInput(i) {
|
|
|
344
501
|
throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
345
502
|
}
|
|
346
503
|
}
|
|
504
|
+
else if (_i.witnessUtxo)
|
|
505
|
+
prevOut = _i.witnessUtxo;
|
|
506
|
+
if (prevOut && !disableScriptCheck) {
|
|
507
|
+
checkScript(prevOut.script, _i.redeemScript, _i.witnessScript);
|
|
508
|
+
checkTaprootInputCommitments(_i, prevOut.script);
|
|
509
|
+
}
|
|
347
510
|
return _i;
|
|
348
511
|
}
|
|
512
|
+
const inputSignatures = (input) => {
|
|
513
|
+
const _input = input;
|
|
514
|
+
const res = [];
|
|
515
|
+
const add = (signature, taproot, scriptPath = false) => {
|
|
516
|
+
const sig = signature;
|
|
517
|
+
if (!sig.length)
|
|
518
|
+
return;
|
|
519
|
+
// Taproot's 64-byte encoding omits the SIGHASH_DEFAULT byte; every other PSBT signature
|
|
520
|
+
// carries its sighash in the final byte, including signatures imported from another signer.
|
|
521
|
+
const sighash = taproot && sig.length === 64 ? SignatureHash.DEFAULT : sig[sig.length - 1];
|
|
522
|
+
res.push({ sighash, taproot, scriptPath });
|
|
523
|
+
};
|
|
524
|
+
for (const [, signature] of _input.partialSig || [])
|
|
525
|
+
add(signature, false);
|
|
526
|
+
if (_input.tapKeySig)
|
|
527
|
+
add(_input.tapKeySig, true);
|
|
528
|
+
for (const [, signature] of _input.tapScriptSig || [])
|
|
529
|
+
add(signature, true, true);
|
|
530
|
+
return res;
|
|
531
|
+
};
|
|
532
|
+
const inputSignedKeys = {
|
|
533
|
+
// sighashType is signer policy rather than a digest byte, but changing it after one signature
|
|
534
|
+
// exists would make later signers interpret the same input under a different policy.
|
|
535
|
+
self: ['txid', 'index', 'sequence', 'nonWitnessUtxo', 'witnessUtxo', 'sighashType'],
|
|
536
|
+
ecdsa: ['redeemScript', 'witnessScript'],
|
|
537
|
+
tapscript: ['tapLeafScript'],
|
|
538
|
+
cross: ['txid', 'index'],
|
|
539
|
+
prevout: ['nonWitnessUtxo', 'witnessUtxo'],
|
|
540
|
+
};
|
|
349
541
|
// Normalizes input
|
|
350
542
|
/**
|
|
351
543
|
* Extracts the previous output referenced by an input.
|
|
@@ -390,9 +582,12 @@ export function getPrevOut(input) {
|
|
|
390
582
|
* @param i - input update to normalize
|
|
391
583
|
* @param cur - existing input value to merge with
|
|
392
584
|
* @param allowedFields - fields that may still change on signed inputs
|
|
393
|
-
* @param disableScriptCheck - whether to skip
|
|
394
|
-
* @param
|
|
585
|
+
* @param disableScriptCheck - whether to skip wrapper and Taproot commitment sanity checks
|
|
586
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
587
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
395
588
|
* @returns Normalized PSBT input.
|
|
589
|
+
* @throws If the update conflicts with the existing input or its signatures. {@link Error}
|
|
590
|
+
* @throws If a numeric input field is outside its wire or protocol range. {@link RangeError}
|
|
396
591
|
* @example
|
|
397
592
|
* Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
|
|
398
593
|
* normalize them into the repo's internal `TransactionInput` shape.
|
|
@@ -406,7 +601,7 @@ export function getPrevOut(input) {
|
|
|
406
601
|
* });
|
|
407
602
|
* ```
|
|
408
603
|
*/
|
|
409
|
-
export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false,
|
|
604
|
+
export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, unknown = 'strip', proprietary = 'strip') {
|
|
410
605
|
validateObject(i, {}, {}, 'i');
|
|
411
606
|
if (cur !== undefined)
|
|
412
607
|
validateObject(cur, {}, {}, 'cur');
|
|
@@ -438,18 +633,19 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
|
|
|
438
633
|
res.sequence = DEFAULT_SEQUENCE;
|
|
439
634
|
if (res.tapMerkleRoot === null)
|
|
440
635
|
delete res.tapMerkleRoot;
|
|
441
|
-
res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields,
|
|
636
|
+
res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, unknown, proprietary);
|
|
637
|
+
// An actual empty repeated field emits no PSBT keypairs. Canonicalize only arrays so malformed
|
|
638
|
+
// falsy values still reach the PSBT coder's validation instead of becoming valid absence.
|
|
639
|
+
if (Array.isArray(res.tapLeafScript) && !res.tapLeafScript.length)
|
|
640
|
+
delete res.tapLeafScript;
|
|
641
|
+
validateRequiredLocktimes(res);
|
|
442
642
|
// Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
|
|
443
643
|
// the repo's historical raw internal shape and casts only at the validation boundary here.
|
|
444
644
|
psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
prevOut = res.witnessUtxo;
|
|
450
|
-
if (prevOut && !disableScriptCheck)
|
|
451
|
-
checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
|
|
452
|
-
return res;
|
|
645
|
+
// Direct construction and UTXO selection consume nonWitnessUtxo amounts without crossing a
|
|
646
|
+
// PSBT serialization boundary. Enforce the same outpoint binding here so a mismatched previous
|
|
647
|
+
// transaction cannot understate a legacy input amount and turn the difference into mining fees.
|
|
648
|
+
return validateInput(res, disableScriptCheck);
|
|
453
649
|
}
|
|
454
650
|
/**
|
|
455
651
|
* Determines how an input should be signed and finalized.
|
|
@@ -480,10 +676,13 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
480
676
|
let txType = 'legacy';
|
|
481
677
|
let defaultSighash = SignatureHash.ALL;
|
|
482
678
|
const prevOut = getPrevOut(_input);
|
|
483
|
-
const first =
|
|
679
|
+
const first = _WitnessOutScript.decode(prevOut.script);
|
|
484
680
|
let type = first.type;
|
|
485
681
|
let cur = first;
|
|
486
682
|
const stack = [first];
|
|
683
|
+
// Classification is semantic, but legacy/BIP143 scriptCode and finalization must retain the
|
|
684
|
+
// exact committed spelling (including consensus-valid non-minimal pushes).
|
|
685
|
+
let lastScript = prevOut.script;
|
|
487
686
|
if (first.type === 'tr') {
|
|
488
687
|
// Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
|
|
489
688
|
// legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
|
|
@@ -503,11 +702,12 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
503
702
|
if (first.type === 'sh') {
|
|
504
703
|
if (!_input.redeemScript)
|
|
505
704
|
throw new Error('inputType: sh without redeemScript');
|
|
506
|
-
let child =
|
|
705
|
+
let child = _WitnessOutScript.decode(_input.redeemScript);
|
|
507
706
|
if (child.type === 'wpkh' || child.type === 'wsh')
|
|
508
707
|
txType = 'segwit';
|
|
509
708
|
stack.push(child);
|
|
510
709
|
cur = child;
|
|
710
|
+
lastScript = _input.redeemScript;
|
|
511
711
|
type += `-${child.type}`;
|
|
512
712
|
}
|
|
513
713
|
// wsh can be inside sh
|
|
@@ -519,12 +719,12 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
519
719
|
txType = 'segwit';
|
|
520
720
|
stack.push(child);
|
|
521
721
|
cur = child;
|
|
722
|
+
lastScript = _input.witnessScript;
|
|
522
723
|
type += `-${child.type}`;
|
|
523
724
|
}
|
|
524
725
|
const last = stack[stack.length - 1];
|
|
525
726
|
if (last.type === 'sh' || last.type === 'wsh')
|
|
526
727
|
throw new Error('inputType: sh/wsh cannot be terminal type');
|
|
527
|
-
const lastScript = OutScript.encode(last);
|
|
528
728
|
const res = {
|
|
529
729
|
type,
|
|
530
730
|
txType,
|
|
@@ -568,9 +768,61 @@ export class Transaction {
|
|
|
568
768
|
constructor(opts = {}) {
|
|
569
769
|
const _opts = (this.opts = validateOpts(opts));
|
|
570
770
|
// Merge with global structure of PSBTv2
|
|
571
|
-
|
|
572
|
-
|
|
771
|
+
// Bitcoin Core sets fallback even when it is zero. Matching its common encoding reduces the
|
|
772
|
+
// fingerprint of locally created PSBTv2s; imported PSBTs replace this map and retain omission.
|
|
773
|
+
this.global.fallbackLocktime = _opts.lockTime;
|
|
573
774
|
this.global.txVersion = _opts.version;
|
|
775
|
+
// A locally-created PSBTv2 is still under construction. Imported PSBTs replace this global
|
|
776
|
+
// map below, so an omitted field there retains BIP370's immutable meaning.
|
|
777
|
+
if (_opts.PSBTVersion === 2)
|
|
778
|
+
this.global.txModifiable = 0b011;
|
|
779
|
+
}
|
|
780
|
+
isPSBTv2() {
|
|
781
|
+
return (this.global.version ?? this.opts.PSBTVersion) === 2;
|
|
782
|
+
}
|
|
783
|
+
requireTxModifiable(bit, kind) {
|
|
784
|
+
if (!this.isPSBTv2())
|
|
785
|
+
return;
|
|
786
|
+
if (!(this.txModifiablePolicy() & bit))
|
|
787
|
+
throw new Error(`PSBTv2: ${kind} are not modifiable`);
|
|
788
|
+
}
|
|
789
|
+
txModifiablePolicy(allowMissing = this.opts.allowMissingTxModifiable, unknownMode = this.opts.unknown) {
|
|
790
|
+
if (!this.isPSBTv2())
|
|
791
|
+
return 0b011;
|
|
792
|
+
if (this.global.txModifiable !== undefined)
|
|
793
|
+
return cleanTxModifiable(this.global.txModifiable, unknownMode);
|
|
794
|
+
return allowMissing ? 0b011 : 0;
|
|
795
|
+
}
|
|
796
|
+
modifiable(allowMissing = this.opts.allowMissingTxModifiable, unknownMode = this.opts.unknown) {
|
|
797
|
+
let flags = this.txModifiablePolicy(allowMissing, unknownMode);
|
|
798
|
+
let hasOpaqueFinal = false;
|
|
799
|
+
let hasSingle = false;
|
|
800
|
+
for (let idx = 0; idx < this.inputs.length; idx++) {
|
|
801
|
+
const signatures = inputSignatures(this.inputs[idx]);
|
|
802
|
+
if (!signatures.length && this.inputStatus(idx) === 'finalized')
|
|
803
|
+
hasOpaqueFinal = true;
|
|
804
|
+
for (const { sighash } of signatures) {
|
|
805
|
+
const { isAny, isNone, isSingle } = unpackSighash(sighash);
|
|
806
|
+
if (!isAny)
|
|
807
|
+
flags &= ~0b001;
|
|
808
|
+
if (!isNone)
|
|
809
|
+
flags &= ~0b010;
|
|
810
|
+
if (isSingle)
|
|
811
|
+
hasSingle = true;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Bit 2 summarizes signatures rather than granting policy. Preserve an imported summary for
|
|
815
|
+
// opaque or externally managed state, and union in every signature visible to this object.
|
|
816
|
+
if (hasSingle)
|
|
817
|
+
flags |= 0b100;
|
|
818
|
+
// PSBTv0 and legacy field-less PSBTv2 cannot describe an opaque finalized sighash. Promotion
|
|
819
|
+
// must therefore deny both mutations instead of manufacturing permissions from absence.
|
|
820
|
+
if (hasOpaqueFinal && this.global.txModifiable === undefined)
|
|
821
|
+
flags &= ~0b011;
|
|
822
|
+
return flags;
|
|
823
|
+
}
|
|
824
|
+
get txModifiable() {
|
|
825
|
+
return this.modifiable();
|
|
574
826
|
}
|
|
575
827
|
// Import
|
|
576
828
|
static fromRaw(raw, opts = {}) {
|
|
@@ -610,19 +862,51 @@ export class Transaction {
|
|
|
610
862
|
const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion });
|
|
611
863
|
// We need slice here, because otherwise
|
|
612
864
|
const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount;
|
|
613
|
-
tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) =>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
865
|
+
tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) => {
|
|
866
|
+
const input = {
|
|
867
|
+
...parsed.global.unsignedTx?.inputs[j],
|
|
868
|
+
...i,
|
|
869
|
+
};
|
|
870
|
+
// The unsigned transaction's empty scriptSig is framing, not a PSBT_IN_FINAL_SCRIPTSIG
|
|
871
|
+
// record. Keeping it makes combination conflict with an otherwise identical finalized PSBT.
|
|
872
|
+
if (!i.finalScriptSig?.length)
|
|
873
|
+
delete input.finalScriptSig;
|
|
874
|
+
return validateInput(input, tx.opts.disableScriptCheck);
|
|
875
|
+
});
|
|
618
876
|
const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount;
|
|
619
|
-
|
|
877
|
+
// bip174js writes a phantom empty input map when a PSBTv0 transaction has zero inputs. Raw v0
|
|
878
|
+
// framing necessarily reads it as the first output map, so skip it before pairing real maps
|
|
879
|
+
// with the unsigned transaction's declared outputs.
|
|
880
|
+
const hasBip174InputMap = PSBTVersion === 0 &&
|
|
881
|
+
inputCount === 0 &&
|
|
882
|
+
Object.keys(parsed.outputs[0] || {}).length === 0 &&
|
|
883
|
+
((outputCount > 0 && parsed.outputs.length === outputCount + 1) ||
|
|
884
|
+
(outputCount === 0 &&
|
|
885
|
+
parsed.outputs.length === 2 &&
|
|
886
|
+
Object.keys(parsed.outputs[1]).length === 0));
|
|
887
|
+
const outputStart = hasBip174InputMap ? 1 : 0;
|
|
888
|
+
tx.outputs = parsed.outputs.slice(outputStart, outputStart + outputCount).map((i, j) => ({
|
|
620
889
|
...i,
|
|
621
890
|
...parsed.global.unsignedTx?.outputs[j],
|
|
622
891
|
}));
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
892
|
+
const unknownMode = tx.opts.unknown;
|
|
893
|
+
const proprietaryMode = tx.opts.proprietary;
|
|
894
|
+
// Unknown PSBT rows can carry opaque metadata between participants. The documented default is
|
|
895
|
+
// to strip them; callers that need forward compatibility must opt in explicitly. Proprietary
|
|
896
|
+
// (0xfc) rows follow the same explicit policy, which defaults to the resolved unknown mode.
|
|
897
|
+
tx.global = cleanExtensions({ ...parsed.global, txVersion: version }, unknownMode, proprietaryMode);
|
|
898
|
+
tx.inputs = tx.inputs.map((input) => cleanExtensions(input, unknownMode, proprietaryMode));
|
|
899
|
+
tx.outputs = tx.outputs.map((output) => cleanExtensions(output, unknownMode, proprietaryMode));
|
|
900
|
+
if (tx.global.txModifiable !== undefined)
|
|
901
|
+
tx.global.txModifiable = cleanTxModifiable(tx.global.txModifiable, unknownMode);
|
|
902
|
+
// A high-level Transaction must have a determinable nLockTime. Raw PSBT coders can still be
|
|
903
|
+
// used by callers that need to inspect or relay a structurally valid but incompatible PSBT.
|
|
904
|
+
resolvePSBTLocktime(tx.inputs, tx.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
905
|
+
// PSBTv0 always provides nLockTime in its unsigned transaction. Retain zero internally too so
|
|
906
|
+
// promotion to v2 matches fresh construction and Bitcoin Core rather than gaining a
|
|
907
|
+
// fingerprint.
|
|
908
|
+
if (PSBTVersion === 0)
|
|
909
|
+
tx.global.fallbackLocktime = def(lockTime, DEFAULT_LOCKTIME);
|
|
626
910
|
return tx;
|
|
627
911
|
}
|
|
628
912
|
// Prefer `global.version` when present so cross-version combiners can serialize at the highest
|
|
@@ -637,10 +921,10 @@ export class Transaction {
|
|
|
637
921
|
// 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.'
|
|
638
922
|
// );
|
|
639
923
|
// }
|
|
640
|
-
const inputs = this.inputs.map((i) =>
|
|
924
|
+
const inputs = this.inputs.map((i) => cleanExtensions(
|
|
641
925
|
// For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
|
|
642
926
|
// validate the full transaction input before version filtering drops those fields.
|
|
643
|
-
psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i)));
|
|
927
|
+
psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i, this.opts.disableScriptCheck)), this.opts.unknown, this.opts.proprietary));
|
|
644
928
|
for (const inp of inputs) {
|
|
645
929
|
// Don't serialize empty fields
|
|
646
930
|
if (inp.partialSig && !inp.partialSig.length)
|
|
@@ -650,8 +934,10 @@ export class Transaction {
|
|
|
650
934
|
if (inp.finalScriptWitness && !inp.finalScriptWitness.length)
|
|
651
935
|
delete inp.finalScriptWitness;
|
|
652
936
|
}
|
|
653
|
-
const outputs = this.outputs.map((i) => psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i));
|
|
654
|
-
const global = { ...this.global };
|
|
937
|
+
const outputs = this.outputs.map((i) => cleanExtensions(psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i), this.opts.unknown, this.opts.proprietary));
|
|
938
|
+
const global = cleanExtensions({ ...this.global }, this.opts.unknown, this.opts.proprietary);
|
|
939
|
+
if (global.txModifiable !== undefined)
|
|
940
|
+
global.txModifiable = cleanTxModifiable(global.txModifiable, this.opts.unknown);
|
|
655
941
|
if (PSBTVersion === 0) {
|
|
656
942
|
/*
|
|
657
943
|
- Bitcoin raw transaction expects to have at least 1 input because it uses case with zero inputs as marker for SegWit
|
|
@@ -674,10 +960,12 @@ export class Transaction {
|
|
|
674
960
|
delete global.txVersion;
|
|
675
961
|
// PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
|
|
676
962
|
// removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
|
|
677
|
-
// PSBTv2 and reject the required `unsignedTx` field.
|
|
963
|
+
// PSBTv2 and reject the required `unsignedTx` field. Transaction-modifiable is also v2-only;
|
|
964
|
+
// its restrictions remain represented by the signatures when explicitly converting to v0.
|
|
678
965
|
delete global.inputCount;
|
|
679
966
|
delete global.outputCount;
|
|
680
967
|
delete global.version;
|
|
968
|
+
delete global.txModifiable;
|
|
681
969
|
}
|
|
682
970
|
else {
|
|
683
971
|
// Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
|
|
@@ -688,15 +976,18 @@ export class Transaction {
|
|
|
688
976
|
global.txVersion = this.version;
|
|
689
977
|
global.inputCount = this.inputs.length;
|
|
690
978
|
global.outputCount = this.outputs.length;
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
inputs.push({});
|
|
697
|
-
if (!outputs.length)
|
|
698
|
-
outputs.push({});
|
|
979
|
+
// Core serializes this optional field exactly as stored. Preserve no-op v2 round-trips;
|
|
980
|
+
// only v0 promotion and the explicit legacy-omission compatibility mode materialize policy.
|
|
981
|
+
if (!this.isPSBTv2() ||
|
|
982
|
+
(global.txModifiable === undefined && this.opts.allowMissingTxModifiable))
|
|
983
|
+
global.txModifiable = this.txModifiable;
|
|
699
984
|
}
|
|
985
|
+
// bip174js historically emits one empty output map for a PSBTv0 transaction with no outputs.
|
|
986
|
+
// Input maps are count-framed by the unsigned transaction, so a phantom input map cannot be
|
|
987
|
+
// represented: with zero inputs it would be decoded as an output map instead. PSBTv2 has
|
|
988
|
+
// explicit counts for both map arrays and does not use this compatibility encoding.
|
|
989
|
+
if (this.opts.bip174jsCompat && PSBTVersion === 0 && !outputs.length)
|
|
990
|
+
outputs.push({});
|
|
700
991
|
const raw = { global, inputs, outputs };
|
|
701
992
|
return PSBTVersion === 0
|
|
702
993
|
? psbt.RawPSBTV0.encode(raw)
|
|
@@ -704,25 +995,7 @@ export class Transaction {
|
|
|
704
995
|
}
|
|
705
996
|
// BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
|
|
706
997
|
get lockTime() {
|
|
707
|
-
|
|
708
|
-
let heightCnt = 0;
|
|
709
|
-
let time = DEFAULT_LOCKTIME;
|
|
710
|
-
let timeCnt = 0;
|
|
711
|
-
for (const i of this.inputs) {
|
|
712
|
-
if (i.requiredHeightLocktime) {
|
|
713
|
-
height = Math.max(height, i.requiredHeightLocktime);
|
|
714
|
-
heightCnt++;
|
|
715
|
-
}
|
|
716
|
-
if (i.requiredTimeLocktime) {
|
|
717
|
-
time = Math.max(time, i.requiredTimeLocktime);
|
|
718
|
-
timeCnt++;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
if (heightCnt && heightCnt >= timeCnt)
|
|
722
|
-
return height;
|
|
723
|
-
if (time !== DEFAULT_LOCKTIME)
|
|
724
|
-
return time;
|
|
725
|
-
return this.global.fallbackLocktime || DEFAULT_LOCKTIME;
|
|
998
|
+
return resolvePSBTLocktime(this.inputs, this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
726
999
|
}
|
|
727
1000
|
get version() {
|
|
728
1001
|
// Should be not possible
|
|
@@ -748,6 +1021,13 @@ export class Transaction {
|
|
|
748
1021
|
return 'signed';
|
|
749
1022
|
return 'unsigned';
|
|
750
1023
|
}
|
|
1024
|
+
cleanFinalInput(input) {
|
|
1025
|
+
// Core preserves producer policy during finalization. Once cleanup makes signatures opaque,
|
|
1026
|
+
// signStatus conservatively locks transaction mutation until the input is explicitly reopened.
|
|
1027
|
+
cleanFinalInput(input, this.opts.unknown, this.opts.proprietary);
|
|
1028
|
+
if (this.global.txModifiable !== undefined)
|
|
1029
|
+
this.global.txModifiable = cleanTxModifiable(this.global.txModifiable, this.opts.unknown);
|
|
1030
|
+
}
|
|
751
1031
|
// Cannot replace unpackSighash, tests rely on very generic implemenetation with signing inputs outside of range
|
|
752
1032
|
// We will lose some vectors -> smaller test coverage of preimages (very important!)
|
|
753
1033
|
inputSighash(idx) {
|
|
@@ -760,32 +1040,81 @@ export class Transaction {
|
|
|
760
1040
|
// ALL + ANYONE -- specific input + all outputs
|
|
761
1041
|
// NONE + ANYONE -- specific input + no outputs
|
|
762
1042
|
// SINGLE -- specific inputs + output with same index
|
|
763
|
-
|
|
764
|
-
const sigInputs = sighash & SignatureHash.ANYONECANPAY;
|
|
765
|
-
return { sigInputs, sigOutputs };
|
|
1043
|
+
return sighashScope(sighash);
|
|
766
1044
|
}
|
|
767
1045
|
// Very nice for debug purposes, but slow. If there is too much inputs/outputs to add, will be quadratic.
|
|
768
1046
|
// Some cache will be nice, but there chance to have bugs with cache invalidation
|
|
769
|
-
|
|
1047
|
+
signatures() {
|
|
1048
|
+
const res = [];
|
|
1049
|
+
for (let idx = 0; idx < this.inputs.length; idx++) {
|
|
1050
|
+
const actual = inputSignatures(this.inputs[idx]);
|
|
1051
|
+
for (const signature of actual)
|
|
1052
|
+
res.push({ idx, ...signature });
|
|
1053
|
+
if (actual.length || this.inputStatus(idx) !== 'finalized')
|
|
1054
|
+
continue;
|
|
1055
|
+
let taproot = true;
|
|
1056
|
+
try {
|
|
1057
|
+
taproot = _WitnessOutScript.decode(getPrevOut(this.inputs[idx]).script).type === 'tr';
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
// Finalization may remove the data needed to classify an imported opaque signature.
|
|
1061
|
+
// Treating it as Taproot conservatively protects the larger all-prevout commitment.
|
|
1062
|
+
}
|
|
1063
|
+
const declared = this.inputs[idx].sighashType;
|
|
1064
|
+
const sighash = declared === undefined ? SignatureHash.DEFAULT : declared;
|
|
1065
|
+
res.push({ idx, sighash, taproot, scriptPath: taproot });
|
|
1066
|
+
}
|
|
1067
|
+
return res;
|
|
1068
|
+
}
|
|
1069
|
+
signedInputKeys(idx, signatures = this.signatures()) {
|
|
1070
|
+
const res = new Set();
|
|
1071
|
+
const add = (keys) => {
|
|
1072
|
+
for (const key of keys)
|
|
1073
|
+
res.add(key);
|
|
1074
|
+
};
|
|
1075
|
+
for (const signature of signatures) {
|
|
1076
|
+
const { isAny, isNone, isSingle } = unpackSighash(signature.sighash);
|
|
1077
|
+
if (signature.idx === idx) {
|
|
1078
|
+
add(inputSignedKeys.self);
|
|
1079
|
+
if (signature.taproot) {
|
|
1080
|
+
if (signature.scriptPath)
|
|
1081
|
+
add(inputSignedKeys.tapscript);
|
|
1082
|
+
}
|
|
1083
|
+
else
|
|
1084
|
+
add(inputSignedKeys.ecdsa);
|
|
1085
|
+
}
|
|
1086
|
+
else if (!isAny) {
|
|
1087
|
+
add(inputSignedKeys.cross);
|
|
1088
|
+
// Legacy and BIP143 omit other sequences for NONE/SINGLE. BIP341 commits every sequence
|
|
1089
|
+
// whenever ANYONECANPAY is absent, independently of the output sighash mode.
|
|
1090
|
+
if (signature.taproot || (!isNone && !isSingle))
|
|
1091
|
+
res.add('sequence');
|
|
1092
|
+
if (signature.taproot)
|
|
1093
|
+
add(inputSignedKeys.prevout);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return [...res];
|
|
1097
|
+
}
|
|
1098
|
+
signStatus(signatures = this.signatures()) {
|
|
770
1099
|
// if addInput or addOutput is not possible, then all inputs or outputs are signed
|
|
771
1100
|
let addInput = true, addOutput = true;
|
|
772
1101
|
let inputs = [], outputs = [];
|
|
773
|
-
for (
|
|
774
|
-
const
|
|
775
|
-
// Unsigned input doesn't affect anything
|
|
776
|
-
if (status === 'unsigned')
|
|
777
|
-
continue;
|
|
778
|
-
const { sigInputs, sigOutputs } = this.inputSighash(idx);
|
|
1102
|
+
for (const { idx, sighash } of signatures) {
|
|
1103
|
+
const { sigInputs, sigOutputs } = sighashScope(sighash);
|
|
779
1104
|
// Input type
|
|
780
|
-
if (sigInputs === SignatureHash.ANYONECANPAY)
|
|
781
|
-
inputs.
|
|
1105
|
+
if (sigInputs === SignatureHash.ANYONECANPAY) {
|
|
1106
|
+
if (!inputs.includes(idx))
|
|
1107
|
+
inputs.push(idx);
|
|
1108
|
+
}
|
|
782
1109
|
else
|
|
783
1110
|
addInput = false;
|
|
784
1111
|
// Output type
|
|
785
1112
|
if (sigOutputs === SignatureHash.ALL)
|
|
786
1113
|
addOutput = false;
|
|
787
|
-
else if (sigOutputs === SignatureHash.SINGLE)
|
|
788
|
-
outputs.
|
|
1114
|
+
else if (sigOutputs === SignatureHash.SINGLE) {
|
|
1115
|
+
if (!outputs.includes(idx))
|
|
1116
|
+
outputs.push(idx);
|
|
1117
|
+
}
|
|
789
1118
|
else if (sigOutputs === SignatureHash.NONE) {
|
|
790
1119
|
// Doesn't affect any outputs at all
|
|
791
1120
|
}
|
|
@@ -871,6 +1200,23 @@ export class Transaction {
|
|
|
871
1200
|
if (idx >= this.inputs.length)
|
|
872
1201
|
throw new Error(`Wrong input index=${idx}`);
|
|
873
1202
|
}
|
|
1203
|
+
validatePrevoutsForSigning() {
|
|
1204
|
+
if (!this.opts.strictPrevoutValidation)
|
|
1205
|
+
return;
|
|
1206
|
+
for (let i = 0; i < this.inputs.length; i++) {
|
|
1207
|
+
const input = this.inputs[i];
|
|
1208
|
+
if (!input.nonWitnessUtxo) {
|
|
1209
|
+
throw new Error(`Transaction/sign: strictPrevoutValidation requires nonWitnessUtxo for input=${i}`);
|
|
1210
|
+
}
|
|
1211
|
+
if (input.txid === undefined || input.index === undefined) {
|
|
1212
|
+
throw new Error(`Transaction/sign: strictPrevoutValidation requires an outpoint for input=${i}`);
|
|
1213
|
+
}
|
|
1214
|
+
// A full previous transaction is only a trusted amount/script commitment after its txid and
|
|
1215
|
+
// selected output have been checked against the unsigned transaction. Also cross-check a
|
|
1216
|
+
// redundant witnessUtxo when one is present.
|
|
1217
|
+
validateInput(input, this.opts.disableScriptCheck);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
874
1220
|
getInput(idx) {
|
|
875
1221
|
this.checkInputIdx(idx);
|
|
876
1222
|
return cloneDeep(this.inputs[idx]);
|
|
@@ -881,24 +1227,57 @@ export class Transaction {
|
|
|
881
1227
|
// Modification
|
|
882
1228
|
addInput(input, _ignoreSignStatus = false) {
|
|
883
1229
|
validateObject(input, {}, {}, 'input');
|
|
884
|
-
|
|
1230
|
+
cleanExtensions(input, this.opts.unknown, this.opts.proprietary, true);
|
|
1231
|
+
this.requireTxModifiable(0b001, 'inputs');
|
|
1232
|
+
const signatures = _ignoreSignStatus ? undefined : this.signatures();
|
|
1233
|
+
const status = signatures && this.signStatus(signatures);
|
|
1234
|
+
if (status && !status.addInput)
|
|
885
1235
|
throw new Error('Tx has signed inputs, cannot add new one');
|
|
886
1236
|
// normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
|
|
887
1237
|
// new input becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
888
|
-
|
|
1238
|
+
const normalized = cloneDeep(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck, this.opts.unknown, this.opts.proprietary));
|
|
1239
|
+
const nextLockTime = resolvePSBTLocktime([...this.inputs, normalized], this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
1240
|
+
// ANYONECANPAY permits adding an outpoint, but every signature still commits to nLockTime.
|
|
1241
|
+
if (signatures?.length && nextLockTime !== this.lockTime)
|
|
1242
|
+
throw new Error('Tx has signed inputs, cannot change lockTime');
|
|
1243
|
+
this.inputs.push(normalized);
|
|
889
1244
|
return this.inputs.length - 1;
|
|
890
1245
|
}
|
|
891
1246
|
updateInput(idx, input, _ignoreSignStatus = false) {
|
|
892
1247
|
this.checkInputIdx(idx);
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1248
|
+
cleanExtensions(input, this.opts.unknown, this.opts.proprietary, true);
|
|
1249
|
+
let allowedFields;
|
|
1250
|
+
const signatures = _ignoreSignStatus ? undefined : this.signatures();
|
|
1251
|
+
if (signatures?.length) {
|
|
1252
|
+
if (this.inputStatus(idx) === 'finalized') {
|
|
1253
|
+
// Once finalized, only already-present signature/final fields may be repeated or removed.
|
|
1254
|
+
// In particular, do not let a native-SegWit final witness gain a stray finalScriptSig.
|
|
1255
|
+
allowedFields = psbt.PSBTInputSignatureKeys.filter((key) => this.inputs[idx][key] !== undefined);
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
const signed = new Set(this.signedInputKeys(idx, signatures));
|
|
1259
|
+
if (signed.size)
|
|
1260
|
+
allowedFields = Object.keys(psbt.PSBTInput).filter((key) => !signed.has(key));
|
|
1261
|
+
}
|
|
898
1262
|
}
|
|
899
1263
|
// normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
|
|
900
1264
|
// before the updated input becomes transaction state and later caller mutation can rewrite it.
|
|
901
|
-
|
|
1265
|
+
const normalized = cloneDeep(normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.unknown, this.opts.proprietary));
|
|
1266
|
+
const inputs = this.inputs.slice();
|
|
1267
|
+
inputs[idx] = normalized;
|
|
1268
|
+
const nextLockTime = resolvePSBTLocktime(inputs, this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
1269
|
+
if (signatures?.length && nextLockTime !== this.lockTime)
|
|
1270
|
+
throw new Error('Tx has signed inputs, cannot change lockTime');
|
|
1271
|
+
const current = this.inputs[idx];
|
|
1272
|
+
const transactionChanged = current.index !== normalized.index ||
|
|
1273
|
+
def(current.sequence, DEFAULT_SEQUENCE) !== def(normalized.sequence, DEFAULT_SEQUENCE) ||
|
|
1274
|
+
(current.txid === undefined
|
|
1275
|
+
? normalized.txid !== undefined
|
|
1276
|
+
: normalized.txid === undefined || !equalBytes(current.txid, normalized.txid)) ||
|
|
1277
|
+
nextLockTime !== this.lockTime;
|
|
1278
|
+
if (transactionChanged)
|
|
1279
|
+
this.requireTxModifiable(0b001, 'inputs');
|
|
1280
|
+
this.inputs[idx] = normalized;
|
|
902
1281
|
}
|
|
903
1282
|
// Output stuff
|
|
904
1283
|
checkOutputIdx(idx) {
|
|
@@ -914,7 +1293,7 @@ export class Transaction {
|
|
|
914
1293
|
const out = this.getOutput(idx);
|
|
915
1294
|
if (!out.script)
|
|
916
1295
|
return;
|
|
917
|
-
return Address(network).encode(
|
|
1296
|
+
return Address(network).encode(_WitnessOutScript.decode(out.script));
|
|
918
1297
|
}
|
|
919
1298
|
get outputsLength() {
|
|
920
1299
|
return this.outputs.length;
|
|
@@ -932,11 +1311,11 @@ export class Transaction {
|
|
|
932
1311
|
let res = { ...cur, ...o, amount, script };
|
|
933
1312
|
if (res.amount === undefined)
|
|
934
1313
|
delete res.amount;
|
|
935
|
-
res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.
|
|
1314
|
+
res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.unknown, this.opts.proprietary);
|
|
936
1315
|
psbt.PSBTOutputCoder.encode(res);
|
|
937
1316
|
if (res.script &&
|
|
938
1317
|
!this.opts.allowUnknownOutputs &&
|
|
939
|
-
|
|
1318
|
+
_WitnessOutScript.decode(res.script).type === 'unknown') {
|
|
940
1319
|
throw new Error('Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure');
|
|
941
1320
|
}
|
|
942
1321
|
if (!this.opts.disableScriptCheck)
|
|
@@ -944,7 +1323,11 @@ export class Transaction {
|
|
|
944
1323
|
return res;
|
|
945
1324
|
}
|
|
946
1325
|
addOutput(o, _ignoreSignStatus = false) {
|
|
947
|
-
|
|
1326
|
+
cleanExtensions(o, this.opts.unknown, this.opts.proprietary, true);
|
|
1327
|
+
this.requireTxModifiable(0b010, 'outputs');
|
|
1328
|
+
const status = _ignoreSignStatus ? undefined : this.signStatus();
|
|
1329
|
+
// Appending the previously missing same-index output changes a SIGHASH_SINGLE digest.
|
|
1330
|
+
if (status && (!status.addOutput || status.outputs.includes(this.outputs.length)))
|
|
948
1331
|
throw new Error('Tx has signed outputs, cannot add new one');
|
|
949
1332
|
// normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
|
|
950
1333
|
// new output becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
@@ -953,6 +1336,7 @@ export class Transaction {
|
|
|
953
1336
|
}
|
|
954
1337
|
updateOutput(idx, output, _ignoreSignStatus = false) {
|
|
955
1338
|
this.checkOutputIdx(idx);
|
|
1339
|
+
cleanExtensions(output, this.opts.unknown, this.opts.proprietary, true);
|
|
956
1340
|
let allowedFields = undefined;
|
|
957
1341
|
if (!_ignoreSignStatus) {
|
|
958
1342
|
const status = this.signStatus();
|
|
@@ -961,7 +1345,15 @@ export class Transaction {
|
|
|
961
1345
|
}
|
|
962
1346
|
// updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
|
|
963
1347
|
// before storing it or later caller mutation of `output.script` will rewrite transaction state.
|
|
964
|
-
|
|
1348
|
+
const current = this.outputs[idx];
|
|
1349
|
+
const normalized = cloneDeep(this.normalizeOutput(output, current, allowedFields));
|
|
1350
|
+
const transactionChanged = current.amount !== normalized.amount ||
|
|
1351
|
+
(current.script === undefined
|
|
1352
|
+
? normalized.script !== undefined
|
|
1353
|
+
: normalized.script === undefined || !equalBytes(current.script, normalized.script));
|
|
1354
|
+
if (transactionChanged)
|
|
1355
|
+
this.requireTxModifiable(0b010, 'outputs');
|
|
1356
|
+
this.outputs[idx] = normalized;
|
|
965
1357
|
}
|
|
966
1358
|
addOutputAddress(address, amount, network = NETWORK) {
|
|
967
1359
|
return this.addOutput({
|
|
@@ -994,7 +1386,8 @@ export class Transaction {
|
|
|
994
1386
|
if (idx < 0 || !Number.isSafeInteger(idx))
|
|
995
1387
|
throw new Error(`Invalid input idx=${idx}`);
|
|
996
1388
|
if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
|
|
997
|
-
|
|
1389
|
+
// Bitcoin Core passes uint256::ONE's internal little-endian bytes directly to ECDSA.
|
|
1390
|
+
return P.U256LE.encode(_1n);
|
|
998
1391
|
prevOutScript = stripCodeSeparator(prevOutScript);
|
|
999
1392
|
let inputs = this.inputs
|
|
1000
1393
|
.map(inputBeforeSign)
|
|
@@ -1075,6 +1468,12 @@ export class Transaction {
|
|
|
1075
1468
|
const inType = hashType & SignatureHash.ANYONECANPAY;
|
|
1076
1469
|
const inputs = this.inputs.map(inputBeforeSign);
|
|
1077
1470
|
const outputs = this.outputs.map(outputBeforeSign);
|
|
1471
|
+
// Unlike legacy and segwit v0, BIP341 defines no digest for SINGLE when the
|
|
1472
|
+
// corresponding output does not exist. Returning a digest here would produce
|
|
1473
|
+
// signatures that consensus can never accept.
|
|
1474
|
+
if (outType === SignatureHash.SINGLE && idx >= outputs.length) {
|
|
1475
|
+
throw new Error(`Input with sighash SINGLE, but there is no output with corresponding index=${idx}`);
|
|
1476
|
+
}
|
|
1078
1477
|
if (inType !== SignatureHash.ANYONECANPAY) {
|
|
1079
1478
|
out.push(...[
|
|
1080
1479
|
inputs.map(TxHashIdx.encode),
|
|
@@ -1097,7 +1496,7 @@ export class Transaction {
|
|
|
1097
1496
|
if (spendType & 1)
|
|
1098
1497
|
out.push(u.sha256(VarBytes.encode(annex || P.EMPTY)));
|
|
1099
1498
|
if (outType === SignatureHash.SINGLE)
|
|
1100
|
-
out.push(
|
|
1499
|
+
out.push(u.sha256(RawOutput.encode(outputs[idx])));
|
|
1101
1500
|
if (leafScript)
|
|
1102
1501
|
out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
|
|
1103
1502
|
return u.tagSchnorr('TapSighash', ...out);
|
|
@@ -1113,7 +1512,8 @@ export class Transaction {
|
|
|
1113
1512
|
throw new TypeError('"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey);
|
|
1114
1513
|
}
|
|
1115
1514
|
this.checkInputIdx(idx);
|
|
1116
|
-
|
|
1515
|
+
this.validatePrevoutsForSigning();
|
|
1516
|
+
const input = validateInput(this.inputs[idx], this.opts.disableScriptCheck);
|
|
1117
1517
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
1118
1518
|
const canSign = (privateKey) => {
|
|
1119
1519
|
if (inputType.txType === 'taproot') {
|
|
@@ -1295,6 +1695,9 @@ export class Transaction {
|
|
|
1295
1695
|
// Even worse: another user can add bip32 derivation, and spend money from different address.
|
|
1296
1696
|
// Better api: signIdx
|
|
1297
1697
|
sign(privateKey, allowedSighash, _auxRand) {
|
|
1698
|
+
// Check transaction-wide strict requirements outside the per-input catch below so callers get
|
|
1699
|
+
// the actionable validation error instead of the generic "No inputs signed" result.
|
|
1700
|
+
this.validatePrevoutsForSigning();
|
|
1298
1701
|
let num = 0;
|
|
1299
1702
|
for (let i = 0; i < this.inputs.length; i++) {
|
|
1300
1703
|
try {
|
|
@@ -1312,15 +1715,23 @@ export class Transaction {
|
|
|
1312
1715
|
if (this.fee < _0n)
|
|
1313
1716
|
throw new Error('Outputs spends more than inputs amount');
|
|
1314
1717
|
const input = this.inputs[idx];
|
|
1718
|
+
// Validate strict extension policy before constructing satisfaction so a rejection is atomic.
|
|
1719
|
+
cleanExtensions(input, this.opts.unknown, this.opts.proprietary);
|
|
1720
|
+
cleanTxModifiable(this.global.txModifiable, this.opts.unknown);
|
|
1315
1721
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
1316
1722
|
// Taproot finalize
|
|
1317
1723
|
if (inputType.txType === 'taproot') {
|
|
1318
1724
|
if (input.tapKeySig)
|
|
1319
1725
|
input.finalScriptWitness = [input.tapKeySig];
|
|
1320
1726
|
else if (input.tapLeafScript && input.tapScriptSig) {
|
|
1321
|
-
//
|
|
1322
|
-
const leafs = input.tapLeafScript
|
|
1727
|
+
// Preserve the old shallowest-path tie-break without mutating caller-visible leaf order.
|
|
1728
|
+
const leafs = input.tapLeafScript
|
|
1729
|
+
.slice()
|
|
1730
|
+
.sort((a, b) => psbt.TaprootControlBlock.encode(a[0]).length -
|
|
1323
1731
|
psbt.TaprootControlBlock.encode(b[0]).length);
|
|
1732
|
+
let smallest;
|
|
1733
|
+
let smallestSize = Number.POSITIVE_INFINITY;
|
|
1734
|
+
let unsupported = false;
|
|
1324
1735
|
for (const [cb, _script] of leafs) {
|
|
1325
1736
|
// Last byte is version
|
|
1326
1737
|
const script = _script.slice(0, -1);
|
|
@@ -1329,6 +1740,7 @@ export class Transaction {
|
|
|
1329
1740
|
const hash = tapLeafHash(script, ver);
|
|
1330
1741
|
const scriptSig = input.tapScriptSig.filter((i) => equalBytes(i[0].leafHash, hash));
|
|
1331
1742
|
let signatures = [];
|
|
1743
|
+
let witness;
|
|
1332
1744
|
if (outScript.type === 'tr_ms') {
|
|
1333
1745
|
const m = outScript.m;
|
|
1334
1746
|
const pubkeys = outScript.pubkeys;
|
|
@@ -1375,6 +1787,7 @@ export class Transaction {
|
|
|
1375
1787
|
}
|
|
1376
1788
|
else {
|
|
1377
1789
|
const custom = this.opts.customScripts;
|
|
1790
|
+
let recognized = false;
|
|
1378
1791
|
if (custom) {
|
|
1379
1792
|
for (const c of custom) {
|
|
1380
1793
|
if (!c.finalizeTaproot)
|
|
@@ -1383,31 +1796,44 @@ export class Transaction {
|
|
|
1383
1796
|
const csEncoded = c.encode(scriptDecoded);
|
|
1384
1797
|
if (csEncoded === undefined)
|
|
1385
1798
|
continue;
|
|
1799
|
+
recognized = true;
|
|
1800
|
+
// Do not catch hook errors here. `undefined` means no satisfaction, while a throw
|
|
1801
|
+
// from a matching custom finalizer reports broken leaf/signature data and must
|
|
1802
|
+
// abort even when another leaf has already produced a valid witness candidate.
|
|
1386
1803
|
const finalized = c.finalizeTaproot(script, csEncoded, scriptSig);
|
|
1387
1804
|
if (!finalized)
|
|
1388
1805
|
continue;
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
cleanFinalInput(input);
|
|
1392
|
-
return;
|
|
1806
|
+
witness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
|
|
1807
|
+
break;
|
|
1393
1808
|
}
|
|
1394
1809
|
}
|
|
1395
|
-
|
|
1810
|
+
// Minimum search inspects every leaf, so an unsupported path cannot block an already
|
|
1811
|
+
// complete known path merely because it appears later. Retain the old error when no
|
|
1812
|
+
// supported satisfaction exists at all.
|
|
1813
|
+
if (!witness && !recognized && scriptSig.length)
|
|
1814
|
+
unsupported = true;
|
|
1815
|
+
if (!witness)
|
|
1816
|
+
continue;
|
|
1396
1817
|
}
|
|
1397
1818
|
// Witness is stack, so last element will be used first
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1819
|
+
witness ||= signatures.reverse().concat([script, psbt.TaprootControlBlock.encode(cb)]);
|
|
1820
|
+
const size = RawWitness.encode(witness).length;
|
|
1821
|
+
if (size >= smallestSize)
|
|
1822
|
+
continue;
|
|
1823
|
+
smallest = witness;
|
|
1824
|
+
smallestSize = size;
|
|
1402
1825
|
}
|
|
1403
|
-
if (!
|
|
1826
|
+
if (!smallest && unsupported)
|
|
1827
|
+
throw new Error('Finalize: Unknown tapLeafScript');
|
|
1828
|
+
if (!smallest)
|
|
1404
1829
|
throw new Error('finalize/taproot: empty witness');
|
|
1830
|
+
input.finalScriptWitness = smallest;
|
|
1405
1831
|
}
|
|
1406
1832
|
else
|
|
1407
1833
|
throw new Error('finalize/taproot: unknown input');
|
|
1408
1834
|
// BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
|
|
1409
1835
|
delete input.finalScriptSig;
|
|
1410
|
-
cleanFinalInput(input);
|
|
1836
|
+
this.cleanFinalInput(input);
|
|
1411
1837
|
return;
|
|
1412
1838
|
}
|
|
1413
1839
|
if (!input.partialSig || !input.partialSig.length)
|
|
@@ -1478,7 +1904,7 @@ export class Transaction {
|
|
|
1478
1904
|
input.finalScriptSig = finalScriptSig;
|
|
1479
1905
|
if (finalScriptWitness)
|
|
1480
1906
|
input.finalScriptWitness = finalScriptWitness;
|
|
1481
|
-
cleanFinalInput(input);
|
|
1907
|
+
this.cleanFinalInput(input);
|
|
1482
1908
|
}
|
|
1483
1909
|
finalize() {
|
|
1484
1910
|
for (let i = 0; i < this.inputs.length; i++)
|
|
@@ -1496,13 +1922,21 @@ export class Transaction {
|
|
|
1496
1922
|
combine(other) {
|
|
1497
1923
|
if (!(other instanceof Transaction))
|
|
1498
1924
|
throw new TypeError('"other" expected Transaction, got type=' + typeof other);
|
|
1925
|
+
// Match main's accumulator model: operation policy belongs to the receiver that is mutated.
|
|
1926
|
+
const opts = this.opts;
|
|
1499
1927
|
// BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
|
|
1500
1928
|
// version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
|
|
1501
1929
|
const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1930
|
+
if (this.opts.version !== other.opts.version)
|
|
1931
|
+
throw new Error(`Transaction/combine: different version this=${this.opts.version} ` +
|
|
1932
|
+
`other=${other.opts.version}`);
|
|
1933
|
+
const thisV2 = this.isPSBTv2();
|
|
1934
|
+
const otherV2 = other.isPSBTv2();
|
|
1935
|
+
if (!thisV2 || !otherV2) {
|
|
1936
|
+
const thisLockTime = this.lockTime;
|
|
1937
|
+
const otherLockTime = other.lockTime;
|
|
1938
|
+
if (thisLockTime !== otherLockTime)
|
|
1939
|
+
throw new Error(`Transaction/combine: different lockTime this=${thisLockTime} other=${otherLockTime}`);
|
|
1506
1940
|
}
|
|
1507
1941
|
for (const k of ['inputs', 'outputs']) {
|
|
1508
1942
|
if (this[k].length !== other[k].length) {
|
|
@@ -1511,15 +1945,122 @@ export class Transaction {
|
|
|
1511
1945
|
}
|
|
1512
1946
|
// Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
|
|
1513
1947
|
// `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
|
|
1514
|
-
|
|
1948
|
+
const unsignedTx = this.unsignedTx;
|
|
1949
|
+
if (!equalBytes(unsignedTx, other.unsignedTx))
|
|
1515
1950
|
throw new Error(`Transaction/combine: different unsigned tx`);
|
|
1516
|
-
|
|
1951
|
+
let txModifiable;
|
|
1952
|
+
if (thisV2 && otherV2) {
|
|
1953
|
+
// Core combines the stored optional bytes without deriving replacements from signatures.
|
|
1954
|
+
// Only explicit legacy-omission compatibility gives an absent field an effective value.
|
|
1955
|
+
const policy = (tx) => {
|
|
1956
|
+
if (tx.global.txModifiable !== undefined)
|
|
1957
|
+
return cleanTxModifiable(tx.global.txModifiable, opts.unknown);
|
|
1958
|
+
return opts.allowMissingTxModifiable ? tx.modifiable(true, opts.unknown) : 0;
|
|
1959
|
+
};
|
|
1960
|
+
const a = policy(this);
|
|
1961
|
+
const b = policy(other);
|
|
1962
|
+
// Known mutability permissions use intersection and SIGHASH_SINGLE presence uses union.
|
|
1963
|
+
// Future flag bits must agree because this implementation does not know how to merge them.
|
|
1964
|
+
if ((a & ~0b111) !== (b & ~0b111))
|
|
1965
|
+
throw new Error('Transaction/combine: conflicting unknown txModifiable flags');
|
|
1966
|
+
txModifiable = (a & ~0b111) | (a & b & 0b011) | ((a | b) & 0b100);
|
|
1967
|
+
// Preserve Core's optional-field semantics when neither participant supplied policy.
|
|
1968
|
+
if (txModifiable === 0 &&
|
|
1969
|
+
this.global.txModifiable === undefined &&
|
|
1970
|
+
other.global.txModifiable === undefined &&
|
|
1971
|
+
!opts.allowMissingTxModifiable)
|
|
1972
|
+
txModifiable = undefined;
|
|
1973
|
+
}
|
|
1974
|
+
else if (thisV2)
|
|
1975
|
+
txModifiable = this.modifiable(opts.allowMissingTxModifiable, opts.unknown);
|
|
1976
|
+
else if (otherV2)
|
|
1977
|
+
txModifiable = other.modifiable(opts.allowMissingTxModifiable, opts.unknown);
|
|
1978
|
+
const thisGlobal = { ...this.global };
|
|
1979
|
+
const otherGlobal = { ...other.global };
|
|
1980
|
+
// PSBTv0 has no fallback-locktime field: fromPSBT caches unsignedTx.nLockTime there only for
|
|
1981
|
+
// effective-locktime resolution and v2 promotion. Do not merge that cache as a v2 wire value.
|
|
1982
|
+
if (thisV2 !== otherV2) {
|
|
1983
|
+
if (!thisV2)
|
|
1984
|
+
delete thisGlobal.fallbackLocktime;
|
|
1985
|
+
if (!otherV2)
|
|
1986
|
+
delete otherGlobal.fallbackLocktime;
|
|
1987
|
+
// BIP174 permits v0 to encode version zero explicitly or omit it. Remove only that v0
|
|
1988
|
+
// spelling before scalar conflicts; retaining the v2 field anchors repeated promotion when
|
|
1989
|
+
// the accumulator's original options still target v0.
|
|
1990
|
+
if (!thisV2)
|
|
1991
|
+
delete thisGlobal.version;
|
|
1992
|
+
if (!otherV2)
|
|
1993
|
+
delete otherGlobal.version;
|
|
1994
|
+
}
|
|
1995
|
+
// Transaction-modifiable has dedicated bitwise merge rules above. Fallback locktime is only
|
|
1996
|
+
// one input to the effective locktime resolved from the combined input maps, so retain receiver
|
|
1997
|
+
// precedence and validate the resulting unsigned transaction after those maps merge below.
|
|
1998
|
+
const fallbackLocktime = thisGlobal.fallbackLocktime !== undefined
|
|
1999
|
+
? thisGlobal.fallbackLocktime
|
|
2000
|
+
: otherGlobal.fallbackLocktime;
|
|
2001
|
+
delete thisGlobal.txModifiable;
|
|
2002
|
+
delete otherGlobal.txModifiable;
|
|
2003
|
+
delete thisGlobal.fallbackLocktime;
|
|
2004
|
+
delete otherGlobal.fallbackLocktime;
|
|
2005
|
+
// Every ordinary global scalar must agree when both participants provide it; silently choosing
|
|
2006
|
+
// either value can detach extension metadata such as a BIP322 message from its signatures.
|
|
2007
|
+
const global = psbt.combineKeyMap(psbt.PSBTGlobal, thisGlobal, otherGlobal, opts.unknown, opts.proprietary);
|
|
2008
|
+
if (fallbackLocktime !== undefined)
|
|
2009
|
+
global.fallbackLocktime = fallbackLocktime;
|
|
1517
2010
|
if (PSBTVersion)
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
2011
|
+
global.version = PSBTVersion;
|
|
2012
|
+
if (txModifiable === undefined)
|
|
2013
|
+
delete global.txModifiable;
|
|
2014
|
+
else
|
|
2015
|
+
global.txModifiable = txModifiable;
|
|
2016
|
+
let hasOpaqueFinalizedV0 = false;
|
|
2017
|
+
const inputs = this.inputs.map((current, i) => {
|
|
2018
|
+
const currentFinal = this.inputStatus(i) === 'finalized';
|
|
2019
|
+
const otherFinal = other.inputStatus(i) === 'finalized';
|
|
2020
|
+
// Finalized v0 maps no longer contain the partial signatures needed to derive v2 flags.
|
|
2021
|
+
if ((!thisV2 && currentFinal) || (!otherV2 && otherFinal))
|
|
2022
|
+
hasOpaqueFinalizedV0 = true;
|
|
2023
|
+
if (currentFinal && otherFinal) {
|
|
2024
|
+
// Two finalized PSBTs must describe the same complete satisfaction. Requiring matching
|
|
2025
|
+
// presence as well as matching values prevents combining witness-only and scriptSig-only
|
|
2026
|
+
// final states into a third, unreviewed satisfaction.
|
|
2027
|
+
for (const k of ['finalScriptSig', 'finalScriptWitness']) {
|
|
2028
|
+
const currentHas = !!this.inputs[i][k]?.length;
|
|
2029
|
+
const otherHas = !!other.inputs[i][k]?.length;
|
|
2030
|
+
if (currentHas !== otherHas)
|
|
2031
|
+
throw new Error(`Transaction/combine: different finalized field=${k} input=${i}`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
const combined = psbt.combineKeyMap(psbt.PSBTInput, current, other.inputs[i], opts.unknown, opts.proprietary);
|
|
2035
|
+
// A final satisfaction supersedes partial signatures and transient signing metadata. This
|
|
2036
|
+
// also avoids manufacturing a contradictory final+partial input from two valid PSBTs.
|
|
2037
|
+
if (currentFinal || otherFinal)
|
|
2038
|
+
cleanFinalInput(combined, opts.unknown, opts.proprietary);
|
|
2039
|
+
return cloneDeep(normalizeInput(combined, undefined, undefined, opts.disableScriptCheck, opts.unknown, opts.proprietary));
|
|
2040
|
+
});
|
|
2041
|
+
// A promoted opaque satisfaction may commit to both transaction halves. Clear only known
|
|
2042
|
+
// permissions; retain a v2 participant's SIGHASH_SINGLE indicator and any future flag bits.
|
|
2043
|
+
if (hasOpaqueFinalizedV0 && global.txModifiable !== undefined)
|
|
2044
|
+
global.txModifiable &= ~0b011;
|
|
2045
|
+
const candidate = new Transaction({ ...opts, PSBTVersion });
|
|
2046
|
+
const outputs = this.outputs.map((current, i) => {
|
|
2047
|
+
const combined = psbt.combineKeyMap(psbt.PSBTOutput, current, other.outputs[i], opts.unknown, opts.proprietary);
|
|
2048
|
+
return cloneDeep(candidate.normalizeOutput(combined));
|
|
2049
|
+
});
|
|
2050
|
+
// Build and validate a detached candidate before touching the receiver. Combining
|
|
2051
|
+
// complementary v2 locktime fields can otherwise create a different unsigned transaction.
|
|
2052
|
+
candidate.global = global;
|
|
2053
|
+
candidate.inputs = inputs;
|
|
2054
|
+
candidate.outputs = outputs;
|
|
2055
|
+
// A v0 input map can contribute signatures while the combined transaction is promoted to v2.
|
|
2056
|
+
// All-v2 restrictions were already intersected above, preserving mutual field omission.
|
|
2057
|
+
if ((!thisV2 || !otherV2) && candidate.isPSBTv2())
|
|
2058
|
+
candidate.global.txModifiable = candidate.txModifiable;
|
|
2059
|
+
if (!equalBytes(candidate.unsignedTx, unsignedTx))
|
|
2060
|
+
throw new Error('Transaction/combine: combined unsigned tx differs');
|
|
2061
|
+
this.global = candidate.global;
|
|
2062
|
+
this.inputs = candidate.inputs;
|
|
2063
|
+
this.outputs = candidate.outputs;
|
|
1523
2064
|
return this;
|
|
1524
2065
|
}
|
|
1525
2066
|
clone() {
|
|
@@ -1530,6 +2071,7 @@ export class Transaction {
|
|
|
1530
2071
|
/**
|
|
1531
2072
|
* Merges multiple PSBT blobs into one.
|
|
1532
2073
|
* @param psbts - PSBT byte arrays to combine
|
|
2074
|
+
* @param opts - Transaction parsing, combination, and serialization options. See {@link TxOpts}.
|
|
1533
2075
|
* @returns Combined PSBT bytes.
|
|
1534
2076
|
* @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
|
|
1535
2077
|
* @example
|
|
@@ -1540,13 +2082,15 @@ export class Transaction {
|
|
|
1540
2082
|
* PSBTCombine([psbt, psbt]);
|
|
1541
2083
|
* ```
|
|
1542
2084
|
*/
|
|
1543
|
-
export function PSBTCombine(psbts) {
|
|
2085
|
+
export function PSBTCombine(psbts, opts = {}) {
|
|
1544
2086
|
if (!psbts || !Array.isArray(psbts) || !psbts.length)
|
|
1545
2087
|
throw new Error('PSBTCombine: wrong PSBT list');
|
|
1546
|
-
|
|
2088
|
+
// Options affect both map cleanup during combination and the encoding of the returned PSBT.
|
|
2089
|
+
const combineOpts = opts;
|
|
2090
|
+
const tx = Transaction.fromPSBT(psbts[0], combineOpts);
|
|
1547
2091
|
for (let i = 1; i < psbts.length; i++)
|
|
1548
|
-
tx.combine(Transaction.fromPSBT(psbts[i]));
|
|
1549
|
-
return tx.toPSBT();
|
|
2092
|
+
tx.combine(Transaction.fromPSBT(psbts[i], tx.opts));
|
|
2093
|
+
return tx.toPSBT(combineOpts.PSBTVersion);
|
|
1550
2094
|
}
|
|
1551
2095
|
// Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
|
|
1552
2096
|
const HARDENED_OFFSET = 0x80000000;
|