@scure/btc-signer 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/psbt.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
|
-
import { anumber } from '@noble/hashes/utils.js';
|
|
2
|
+
import { anumber, concatBytes } from '@noble/hashes/utils.js';
|
|
3
3
|
import * as P from 'micro-packed';
|
|
4
4
|
import { CompactSize, CompactSizeLen, RawOldTx, RawOutput, RawTx, RawWitness, VarBytes, } from "./script.js";
|
|
5
5
|
import { aarray, compareBytes, equalBytes, PubT, validateObject, validatePubkey, } from "./utils.js";
|
|
6
|
-
|
|
6
|
+
const unknowns = (mode, name) => {
|
|
7
|
+
if (mode === true)
|
|
8
|
+
return 'ignore';
|
|
9
|
+
if (mode === false)
|
|
10
|
+
return 'strip';
|
|
11
|
+
if (mode === 'ignore' || mode === 'strip' || mode === 'strict')
|
|
12
|
+
return mode;
|
|
13
|
+
throw new Error(`PSBT: invalid ${name} policy=${mode}`);
|
|
14
|
+
};
|
|
7
15
|
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
8
16
|
// prettier-ignore
|
|
9
17
|
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
|
|
@@ -136,11 +144,58 @@ const tapTree = /* @__PURE__ */ (() => P.validate(P.array(null, P.struct({
|
|
|
136
144
|
// field-specific structure and length checks still live at the individual field definitions.
|
|
137
145
|
// Keep a distinct name here so the byte coder does not collide with the Bytes type alias.
|
|
138
146
|
const BytesInf = /* @__PURE__ */ P.bytes(null);
|
|
147
|
+
// PSBTKeyMap emits the 0xfc type byte itself, so the public value remains only the bytes after it.
|
|
148
|
+
// Validate that raw suffix here: otherwise a caller-supplied leading 0xfc becomes a 252-byte
|
|
149
|
+
// identifier declaration which scure can relay but Bitcoin Core cannot parse.
|
|
150
|
+
const ProprietaryKey = /* @__PURE__ */ (() => {
|
|
151
|
+
const suffix = P.struct({
|
|
152
|
+
identifier: P.bytes(CompactSizeLen),
|
|
153
|
+
subtype: CompactSizeLen,
|
|
154
|
+
data: BytesInf,
|
|
155
|
+
});
|
|
156
|
+
return P.validate(BytesInf, (key) => {
|
|
157
|
+
try {
|
|
158
|
+
suffix.decode(key);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
throw new Error(`Proprietary key: expected BIP174 identifier and subtype, got ${message}`);
|
|
163
|
+
}
|
|
164
|
+
return key;
|
|
165
|
+
});
|
|
166
|
+
})();
|
|
139
167
|
// Shared 20-byte key-data helper for the BIP174 RIPEMD160 and HASH160 preimage maps.
|
|
140
168
|
const Bytes20 = /* @__PURE__ */ P.bytes(20);
|
|
141
169
|
// Shared 32-byte helper for fixed-size hash / txid / merkle-root byte fields; any stronger
|
|
142
170
|
// semantics such as x-only pubkey validity still need to be enforced by the field that uses it.
|
|
143
171
|
const Bytes32 = /* @__PURE__ */ P.bytes(32);
|
|
172
|
+
const Bytes64 = /* @__PURE__ */ P.bytes(64);
|
|
173
|
+
const Bytes66 = /* @__PURE__ */ P.bytes(66);
|
|
174
|
+
// BIP373 uses the same aggregate-key participant list in input and output maps.
|
|
175
|
+
const MuSig2Participants = /* @__PURE__ */ (() => P.array(null, PubKeyECDSACompressed))();
|
|
176
|
+
// BIP373 keys append an optional tapleaf hash to participant || aggregate. A structured key keeps
|
|
177
|
+
// the optional hash distinct while preserving the exact 66/98-byte wire encoding.
|
|
178
|
+
const MuSig2Key = /* @__PURE__ */ (() => P.apply(BytesInf, {
|
|
179
|
+
decode: (key) => {
|
|
180
|
+
const _key = key;
|
|
181
|
+
return concatBytes(PubKeyECDSACompressed.encode(_key.participantPubkey), PubKeyECDSACompressed.encode(_key.aggregatePubkey), _key.leafHash === undefined ? P.EMPTY : Bytes32.encode(_key.leafHash));
|
|
182
|
+
},
|
|
183
|
+
encode: (raw) => {
|
|
184
|
+
const _raw = raw;
|
|
185
|
+
if (_raw.length !== 66 && _raw.length !== 98)
|
|
186
|
+
throw new Error(`MuSig2 key: expected 66 or 98 bytes, got ${_raw.length}`);
|
|
187
|
+
const key = {
|
|
188
|
+
participantPubkey: PubKeyECDSACompressed.decode(_raw.subarray(0, 33)),
|
|
189
|
+
aggregatePubkey: PubKeyECDSACompressed.decode(_raw.subarray(33, 66)),
|
|
190
|
+
};
|
|
191
|
+
if (_raw.length === 98)
|
|
192
|
+
key.leafHash = Bytes32.decode(_raw.subarray(66));
|
|
193
|
+
return key;
|
|
194
|
+
},
|
|
195
|
+
}))();
|
|
196
|
+
const SilentPaymentInfo = /* @__PURE__ */ (() => P.struct({ scanKey: PubKeyECDSACompressed, spendKey: PubKeyECDSACompressed }))();
|
|
197
|
+
// BIP353 prefixes the human-readable name with one byte; the RFC9102 proof is opaque here.
|
|
198
|
+
const DNSSECProof = /* @__PURE__ */ (() => P.struct({ name: P.bytes(P.U8), proof: BytesInf }))();
|
|
144
199
|
// jsbt mutate checks exported PSBT tables recursively, so freeze each field tuple and its
|
|
145
200
|
// nested version arrays here while preserving the original coder slot types for local inference.
|
|
146
201
|
const PSBTInfo = (type, kc, vc, reqInc, allowInc, silentIgnore) =>
|
|
@@ -179,8 +234,12 @@ export const PSBTGlobal = /* @__PURE__ */ (() => Object.freeze({
|
|
|
179
234
|
outputCount: PSBTInfo(0x05, false, CompactSizeLen, [2], [2], false),
|
|
180
235
|
// TODO: bitfield
|
|
181
236
|
txModifiable: PSBTInfo(0x06, false, P.U8, [], [2], false),
|
|
237
|
+
// These assigned extension fields must not fall through the unknown-field policy.
|
|
238
|
+
spEcdhShare: PSBTInfo(0x07, PubKeyECDSACompressed, PubKeyECDSACompressed, [], [2], false),
|
|
239
|
+
spDleq: PSBTInfo(0x08, PubKeyECDSACompressed, Bytes64, [], [2], false),
|
|
240
|
+
genericSignedMessage: PSBTInfo(0x09, false, BytesInf, [], [0, 2], false),
|
|
182
241
|
version: PSBTInfo(0xfb, false, P.U32LE, [], [0, 2], false),
|
|
183
|
-
proprietary: PSBTInfo(0xfc,
|
|
242
|
+
proprietary: PSBTInfo(0xfc, ProprietaryKey, BytesInf, [], [0, 2], false),
|
|
184
243
|
}))();
|
|
185
244
|
// prettier-ignore
|
|
186
245
|
/**
|
|
@@ -228,7 +287,15 @@ export const PSBTInput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
228
287
|
tapBip32Derivation: PSBTInfo(0x16, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
|
|
229
288
|
tapInternalKey: PSBTInfo(0x17, false, PubKeySchnorr, [], [0, 2], false),
|
|
230
289
|
tapMerkleRoot: PSBTInfo(0x18, false, Bytes32, [], [0, 2], false),
|
|
231
|
-
|
|
290
|
+
p2cKeyTweak: PSBTInfo(0x19, PubKeyECDSACompressed, Bytes32, [], [0, 2], false),
|
|
291
|
+
musig2ParticipantPubkeys: PSBTInfo(0x1a, PubKeyECDSACompressed, MuSig2Participants, [], [0, 2], false),
|
|
292
|
+
musig2PubNonce: PSBTInfo(0x1b, MuSig2Key, Bytes66, [], [0, 2], false),
|
|
293
|
+
musig2PartialSig: PSBTInfo(0x1c, MuSig2Key, Bytes32, [], [0, 2], false),
|
|
294
|
+
spEcdhShare: PSBTInfo(0x1d, PubKeyECDSACompressed, PubKeyECDSACompressed, [], [2], false),
|
|
295
|
+
spDleq: PSBTInfo(0x1e, PubKeyECDSACompressed, Bytes64, [], [2], false),
|
|
296
|
+
spSpendBip32Derivation: PSBTInfo(0x1f, PubKeyECDSACompressed, BIP32Der, [], [2], false),
|
|
297
|
+
spTweak: PSBTInfo(0x20, false, Bytes32, [], [2], false),
|
|
298
|
+
proprietary: PSBTInfo(0xfc, ProprietaryKey, BytesInf, [], [0, 2], false),
|
|
232
299
|
}))();
|
|
233
300
|
// All other keys removed when finalizing
|
|
234
301
|
/**
|
|
@@ -243,37 +310,43 @@ export const PSBTInput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
243
310
|
*/
|
|
244
311
|
export const PSBTInputFinalKeys = /* @__PURE__ */ Object.freeze([
|
|
245
312
|
// PSBTv2 extractors rebuild the final transaction from per-input fields, so
|
|
246
|
-
// finalized inputs still need txid/index
|
|
313
|
+
// finalized inputs still need txid/index, any non-default sequence, and locktime requirements
|
|
247
314
|
// even though BIP174's generic cleanup is stricter.
|
|
248
315
|
'txid',
|
|
249
316
|
'sequence',
|
|
250
317
|
'index',
|
|
251
318
|
'witnessUtxo',
|
|
252
319
|
'nonWitnessUtxo',
|
|
320
|
+
'requiredTimeLocktime',
|
|
321
|
+
'requiredHeightLocktime',
|
|
253
322
|
'finalScriptSig',
|
|
254
323
|
'finalScriptWitness',
|
|
255
324
|
'unknown',
|
|
256
325
|
]);
|
|
257
|
-
// Can be modified even on signed input
|
|
258
326
|
/**
|
|
259
|
-
*
|
|
327
|
+
* Signature and final-satisfaction fields used while reopening a finalized input.
|
|
260
328
|
* @example
|
|
261
|
-
*
|
|
329
|
+
* Finalized inputs may remove their existing satisfaction before further mutation.
|
|
262
330
|
* ```ts
|
|
263
|
-
* import {
|
|
264
|
-
* const mutableKeys = new Set(
|
|
331
|
+
* import { PSBTInputSignatureKeys } from '@scure/btc-signer/psbt.js';
|
|
332
|
+
* const mutableKeys = new Set(PSBTInputSignatureKeys);
|
|
265
333
|
* mutableKeys.has('tapScriptSig');
|
|
266
334
|
* ```
|
|
267
335
|
*/
|
|
268
|
-
export const
|
|
269
|
-
// This is the replace/remove allowlist for signed inputs; mergeKeyMap() can still append
|
|
270
|
-
// previously absent metadata or new KV entries for other fields when they don't conflict.
|
|
336
|
+
export const PSBTInputSignatureKeys = /* @__PURE__ */ Object.freeze([
|
|
271
337
|
'partialSig',
|
|
272
338
|
'finalScriptSig',
|
|
273
339
|
'finalScriptWitness',
|
|
274
340
|
'tapKeySig',
|
|
275
341
|
'tapScriptSig',
|
|
276
342
|
]);
|
|
343
|
+
/**
|
|
344
|
+
* A static list cannot describe mutable input fields because that depends on each signature's
|
|
345
|
+
* algorithm, sighash, and target index.
|
|
346
|
+
* @deprecated Use {@link PSBTInputSignatureKeys} only for signature/final-satisfaction records;
|
|
347
|
+
* transaction mutation is enforced by {@link Transaction.updateInput}.
|
|
348
|
+
*/
|
|
349
|
+
export const PSBTInputUnsignedKeys = PSBTInputSignatureKeys;
|
|
277
350
|
// prettier-ignore
|
|
278
351
|
/**
|
|
279
352
|
* PSBT output key definitions.
|
|
@@ -297,7 +370,11 @@ export const PSBTOutput = /* @__PURE__ */ (() => Object.freeze({
|
|
|
297
370
|
// reconstruct the same Taproot tree, not just an arbitrary list of serialized leaves.
|
|
298
371
|
tapTree: PSBTInfo(0x06, false, tapTree, [], [0, 2], false),
|
|
299
372
|
tapBip32Derivation: PSBTInfo(0x07, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false),
|
|
300
|
-
|
|
373
|
+
musig2ParticipantPubkeys: PSBTInfo(0x08, PubKeyECDSACompressed, MuSig2Participants, [], [0, 2], false),
|
|
374
|
+
spV0Info: PSBTInfo(0x09, false, SilentPaymentInfo, [], [2], false),
|
|
375
|
+
spV0Label: PSBTInfo(0x0a, false, P.U32LE, [], [2], false),
|
|
376
|
+
dnssecProof: PSBTInfo(0x35, false, DNSSECProof, [], [0, 2], false),
|
|
377
|
+
proprietary: PSBTInfo(0xfc, ProprietaryKey, BytesInf, [], [0, 2], false),
|
|
301
378
|
}))();
|
|
302
379
|
// Can be modified even on signed input
|
|
303
380
|
/**
|
|
@@ -610,22 +687,27 @@ function validatePSBT(tx) {
|
|
|
610
687
|
validatePSBTFields(version, PSBTOutput, o);
|
|
611
688
|
// BIP174 defines `<psbt> := <magic> <global-map> <input-map>* <output-map>*`, so after decode the
|
|
612
689
|
// number of input/output maps should match the unsigned tx. PSBTv2 makes the same shape explicit
|
|
613
|
-
// through `inputCount` / `outputCount`.
|
|
614
|
-
// keep accepting exactly one trailing empty map because the separate bitcoinjs compatibility PSBT
|
|
615
|
-
// fixture corpus still contains that encoding. Anything non-empty or more than one extra map is
|
|
616
|
-
// still rejected here.
|
|
690
|
+
// through `inputCount` / `outputCount`. Input maps are count-framed and must match exactly.
|
|
617
691
|
const inputCount = !version ? tx.global.unsignedTx.inputs.length : tx.global.inputCount;
|
|
618
|
-
if (tx.inputs.length
|
|
619
|
-
throw new Error(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
//
|
|
692
|
+
if (tx.inputs.length !== inputCount)
|
|
693
|
+
throw new Error(`Wrong number of input maps=${tx.inputs.length}, expected=${inputCount}`);
|
|
694
|
+
// PSBTv0 compatibility mode may append exactly one empty output map when the unsigned
|
|
695
|
+
// transaction has no outputs. bip174js additionally inserts an empty input map when there are
|
|
696
|
+
// no inputs; count-framing makes that map appear before the real output maps in this array.
|
|
697
|
+
// PSBTv2 map counts remain strict.
|
|
624
698
|
const outputCount = !version ? tx.global.unsignedTx.outputs.length : tx.global.outputCount;
|
|
625
699
|
if (tx.outputs.length < outputCount)
|
|
626
700
|
throw new Error('Not outputs inputs');
|
|
701
|
+
const hasBip174InputMap = version === 0 &&
|
|
702
|
+
inputCount === 0 &&
|
|
703
|
+
Object.keys(tx.outputs[0] || {}).length === 0 &&
|
|
704
|
+
((outputCount > 0 && tx.outputs.length === outputCount + 1) ||
|
|
705
|
+
(outputCount === 0 && tx.outputs.length === 2 && Object.keys(tx.outputs[1]).length === 0));
|
|
706
|
+
if (hasBip174InputMap)
|
|
707
|
+
return tx;
|
|
627
708
|
const outputsLeft = tx.outputs.slice(outputCount);
|
|
628
|
-
if (outputsLeft.length > 1 ||
|
|
709
|
+
if (outputsLeft.length > 1 ||
|
|
710
|
+
(outputsLeft.length && (version !== 0 || Object.keys(outputsLeft[0]).length)))
|
|
629
711
|
throw new Error(`Unexpected outputs left in tx=${outputsLeft}`);
|
|
630
712
|
return tx;
|
|
631
713
|
}
|
|
@@ -635,7 +717,8 @@ function validatePSBT(tx) {
|
|
|
635
717
|
* @param val - new values to merge in
|
|
636
718
|
* @param cur - existing decoded PSBT key map
|
|
637
719
|
* @param allowedFields - fields still allowed to change
|
|
638
|
-
* @param
|
|
720
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
721
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
639
722
|
* @returns Merged PSBT key map.
|
|
640
723
|
* @throws If keyed PSBT fields conflict or signed fields would be removed. {@link Error}
|
|
641
724
|
* @example
|
|
@@ -653,7 +736,7 @@ function validatePSBT(tx) {
|
|
|
653
736
|
* );
|
|
654
737
|
* ```
|
|
655
738
|
*/
|
|
656
|
-
export function mergeKeyMap(psbtEnum, val, cur, allowedFields,
|
|
739
|
+
export function mergeKeyMap(psbtEnum, val, cur, allowedFields, unknown = 'strip', proprietary = 'strip') {
|
|
657
740
|
validateObject(psbtEnum, {}, {}, 'psbtEnum');
|
|
658
741
|
validateObject(val, {}, {}, 'val');
|
|
659
742
|
if (cur !== undefined)
|
|
@@ -663,10 +746,24 @@ export function mergeKeyMap(psbtEnum, val, cur, allowedFields, allowUnknown) {
|
|
|
663
746
|
const _val = val;
|
|
664
747
|
const _cur = cur;
|
|
665
748
|
const _allowedFields = allowedFields;
|
|
749
|
+
const unknownMode = unknowns(unknown, 'unknown');
|
|
750
|
+
const proprietaryMode = unknowns(proprietary, 'proprietary');
|
|
751
|
+
for (const [name, mode] of [
|
|
752
|
+
['unknown', unknownMode],
|
|
753
|
+
['proprietary', proprietaryMode],
|
|
754
|
+
]) {
|
|
755
|
+
if (mode !== 'strict')
|
|
756
|
+
continue;
|
|
757
|
+
if (_val[name]?.length ||
|
|
758
|
+
_cur?.[name]?.length)
|
|
759
|
+
throw new Error(`PSBT: ${name} PSBT field is not allowed in strict mode`);
|
|
760
|
+
}
|
|
666
761
|
const res = { ..._cur, ..._val };
|
|
667
762
|
// All arguments can be provided as hex
|
|
668
763
|
for (const k in psbtEnum) {
|
|
669
764
|
const key = k;
|
|
765
|
+
if (k === 'proprietary' && proprietaryMode !== 'ignore')
|
|
766
|
+
continue;
|
|
670
767
|
const [_, kC, vC] = psbtEnum[key];
|
|
671
768
|
const cannotChange = _allowedFields && !_allowedFields.includes(k);
|
|
672
769
|
if (_val[k] === undefined && k in _val) {
|
|
@@ -712,22 +809,31 @@ export function mergeKeyMap(psbtEnum, val, cur, allowedFields, allowUnknown) {
|
|
|
712
809
|
throw new Error(`Cannot remove signed field=${key}/${k}`);
|
|
713
810
|
delete map[kStr];
|
|
714
811
|
}
|
|
715
|
-
else
|
|
812
|
+
else {
|
|
813
|
+
if (cannotChange && map[kStr] === undefined)
|
|
814
|
+
throw new Error(`Cannot add signed field=${key}/${kStr}`);
|
|
716
815
|
add(kStr, k, v);
|
|
816
|
+
}
|
|
717
817
|
}
|
|
718
818
|
res[key] = Object.values(map);
|
|
719
819
|
}
|
|
720
820
|
}
|
|
721
|
-
else
|
|
722
|
-
res[k]
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
821
|
+
else {
|
|
822
|
+
if (typeof res[k] === 'string')
|
|
823
|
+
res[k] = vC.decode(hex.decode(res[k]));
|
|
824
|
+
if (cannotChange && k in _val) {
|
|
825
|
+
if (!_cur || _cur[k] === undefined)
|
|
826
|
+
throw new Error(`Cannot add signed field=${k}`);
|
|
827
|
+
let current = _cur[k];
|
|
828
|
+
if (typeof current === 'string')
|
|
829
|
+
current = vC.decode(hex.decode(current));
|
|
830
|
+
if (!equalBytes(vC.encode(res[k]), vC.encode(current)))
|
|
831
|
+
throw new Error(`Cannot change signed field=${k}`);
|
|
832
|
+
}
|
|
727
833
|
}
|
|
728
834
|
}
|
|
729
|
-
if (
|
|
730
|
-
// Unknown PSBT rows are stripped by default here, but explicit
|
|
835
|
+
if (unknownMode === 'ignore' && _val.unknown) {
|
|
836
|
+
// Unknown PSBT rows are stripped by default here, but explicit ignore mode is pass-through.
|
|
731
837
|
// Merge them by full serialized unknown key so repeated updates do not clobber earlier opaque rows.
|
|
732
838
|
const map = {};
|
|
733
839
|
for (const [k, v] of _cur?.unknown || [])
|
|
@@ -745,16 +851,59 @@ export function mergeKeyMap(psbtEnum, val, cur, allowedFields, allowUnknown) {
|
|
|
745
851
|
}
|
|
746
852
|
res.unknown = Object.values(map);
|
|
747
853
|
}
|
|
748
|
-
// Remove
|
|
854
|
+
// Remove properties outside the table, except opaque rows in explicit ignore mode.
|
|
749
855
|
for (const k in res) {
|
|
750
856
|
if (!psbtEnum[k]) {
|
|
751
|
-
if (
|
|
857
|
+
if (unknownMode === 'ignore' && k === 'unknown')
|
|
752
858
|
continue;
|
|
753
859
|
delete res[k];
|
|
754
860
|
}
|
|
755
861
|
}
|
|
862
|
+
if (unknownMode !== 'ignore')
|
|
863
|
+
delete res.unknown;
|
|
864
|
+
if (proprietaryMode !== 'ignore')
|
|
865
|
+
delete res.proprietary;
|
|
756
866
|
return res;
|
|
757
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Combines two independently produced PSBT maps without choosing between conflicting scalar
|
|
870
|
+
* values. Keyed fields retain {@link mergeKeyMap}'s union semantics.
|
|
871
|
+
* @param psbtEnum - PSBT field definition table
|
|
872
|
+
* @param current - first map
|
|
873
|
+
* @param other - second map
|
|
874
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
875
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
876
|
+
* @returns The symmetric map union.
|
|
877
|
+
* @throws If both maps provide different values for the same scalar field. {@link Error}
|
|
878
|
+
* @example
|
|
879
|
+
* Combine disjoint global metadata without choosing an operand as authoritative.
|
|
880
|
+
* ```ts
|
|
881
|
+
* import { combineKeyMap, PSBTGlobal } from '@scure/btc-signer/psbt.js';
|
|
882
|
+
* combineKeyMap(PSBTGlobal, { txVersion: 2 }, { fallbackLocktime: 0 });
|
|
883
|
+
* ```
|
|
884
|
+
*/
|
|
885
|
+
export function combineKeyMap(psbtEnum, current, other, unknown = 'strip', proprietary = 'strip') {
|
|
886
|
+
validateObject(psbtEnum, {}, {}, 'psbtEnum');
|
|
887
|
+
validateObject(current, {}, {}, 'current');
|
|
888
|
+
validateObject(other, {}, {}, 'other');
|
|
889
|
+
const _current = current;
|
|
890
|
+
const _other = other;
|
|
891
|
+
for (const k in psbtEnum) {
|
|
892
|
+
const key = k;
|
|
893
|
+
const [_, keyCoder, valueCoder] = psbtEnum[key];
|
|
894
|
+
if (keyCoder || _current[key] === undefined || _other[key] === undefined)
|
|
895
|
+
continue;
|
|
896
|
+
let a = _current[key];
|
|
897
|
+
let b = _other[key];
|
|
898
|
+
if (typeof a === 'string')
|
|
899
|
+
a = valueCoder.decode(hex.decode(a));
|
|
900
|
+
if (typeof b === 'string')
|
|
901
|
+
b = valueCoder.decode(hex.decode(b));
|
|
902
|
+
if (!equalBytes(valueCoder.encode(a), valueCoder.encode(b)))
|
|
903
|
+
throw new Error(`Cannot combine conflicting field=${k}`);
|
|
904
|
+
}
|
|
905
|
+
return mergeKeyMap(psbtEnum, _other, _current, undefined, unknown, proprietary);
|
|
906
|
+
}
|
|
758
907
|
/** Validated PSBTv0 coder. */
|
|
759
908
|
// This wrapper only layers `validatePSBT`'s PSBTv0 field/count reconciliation on top of
|
|
760
909
|
// `_RawPSBTV0`; field-specific payload invariants still depend on the nested coders/tables.
|
package/src/_type_test.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { secp256k1 as secp } from '@noble/curves/secp256k1.js';
|
|
2
2
|
import { hex } from '@scure/base';
|
|
3
3
|
import * as btc from './index.ts';
|
|
4
|
+
import type { TxOpts, Unknowns } from './index.ts';
|
|
5
|
+
import type { TArg } from './utils.ts';
|
|
6
|
+
|
|
7
|
+
const unknownPolicy: Unknowns = 'strip';
|
|
8
|
+
const extensionOpts: TxOpts = { unknown: unknownPolicy, proprietary: 'strict' };
|
|
9
|
+
new btc.Transaction(extensionOpts);
|
|
10
|
+
|
|
11
|
+
declare const combineOpts: TArg<TxOpts>;
|
|
12
|
+
btc.PSBTCombine([], combineOpts);
|
|
13
|
+
declare const first: btc.Transaction;
|
|
14
|
+
declare const second: btc.Transaction;
|
|
15
|
+
// Direct combination retains main's receiver-options API; byte-only PSBTCombine owns policy opts.
|
|
16
|
+
// @ts-expect-error Transaction.combine does not accept operation-specific options.
|
|
17
|
+
first.combine(second, combineOpts);
|
|
4
18
|
|
|
5
19
|
const privKey1 = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
|
|
6
20
|
const P1 = secp.getPublicKey(privKey1, true);
|
package/src/index.ts
CHANGED
|
@@ -25,9 +25,10 @@ export {
|
|
|
25
25
|
} from './script.ts';
|
|
26
26
|
export type { ScriptType } from './script.ts';
|
|
27
27
|
export { getInputType, Transaction } from './transaction.ts';
|
|
28
|
-
export {
|
|
28
|
+
export type { TxOpts, Unknowns } from './transaction.ts';
|
|
29
|
+
export { NETWORK, TAPROOT_UNSPENDABLE_KEY, TEST_NETWORK, taprootNumsKey } from './utils.ts';
|
|
29
30
|
export type { TArg, TRet } from './utils.ts';
|
|
30
|
-
export { selectUTXO } from './utxo.ts';
|
|
31
|
+
export { filterTaproot, selectUTXO } from './utxo.ts';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Small collection of commonly used utility exports.
|
|
@@ -62,6 +63,7 @@ export {
|
|
|
62
63
|
Address,
|
|
63
64
|
combinations,
|
|
64
65
|
getAddress,
|
|
66
|
+
MAX_COMBINATIONS,
|
|
65
67
|
OutScript,
|
|
66
68
|
sortedMultisig,
|
|
67
69
|
taprootListToTree,
|
package/src/musig2.ts
CHANGED
|
@@ -11,9 +11,16 @@ The implementation can be used to create own protocol,
|
|
|
11
11
|
but you need to implement nonce/partial signatures exchange yourself.
|
|
12
12
|
Someday BIP-373 will be more "implementable" and we can use this from PSBT.
|
|
13
13
|
|
|
14
|
+
SECURITY: A secret nonce MUST be used for exactly one partial signature. Session.sign() zeroes
|
|
15
|
+
only the Uint8Array instance passed to it; copies, serialized values, database records, and process
|
|
16
|
+
snapshots are not erased. Reusing the same nonce scalars in distinct sessions can reveal the
|
|
17
|
+
signer's secret key. Stateful integrations must keep one authoritative nonce record and atomically
|
|
18
|
+
consume it before releasing a partial signature.
|
|
19
|
+
|
|
14
20
|
Links:
|
|
15
21
|
- https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Test_Vectors_and_Reference_Code
|
|
16
|
-
- https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki (PSBT
|
|
22
|
+
- https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki (PSBT MuSig2): psbt.ts supports
|
|
23
|
+
its transport fields and vectors, but this module does not orchestrate the signing protocol.
|
|
17
24
|
- https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py
|
|
18
25
|
*/
|
|
19
26
|
// Types
|
|
@@ -21,7 +28,10 @@ Links:
|
|
|
21
28
|
export type Nonces = {
|
|
22
29
|
/** Public nonce that gets shared with the other participants. */
|
|
23
30
|
public: Uint8Array;
|
|
24
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Secret nonce that stays local until partial signing finishes. It MUST be consumed exactly once;
|
|
33
|
+
* never retain a copy that could be loaded for another signing session.
|
|
34
|
+
*/
|
|
25
35
|
secret: Uint8Array;
|
|
26
36
|
};
|
|
27
37
|
/**
|
|
@@ -362,6 +372,12 @@ const nonceHash = (
|
|
|
362
372
|
|
|
363
373
|
/**
|
|
364
374
|
* Generates a nonce pair (public and secret) for MuSig2 signing.
|
|
375
|
+
*
|
|
376
|
+
* SECURITY: The returned secret nonce MUST be used for exactly one partial signature. Keep one
|
|
377
|
+
* authoritative copy and atomically consume it when calling {@link Session.sign}. That method
|
|
378
|
+
* zeroes only the exact `Uint8Array` passed to it; clones, serialized values, database records, and
|
|
379
|
+
* snapshots remain live. Reusing a secret nonce in distinct sessions can reveal the secret key.
|
|
380
|
+
*
|
|
365
381
|
* @param publicKey - individual public key of the signer
|
|
366
382
|
* @param secretKey - optional secret key, mixed in to blind the randomness source
|
|
367
383
|
* @param aggPublicKey - aggregate public key of all signers
|
|
@@ -595,7 +611,12 @@ export class Session {
|
|
|
595
611
|
/**
|
|
596
612
|
* Generates a partial signature for a given message, secret nonce,
|
|
597
613
|
* secret key, and session context.
|
|
598
|
-
*
|
|
614
|
+
*
|
|
615
|
+
* SECURITY: `secretNonce` MUST be used exactly once. This method zeroes the first 64 bytes of the
|
|
616
|
+
* supplied array, including when later validation fails, but cannot erase copies or persisted
|
|
617
|
+
* representations. Reusing those nonce scalars in a distinct session can reveal the secret key.
|
|
618
|
+
*
|
|
619
|
+
* @param secretNonce - sole authoritative secret-nonce buffer for this signing session
|
|
599
620
|
* @param secret - secret key of the signer
|
|
600
621
|
* @param fastSign - if `true`, skip the self-verification pass
|
|
601
622
|
* @returns The partial signature (Uint8Array).
|
|
@@ -670,16 +691,20 @@ export class Session {
|
|
|
670
691
|
}
|
|
671
692
|
/**
|
|
672
693
|
* Aggregates partial signatures from multiple signers into a single final signature.
|
|
673
|
-
* @param partialSigs - partial
|
|
694
|
+
* @param partialSigs - exactly one positional partial signature per session participant
|
|
674
695
|
* @returns The final aggregate signature (Uint8Array).
|
|
675
696
|
* @throws If the input is invalid, such as wrong array sizes or malformed
|
|
676
697
|
* signatures. {@link Error}
|
|
677
698
|
*/
|
|
678
699
|
partialSigAgg(partialSigs: TArg<Uint8Array[]>): TRet<Uint8Array> {
|
|
679
700
|
abytesArray(partialSigs, 32);
|
|
680
|
-
// BIP327 PartialSigAgg
|
|
681
|
-
//
|
|
682
|
-
if (partialSigs.length
|
|
701
|
+
// BIP327 PartialSigAgg consumes psig_1..u for the same u signers in session_ctx. Accepting
|
|
702
|
+
// fewer or more scalars would return a signature-shaped value for a different equation.
|
|
703
|
+
if (partialSigs.length !== this.publicKeys.length)
|
|
704
|
+
throw new RangeError(
|
|
705
|
+
`partialSigs.length=${partialSigs.length} must equal ` +
|
|
706
|
+
`participant count=${this.publicKeys.length}`
|
|
707
|
+
);
|
|
683
708
|
const { Q, tweakAcc, R, e } = this;
|
|
684
709
|
let s = _0n;
|
|
685
710
|
for (let i = 0; i < partialSigs.length; i++) {
|
package/src/net.ts
CHANGED
|
@@ -950,8 +950,13 @@ export class EsploraProvider {
|
|
|
950
950
|
const remaining =
|
|
951
951
|
options.timeoutMs === undefined ? pollIntervalMs : options.timeoutMs - (Date.now() - start);
|
|
952
952
|
if (remaining <= 0) throw new EsploraError('waitForTx: timeout');
|
|
953
|
-
|
|
954
|
-
|
|
953
|
+
if (options.timeoutMs !== undefined && remaining <= pollIntervalMs) {
|
|
954
|
+
// Sleeping the rest of the window reaches the deadline; the timer may
|
|
955
|
+
// wake before Date.now() agrees, so don't re-poll on a clock check.
|
|
956
|
+
await sleep(remaining, options.signal);
|
|
957
|
+
throw new EsploraError('waitForTx: timeout');
|
|
958
|
+
}
|
|
959
|
+
await sleep(pollIntervalMs, options.signal);
|
|
955
960
|
}
|
|
956
961
|
}
|
|
957
962
|
async txInfo(txid: string): Promise<TxInfo> {
|