@scure/btc-signer 2.2.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 CHANGED
@@ -4,7 +4,7 @@ Audited & minimal library for creating, signing & decoding Bitcoin transactions.
4
4
 
5
5
  - ๐Ÿ”’ [**Audited**](#security) by an independent security firm
6
6
  - โœ๏ธ Create transactions, inputs, outputs, sign them
7
- - ๐Ÿ“ก No network code: simplified audits and offline usage
7
+ - ๐Ÿ“ก Optional network helper; core signer works offline
8
8
  - ๐Ÿ”€ UTXO selection with different strategies
9
9
  - ๐ŸŽป Classic & SegWit: P2PK, P2PKH, P2WPKH, P2SH, P2WSH, P2MS
10
10
  - ๐Ÿงช Schnorr & Taproot BIP340/BIP341: P2TR, P2TR-NS, P2TR-MS
@@ -49,6 +49,40 @@ For React Native, you may need a [polyfill for crypto.getRandomValues](https://g
49
49
  import * as btc from '@scure/btc-signer';
50
50
  ```
51
51
 
52
+ ### Quickstart
53
+
54
+ Create a Taproot address, build a transaction spending one of its UTXOs, sign and finalize it:
55
+
56
+ ```ts
57
+ import * as btc from '@scure/btc-signer';
58
+ import { hex } from '@scure/base';
59
+
60
+ const privKey = hex.decode('a1547d0a01c9acb2b8a4128f97bbcd74d9a5750a9a3d1571ee3d9840b41d1fbb');
61
+ const pubKey = btc.utils.pubSchnorr(privKey);
62
+
63
+ // Taproot payment for a single key. Also check out p2wpkh, p2sh, p2wsh & others below
64
+ const spend = btc.p2tr(pubKey);
65
+ console.log(spend.address);
66
+ // bc1pkw4e67whvet7q6xa854tstt7kjn9f7n3gqvyjqznezqwux0gnzhsrtlvy3
67
+
68
+ const tx = new btc.Transaction();
69
+ tx.addInput({
70
+ ...spend, // adds tapInternalKey & other fields required for signing
71
+ txid: '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858',
72
+ index: 0,
73
+ witnessUtxo: { script: spend.script, amount: 100_000n }, // amounts are always bigint sats
74
+ });
75
+ tx.addOutputAddress(spend.address!, 90_000n); // leftover 10k sats become the fee
76
+ tx.sign(privKey);
77
+ tx.finalize();
78
+
79
+ console.log(tx.id);
80
+ // 927bc3441c7b376a83913b534bf10430139d9f64d83cbc38eb3a4e24cce54dc5
81
+ console.log(tx.hex); // ready to broadcast, e.g. via net.ts EsploraProvider
82
+ ```
83
+
84
+ The rest of the docs cover every payment type, PSBT workflows, UTXO selection and more:
85
+
52
86
  - [Payments](#payments)
53
87
  - [P2PK Pay To Public Key](#p2pk-pay-to-public-key)
54
88
  - [P2PKH Public Key Hash](#p2pkh-public-key-hash)
@@ -69,6 +103,7 @@ import * as btc from '@scure/btc-signer';
69
103
  - [Basic transaction sign](#basic-transaction-sign)
70
104
  - [BIP174 PSBT multi-sig example](#bip174-psbt-multi-sig-example)
71
105
  - [UTXO selection](#utxo-selection)
106
+ - [Network](#network)
72
107
  - [MuSig2](#musig2)
73
108
  - [P2P, ElligatorSwift, BIP324](#p2p-elligatorswift-bip324)
74
109
  - [Ordinals and custom scripts](#ordinals-and-custom-scripts)
@@ -480,13 +515,28 @@ deepStrictEqual(hex.encode(btc.OutScript.encode(decoded)), '51024e73');
480
515
 
481
516
  ### Encode/decode
482
517
 
483
- We support both PSBTv0 and draft PSBTv2 (there is no PSBTv1). If PSBTv2 transaction is encoded into PSBTv1, all PSBTv2 fields will be stripped.
518
+ We support both PSBTv0 and PSBTv2 (there is no PSBTv1). Explicit PSBTv0 conversion removes
519
+ PSBTv2-only fields after translating the unsigned transaction representation.
520
+
521
+ Assigned fields from BIPs 322, 353, 372, 373, 375, and 376 are decoded explicitly and survive the
522
+ default path. `TxOpts.unknown` and `TxOpts.proprietary` govern only genuinely unknown and
523
+ proprietary records:
524
+
525
+ - `ignore` preserves them, but the library may operate without understanding future constraints.
526
+ - `strip` accepts and removes them; this is the default and can break interoperability with future
527
+ extensions.
528
+ - `strict` rejects them.
484
529
 
485
- We strip 'unknown' keys inside PSBT, they needed for new version/features support,
486
- however any unsupported feature/new version can significantly break assumptions about code.
487
- If you have use-case where they are needed, create a github issue.
530
+ Undefined bits in PSBTv2 `txModifiable` follow the `unknown` policy. `strip` and `strict` can also
531
+ fingerprint this library or version, while `ignore` offers better forwarding compatibility at the
532
+ cost of interpreting less protocol state. The deprecated `allowUnknown` boolean maps `true` to
533
+ `ignore` and `false` to `strip`.
488
534
 
489
- PSBTv2 features tx_modifiable and taproot+bip32 are not supported yet.
535
+ When signing an untrusted or multi-party PSBT, pass
536
+ `{ strictPrevoutValidation: true }` to `Transaction.fromPSBT`. This requires every input to include
537
+ a full `nonWitnessUtxo` matching its outpoint before any signature is produced. It prevents forged
538
+ `witnessUtxo` amounts from making the displayed transaction fee appear lower than the fee that the
539
+ valid transaction will actually pay.
490
540
 
491
541
  ```text
492
542
  // Decode
@@ -519,27 +569,27 @@ type DerivationPath = { fingerprint: number; path: number[] };
519
569
  type TapScriptSigKey = { pubKey: Bytes; leafHash: Bytes };
520
570
  type TapLeafScriptKey = { version: number; internalKey: Bytes; merklePath: Bytes[] };
521
571
  type TransactionInput = {
522
- txid?: Bytes,
523
- index?: number,
524
- nonWitnessUtxo?: RawTransactionBytesOrHex,
525
- witnessUtxo?: { script?: Bytes; amount: bigint },
572
+ txid?: Bytes;
573
+ index?: number;
574
+ nonWitnessUtxo?: RawTransactionBytesOrHex;
575
+ witnessUtxo?: { script?: Bytes; amount: bigint };
526
576
  partialSig?: [Bytes, Bytes][]; // [PubKey, Signature]
527
- sighashType?: number,
528
- redeemScript?: Bytes,
529
- witnessScript?: Bytes,
577
+ sighashType?: number;
578
+ redeemScript?: Bytes;
579
+ witnessScript?: Bytes;
530
580
  bip32Derivation?: [Bytes, DerivationPath | undefined][]; // [PubKey, DeriviationPath]
531
- finalScriptSig?: Bytes,
532
- finalScriptWitness?: Bytes[],
533
- porCommitment?: Bytes,
534
- sequence?: number,
535
- requiredTimeLocktime?: number,
536
- requiredHeightLocktime?: number,
537
- tapKeySig?: Bytes,
581
+ finalScriptSig?: Bytes;
582
+ finalScriptWitness?: Bytes[];
583
+ porCommitment?: Bytes;
584
+ sequence?: number;
585
+ requiredTimeLocktime?: number;
586
+ requiredHeightLocktime?: number;
587
+ tapKeySig?: Bytes;
538
588
  tapScriptSig?: [TapScriptSigKey, Bytes][]; // [PubKeySchnorr, LeafHash]
539
589
  // [ControlBlock, ScriptWithVersion]
540
590
  tapLeafScript?: [TapLeafScriptKey, Bytes][];
541
- tapInternalKey?: Bytes,
542
- tapMerkleRoot?: Bytes,
591
+ tapInternalKey?: Bytes;
592
+ tapMerkleRoot?: Bytes;
543
593
  };
544
594
 
545
595
  // tx.addInput(input: TransactionInput): number;
@@ -632,12 +682,12 @@ const tx = new btc.Transaction();
632
682
  type Bytes = Uint8Array | string;
633
683
  type DerivationPath = { fingerprint: number; path: number[] };
634
684
  type TransactionOutput = {
635
- script?: Bytes,
636
- amount?: bigint,
637
- redeemScript?: Bytes,
638
- witnessScript?: Bytes,
685
+ script?: Bytes;
686
+ amount?: bigint;
687
+ redeemScript?: Bytes;
688
+ witnessScript?: Bytes;
639
689
  bip32Derivation?: [Bytes, DerivationPath | undefined][]; // [PubKey, DeriviationPath]
640
- tapInternalKey?: Bytes,
690
+ tapInternalKey?: Bytes;
641
691
  };
642
692
 
643
693
  // tx.addOutput(o: TransactionOutput): number;
@@ -902,10 +952,28 @@ when making an on-chain bitcoin payment. The library:
902
952
  - calculates weight with good precision
903
953
  - implements multiple strategies
904
954
 
905
- Taproot estimation is precise, but you have to pass sighash if you want to use non-default one,
906
- because it changes signature size. For complex taproot trees you need to filter tapLeafScript
907
- to include only leafs which you can sign we estimate size with smallest leaf (same as finalization),
908
- but in specific case keys for this leaf can be unavailable (complex multisig)
955
+ Taproot estimation is precise, but you have to pass `sighashType` when using a non-default
956
+ sighash because it changes signature size. A Taproot input can advertise paths that the current
957
+ wallet cannot spend. Pass the wallet's x-only public keys as `filterTaproot` to remove unavailable
958
+ internal-key and script paths before selection:
959
+
960
+ ```js
961
+ import * as btc from '@scure/btc-signer';
962
+
963
+ const select = (utxos, outputs, changeAddress, feePerByte, myPublicKey) =>
964
+ btc.selectUTXO(utxos, outputs, 'default', {
965
+ changeAddress,
966
+ feePerByte,
967
+ filterTaproot: [myPublicKey],
968
+ });
969
+ ```
970
+
971
+ Filtering is opt-in: when `filterTaproot` is omitted, no paths are removed. The standalone
972
+ `btc.filterTaproot(utxos, [myPublicKey])` utility returns the same filtered candidate records.
973
+ Built-in `tr_ns` and `tr_ms` paths are checked directly. Custom or unknown leaves are rejected;
974
+ callers must filter those paths using their own semantics before invoking this helper. Estimation
975
+ and finalization choose the available path with the smallest complete witness, retaining the old
976
+ shallowest-path order for equal weights.
909
977
 
910
978
  `Oldest` / `Newest` expects UTXO provided in historical order (oldest first),
911
979
  otherwise we have no way to detect age of tx.
@@ -1025,11 +1093,108 @@ deepStrictEqual(tx.id, 'b702078d65edd65a84b2a97a669df5631b06f42a67b0d7090e540b02
1025
1093
  deepStrictEqual(tx.fee, 394n);
1026
1094
  ```
1027
1095
 
1096
+ ## Network
1097
+
1098
+ Bitcoin nodes can't be used as source-of-truth to construct transactions & get UTXOs.
1099
+ An extra indexer is required. Our `net.js` submodule allows to easily fetch UTXOs,
1100
+ balances, and other data for an address.
1101
+ See [README-fullnode.md](./README-fullnode.md) for details and
1102
+ guide on running a full node with an indexer.
1103
+
1104
+ ```ts
1105
+ import * as btc from '@scure/btc-signer';
1106
+ import { EsploraProvider } from '@scure/btc-signer/net.js';
1107
+ import { pubECDSA } from '@scure/btc-signer/utils.js';
1108
+ const net = new EsploraProvider(fetch, 'http://127.0.0.1:3000');
1109
+ // Methods: `height`, `blockInfo`, `fee`, `balance`, `txCount`, `sendTx`, `waitForTx`, `txInfo`,
1110
+ // `unspent`, `transfers`, `history`, `historyMulti`.
1111
+ // Transient backend failures (429/5xx, dropped connections) are retried with backoff on GETs.
1112
+ // Long scans accept `signal` (AbortSignal) and `onProgress`; raw-tx fan-out is
1113
+ // bounded by `concurrency` (default 8).
1114
+
1115
+ // Get latest block.
1116
+ async function latestBlock() {
1117
+ const height = await net.height();
1118
+ const block = await net.blockInfo(height);
1119
+ return { number: block.number, hash: block.hash, timestamp: new Date(block.timestamp) };
1120
+ }
1121
+
1122
+ // Get per-address transaction history.
1123
+ async function addressTransactions(address: string) {
1124
+ const txs = await net.transfers(address, { limit: 10 });
1125
+ return txs.map((tx) => ({ txid: tx.txid, block: tx.block, fee: tx.info.fee }));
1126
+ }
1127
+
1128
+ // Stream history instead of buffering it: rows arrive newest-first while pages
1129
+ // are fetched, and stopping early also stops fetching.
1130
+ async function streamHistory(address: string) {
1131
+ for await (const tx of net.history(address, { onProgress: (p) => console.log(p.percent) })) {
1132
+ if (tx.block !== undefined && tx.block < 800_000) break;
1133
+ console.log(tx.txid, tx.transfers);
1134
+ }
1135
+ }
1136
+
1137
+ // Merged history for a set of addresses (HD wallets): one txid-deduplicated
1138
+ // stream; each row lists the watched addresses participating in it.
1139
+ async function walletHistory(addresses: string[]) {
1140
+ for await (const tx of net.historyMulti(addresses)) console.log(tx.txid, tx.addresses);
1141
+ }
1142
+
1143
+ // Broadcast, then wait for confirmation.
1144
+ async function sendAndWait(rawTx: string) {
1145
+ const txid = await net.sendTx(rawTx);
1146
+ return await net.waitForTx(txid, { confirmations: 2, timeoutMs: 3_600_000 });
1147
+ }
1148
+
1149
+ // Get UTXOs, select inputs, and sign. Call with a key that controls funded UTXOs.
1150
+ async function signSpend(privKey: Uint8Array, to: string, amount: bigint) {
1151
+ const spend = btc.p2wpkh(pubECDSA(privKey));
1152
+ const unspent = await net.unspent(spend.address!);
1153
+ const feePerByte = await net.fee(2);
1154
+ const selected = btc.selectUTXO(unspent.utxo, [{ address: to, amount }], 'default', {
1155
+ feePerByte,
1156
+ changeAddress: spend.address!,
1157
+ });
1158
+ if (!selected) throw new Error(`not enough funds for ${spend.address}`);
1159
+ const { tx } = selected;
1160
+ tx.sign(privKey);
1161
+ tx.finalize();
1162
+ return { txid: tx.id, raw: tx.hex };
1163
+ }
1164
+
1165
+ // For wrapped or script spends, add caller-owned metadata before selection:
1166
+ // const wrapped = btc.p2sh(btc.p2wpkh(pubECDSA(privKey)));
1167
+ // const base = await net.unspent(wrapped.address!);
1168
+ // const utxo = base.utxo.map((u) => ({ ...u, redeemScript: wrapped.redeemScript }));
1169
+ // const selected = btc.selectUTXO(utxo, outputs, 'default', opts);
1170
+ ```
1171
+
1172
+ First argument is `fetch` API-compatible transport.
1173
+ We suggest using `micro-ftch` package - a wrapper, which supports kill-switch,
1174
+ logging, timeouts, concurrency limits, replay fixtures, and other useful network controls:
1175
+
1176
+ ```ts
1177
+ import { ftch } from 'micro-ftch';
1178
+ let NETWORK_ENABLED = true;
1179
+ const fetcher = ftch(fetch, {
1180
+ isValidRequest: () => NETWORK_ENABLED,
1181
+ timeout: 10_000,
1182
+ concurrencyLimit: 4,
1183
+ });
1184
+ const net = new EsploraProvider(fetcher, 'http://127.0.0.1:3000');
1185
+ ```
1186
+
1028
1187
  ## MuSig2
1029
1188
 
1030
1189
  MuSig2 implementation conforming to [BIP-327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki)
1031
1190
  is available in `@scure/btc-signer/musig2.js`. Check out [bip327-musig2.test.ts](./test/bip327-musig2.test.ts) as well:
1032
1191
 
1192
+ **Security warning:** every MuSig2 secret nonce must be used for exactly one partial signature.
1193
+ `Session.sign()` zeroes only the exact `Uint8Array` passed to it; clones, serialized values,
1194
+ database records, and process snapshots remain live. Reusing the same nonce in distinct sessions
1195
+ can reveal the signer's secret key. Stateful signers must keep one authoritative nonce record and
1196
+ atomically consume it before releasing a partial signature.
1197
+
1033
1198
  > `npm install @noble/curves`
1034
1199
 
1035
1200
  ```ts
@@ -1348,12 +1513,6 @@ For this package, there are 4 dependencies; and a few dev dependencies:
1348
1513
  - jsbt is used for benchmarking / testing / build tooling and developed by the same author
1349
1514
  - prettier, fast-check and typescript are used for code quality / test generation / ts compilation. It's hard to audit their source code thoroughly and fully because of their size
1350
1515
 
1351
- ## Contributing & testing
1352
-
1353
- - `npm install && npm run build && npm test` will build the code and run tests.
1354
- - `npm run lint` / `npm run format` will run linter / fix linter issues.
1355
- - `npm run build:release` will build single file
1356
-
1357
1516
  ## Learning & documentation
1358
1517
 
1359
1518
  There are several nice resources on the topic:
package/index.d.ts CHANGED
@@ -5,9 +5,10 @@ export { multisig, p2ms, p2pk, p2pkh, p2sh, p2tr, p2tr_ms, p2tr_ns, p2tr_pk, p2w
5
5
  export { CompactSize, MAX_SCRIPT_BYTE_LENGTH, OP, RawTx, RawWitness, Script, ScriptNum, } from './script.ts';
6
6
  export type { ScriptType } from './script.ts';
7
7
  export { getInputType, Transaction } from './transaction.ts';
8
- export { NETWORK, TAPROOT_UNSPENDABLE_KEY, TEST_NETWORK } from './utils.ts';
8
+ export type { TxOpts, Unknowns } from './transaction.ts';
9
+ export { NETWORK, TAPROOT_UNSPENDABLE_KEY, TEST_NETWORK, taprootNumsKey } from './utils.ts';
9
10
  export type { TArg, TRet } from './utils.ts';
10
- export { selectUTXO } from './utxo.ts';
11
+ export { filterTaproot, selectUTXO } from './utxo.ts';
11
12
  /**
12
13
  * Small collection of commonly used utility exports.
13
14
  * @example
@@ -24,9 +25,8 @@ export declare const utils: TRet<Readonly<{
24
25
  randomPrivateKeyBytes: () => TRet<Uint8Array>;
25
26
  taprootTweakPubkey: typeof taprootTweakPubkey;
26
27
  }>>;
27
- export { _sortPubkeys, Address, combinations, getAddress, OutScript, sortedMultisig, taprootListToTree, WIF, } from './payment.ts';
28
+ export { _sortPubkeys, Address, combinations, getAddress, MAX_COMBINATIONS, OutScript, sortedMultisig, taprootListToTree, WIF, } from './payment.ts';
28
29
  export type { CustomScript, OptScript } from './payment.ts';
29
30
  export { _DebugPSBT, TaprootControlBlock } from './psbt.ts';
30
31
  export { bip32Path, Decimal, DEFAULT_SEQUENCE, PSBTCombine, SigHash } from './transaction.ts';
31
32
  export { _cmpBig, _Estimator } from './utxo.ts';
32
- //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -4,8 +4,8 @@ import { compareBytes, concatBytes, isBytes, pubSchnorr, randomPrivateKeyBytes,
4
4
  export { multisig, p2ms, p2pk, p2pkh, p2sh, p2tr, p2tr_ms, p2tr_ns, p2tr_pk, p2wpkh, p2wsh } from "./payment.js";
5
5
  export { CompactSize, MAX_SCRIPT_BYTE_LENGTH, OP, RawTx, RawWitness, Script, ScriptNum, } from "./script.js";
6
6
  export { getInputType, Transaction } from "./transaction.js";
7
- export { NETWORK, TAPROOT_UNSPENDABLE_KEY, TEST_NETWORK } from "./utils.js";
8
- export { selectUTXO } from "./utxo.js";
7
+ export { NETWORK, TAPROOT_UNSPENDABLE_KEY, TEST_NETWORK, taprootNumsKey } from "./utils.js";
8
+ export { filterTaproot, selectUTXO } from "./utxo.js";
9
9
  /**
10
10
  * Small collection of commonly used utility exports.
11
11
  * @example
@@ -23,10 +23,9 @@ export const utils = /* @__PURE__ */ (() => Object.freeze({
23
23
  randomPrivateKeyBytes,
24
24
  taprootTweakPubkey,
25
25
  }))();
26
- export { _sortPubkeys, Address, combinations, getAddress, OutScript, sortedMultisig, taprootListToTree, WIF, } from "./payment.js";
26
+ export { _sortPubkeys, Address, combinations, getAddress, MAX_COMBINATIONS, OutScript, sortedMultisig, taprootListToTree, WIF, } from "./payment.js";
27
27
  // remove
28
28
  export { _DebugPSBT, TaprootControlBlock } from "./psbt.js";
29
29
  // remove
30
30
  export { bip32Path, Decimal, DEFAULT_SEQUENCE, PSBTCombine, SigHash } from "./transaction.js";
31
31
  export { _cmpBig, _Estimator } from "./utxo.js";
32
- //# sourceMappingURL=index.js.map
package/musig2.d.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import type { WeierstrassPoint } from '@noble/curves/abstract/weierstrass.js';
1
2
  import { type TArg, type TRet } from './utils.ts';
2
3
  /** Represents a pair of public and secret nonces used in MuSig2 signing. */
3
4
  export type Nonces = {
4
5
  /** Public nonce that gets shared with the other participants. */
5
6
  public: Uint8Array;
6
- /** Secret nonce that stays local until partial signing finishes. */
7
+ /**
8
+ * Secret nonce that stays local until partial signing finishes. It MUST be consumed exactly once;
9
+ * never retain a copy that could be loaded for another signing session.
10
+ */
7
11
  secret: Uint8Array;
8
12
  };
9
13
  /**
@@ -16,6 +20,15 @@ export type DetNonce = {
16
20
  /** Partial signature produced after combining all participant data. */
17
21
  partialSig: Uint8Array;
18
22
  };
23
+ /** MuSig2 key aggregation context used by signing sessions. */
24
+ export type KeyAggregate = {
25
+ /** Aggregate public key before x-only export. */
26
+ aggPublicKey: WeierstrassPoint<bigint>;
27
+ /** Accumulated sign from x-only tweaks. */
28
+ gAcc: bigint;
29
+ /** Accumulated tweak scalar. */
30
+ tweakAcc: bigint;
31
+ };
19
32
  /**
20
33
  * Represents an error indicating an invalid contribution from a signer.
21
34
  * This allows pointing out which participant is malicious and what specifically is wrong.
@@ -86,11 +99,7 @@ export declare function sortKeys(publicKeys: TArg<Uint8Array[]>): TRet<Uint8Arra
86
99
  * ]);
87
100
  * ```
88
101
  */
89
- export declare function keyAggregate(publicKeys: TArg<Uint8Array[]>, tweaks?: TArg<Uint8Array[]>, isXonly?: boolean[]): {
90
- aggPublicKey: import("@noble/curves/abstract/weierstrass.js").WeierstrassPoint<bigint>;
91
- gAcc: bigint;
92
- tweakAcc: bigint;
93
- };
102
+ export declare function keyAggregate(publicKeys: TArg<Uint8Array[]>, tweaks?: TArg<Uint8Array[]>, isXonly?: boolean[]): KeyAggregate;
94
103
  /**
95
104
  * Exports the aggregate public key to a byte array.
96
105
  * @param ctx - result of {@link keyAggregate}
@@ -110,6 +119,12 @@ export declare function keyAggregate(publicKeys: TArg<Uint8Array[]>, tweaks?: TA
110
119
  export declare function keyAggExport(ctx: ReturnType<typeof keyAggregate>): TRet<Uint8Array>;
111
120
  /**
112
121
  * Generates a nonce pair (public and secret) for MuSig2 signing.
122
+ *
123
+ * SECURITY: The returned secret nonce MUST be used for exactly one partial signature. Keep one
124
+ * authoritative copy and atomically consume it when calling {@link Session.sign}. That method
125
+ * zeroes only the exact `Uint8Array` passed to it; clones, serialized values, database records, and
126
+ * snapshots remain live. Reusing a secret nonce in distinct sessions can reveal the secret key.
127
+ *
113
128
  * @param publicKey - individual public key of the signer
114
129
  * @param secretKey - optional secret key, mixed in to blind the randomness source
115
130
  * @param aggPublicKey - aggregate public key of all signers
@@ -221,7 +236,12 @@ export declare class Session {
221
236
  /**
222
237
  * Generates a partial signature for a given message, secret nonce,
223
238
  * secret key, and session context.
224
- * @param secretNonce - secret nonce for this signing session; it is zeroed after use
239
+ *
240
+ * SECURITY: `secretNonce` MUST be used exactly once. This method zeroes the first 64 bytes of the
241
+ * supplied array, including when later validation fails, but cannot erase copies or persisted
242
+ * representations. Reusing those nonce scalars in a distinct session can reveal the secret key.
243
+ *
244
+ * @param secretNonce - sole authoritative secret-nonce buffer for this signing session
225
245
  * @param secret - secret key of the signer
226
246
  * @param fastSign - if `true`, skip the self-verification pass
227
247
  * @returns The partial signature (Uint8Array).
@@ -241,7 +261,7 @@ export declare class Session {
241
261
  partialSigVerify(partialSig: Uint8Array, pubNonces: Uint8Array[], i: number): boolean;
242
262
  /**
243
263
  * Aggregates partial signatures from multiple signers into a single final signature.
244
- * @param partialSigs - partial signatures from each signer
264
+ * @param partialSigs - exactly one positional partial signature per session participant
245
265
  * @returns The final aggregate signature (Uint8Array).
246
266
  * @throws If the input is invalid, such as wrong array sizes or malformed
247
267
  * signatures. {@link Error}
@@ -283,4 +303,3 @@ export declare class Session {
283
303
  * ```
284
304
  */
285
305
  export declare function deterministicSign(secret: TArg<Uint8Array>, aggOtherNonce: TArg<Uint8Array>, publicKeys: TArg<Uint8Array[]>, msg: TArg<Uint8Array>, tweaks?: TArg<Uint8Array[]>, isXonly?: boolean[], rand?: TArg<Uint8Array>, fastSign?: boolean): TRet<DetNonce>;
286
- //# sourceMappingURL=musig2.d.ts.map
package/musig2.js CHANGED
@@ -2,7 +2,7 @@ import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js';
2
2
  import { aInRange, concatBytes, equalBytes, numberToBytesBE } from '@noble/curves/utils.js';
3
3
  import { abytes, anumber, randomBytes } from '@noble/hashes/utils.js';
4
4
  import * as P from 'micro-packed';
5
- import { compareBytes, hasEven } from "./utils.js";
5
+ import { compareBytes, hasEven, validateObject } from "./utils.js";
6
6
  /**
7
7
  * Represents an error indicating an invalid contribution from a signer.
8
8
  * This allows pointing out which participant is malicious and what specifically is wrong.
@@ -43,6 +43,9 @@ const PUBKEY_LEN = /* @__PURE__ */ (() => secp256k1.lengths.publicKey)();
43
43
  // BIP327 uses bytes(33, 0) both as cbytes_ext(Point.ZERO) for infinity and as GetSecondKey's
44
44
  // "no second distinct key" sentinel, so this all-zero compressed slot is intentionally out-of-band.
45
45
  const ZERO = /* @__PURE__ */ new Uint8Array(PUBKEY_LEN); // Compressed zero point
46
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
47
+ // prettier-ignore
48
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
46
49
  // Encoding
47
50
  // TODO: re-use in PSBT?
48
51
  // This is BIP327's cbytes_ext/cpoint_ext adapter: normal points stay in compressed SEC1,
@@ -54,7 +57,7 @@ const compressed = /* @__PURE__ */ (() => P.apply(P.bytes(33), {
54
57
  // This coder is only for stored secnonce limbs k1/k2, which BIP327 requires to be
55
58
  // nonzero scalars in [1, n); tweak scalars use different validation because 0 is allowed there.
56
59
  const scalar = /* @__PURE__ */ (() => P.validate(P.U256BE, (n) => {
57
- aInRange('n', n, 1n, Fn.ORDER);
60
+ aInRange('n', n, _1n, Fn.ORDER);
58
61
  return n;
59
62
  }))();
60
63
  // Shared for both per-signer pubnonce bytes and aggregate aggnonce bytes. Because it accepts
@@ -172,7 +175,7 @@ function keyAggCoeffInternal(publicKey1, publicKey2, L) {
172
175
  abytes(publicKey1, PUBKEY_LEN);
173
176
  abytes(publicKey2, PUBKEY_LEN);
174
177
  if (equalBytes(publicKey1, publicKey2))
175
- return 1n;
178
+ return _1n;
176
179
  return taggedInt('KeyAgg coefficient', L, publicKey1);
177
180
  }
178
181
  /**
@@ -223,6 +226,10 @@ export function keyAggregate(publicKeys, tweaks = [], isXonly = []) {
223
226
  }
224
227
  aggPublicKey = aggPublicKey.add(Pi.multiply(keyAggCoeffInternal(publicKeys[i], pk2, L)));
225
228
  }
229
+ // BIP327 KeyAggInternal: "Fail if is_infinite(Q)". Computationally unreachable for
230
+ // hash-derived coefficients, but the spec mandates the explicit check before tweaking.
231
+ if (isZero(aggPublicKey))
232
+ throw new Error('keyAggregate: aggregate public key cannot be infinity');
226
233
  let gAcc = Fn.ONE;
227
234
  let tweakAcc = Fn.ZERO;
228
235
  // Apply tweaks
@@ -257,6 +264,9 @@ export function keyAggregate(publicKeys, tweaks = [], isXonly = []) {
257
264
  * ```
258
265
  */
259
266
  export function keyAggExport(ctx) {
267
+ validateObject(ctx, {}, {}, 'ctx');
268
+ if (!(ctx.aggPublicKey instanceof Point))
269
+ throw new TypeError('"ctx.aggPublicKey" expected point, got type=' + typeof ctx.aggPublicKey);
260
270
  // BIP327 GetXonlyPubkey returns xbytes(Q), so this is the 32-byte x-only aggregate key
261
271
  // instead of the 33-byte compressed SEC1 form.
262
272
  return pointToBytes(ctx.aggPublicKey);
@@ -278,6 +288,12 @@ const nonceHash = (rand, publicKey, aggPublicKey, i, msgPrefixed, extraIn) =>
278
288
  taggedInt('MuSig/nonce', rand, new Uint8Array([publicKey.length]), publicKey, new Uint8Array([aggPublicKey.length]), aggPublicKey, msgPrefixed, numberToBytesBE(extraIn.length, 4), extraIn, new Uint8Array([i]));
279
289
  /**
280
290
  * Generates a nonce pair (public and secret) for MuSig2 signing.
291
+ *
292
+ * SECURITY: The returned secret nonce MUST be used for exactly one partial signature. Keep one
293
+ * authoritative copy and atomically consume it when calling {@link Session.sign}. That method
294
+ * zeroes only the exact `Uint8Array` passed to it; clones, serialized values, database records, and
295
+ * snapshots remain live. Reusing a secret nonce in distinct sessions can reveal the secret key.
296
+ *
281
297
  * @param publicKey - individual public key of the signer
282
298
  * @param secretKey - optional secret key, mixed in to blind the randomness source
283
299
  * @param aggPublicKey - aggregate public key of all signers
@@ -425,6 +441,7 @@ export class Session {
425
441
  * @throws If the input is invalid, such as wrong array sizes or lengths. {@link Error}
426
442
  */
427
443
  constructor(aggNonce, publicKeys, msg, tweaks = [], isXonly = []) {
444
+ abytes(aggNonce, 66);
428
445
  abytesArray(publicKeys, 33);
429
446
  abytesArray(tweaks, 32);
430
447
  aXonly(isXonly);
@@ -443,7 +460,10 @@ export class Session {
443
460
  this.gAcc = gAcc;
444
461
  this.tweakAcc = tweakAcc;
445
462
  this.b = taggedInt('MuSig/noncecoef', aggNonce, pointToBytes(aggPublicKey), msg);
446
- const R = R1.add(R2.multiply(this.b));
463
+ // b and the nonce points are public session values, so the faster variable-time
464
+ // multiplication is safe here; it also matches the reference point_mul, which
465
+ // maps a (negligible-probability) zero coefficient to infinity instead of failing.
466
+ const R = R1.add(R2.multiplyUnsafe(this.b));
447
467
  this.R = isZero(R) ? Point.BASE : R;
448
468
  this.e = taggedInt('BIP0340/challenge', pointToBytes(this.R), pointToBytes(aggPublicKey), msg);
449
469
  this.tweaks = tweaks.map((t) => Uint8Array.from(t));
@@ -476,19 +496,27 @@ export class Session {
476
496
  // BIP327 PartialSigVerifyInternal: `Let s = int(psig); fail if s >= n`, so s=0 must stay
477
497
  // in the public verification equation and return false on mismatch instead of throwing.
478
498
  const { R1, R2 } = PubNonce.decode(publicNonce);
479
- const Re_s_ = R1.add(R2.multiply(b));
499
+ // Verification only handles public data (nonces, pubkeys, hash-derived scalars),
500
+ // so the faster variable-time multiplications are safe here; they also match the
501
+ // reference point_mul, which maps zero scalars to infinity instead of failing.
502
+ const Re_s_ = R1.add(R2.multiplyUnsafe(b));
480
503
  const Re_s = hasEven(R.y) ? Re_s_ : Re_s_.negate();
481
504
  const P = Point.fromBytes(publicKey);
482
505
  const a = this.getSessionKeyAggCoeff(P);
483
- const g = Fn.mul(evenScalar(Q, 1n), gAcc);
506
+ const g = Fn.mul(evenScalar(Q, _1n), gAcc);
484
507
  const left = Point.BASE.multiplyUnsafe(s);
485
- const right = Re_s.add(P.multiply(Fn.mul(e, Fn.mul(a, g))));
508
+ const right = Re_s.add(P.multiplyUnsafe(Fn.mul(e, Fn.mul(a, g))));
486
509
  return left.equals(right);
487
510
  }
488
511
  /**
489
512
  * Generates a partial signature for a given message, secret nonce,
490
513
  * secret key, and session context.
491
- * @param secretNonce - secret nonce for this signing session; it is zeroed after use
514
+ *
515
+ * SECURITY: `secretNonce` MUST be used exactly once. This method zeroes the first 64 bytes of the
516
+ * supplied array, including when later validation fails, but cannot erase copies or persisted
517
+ * representations. Reusing those nonce scalars in a distinct session can reveal the secret key.
518
+ *
519
+ * @param secretNonce - sole authoritative secret-nonce buffer for this signing session
492
520
  * @param secret - secret key of the signer
493
521
  * @param fastSign - if `true`, skip the self-verification pass
494
522
  * @returns The partial signature (Uint8Array).
@@ -510,7 +538,7 @@ export class Session {
510
538
  if (!Fn.isValid(k1_))
511
539
  throw new Error('wrong k1');
512
540
  if (!Fn.isValid(k2_))
513
- throw new Error('wrong k1');
541
+ throw new Error('wrong k2');
514
542
  const k1 = evenScalar(R, k1_);
515
543
  const k2 = evenScalar(R, k2_);
516
544
  const d_ = Fn.fromBytes(secret);
@@ -521,7 +549,7 @@ export class Session {
521
549
  if (!equalBytes(pk, originalPk))
522
550
  throw new Error('Public key does not match nonceGen argument');
523
551
  const a = this.getSessionKeyAggCoeff(P);
524
- const g = evenScalar(Q, 1n);
552
+ const g = evenScalar(Q, _1n);
525
553
  const d = Fn.mul(g, Fn.mul(gAcc, d_));
526
554
  /// k1 + (b*k2) + (e*a*d)
527
555
  const s = Fn.add(k1, Fn.add(Fn.mul(b, k2), Fn.mul(e, Fn.mul(a, d))));
@@ -570,26 +598,27 @@ export class Session {
570
598
  }
571
599
  /**
572
600
  * Aggregates partial signatures from multiple signers into a single final signature.
573
- * @param partialSigs - partial signatures from each signer
601
+ * @param partialSigs - exactly one positional partial signature per session participant
574
602
  * @returns The final aggregate signature (Uint8Array).
575
603
  * @throws If the input is invalid, such as wrong array sizes or malformed
576
604
  * signatures. {@link Error}
577
605
  */
578
606
  partialSigAgg(partialSigs) {
579
607
  abytesArray(partialSigs, 32);
580
- // BIP327 PartialSigAgg is defined for a non-empty psig_1..u list tied to this session_ctx;
581
- // [] is not a valid aggregate-signature input even though the sum starts from zero.
582
- if (partialSigs.length < 1)
583
- throw new RangeError('partialSigs.length must be >= 1');
608
+ // BIP327 PartialSigAgg consumes psig_1..u for the same u signers in session_ctx. Accepting
609
+ // fewer or more scalars would return a signature-shaped value for a different equation.
610
+ if (partialSigs.length !== this.publicKeys.length)
611
+ throw new RangeError(`partialSigs.length=${partialSigs.length} must equal ` +
612
+ `participant count=${this.publicKeys.length}`);
584
613
  const { Q, tweakAcc, R, e } = this;
585
- let s = 0n;
614
+ let s = _0n;
586
615
  for (let i = 0; i < partialSigs.length; i++) {
587
616
  const si = Fn.fromBytes(partialSigs[i], true);
588
617
  if (!Fn.isValid(si))
589
618
  throw new InvalidContributionErr(i, 'psig');
590
619
  s = Fn.add(s, si);
591
620
  }
592
- const g = evenScalar(Q, 1n);
621
+ const g = evenScalar(Q, _1n);
593
622
  s = Fn.add(s, Fn.mul(e, Fn.mul(g, tweakAcc))); // s + e * g * tweakAcc
594
623
  return concatBytes(pointToBytes(R), Fn.toBytes(s));
595
624
  }
@@ -653,4 +682,3 @@ export function deterministicSign(secret, aggOtherNonce, publicKeys, msg, tweaks
653
682
  const partialSig = session.sign(secretNonce, secret, fastSign);
654
683
  return { publicNonce, partialSig };
655
684
  }
656
- //# sourceMappingURL=musig2.js.map