@scure/btc-signer 2.0.0 → 2.2.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 +189 -39
- package/index.d.ts +15 -5
- package/index.d.ts.map +1 -1
- package/index.js +16 -6
- package/index.js.map +1 -1
- package/musig2.d.ts +202 -64
- package/musig2.d.ts.map +1 -1
- package/musig2.js +324 -87
- package/musig2.js.map +1 -1
- package/p2p.d.ts +17 -7
- package/p2p.d.ts.map +1 -1
- package/p2p.js +40 -4
- package/p2p.js.map +1 -1
- package/package.json +25 -10
- package/payment.d.ts +407 -38
- package/payment.d.ts.map +1 -1
- package/payment.js +504 -57
- package/payment.js.map +1 -1
- package/psbt.d.ts +2958 -559
- package/psbt.d.ts.map +1 -1
- package/psbt.js +462 -118
- package/psbt.js.map +1 -1
- package/script.d.ts +311 -132
- package/script.d.ts.map +1 -1
- package/script.js +246 -35
- package/script.js.map +1 -1
- package/src/index.ts +34 -11
- package/src/musig2.ts +387 -145
- package/src/p2p.ts +54 -18
- package/src/payment.ts +823 -226
- package/src/psbt.ts +633 -228
- package/src/script.ts +353 -117
- package/src/transaction.ts +593 -169
- package/src/utils.ts +322 -43
- package/src/utxo.ts +154 -51
- package/transaction.d.ts +242 -31
- package/transaction.d.ts.map +1 -1
- package/transaction.js +460 -100
- package/transaction.js.map +1 -1
- package/utils.d.ts +266 -24
- package/utils.d.ts.map +1 -1
- package/utils.js +278 -27
- package/utils.js.map +1 -1
- package/utxo.d.ts +438 -75
- package/utxo.d.ts.map +1 -1
- package/utxo.js +123 -36
- package/utxo.js.map +1 -1
package/src/musig2.ts
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.ts';
|
|
5
|
+
import { compareBytes, hasEven, type TArg, type TRet } from './utils.ts';
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
MuSig2. This is not the full protocol: only an implementation of primitives from BIP-327.
|
|
@@ -16,19 +16,37 @@ Links:
|
|
|
16
16
|
- https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py
|
|
17
17
|
*/
|
|
18
18
|
// Types
|
|
19
|
+
/** Represents a pair of public and secret nonces used in MuSig2 signing. */
|
|
20
|
+
export type Nonces = {
|
|
21
|
+
/** Public nonce that gets shared with the other participants. */
|
|
22
|
+
public: Uint8Array;
|
|
23
|
+
/** Secret nonce that stays local until partial signing finishes. */
|
|
24
|
+
secret: Uint8Array;
|
|
25
|
+
};
|
|
19
26
|
/**
|
|
20
|
-
* Represents a
|
|
27
|
+
* Represents a deterministic nonce, including its public part and the
|
|
28
|
+
* resulting partial signature.
|
|
21
29
|
*/
|
|
22
|
-
export type
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
|
|
30
|
+
export type DetNonce = {
|
|
31
|
+
/** Public nonce that the signer shares for this deterministic signing round. */
|
|
32
|
+
publicNonce: Uint8Array;
|
|
33
|
+
/** Partial signature produced after combining all participant data. */
|
|
34
|
+
partialSig: Uint8Array;
|
|
35
|
+
};
|
|
27
36
|
/**
|
|
28
37
|
* Represents an error indicating an invalid contribution from a signer.
|
|
29
38
|
* This allows pointing out which participant is malicious and what specifically is wrong.
|
|
39
|
+
* @param idx - signer index with the invalid contribution
|
|
40
|
+
* @param m - error message
|
|
41
|
+
* @example
|
|
42
|
+
* Create an error that points to the participant who sent invalid data.
|
|
43
|
+
* ```ts
|
|
44
|
+
* new InvalidContributionErr(0, 'pubkey');
|
|
45
|
+
* ```
|
|
30
46
|
*/
|
|
31
47
|
export class InvalidContributionErr extends Error {
|
|
48
|
+
// BIP327 identifiable aborts blame exactly one signer by participant index in the
|
|
49
|
+
// caller's session ordering, so callers interpret idx using the same ordering they signed with.
|
|
32
50
|
readonly idx: number; // Indice of participant
|
|
33
51
|
constructor(idx: number, m: string) {
|
|
34
52
|
super(m);
|
|
@@ -37,88 +55,165 @@ export class InvalidContributionErr extends Error {
|
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
// Utils
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
// MuSig2 reuses BIP340 tagged hashing, i.e. SHA256(SHA256(tag) || SHA256(tag) || msg...),
|
|
59
|
+
// for all of the domain-separated hashes below (KeyAgg list, noncecoef, challenge, aux, ...).
|
|
60
|
+
const taggedHash = /* @__PURE__ */ (() => schnorr.utils.taggedHash)();
|
|
61
|
+
// BIP327 uses xbytes(P) = bytes(32, x(P)) for aggregate keys, nonce/challenge hashes,
|
|
62
|
+
// and final signatures, so this alias is intentionally x-only instead of 33-byte SEC1.
|
|
63
|
+
const pointToBytes = /* @__PURE__ */ (() => schnorr.utils.pointToBytes)();
|
|
64
|
+
// MuSig2 keeps aggregate keys/nonces as full secp256k1 points so it can represent infinity
|
|
65
|
+
// and inspect parity before exporting compressed or x-only encodings at the API boundaries.
|
|
66
|
+
const Point = /* @__PURE__ */ (() => secp256k1.Point)();
|
|
42
67
|
type Point = typeof Point.BASE;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
// MuSig2 scalars live in Z_n with fixed 32-byte encodings: strict inputs like secret keys
|
|
69
|
+
// use Fn.fromBytes(...), tweak bytes allow 0 per ApplyTweak, and hash-derived values are
|
|
70
|
+
// intentionally reduced mod n.
|
|
71
|
+
const Fn = /* @__PURE__ */ (() => Point.Fn)();
|
|
72
|
+
// BIP327 signer keys are plain compressed pubkeys (33 bytes); only aggregate exports switch
|
|
73
|
+
// to BIP340's 32-byte x-only format, so the internal input length stays on secp256k1.publicKey.
|
|
74
|
+
const PUBKEY_LEN = /* @__PURE__ */ (() => secp256k1.lengths.publicKey!)();
|
|
75
|
+
// BIP327 uses bytes(33, 0) both as cbytes_ext(Point.ZERO) for infinity and as GetSecondKey's
|
|
76
|
+
// "no second distinct key" sentinel, so this all-zero compressed slot is intentionally out-of-band.
|
|
77
|
+
const ZERO = /* @__PURE__ */ new Uint8Array(PUBKEY_LEN); // Compressed zero point
|
|
46
78
|
|
|
47
79
|
// Encoding
|
|
48
80
|
// TODO: re-use in PSBT?
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
81
|
+
// This is BIP327's cbytes_ext/cpoint_ext adapter: normal points stay in compressed SEC1,
|
|
82
|
+
// while Point.ZERO maps to bytes(33, 0) as the out-of-band infinity sentinel for aggnonce.
|
|
83
|
+
const compressed = /* @__PURE__ */ (() =>
|
|
84
|
+
P.apply(P.bytes(33), {
|
|
85
|
+
decode: (p: Point) => (isZero(p) ? ZERO : p.toBytes(true)),
|
|
86
|
+
encode: (b: TArg<Uint8Array>) => (equalBytes(b, ZERO) ? Point.ZERO : Point.fromBytes(b)),
|
|
87
|
+
}))();
|
|
88
|
+
// This coder is only for stored secnonce limbs k1/k2, which BIP327 requires to be
|
|
89
|
+
// nonzero scalars in [1, n); tweak scalars use different validation because 0 is allowed there.
|
|
90
|
+
const scalar = /* @__PURE__ */ (() =>
|
|
91
|
+
P.validate(P.U256BE, (n) => {
|
|
92
|
+
aInRange('n', n, 1n, Fn.ORDER);
|
|
93
|
+
return n;
|
|
94
|
+
}))();
|
|
95
|
+
// Shared for both per-signer pubnonce bytes and aggregate aggnonce bytes. Because it accepts
|
|
96
|
+
// the BIP327 infinity sentinel, so individual pubnonce callers still need
|
|
97
|
+
// an explicit zero-point check.
|
|
98
|
+
const PubNonce = /* @__PURE__ */ (() => P.struct({ R1: compressed, R2: compressed }))();
|
|
99
|
+
// BIP327 stores secnonce as k1 || k2 || pk so Sign can reject reused or
|
|
100
|
+
// invalid nonce limbs and detect when the caller pairs the nonce with a
|
|
101
|
+
// different individual public key than NonceGen used.
|
|
102
|
+
const SecretNonce = /* @__PURE__ */ (() =>
|
|
103
|
+
P.struct({
|
|
104
|
+
k1: scalar,
|
|
105
|
+
k2: scalar,
|
|
106
|
+
publicKey: P.bytes(PUBKEY_LEN),
|
|
107
|
+
}))();
|
|
59
108
|
|
|
60
|
-
function abytesOptional(b: Uint8Array | undefined
|
|
109
|
+
function abytesOptional(b: TArg<Uint8Array | undefined>, ...lengths: number[]) {
|
|
110
|
+
// Optional-byte helper: exact-length checks only happen when callers pass them explicitly.
|
|
61
111
|
if (b !== undefined) abytes(b, ...lengths);
|
|
62
112
|
}
|
|
63
113
|
|
|
64
|
-
function abytesArray(lst: Uint8Array[]
|
|
65
|
-
|
|
114
|
+
function abytesArray(lst: TArg<Uint8Array[]>, ...lengths: number[]) {
|
|
115
|
+
// Element-shape helper only: callers still enforce list-size rules like BIP327's 0 < u < 2^32.
|
|
116
|
+
if (!Array.isArray(lst)) throw new TypeError('expected array');
|
|
66
117
|
lst.forEach((i) => abytes(i, ...lengths));
|
|
67
118
|
}
|
|
68
119
|
|
|
69
120
|
function aXonly(lst: boolean[]) {
|
|
70
|
-
|
|
121
|
+
// BIP327 tweak modes are strict booleans; callers should run this before
|
|
122
|
+
// branching on isXonly values because plain JS truthiness would accept invalid inputs.
|
|
123
|
+
if (!Array.isArray(lst)) throw new TypeError('expected array');
|
|
71
124
|
lst.forEach((i, j) => {
|
|
72
125
|
if (typeof i !== 'boolean')
|
|
73
|
-
throw new
|
|
126
|
+
throw new TypeError('expected boolean in xOnly array, got' + i + '(' + j + ')');
|
|
74
127
|
});
|
|
75
128
|
}
|
|
76
129
|
|
|
77
|
-
|
|
130
|
+
// BIP327/BIP340 treat tagged-hash outputs as big-endian integers reduced mod n for
|
|
131
|
+
// coefficients, nonce scalars, and challenges; this helper is that int(hash_tag(...)) mod n step.
|
|
132
|
+
const taggedInt = (tag: string, ...messages: TArg<Uint8Array[]>) =>
|
|
78
133
|
Fn.create(Fn.fromBytes(taggedHash(tag, ...messages), true));
|
|
134
|
+
// BIP327 repeatedly says "use x if the point has even Y, otherwise use n - x"; this
|
|
135
|
+
// helper is that parity-conditioned scalar negation for nonce limbs and Q-dependent signs.
|
|
79
136
|
const evenScalar = (p: Point, n: bigint) => (hasEven(p.y) ? n : Fn.neg(n));
|
|
80
137
|
|
|
81
138
|
// Short utility for compat with reference implementation
|
|
82
|
-
|
|
83
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Derives a compressed secp256k1 public key from a private key.
|
|
141
|
+
* @param seckey - signer private key
|
|
142
|
+
* @returns Compressed public key bytes.
|
|
143
|
+
* @example
|
|
144
|
+
* Turn a signer's secret key into the compressed key format MuSig2 expects.
|
|
145
|
+
* ```ts
|
|
146
|
+
* import { schnorr } from '@noble/curves/secp256k1.js';
|
|
147
|
+
* import { IndividualPubkey } from '@scure/btc-signer/musig2.js';
|
|
148
|
+
* IndividualPubkey(schnorr.utils.randomSecretKey());
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
// BIP327 IndividualPubkey is cbytes(d*G): the 33-byte compressed signer key.
|
|
152
|
+
// Aggregate exports use separate x-only encoding and should not call this helper.
|
|
153
|
+
export function IndividualPubkey(seckey: TArg<Uint8Array>): TRet<Uint8Array> {
|
|
154
|
+
return secp256k1.getPublicKey(seckey, true) as TRet<Uint8Array>;
|
|
84
155
|
}
|
|
85
156
|
// Same, but returns Point
|
|
157
|
+
// Base-point multiply helper for the BIP327 x*G steps below. Point.BASE.multiply rejects 0,
|
|
158
|
+
// so call sites that allow zero scalars need to handle that case explicitly instead of using this.
|
|
86
159
|
function mulBase(n: bigint): Point {
|
|
87
160
|
return Point.BASE.multiply(n);
|
|
88
161
|
}
|
|
162
|
+
// Local alias for BIP327's is_infinite(P): used both for rejecting infinity where cpoint(...)
|
|
163
|
+
// must never yield it and for the "if R' is infinite, use G" nonce-aggregation fallback.
|
|
89
164
|
function isZero(point: Point): boolean {
|
|
90
165
|
return point.equals(Point.ZERO);
|
|
91
166
|
}
|
|
92
167
|
|
|
93
168
|
/**
|
|
94
169
|
* Lexicographically sorts an array of public keys.
|
|
95
|
-
* @param publicKeys
|
|
170
|
+
* @param publicKeys - array of public keys
|
|
96
171
|
* @returns A new array containing the sorted public keys.
|
|
97
|
-
* @throws
|
|
172
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
173
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
174
|
+
* @example
|
|
175
|
+
* Sort participant public keys before building the aggregate MuSig2 key.
|
|
176
|
+
* ```ts
|
|
177
|
+
* import { IndividualPubkey, sortKeys } from '@scure/btc-signer/musig2.js';
|
|
178
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
179
|
+
* sortKeys([
|
|
180
|
+
* IndividualPubkey(randomPrivateKeyBytes()),
|
|
181
|
+
* IndividualPubkey(randomPrivateKeyBytes()),
|
|
182
|
+
* ]);
|
|
183
|
+
* ```
|
|
98
184
|
*/
|
|
99
|
-
export function sortKeys(publicKeys: Uint8Array[]): Uint8Array[] {
|
|
185
|
+
export function sortKeys(publicKeys: TArg<Uint8Array[]>): TRet<Uint8Array[]> {
|
|
186
|
+
// BIP327 KeySort is defined for non-empty signer-key lists, and this helper returns a sorted copy
|
|
187
|
+
// so callers do not lose their original participant ordering as a side effect of key aggregation.
|
|
100
188
|
abytesArray(publicKeys, PUBKEY_LEN);
|
|
101
|
-
|
|
189
|
+
if (!publicKeys.length) throw new RangeError('sortKeys: expected non-empty signer key list');
|
|
190
|
+
return Array.from(publicKeys).sort(compareBytes) as TRet<Uint8Array[]>;
|
|
102
191
|
}
|
|
103
192
|
|
|
104
193
|
// Finds second distinct key (to make coefficient 1)
|
|
105
|
-
function getSecondKey(publicKeys: Uint8Array[]): Uint8Array {
|
|
194
|
+
function getSecondKey(publicKeys: TArg<Uint8Array[]>): TRet<Uint8Array> {
|
|
195
|
+
// BIP327 GetSecondKey returns bytes(33, 0) when all signer keys are equal; that sentinel
|
|
196
|
+
// means no key hits the special pk' = pk2 coefficient-1 shortcut in the all-equal case.
|
|
106
197
|
abytesArray(publicKeys, PUBKEY_LEN);
|
|
107
198
|
for (let j = 1; j < publicKeys.length; j++)
|
|
108
|
-
if (!equalBytes(publicKeys[j], publicKeys[0])) return publicKeys[j]
|
|
109
|
-
return ZERO
|
|
199
|
+
if (!equalBytes(publicKeys[j], publicKeys[0])) return publicKeys[j] as TRet<Uint8Array>;
|
|
200
|
+
return ZERO as TRet<Uint8Array>;
|
|
110
201
|
}
|
|
111
202
|
|
|
112
|
-
function keyAggL(publicKeys: Uint8Array[]) {
|
|
203
|
+
function keyAggL(publicKeys: TArg<Uint8Array[]>): TRet<Uint8Array> {
|
|
204
|
+
// BIP327 HashKeys hashes keys in the caller-provided order. Run KeySort first when
|
|
205
|
+
// the surrounding protocol requires the canonical lexicographic participant ordering.
|
|
113
206
|
abytesArray(publicKeys, PUBKEY_LEN);
|
|
114
|
-
return taggedHash('KeyAgg list', ...publicKeys)
|
|
207
|
+
return taggedHash('KeyAgg list', ...publicKeys) as TRet<Uint8Array>;
|
|
115
208
|
}
|
|
116
209
|
|
|
117
210
|
function keyAggCoeffInternal(
|
|
118
|
-
publicKey1: Uint8Array
|
|
119
|
-
publicKey2: Uint8Array
|
|
120
|
-
L: Uint8Array
|
|
211
|
+
publicKey1: TArg<Uint8Array>,
|
|
212
|
+
publicKey2: TArg<Uint8Array>,
|
|
213
|
+
L: TArg<Uint8Array>
|
|
121
214
|
): bigint {
|
|
215
|
+
// BIP327 only short-circuits to coefficient 1 for pk' = pk2. When all keys are equal,
|
|
216
|
+
// pk2 is the all-zero sentinel from GetSecondKey, so every real signer key still hashes.
|
|
122
217
|
abytes(publicKey1, PUBKEY_LEN);
|
|
123
218
|
abytes(publicKey2, PUBKEY_LEN);
|
|
124
219
|
if (equalBytes(publicKey1, publicKey2)) return 1n;
|
|
@@ -127,22 +222,41 @@ function keyAggCoeffInternal(
|
|
|
127
222
|
|
|
128
223
|
/**
|
|
129
224
|
* Aggregates multiple public keys using the MuSig2 key aggregation algorithm.
|
|
130
|
-
* @param publicKeys
|
|
131
|
-
* @param tweaks
|
|
132
|
-
* @param isXonly
|
|
133
|
-
* @returns An object containing the aggregate public key, accumulated sign,
|
|
134
|
-
*
|
|
135
|
-
* @throws
|
|
225
|
+
* @param publicKeys - individual participant public keys
|
|
226
|
+
* @param tweaks - optional tweaks applied to the aggregate key
|
|
227
|
+
* @param isXonly - whether each tweak uses x-only semantics
|
|
228
|
+
* @returns An object containing the aggregate public key, accumulated sign,
|
|
229
|
+
* and accumulated tweak.
|
|
230
|
+
* @throws If the input is invalid, such as non-array public keys or mismatched
|
|
231
|
+
* tweak metadata. {@link Error}
|
|
232
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
233
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
234
|
+
* @throws If any of the public keys are invalid and cannot be processed.
|
|
235
|
+
* {@link InvalidContributionErr}
|
|
236
|
+
* @example
|
|
237
|
+
* Combine all participant public keys into the shared MuSig2 context.
|
|
238
|
+
* ```ts
|
|
239
|
+
* import { IndividualPubkey, keyAggregate } from '@scure/btc-signer/musig2.js';
|
|
240
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
241
|
+
* keyAggregate([
|
|
242
|
+
* IndividualPubkey(randomPrivateKeyBytes()),
|
|
243
|
+
* IndividualPubkey(randomPrivateKeyBytes()),
|
|
244
|
+
* ]);
|
|
245
|
+
* ```
|
|
136
246
|
*/
|
|
137
247
|
export function keyAggregate(
|
|
138
|
-
publicKeys: Uint8Array[]
|
|
139
|
-
tweaks: Uint8Array[] = [],
|
|
248
|
+
publicKeys: TArg<Uint8Array[]>,
|
|
249
|
+
tweaks: TArg<Uint8Array[]> = [],
|
|
140
250
|
isXonly: boolean[] = []
|
|
141
251
|
) {
|
|
252
|
+
// BIP327 KeyAgg inputs require `0 < u < 2^32`, and ApplyTweak consumes a one-for-one
|
|
253
|
+
// list of boolean tweak modes; callers should enforce that public contract here.
|
|
142
254
|
abytesArray(publicKeys, PUBKEY_LEN);
|
|
255
|
+
if (publicKeys.length < 1) throw new RangeError('keyAggregate: expected at least 1 public key');
|
|
143
256
|
abytesArray(tweaks, 32);
|
|
257
|
+
aXonly(isXonly);
|
|
144
258
|
if (tweaks.length !== isXonly.length)
|
|
145
|
-
throw new
|
|
259
|
+
throw new RangeError('The tweaks and isXonly arrays must have the same length');
|
|
146
260
|
// Aggregate
|
|
147
261
|
const pk2 = getSecondKey(publicKeys);
|
|
148
262
|
const L = keyAggL(publicKeys);
|
|
@@ -161,8 +275,10 @@ export function keyAggregate(
|
|
|
161
275
|
// Apply tweaks
|
|
162
276
|
for (let i = 0; i < tweaks.length; i++) {
|
|
163
277
|
const g = isXonly[i] && !hasEven(aggPublicKey.y) ? Fn.neg(Fn.ONE) : Fn.ONE;
|
|
164
|
-
|
|
165
|
-
|
|
278
|
+
// BIP327 ApplyTweak: `Let t = int(tweak); fail if t >= n`, so 32-byte zero tweaks are valid.
|
|
279
|
+
const t = Fn.fromBytes(tweaks[i], true);
|
|
280
|
+
if (!Fn.isValid(t)) throw new RangeError('invalid scalar: out of range');
|
|
281
|
+
aggPublicKey = aggPublicKey.multiply(g).add(Fn.is0(t) ? Point.ZERO : mulBase(t));
|
|
166
282
|
if (isZero(aggPublicKey)) throw new Error('The result of tweaking cannot be infinity');
|
|
167
283
|
gAcc = Fn.mul(g, gAcc);
|
|
168
284
|
tweakAcc = Fn.add(t, Fn.mul(g, tweakAcc));
|
|
@@ -171,29 +287,46 @@ export function keyAggregate(
|
|
|
171
287
|
}
|
|
172
288
|
/**
|
|
173
289
|
* Exports the aggregate public key to a byte array.
|
|
174
|
-
* @param ctx
|
|
290
|
+
* @param ctx - result of {@link keyAggregate}
|
|
175
291
|
* @returns The aggregate public key as a byte array.
|
|
292
|
+
* @example
|
|
293
|
+
* Serialize the aggregate key after building the MuSig2 context.
|
|
294
|
+
* ```ts
|
|
295
|
+
* import { IndividualPubkey, keyAggregate, keyAggExport } from '@scure/btc-signer/musig2.js';
|
|
296
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
297
|
+
* const ctx = keyAggregate([
|
|
298
|
+
* IndividualPubkey(randomPrivateKeyBytes()),
|
|
299
|
+
* IndividualPubkey(randomPrivateKeyBytes()),
|
|
300
|
+
* ]);
|
|
301
|
+
* keyAggExport(ctx);
|
|
302
|
+
* ```
|
|
176
303
|
*/
|
|
177
|
-
export function keyAggExport(ctx: ReturnType<typeof keyAggregate>): Uint8Array {
|
|
178
|
-
|
|
304
|
+
export function keyAggExport(ctx: ReturnType<typeof keyAggregate>): TRet<Uint8Array> {
|
|
305
|
+
// BIP327 GetXonlyPubkey returns xbytes(Q), so this is the 32-byte x-only aggregate key
|
|
306
|
+
// instead of the 33-byte compressed SEC1 form.
|
|
307
|
+
return pointToBytes(ctx.aggPublicKey) as TRet<Uint8Array>;
|
|
179
308
|
}
|
|
180
309
|
|
|
181
|
-
function aux(secret: Uint8Array
|
|
310
|
+
function aux(secret: TArg<Uint8Array>, rand: TArg<Uint8Array>): TRet<Uint8Array> {
|
|
311
|
+
// BIP327 NonceGen and DeterministicSign blind the caller-provided 32-byte randomness with
|
|
312
|
+
// hash_MuSig/aux(rand) before hashing the session inputs into nonce scalars.
|
|
182
313
|
const rand2 = taggedHash('MuSig/aux', rand);
|
|
183
314
|
if (secret.length !== rand2.length) throw new Error('Cannot XOR arrays of different lengths');
|
|
184
315
|
const res = new Uint8Array(secret.length);
|
|
185
316
|
for (let i = 0; i < secret.length; i++) res[i] = secret[i] ^ rand2[i];
|
|
186
|
-
return res
|
|
317
|
+
return res as TRet<Uint8Array>;
|
|
187
318
|
}
|
|
188
319
|
|
|
189
320
|
const nonceHash = (
|
|
190
|
-
rand: Uint8Array
|
|
191
|
-
publicKey: Uint8Array
|
|
192
|
-
aggPublicKey: Uint8Array
|
|
321
|
+
rand: TArg<Uint8Array>,
|
|
322
|
+
publicKey: TArg<Uint8Array>,
|
|
323
|
+
aggPublicKey: TArg<Uint8Array>,
|
|
193
324
|
i: number,
|
|
194
|
-
msgPrefixed: Uint8Array
|
|
195
|
-
extraIn: Uint8Array
|
|
325
|
+
msgPrefixed: TArg<Uint8Array>,
|
|
326
|
+
extraIn: TArg<Uint8Array>
|
|
196
327
|
): bigint =>
|
|
328
|
+
// BIP327 NonceGen hashes rand || len(pk) || pk || len(aggpk) || aggpk || m_prefixed ||
|
|
329
|
+
// len(extra_in) || extra_in || bytes(1, i - 1), so callers pass i = 0/1 here.
|
|
197
330
|
taggedInt(
|
|
198
331
|
'MuSig/nonce',
|
|
199
332
|
rand,
|
|
@@ -209,32 +342,44 @@ const nonceHash = (
|
|
|
209
342
|
|
|
210
343
|
/**
|
|
211
344
|
* Generates a nonce pair (public and secret) for MuSig2 signing.
|
|
212
|
-
* @param publicKey
|
|
213
|
-
* @param secretKey
|
|
214
|
-
* @param aggPublicKey
|
|
215
|
-
* @param msg
|
|
216
|
-
* @param extraIn
|
|
217
|
-
* @param rand
|
|
345
|
+
* @param publicKey - individual public key of the signer
|
|
346
|
+
* @param secretKey - optional secret key, mixed in to blind the randomness source
|
|
347
|
+
* @param aggPublicKey - aggregate public key of all signers
|
|
348
|
+
* @param msg - message to be signed
|
|
349
|
+
* @param extraIn - extra input mixed into nonce generation
|
|
350
|
+
* @param rand - random 32-byte seed for the nonce derivation
|
|
218
351
|
* @returns An object containing the public and secret nonces.
|
|
219
|
-
* @throws
|
|
352
|
+
* @throws If the input is invalid, such as non-array public keys or malformed
|
|
353
|
+
* nonce inputs. {@link Error}
|
|
354
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
355
|
+
* @example
|
|
356
|
+
* Generate the signer-local nonce pair before sharing the public nonce with peers.
|
|
357
|
+
* ```ts
|
|
358
|
+
* import { IndividualPubkey, nonceGen } from '@scure/btc-signer/musig2.js';
|
|
359
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
360
|
+
* const secretKey = randomPrivateKeyBytes();
|
|
361
|
+
* nonceGen(IndividualPubkey(secretKey), secretKey);
|
|
362
|
+
* ```
|
|
220
363
|
*/
|
|
221
364
|
export function nonceGen(
|
|
222
|
-
publicKey: Uint8Array
|
|
223
|
-
secretKey?: Uint8Array
|
|
224
|
-
aggPublicKey: Uint8Array = new Uint8Array(0),
|
|
225
|
-
msg?: Uint8Array
|
|
226
|
-
extraIn: Uint8Array = new Uint8Array(0),
|
|
227
|
-
rand: Uint8Array = randomBytes(32)
|
|
228
|
-
): Nonces {
|
|
365
|
+
publicKey: TArg<Uint8Array>,
|
|
366
|
+
secretKey?: TArg<Uint8Array>,
|
|
367
|
+
aggPublicKey: TArg<Uint8Array> = new Uint8Array(0),
|
|
368
|
+
msg?: TArg<Uint8Array>,
|
|
369
|
+
extraIn: TArg<Uint8Array> = new Uint8Array(0),
|
|
370
|
+
rand: TArg<Uint8Array> = randomBytes(32)
|
|
371
|
+
): TRet<Nonces> {
|
|
229
372
|
abytes(publicKey, PUBKEY_LEN);
|
|
230
373
|
abytesOptional(secretKey, 32);
|
|
231
374
|
abytes(aggPublicKey);
|
|
232
|
-
if (![0, 32].includes(aggPublicKey.length)) throw new
|
|
375
|
+
if (![0, 32].includes(aggPublicKey.length)) throw new RangeError('wrong aggPublicKey');
|
|
233
376
|
abytesOptional(msg);
|
|
234
377
|
abytes(extraIn);
|
|
235
378
|
abytes(rand, 32);
|
|
236
379
|
|
|
237
380
|
if (secretKey !== undefined) rand = aux(secretKey, rand);
|
|
381
|
+
// BIP327 distinguishes an omitted message from an explicitly empty one so the two cases
|
|
382
|
+
// derive different nonces even when every other session parameter matches.
|
|
238
383
|
const msgPrefixed =
|
|
239
384
|
msg !== undefined
|
|
240
385
|
? concatBytes(Uint8Array.of(1), numberToBytesBE(msg.length, 8), msg)
|
|
@@ -244,18 +389,38 @@ export function nonceGen(
|
|
|
244
389
|
return {
|
|
245
390
|
secret: SecretNonce.encode({ k1, k2, publicKey }),
|
|
246
391
|
public: PubNonce.encode({ R1: mulBase(k1), R2: mulBase(k2) }),
|
|
247
|
-
}
|
|
392
|
+
} as TRet<Nonces>;
|
|
248
393
|
}
|
|
249
394
|
|
|
250
395
|
/**
|
|
251
396
|
* Aggregates public nonces from multiple signers into a single aggregate nonce.
|
|
252
|
-
* @param pubNonces
|
|
397
|
+
* @param pubNonces - public nonces from each signer
|
|
253
398
|
* @returns The aggregate nonce (Uint8Array).
|
|
254
|
-
* @throws
|
|
255
|
-
* @throws
|
|
399
|
+
* @throws If the nonce payloads are malformed or contain infinity points. {@link Error}
|
|
400
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
401
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
402
|
+
* @throws If any of the public nonces are invalid and cannot be processed.
|
|
403
|
+
* {@link InvalidContributionErr}
|
|
404
|
+
* @example
|
|
405
|
+
* Combine all participant public nonces before building the session.
|
|
406
|
+
* ```ts
|
|
407
|
+
* import { IndividualPubkey, nonceAggregate, nonceGen } from '@scure/btc-signer/musig2.js';
|
|
408
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
409
|
+
* const alice = randomPrivateKeyBytes();
|
|
410
|
+
* const bob = randomPrivateKeyBytes();
|
|
411
|
+
* nonceAggregate([
|
|
412
|
+
* nonceGen(IndividualPubkey(alice), alice).public,
|
|
413
|
+
* nonceGen(IndividualPubkey(bob), bob).public,
|
|
414
|
+
* ]);
|
|
415
|
+
* ```
|
|
256
416
|
*/
|
|
257
|
-
export function nonceAggregate(pubNonces: Uint8Array[]): Uint8Array {
|
|
417
|
+
export function nonceAggregate(pubNonces: TArg<Uint8Array[]>): TRet<Uint8Array> {
|
|
258
418
|
abytesArray(pubNonces, 66);
|
|
419
|
+
// BIP327 NonceAgg input: "The number u of pubnonces with 0 < u < 2^32".
|
|
420
|
+
// cbytes_ext uses bytes(33, 0) as the infinity sentinel for summed real contributions,
|
|
421
|
+
// not as an "empty aggregate" encoding.
|
|
422
|
+
if (pubNonces.length < 1)
|
|
423
|
+
throw new RangeError('nonceAggregate: expected at least 1 public nonce');
|
|
259
424
|
let R1 = Point.ZERO;
|
|
260
425
|
let R2 = Point.ZERO;
|
|
261
426
|
for (let i = 0; i < pubNonces.length; i++) {
|
|
@@ -269,12 +434,45 @@ export function nonceAggregate(pubNonces: Uint8Array[]): Uint8Array {
|
|
|
269
434
|
throw new InvalidContributionErr(i, 'pubnonce');
|
|
270
435
|
}
|
|
271
436
|
}
|
|
272
|
-
return PubNonce.encode({ R1, R2 })
|
|
437
|
+
return PubNonce.encode({ R1, R2 }) as TRet<Uint8Array>;
|
|
273
438
|
}
|
|
274
439
|
|
|
275
440
|
// Class allows us re-use pre-computed stuff
|
|
276
|
-
// NOTE: it would be nice to aggregate nonce in
|
|
441
|
+
// NOTE: it would be nice to aggregate nonce in constructor, but there is a
|
|
442
|
+
// test that passes an already aggregated nonce here.
|
|
443
|
+
/**
|
|
444
|
+
* MuSig2 session context for partial signing and aggregation.
|
|
445
|
+
* @param aggNonce - aggregate nonce from all participants combined
|
|
446
|
+
* @param publicKeys - all participant public keys
|
|
447
|
+
* @param msg - message to be signed
|
|
448
|
+
* @param tweaks - optional tweaks applied to the aggregate public key
|
|
449
|
+
* @param isXonly - whether each tweak uses x-only semantics
|
|
450
|
+
* @example
|
|
451
|
+
* Build one session object and reuse it for all partial-signature steps.
|
|
452
|
+
* ```ts
|
|
453
|
+
* import {
|
|
454
|
+
* IndividualPubkey,
|
|
455
|
+
* Session,
|
|
456
|
+
* keyAggregate,
|
|
457
|
+
* keyAggExport,
|
|
458
|
+
* nonceAggregate,
|
|
459
|
+
* nonceGen,
|
|
460
|
+
* } from '@scure/btc-signer/musig2.js';
|
|
461
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
462
|
+
* const alice = randomPrivateKeyBytes();
|
|
463
|
+
* const bob = randomPrivateKeyBytes();
|
|
464
|
+
* const publicKeys = [IndividualPubkey(alice), IndividualPubkey(bob)];
|
|
465
|
+
* const agg = keyAggregate(publicKeys);
|
|
466
|
+
* const aggNonce = nonceAggregate([
|
|
467
|
+
* nonceGen(publicKeys[0], alice, keyAggExport(agg)).public,
|
|
468
|
+
* nonceGen(publicKeys[1], bob, keyAggExport(agg)).public,
|
|
469
|
+
* ]);
|
|
470
|
+
* const msg = new TextEncoder().encode('hello musig2');
|
|
471
|
+
* new Session(aggNonce, publicKeys, msg);
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
277
474
|
export class Session {
|
|
475
|
+
private aggNonce: Uint8Array;
|
|
278
476
|
private publicKeys: Uint8Array[];
|
|
279
477
|
private Q: Point;
|
|
280
478
|
private gAcc: bigint;
|
|
@@ -290,12 +488,12 @@ export class Session {
|
|
|
290
488
|
* Constructor for the Session class.
|
|
291
489
|
* It precomputes and stores values derived from the aggregate nonce, public keys,
|
|
292
490
|
* message, and optional tweaks, optimizing the signing process.
|
|
293
|
-
* @param aggNonce
|
|
294
|
-
* @param publicKeys
|
|
295
|
-
* @param msg
|
|
296
|
-
* @param tweaks
|
|
297
|
-
* @param isXonly
|
|
298
|
-
* @throws
|
|
491
|
+
* @param aggNonce - aggregate nonce from all participants combined
|
|
492
|
+
* @param publicKeys - participant public keys
|
|
493
|
+
* @param msg - message to be signed
|
|
494
|
+
* @param tweaks - tweaks applied to the aggregate public key
|
|
495
|
+
* @param isXonly - whether each tweak uses x-only semantics
|
|
496
|
+
* @throws If the input is invalid, such as wrong array sizes or lengths. {@link Error}
|
|
299
497
|
*/
|
|
300
498
|
constructor(
|
|
301
499
|
aggNonce: Uint8Array,
|
|
@@ -309,10 +507,15 @@ export class Session {
|
|
|
309
507
|
aXonly(isXonly);
|
|
310
508
|
abytes(msg);
|
|
311
509
|
if (tweaks.length !== isXonly.length)
|
|
312
|
-
throw new
|
|
510
|
+
throw new RangeError('The tweaks and isXonly arrays must have the same length');
|
|
313
511
|
const { aggPublicKey, gAcc, tweakAcc } = keyAggregate(publicKeys, tweaks, isXonly);
|
|
314
512
|
const { R1, R2 } = PubNonce.decode(aggNonce);
|
|
315
|
-
|
|
513
|
+
// BIP327 requires cached session-context state to be protected from third-party mutation,
|
|
514
|
+
// so Session must own detached copies of the caller-provided arrays and byte entries.
|
|
515
|
+
// Use Uint8Array.from(...) here instead of .slice() because Node Buffer overrides slice()
|
|
516
|
+
// to return a shared-memory view, which would let later caller mutation rewrite session state.
|
|
517
|
+
this.aggNonce = Uint8Array.from(aggNonce);
|
|
518
|
+
this.publicKeys = publicKeys.map((pk) => Uint8Array.from(pk));
|
|
316
519
|
this.Q = aggPublicKey;
|
|
317
520
|
this.gAcc = gAcc;
|
|
318
521
|
this.tweakAcc = tweakAcc;
|
|
@@ -320,21 +523,23 @@ export class Session {
|
|
|
320
523
|
const R = R1.add(R2.multiply(this.b));
|
|
321
524
|
this.R = isZero(R) ? Point.BASE : R;
|
|
322
525
|
this.e = taggedInt('BIP0340/challenge', pointToBytes(this.R), pointToBytes(aggPublicKey), msg);
|
|
323
|
-
this.tweaks = tweaks;
|
|
324
|
-
this.isXonly = isXonly;
|
|
325
|
-
this.L = keyAggL(publicKeys);
|
|
326
|
-
this.secondKey = getSecondKey(publicKeys);
|
|
526
|
+
this.tweaks = tweaks.map((t) => Uint8Array.from(t));
|
|
527
|
+
this.isXonly = isXonly.slice();
|
|
528
|
+
this.L = keyAggL(this.publicKeys);
|
|
529
|
+
this.secondKey = getSecondKey(this.publicKeys);
|
|
327
530
|
}
|
|
328
531
|
/**
|
|
329
532
|
* Calculates the key aggregation coefficient for a given point.
|
|
330
533
|
* @private
|
|
331
|
-
* @param P
|
|
534
|
+
* @param P - point to calculate the coefficient for
|
|
332
535
|
* @returns The key aggregation coefficient as a bigint.
|
|
333
|
-
* @throws
|
|
536
|
+
* @throws If the provided public key is not included in the list of pubkeys. {@link Error}
|
|
334
537
|
*/
|
|
335
538
|
private getSessionKeyAggCoeff(P: Point): bigint {
|
|
336
539
|
const { publicKeys } = this;
|
|
337
540
|
const pk = P.toBytes(true);
|
|
541
|
+
// BIP327 GetSessionKeyAggCoeff fails if cbytes(P) is not one of the session pubkeys;
|
|
542
|
+
// once membership is confirmed, the cached L/secondKey state is enough to derive KeyAggCoeff.
|
|
338
543
|
const found = publicKeys.some((p) => equalBytes(p, pk));
|
|
339
544
|
if (!found) throw new Error("The signer's pubkey must be included in the list of pubkeys");
|
|
340
545
|
return keyAggCoeffInternal(pk, this.secondKey, this.L);
|
|
@@ -347,33 +552,39 @@ export class Session {
|
|
|
347
552
|
const { Q, gAcc, b, R, e } = this;
|
|
348
553
|
const s = Fn.fromBytes(partialSig, true);
|
|
349
554
|
if (!Fn.isValid(s)) return false;
|
|
555
|
+
// BIP327 PartialSigVerifyInternal: `Let s = int(psig); fail if s >= n`, so s=0 must stay
|
|
556
|
+
// in the public verification equation and return false on mismatch instead of throwing.
|
|
350
557
|
const { R1, R2 } = PubNonce.decode(publicNonce);
|
|
351
558
|
const Re_s_ = R1.add(R2.multiply(b));
|
|
352
559
|
const Re_s = hasEven(R.y) ? Re_s_ : Re_s_.negate();
|
|
353
560
|
const P = Point.fromBytes(publicKey);
|
|
354
561
|
const a = this.getSessionKeyAggCoeff(P);
|
|
355
562
|
const g = Fn.mul(evenScalar(Q, 1n), gAcc);
|
|
356
|
-
const left =
|
|
563
|
+
const left = Point.BASE.multiplyUnsafe(s);
|
|
357
564
|
const right = Re_s.add(P.multiply(Fn.mul(e, Fn.mul(a, g))));
|
|
358
565
|
return left.equals(right);
|
|
359
566
|
}
|
|
360
567
|
|
|
361
568
|
/**
|
|
362
|
-
* Generates a partial signature for a given message, secret nonce,
|
|
363
|
-
*
|
|
364
|
-
* @param
|
|
365
|
-
* @param
|
|
366
|
-
* @param fastSign if
|
|
569
|
+
* Generates a partial signature for a given message, secret nonce,
|
|
570
|
+
* secret key, and session context.
|
|
571
|
+
* @param secretNonce - secret nonce for this signing session; it is zeroed after use
|
|
572
|
+
* @param secret - secret key of the signer
|
|
573
|
+
* @param fastSign - if `true`, skip the self-verification pass
|
|
367
574
|
* @returns The partial signature (Uint8Array).
|
|
368
|
-
* @throws
|
|
575
|
+
* @throws If the input is invalid, such as wrong array sizes,
|
|
576
|
+
* invalid nonce, or invalid secret key. {@link Error}
|
|
369
577
|
*/
|
|
370
578
|
sign(secretNonce: Uint8Array, secret: Uint8Array, fastSign = false): Uint8Array {
|
|
371
579
|
abytes(secret, 32);
|
|
372
|
-
if (typeof fastSign !== 'boolean') throw new
|
|
580
|
+
if (typeof fastSign !== 'boolean') throw new TypeError('expected boolean');
|
|
373
581
|
const { Q, gAcc, b, R, e } = this;
|
|
374
582
|
const { k1: k1_, k2: k2_, publicKey: originalPk } = SecretNonce.decode(secretNonce);
|
|
375
583
|
// zero-out the first 64 bytes of secretNonce so it cannot be reused
|
|
376
|
-
//
|
|
584
|
+
// BIP327 permits overwriting the first 64 secnonce bytes after reading k1/k2 so
|
|
585
|
+
// accidental reuse fails fast instead of reusing the same nonce scalars.
|
|
586
|
+
// TODO: this was in the reference implementation, but feels very broken.
|
|
587
|
+
// Modifying input arguments is pretty bad.
|
|
377
588
|
secretNonce.fill(0, 0, 64);
|
|
378
589
|
if (!Fn.isValid(k1_)) throw new Error('wrong k1');
|
|
379
590
|
if (!Fn.isValid(k2_)) throw new Error('wrong k1');
|
|
@@ -403,15 +614,12 @@ export class Session {
|
|
|
403
614
|
}
|
|
404
615
|
/**
|
|
405
616
|
* Verifies a partial signature against the aggregate public key and other session parameters.
|
|
406
|
-
* @param partialSig
|
|
407
|
-
* @param pubNonces
|
|
408
|
-
* @param
|
|
409
|
-
* @
|
|
410
|
-
* @
|
|
411
|
-
*
|
|
412
|
-
* @param i The index of the signer whose partial signature is being verified.
|
|
413
|
-
* @returns True if the partial signature is valid, false otherwise.
|
|
414
|
-
* @throws {Error} If the input is invalid, such as non array partialSig, pubNonces, pubKeys, tweaks.
|
|
617
|
+
* @param partialSig - partial signature to verify
|
|
618
|
+
* @param pubNonces - public nonces from each signer
|
|
619
|
+
* @param i - index of the signer whose partial signature is being verified
|
|
620
|
+
* @returns `true` if the partial signature is valid.
|
|
621
|
+
* @throws If the input is invalid, such as non-array partial signatures
|
|
622
|
+
* or mismatched nonce counts. {@link Error}
|
|
415
623
|
*/
|
|
416
624
|
partialSigVerify(partialSig: Uint8Array, pubNonces: Uint8Array[], i: number): boolean {
|
|
417
625
|
const { publicKeys, tweaks, isXonly } = this;
|
|
@@ -422,21 +630,29 @@ export class Session {
|
|
|
422
630
|
aXonly(isXonly);
|
|
423
631
|
anumber(i);
|
|
424
632
|
if (pubNonces.length !== publicKeys.length)
|
|
425
|
-
throw new
|
|
633
|
+
throw new RangeError('The pubNonces and publicKeys arrays must have the same length');
|
|
426
634
|
if (tweaks.length !== isXonly.length)
|
|
427
|
-
throw new
|
|
428
|
-
|
|
635
|
+
throw new RangeError('The tweaks and isXonly arrays must have the same length');
|
|
636
|
+
// BIP327 PartialSigVerify rebuilds session_ctx from aggnonce = NonceAgg(pubnonce_1..u),
|
|
637
|
+
// and GetSessionValues derives b and R from that aggnonce. This Session caches b/R/e from
|
|
638
|
+
// the constructor aggNonce, so a stale Session would otherwise accept mismatched pubNonces
|
|
639
|
+
// as long as pubNonces[i] still matched the signer slot.
|
|
640
|
+
if (i >= pubNonces.length) throw new RangeError('index outside of pubKeys/pubNonces');
|
|
641
|
+
if (!equalBytes(this.aggNonce, nonceAggregate(pubNonces))) return false;
|
|
429
642
|
return this.partialSigVerifyInternal(partialSig, pubNonces[i], publicKeys[i]);
|
|
430
643
|
}
|
|
431
644
|
/**
|
|
432
645
|
* Aggregates partial signatures from multiple signers into a single final signature.
|
|
433
|
-
* @param partialSigs
|
|
434
|
-
* @param sessionCtx The session context containing all necessary information for signing.
|
|
646
|
+
* @param partialSigs - partial signatures from each signer
|
|
435
647
|
* @returns The final aggregate signature (Uint8Array).
|
|
436
|
-
* @throws
|
|
648
|
+
* @throws If the input is invalid, such as wrong array sizes or malformed
|
|
649
|
+
* signatures. {@link Error}
|
|
437
650
|
*/
|
|
438
|
-
partialSigAgg(partialSigs: Uint8Array[]): Uint8Array {
|
|
651
|
+
partialSigAgg(partialSigs: TArg<Uint8Array[]>): TRet<Uint8Array> {
|
|
439
652
|
abytesArray(partialSigs, 32);
|
|
653
|
+
// BIP327 PartialSigAgg is defined for a non-empty psig_1..u list tied to this session_ctx;
|
|
654
|
+
// [] is not a valid aggregate-signature input even though the sum starts from zero.
|
|
655
|
+
if (partialSigs.length < 1) throw new RangeError('partialSigs.length must be >= 1');
|
|
440
656
|
const { Q, tweakAcc, R, e } = this;
|
|
441
657
|
let s = 0n;
|
|
442
658
|
for (let i = 0; i < partialSigs.length; i++) {
|
|
@@ -446,15 +662,15 @@ export class Session {
|
|
|
446
662
|
}
|
|
447
663
|
const g = evenScalar(Q, 1n);
|
|
448
664
|
s = Fn.add(s, Fn.mul(e, Fn.mul(g, tweakAcc))); // s + e * g * tweakAcc
|
|
449
|
-
return concatBytes(pointToBytes(R), Fn.toBytes(s))
|
|
665
|
+
return concatBytes(pointToBytes(R), Fn.toBytes(s)) as TRet<Uint8Array>;
|
|
450
666
|
}
|
|
451
667
|
}
|
|
452
668
|
|
|
453
669
|
const deterministicNonceHash = (
|
|
454
|
-
secret: Uint8Array
|
|
455
|
-
aggOtherNonce: Uint8Array
|
|
456
|
-
aggPublicKey: Uint8Array
|
|
457
|
-
msg: Uint8Array
|
|
670
|
+
secret: TArg<Uint8Array>,
|
|
671
|
+
aggOtherNonce: TArg<Uint8Array>,
|
|
672
|
+
aggPublicKey: TArg<Uint8Array>,
|
|
673
|
+
msg: TArg<Uint8Array>,
|
|
458
674
|
i: number
|
|
459
675
|
): bigint =>
|
|
460
676
|
taggedInt(
|
|
@@ -464,36 +680,62 @@ const deterministicNonceHash = (
|
|
|
464
680
|
aggPublicKey,
|
|
465
681
|
numberToBytesBE(msg.length, 8),
|
|
466
682
|
msg,
|
|
683
|
+
// BIP327 hashes bytes(1, i - 1) for i=1,2; callers pass 0/1 directly here.
|
|
467
684
|
new Uint8Array([i])
|
|
468
685
|
);
|
|
469
686
|
|
|
470
687
|
/**
|
|
471
688
|
* Generates a nonce pair and partial signature deterministically for a single signer.
|
|
472
|
-
* @param secret
|
|
473
|
-
* @param aggOtherNonce
|
|
474
|
-
* @param publicKeys
|
|
475
|
-
* @param
|
|
476
|
-
* @param
|
|
477
|
-
* @param
|
|
478
|
-
* @param rand
|
|
689
|
+
* @param secret - secret key of the signer
|
|
690
|
+
* @param aggOtherNonce - aggregate public nonce of the other signers
|
|
691
|
+
* @param publicKeys - public keys of all signers
|
|
692
|
+
* @param msg - message to be signed
|
|
693
|
+
* @param tweaks - tweaks applied to the aggregate public key
|
|
694
|
+
* @param isXonly - whether each tweak uses x-only semantics
|
|
695
|
+
* @param rand - optional extra randomness
|
|
696
|
+
* @param fastSign - whether to skip partial-signature self-verification
|
|
479
697
|
* @returns An object containing the public nonce and partial signature.
|
|
698
|
+
* @throws If MuSig2 session setup or signing fails. {@link Error}
|
|
699
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
700
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
701
|
+
* @throws If one of the aggregated public keys or nonces is invalid. {@link InvalidContributionErr}
|
|
702
|
+
* @example
|
|
703
|
+
* Generate one signer's deterministic nonce and partial signature in one step.
|
|
704
|
+
* ```ts
|
|
705
|
+
* import {
|
|
706
|
+
* IndividualPubkey,
|
|
707
|
+
* deterministicSign,
|
|
708
|
+
* keyAggregate,
|
|
709
|
+
* keyAggExport,
|
|
710
|
+
* nonceGen,
|
|
711
|
+
* } from '@scure/btc-signer/musig2.js';
|
|
712
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
713
|
+
* const alice = randomPrivateKeyBytes();
|
|
714
|
+
* const bob = randomPrivateKeyBytes();
|
|
715
|
+
* const publicKeys = [IndividualPubkey(alice), IndividualPubkey(bob)];
|
|
716
|
+
* const agg = keyAggregate(publicKeys);
|
|
717
|
+
* const otherNonce = nonceGen(publicKeys[1], bob, keyAggExport(agg)).public;
|
|
718
|
+
* deterministicSign(alice, otherNonce, publicKeys, new TextEncoder().encode('hello musig2'));
|
|
719
|
+
* ```
|
|
480
720
|
*/
|
|
481
721
|
export function deterministicSign(
|
|
482
|
-
secret: Uint8Array
|
|
483
|
-
aggOtherNonce: Uint8Array
|
|
484
|
-
publicKeys: Uint8Array[]
|
|
485
|
-
msg: Uint8Array
|
|
486
|
-
tweaks: Uint8Array[] = [],
|
|
722
|
+
secret: TArg<Uint8Array>,
|
|
723
|
+
aggOtherNonce: TArg<Uint8Array>,
|
|
724
|
+
publicKeys: TArg<Uint8Array[]>,
|
|
725
|
+
msg: TArg<Uint8Array>,
|
|
726
|
+
tweaks: TArg<Uint8Array[]> = [],
|
|
487
727
|
isXonly: boolean[] = [],
|
|
488
|
-
rand?: Uint8Array
|
|
728
|
+
rand?: TArg<Uint8Array>,
|
|
489
729
|
fastSign = false
|
|
490
|
-
): DetNonce {
|
|
730
|
+
): TRet<DetNonce> {
|
|
491
731
|
abytes(secret, 32);
|
|
492
732
|
abytes(aggOtherNonce, 66);
|
|
493
733
|
abytesArray(publicKeys, PUBKEY_LEN);
|
|
494
734
|
abytesArray(tweaks, 32);
|
|
495
735
|
abytes(msg);
|
|
496
|
-
abytesOptional(rand);
|
|
736
|
+
abytesOptional(rand, 32);
|
|
737
|
+
// BIP327 DeterministicSign input bullet: `The auxiliary randomness rand: a 32-byte array`
|
|
738
|
+
// when present, so this optional argument still needs the exact-length check here.
|
|
497
739
|
const sk = rand !== undefined ? aux(secret, rand) : secret;
|
|
498
740
|
const aggPublicKey = keyAggExport(keyAggregate(publicKeys, tweaks, isXonly));
|
|
499
741
|
const k1 = deterministicNonceHash(sk, aggOtherNonce, aggPublicKey, msg, 0);
|
|
@@ -505,5 +747,5 @@ export function deterministicSign(
|
|
|
505
747
|
const aggNonce = nonceAggregate([publicNonce, aggOtherNonce]);
|
|
506
748
|
const session = new Session(aggNonce, publicKeys, msg, tweaks, isXonly);
|
|
507
749
|
const partialSig = session.sign(secretNonce, secret, fastSign);
|
|
508
|
-
return { publicNonce, partialSig }
|
|
750
|
+
return { publicNonce, partialSig } as TRet<DetNonce>;
|
|
509
751
|
}
|