@noble/curves 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.
Files changed (110) hide show
  1. package/README.md +214 -122
  2. package/abstract/bls.d.ts +299 -16
  3. package/abstract/bls.d.ts.map +1 -1
  4. package/abstract/bls.js +89 -24
  5. package/abstract/bls.js.map +1 -1
  6. package/abstract/curve.d.ts +274 -27
  7. package/abstract/curve.d.ts.map +1 -1
  8. package/abstract/curve.js +177 -23
  9. package/abstract/curve.js.map +1 -1
  10. package/abstract/edwards.d.ts +166 -30
  11. package/abstract/edwards.d.ts.map +1 -1
  12. package/abstract/edwards.js +221 -86
  13. package/abstract/edwards.js.map +1 -1
  14. package/abstract/fft.d.ts +327 -10
  15. package/abstract/fft.d.ts.map +1 -1
  16. package/abstract/fft.js +155 -12
  17. package/abstract/fft.js.map +1 -1
  18. package/abstract/frost.d.ts +293 -0
  19. package/abstract/frost.d.ts.map +1 -0
  20. package/abstract/frost.js +704 -0
  21. package/abstract/frost.js.map +1 -0
  22. package/abstract/hash-to-curve.d.ts +173 -24
  23. package/abstract/hash-to-curve.d.ts.map +1 -1
  24. package/abstract/hash-to-curve.js +170 -31
  25. package/abstract/hash-to-curve.js.map +1 -1
  26. package/abstract/modular.d.ts +429 -37
  27. package/abstract/modular.d.ts.map +1 -1
  28. package/abstract/modular.js +414 -119
  29. package/abstract/modular.js.map +1 -1
  30. package/abstract/montgomery.d.ts +83 -12
  31. package/abstract/montgomery.d.ts.map +1 -1
  32. package/abstract/montgomery.js +32 -7
  33. package/abstract/montgomery.js.map +1 -1
  34. package/abstract/oprf.d.ts +164 -91
  35. package/abstract/oprf.d.ts.map +1 -1
  36. package/abstract/oprf.js +88 -29
  37. package/abstract/oprf.js.map +1 -1
  38. package/abstract/poseidon.d.ts +138 -7
  39. package/abstract/poseidon.d.ts.map +1 -1
  40. package/abstract/poseidon.js +178 -15
  41. package/abstract/poseidon.js.map +1 -1
  42. package/abstract/tower.d.ts +122 -3
  43. package/abstract/tower.d.ts.map +1 -1
  44. package/abstract/tower.js +323 -139
  45. package/abstract/tower.js.map +1 -1
  46. package/abstract/weierstrass.d.ts +339 -76
  47. package/abstract/weierstrass.d.ts.map +1 -1
  48. package/abstract/weierstrass.js +395 -205
  49. package/abstract/weierstrass.js.map +1 -1
  50. package/bls12-381.d.ts +16 -2
  51. package/bls12-381.d.ts.map +1 -1
  52. package/bls12-381.js +199 -209
  53. package/bls12-381.js.map +1 -1
  54. package/bn254.d.ts +11 -2
  55. package/bn254.d.ts.map +1 -1
  56. package/bn254.js +93 -38
  57. package/bn254.js.map +1 -1
  58. package/ed25519.d.ts +135 -14
  59. package/ed25519.d.ts.map +1 -1
  60. package/ed25519.js +207 -41
  61. package/ed25519.js.map +1 -1
  62. package/ed448.d.ts +108 -14
  63. package/ed448.d.ts.map +1 -1
  64. package/ed448.js +194 -42
  65. package/ed448.js.map +1 -1
  66. package/index.js +7 -1
  67. package/index.js.map +1 -1
  68. package/misc.d.ts +106 -7
  69. package/misc.d.ts.map +1 -1
  70. package/misc.js +141 -32
  71. package/misc.js.map +1 -1
  72. package/nist.d.ts +112 -11
  73. package/nist.d.ts.map +1 -1
  74. package/nist.js +139 -17
  75. package/nist.js.map +1 -1
  76. package/package.json +34 -6
  77. package/secp256k1.d.ts +92 -15
  78. package/secp256k1.d.ts.map +1 -1
  79. package/secp256k1.js +211 -28
  80. package/secp256k1.js.map +1 -1
  81. package/src/abstract/bls.ts +356 -69
  82. package/src/abstract/curve.ts +327 -44
  83. package/src/abstract/edwards.ts +367 -143
  84. package/src/abstract/fft.ts +371 -36
  85. package/src/abstract/frost.ts +1092 -0
  86. package/src/abstract/hash-to-curve.ts +255 -56
  87. package/src/abstract/modular.ts +591 -144
  88. package/src/abstract/montgomery.ts +114 -30
  89. package/src/abstract/oprf.ts +383 -194
  90. package/src/abstract/poseidon.ts +235 -35
  91. package/src/abstract/tower.ts +428 -159
  92. package/src/abstract/weierstrass.ts +710 -312
  93. package/src/bls12-381.ts +239 -236
  94. package/src/bn254.ts +107 -46
  95. package/src/ed25519.ts +234 -56
  96. package/src/ed448.ts +227 -57
  97. package/src/index.ts +7 -1
  98. package/src/misc.ts +154 -35
  99. package/src/nist.ts +143 -20
  100. package/src/secp256k1.ts +284 -41
  101. package/src/utils.ts +583 -81
  102. package/src/webcrypto.ts +302 -73
  103. package/utils.d.ts +457 -24
  104. package/utils.d.ts.map +1 -1
  105. package/utils.js +410 -53
  106. package/utils.js.map +1 -1
  107. package/webcrypto.d.ts +167 -25
  108. package/webcrypto.d.ts.map +1 -1
  109. package/webcrypto.js +165 -58
  110. package/webcrypto.js.map +1 -1
@@ -0,0 +1,1092 @@
1
+ /**
2
+ * FROST: Flexible Round-Optimized Schnorr Threshold Protocol for Two-Round Schnorr Signatures.
3
+ *
4
+ * See [RFC 9591](https://datatracker.ietf.org/doc/rfc9591/) and [frost.zfnd.org](https://frost.zfnd.org).
5
+ * @module
6
+ */
7
+ import { utf8ToBytes } from '@noble/hashes/utils.js';
8
+ import {
9
+ bytesToHex,
10
+ bytesToNumberBE,
11
+ bytesToNumberLE,
12
+ concatBytes,
13
+ hexToBytes,
14
+ randomBytes,
15
+ validateObject,
16
+ type TArg,
17
+ type TRet,
18
+ } from '../utils.ts';
19
+ import { pippenger, validatePointCons, type CurvePoint, type CurvePointCons } from './curve.ts';
20
+ import { poly, type RootsOfUnity } from './fft.ts';
21
+ import { type H2CDSTOpts } from './hash-to-curve.ts';
22
+ import { getMinHashLength, mapHashToField, type IField } from './modular.ts';
23
+
24
+ export type RNG = typeof randomBytes;
25
+ export type Identifier = string; // Identifiers are hex to make comparison easier
26
+ export type Commitment = Uint8Array; // serialized point
27
+ export type Coefficient = Uint8Array; // serialized scalar
28
+ export type Signature = Uint8Array;
29
+ export type Signers = { min: number; max: number };
30
+ export type SecretKey = Uint8Array; // Secret key
31
+ export type Bytes = Uint8Array;
32
+ type Point = Uint8Array;
33
+
34
+ export type DKG_Round1 = {
35
+ // If identifiers were assigned via fromNumber before, it is worth checking
36
+ // that a party doesn't impersonate another one.
37
+ // But we throw on duplicate identifiers.
38
+ identifier: Identifier;
39
+ commitment: TRet<Commitment[]>; // sender identifier
40
+ proofOfKnowledge: TRet<Signature>;
41
+ };
42
+ export type DKG_Round2 = {
43
+ identifier: Identifier; // sender identifier
44
+ signingShare: TRet<Bytes>;
45
+ };
46
+ // This is internal, so we can use bigints
47
+ export type DKG_Secret = {
48
+ identifier: bigint;
49
+ coefficients?: bigint[];
50
+ commitment: TRet<Point[]>;
51
+ signers: Signers;
52
+ // Keep the local polynomial until round3 succeeds so late DKG failures can be retried.
53
+ step?: 1 | 2 | 3;
54
+ };
55
+
56
+ export type FrostPublic = {
57
+ signers: Signers;
58
+ commitments: TRet<Bytes[]>; // Point[], where commitments[0] is the group public key
59
+ verifyingShares: TRet<Record<Identifier, Bytes>>; // id -> Point
60
+ };
61
+ export type FrostSecret = {
62
+ identifier: Identifier;
63
+ signingShare: TRet<Bytes>; // Scalar
64
+ };
65
+ export type Key = { public: FrostPublic; secret: FrostSecret };
66
+ export type DealerShares = {
67
+ public: FrostPublic;
68
+ secretShares: Record<Identifier, FrostSecret>;
69
+ };
70
+ // Sign stuff
71
+ export type Nonces = {
72
+ hiding: TRet<Bytes>; // Scalar
73
+ binding: TRet<Bytes>; // Scalar
74
+ };
75
+ export type NonceCommitments = {
76
+ identifier: Identifier;
77
+ hiding: TRet<Bytes>; // Point
78
+ binding: TRet<Bytes>; // Point
79
+ };
80
+ export type GenNonce = {
81
+ nonces: Nonces;
82
+ commitments: NonceCommitments;
83
+ };
84
+
85
+ export interface FROSTPoint<T extends CurvePoint<any, T>> extends CurvePoint<any, T> {
86
+ add(rhs: T): T;
87
+ multiply(rhs: bigint): T;
88
+ equals(rhs: T): boolean;
89
+ toBytes(compressed?: boolean): Bytes;
90
+ clearCofactor(): T;
91
+ }
92
+ export interface FROSTPointConstructor<T extends FROSTPoint<T>> extends CurvePointCons<T> {
93
+ fromBytes(a: Bytes): T;
94
+ Fn: IField<bigint>;
95
+ }
96
+
97
+ // Opts
98
+ export type FrostOpts<P extends FROSTPoint<P>> = {
99
+ readonly name: string;
100
+ readonly Point: FROSTPointConstructor<P>;
101
+ readonly Fn?: IField<bigint>;
102
+ /** Optional suite hook that tightens canonical decoding with subgroup / identity checks. */
103
+ readonly validatePoint?: (p: P) => void;
104
+ /** Optional public-key parser. Implementations MUST preserve the same subgroup / identity policy
105
+ * as `validatePoint`, because this bypasses generic canonical decoding in `parsePoint()`. */
106
+ readonly parsePublicKey?: (bytes: TArg<Uint8Array>) => P;
107
+ readonly hash: (msg: TArg<Uint8Array>) => TRet<Uint8Array>;
108
+ /** Custom scalar hash hook. Implementations MUST treat `msg` and `options` as read-only. */
109
+ readonly hashToScalar?: (msg: TArg<Uint8Array>, options?: TArg<H2CDSTOpts>) => bigint;
110
+ // Hacks for taproot support
111
+ readonly adjustScalar?: (n: bigint) => bigint;
112
+ readonly adjustPoint?: (n: P) => P;
113
+ readonly challenge?: (R: P, PK: P, msg: TArg<Uint8Array>) => bigint;
114
+ readonly adjustNonces?: (PK: P, nonces: TArg<Nonces>) => TRet<Nonces>;
115
+ readonly adjustSecret?: (secret: TArg<FrostSecret>, pub: TArg<FrostPublic>) => TRet<FrostSecret>;
116
+ readonly adjustPublic?: (pub: TArg<FrostPublic>) => TRet<FrostPublic>;
117
+ readonly adjustGroupCommitmentShare?: (GC: P, GCShare: P) => P;
118
+ readonly adjustTx?: {
119
+ readonly encode: (tx: TArg<Uint8Array>) => TRet<Uint8Array>;
120
+ readonly decode: (tx: TArg<Uint8Array>) => TRet<Uint8Array>;
121
+ };
122
+ readonly adjustDKG?: (k: TArg<Key>) => TRet<Key>;
123
+ // Hash function prefixes
124
+ readonly H1?: string;
125
+ readonly H2?: string;
126
+ readonly H3?: string;
127
+ readonly H4?: string;
128
+ readonly H5?: string;
129
+ readonly HDKG?: string;
130
+ readonly HID?: string;
131
+ };
132
+
133
+ /**
134
+ * FROST: Threshold Protocol for Two‑Round Schnorr Signatures
135
+ * from [RFC 9591](https://datatracker.ietf.org/doc/rfc9591/).
136
+ */
137
+ export type FROST = {
138
+ /**
139
+ * Methods to construct participant identifiers.
140
+ */
141
+ Identifier: {
142
+ /**
143
+ * Constructs an identifier from a numeric index.
144
+ * @param n - A positive integer.
145
+ * @returns A canonical serialized Identifier.
146
+ */
147
+ fromNumber(n: number): Identifier;
148
+ /**
149
+ * Derives an identifier deterministically from a string (e.g. an email).
150
+ * @param s - Arbitrary string.
151
+ * @returns A canonical serialized Identifier.
152
+ */
153
+ derive(s: string): Identifier;
154
+ };
155
+ /**
156
+ * Distributed Key Generation (DKG) protocol interface.
157
+ * RFC 9591 leaves DKG out of scope; Appendix C only specifies dealer/VSS key generation.
158
+ * These helpers follow the split-round API used by frost-rs for interoperable testing.
159
+ */
160
+ DKG: {
161
+ /**
162
+ * Generates the first round of DKG.
163
+ * @param id - Participant's identifier.
164
+ * @param signers - Set of all participants (min/max threshold).
165
+ * @param secret - Optional initial secret scalar.
166
+ * @param rng - Optional RNG for nonce generation.
167
+ * @returns Public broadcast and private DKG state. The returned `secret` package is mutable
168
+ * round state that will be consumed by `round2()` and `round3()`.
169
+ */
170
+ round1: (
171
+ id: Identifier,
172
+ signers: Signers,
173
+ secret?: TArg<SecretKey>,
174
+ rng?: RNG
175
+ ) => {
176
+ public: DKG_Round1;
177
+ secret: DKG_Secret;
178
+ };
179
+ /**
180
+ * Executes DKG round 2 given public round1 data from others.
181
+ * @param secret - Private DKG state from round1. This mutates `secret.step` in place.
182
+ * @param others - Public round1 broadcasts from other participants.
183
+ * @returns A map of round2 messages to be sent to others.
184
+ */
185
+ round2: (
186
+ secret: TArg<DKG_Secret>,
187
+ others: TArg<DKG_Round1[]>
188
+ ) => TRet<Record<string, DKG_Round2>>;
189
+ /**
190
+ * Finalizes key generation in round3 using received round1 + round2 messages.
191
+ * @param secret - Private DKG state. This consumes the remaining local polynomial coefficients
192
+ * and transitions the package to its final post-round3 state.
193
+ * @param round1 - Public round1 broadcasts from all participants.
194
+ * @param round2 - Round2 messages received from others.
195
+ * @returns Final secret/public key information for the participant.
196
+ * Callers MUST pass the same verified remote `round1` package set that was already
197
+ * accepted in `round2()`, rather than re-fetching or rebuilding it from the network.
198
+ */
199
+ round3: (
200
+ secret: TArg<DKG_Secret>,
201
+ round1: TArg<DKG_Round1[]>,
202
+ round2: TArg<DKG_Round2[]>
203
+ ) => TRet<Key>;
204
+ /**
205
+ * Best-effort erasure of internal secret state. Bigint/JIT copies may still survive outside the
206
+ * local object even after cleanup.
207
+ * @param secret - Private DKG state from round1.
208
+ */
209
+ clean(secret: TArg<DKG_Secret>): void;
210
+ };
211
+ /**
212
+ * Trusted dealer mode: generates key shares from a central trusted authority.
213
+ * Mirrors RFC 9591 Appendix C and returns one shared VSS commitment package
214
+ * plus per-participant shares.
215
+ * @param signers - Threshold parameters (min/max).
216
+ * @param identifiers - Optional explicit participant list.
217
+ * @param secret - Optional secret scalar.
218
+ * @param rng - Optional RNG.
219
+ * @returns One shared public package plus the participant secret-share packages.
220
+ */
221
+ trustedDealer(
222
+ signers: Signers,
223
+ identifiers?: Identifier[],
224
+ secret?: TArg<SecretKey>,
225
+ rng?: RNG
226
+ ): TRet<DealerShares>;
227
+ /**
228
+ * Validates the consistency of a secret share against the shared public commitments.
229
+ * This is the RFC 9591 Appendix C.2 `vss_verify` check against the shared dealer/DKG commitment.
230
+ * It does not relax RFC 9591 Section 3.1: public identity elements are still invalid even when
231
+ * the scalar/share algebra would otherwise be self-consistent.
232
+ * Throws if invalid.
233
+ * @param secret - A FrostSecret containing identifier and signing share.
234
+ * @param pub - Shared public package containing commitments.
235
+ */
236
+ validateSecret(secret: TArg<FrostSecret>, pub: TArg<FrostPublic>): void;
237
+ /**
238
+ * Produces nonces and public commitments used in signing.
239
+ * RFC 9591 Section 5.1 `commit()`.
240
+ * @param secret - Participant's secret share.
241
+ * @param rng - Optional RNG.
242
+ * @returns Nonce values and their public commitments.
243
+ * Returned nonces are one-time-use and MUST NOT be reused across signing sessions.
244
+ * This API does not mutate or zeroize caller-owned nonce objects.
245
+ */
246
+ commit(secret: TArg<FrostSecret>, rng?: RNG): TRet<GenNonce>;
247
+ /**
248
+ * Signs a message using the participant's secret and nonce.
249
+ * @param secret - Participant's secret share.
250
+ * @param pub - Shared public package containing commitments.
251
+ * @param nonces - Participant's nonce pair.
252
+ * @param commitmentList - Commitments from all signing participants.
253
+ * @param msg - Message to be signed.
254
+ * @returns Signature share as a byte array.
255
+ * RFC 9591 Sections 4.1/5.1 require round-one commitments to be one-time-use, and
256
+ * Section 5.2 signs with the nonce corresponding to that published commitment.
257
+ * The caller MUST pass fresh nonces from `commit()`. On successful signing, this helper
258
+ * consumes the caller-owned nonce object by zeroing both nonce byte arrays in place.
259
+ * Later calls reject an all-zero nonce package, so same-object reuse fails closed and an
260
+ * accidentally generated zero nonce package is not silently used for signing.
261
+ */
262
+ signShare(
263
+ secret: TArg<FrostSecret>,
264
+ pub: TArg<FrostPublic>,
265
+ nonces: TArg<Nonces>,
266
+ commitmentList: TArg<NonceCommitments[]>,
267
+ msg: TArg<Uint8Array>
268
+ ): TRet<Uint8Array>;
269
+ /**
270
+ * Verifies a signature share against public commitments.
271
+ * Matches the coordinator-side individual-share verification from RFC 9591 Section 5.4.
272
+ * @param pub - Group public key information.
273
+ * @param commitmentList - Commitments from all signing participants.
274
+ * @param msg - Message being signed.
275
+ * @param identifier - Identifier of the signer whose share is being verified.
276
+ * @param sigShare - Signature share to verify.
277
+ * @returns True if valid, false otherwise.
278
+ */
279
+ verifyShare(
280
+ pub: TArg<FrostPublic>,
281
+ commitmentList: TArg<NonceCommitments[]>,
282
+ msg: TArg<Uint8Array>,
283
+ identifier: Identifier,
284
+ sigShare: TArg<Uint8Array>
285
+ ): boolean;
286
+ /**
287
+ * Aggregates signature shares into a full signature.
288
+ * RFC 9591 Section 5.3 `aggregate()`.
289
+ * @param pub - Group public key.
290
+ * @param commitmentList - Nonce commitments from all signers.
291
+ * @param msg - Message to sign.
292
+ * @param sigShares - Map from identifier to their signature share.
293
+ * @returns Final aggregated signature.
294
+ */
295
+ aggregate(
296
+ pub: TArg<FrostPublic>,
297
+ commitmentList: TArg<NonceCommitments[]>,
298
+ msg: TArg<Uint8Array>,
299
+ sigShares: TArg<Record<Identifier, Uint8Array>>
300
+ ): TRet<Uint8Array>;
301
+ /**
302
+ * Signs a message using a raw secret key (e.g. from combineSecret).
303
+ * @param msg - Message to sign.
304
+ * @param secretKey - Group secret key as bytes.
305
+ * @returns Signature bytes.
306
+ */
307
+ sign(msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array>;
308
+ /**
309
+ * Verifies a full signature against the group public key.
310
+ * @param sig - Signature bytes.
311
+ * @param msg - Message that was signed.
312
+ * @param publicKey - Group public key.
313
+ * @returns True if valid, false otherwise.
314
+ */
315
+ verify(sig: TArg<Signature>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>): boolean;
316
+ /**
317
+ * Combines multiple secret shares into a single secret key (e.g. for recovery).
318
+ * @param shares - Set of FrostSecret shares.
319
+ * @param signers - Threshold parameters.
320
+ * @returns Group secret key as bytes.
321
+ */
322
+ combineSecret(shares: TArg<FrostSecret[]>, signers: Signers): TRet<Uint8Array>;
323
+ /**
324
+ * Low-level helper utilities (field arithmetic and polynomial tools).
325
+ */
326
+ utils: {
327
+ /**
328
+ * Finite field used for scalars.
329
+ */
330
+ Fn: IField<bigint>;
331
+ /**
332
+ * Generates a random scalar (private key).
333
+ * @param rng - Optional RNG source.
334
+ * @returns Scalar as 32-byte Uint8Array.
335
+ */
336
+ randomScalar: (rng?: RNG) => TRet<Uint8Array>;
337
+ /**
338
+ * Generates a secret-sharing polynomial and its public commitments.
339
+ * @param signers - Threshold parameters.
340
+ * @param secret - Optional initial secret scalar.
341
+ * @param coeffs - Optional manual coefficients.
342
+ * @param rng - Optional RNG.
343
+ * @returns Polynomial coefficients, commitments, and secret value.
344
+ */
345
+ generateSecretPolynomial: (
346
+ signers: Signers,
347
+ secret?: TArg<Uint8Array>,
348
+ coeffs?: bigint[],
349
+ rng?: RNG
350
+ ) => {
351
+ coefficients: bigint[];
352
+ commitment: TRet<Point[]>;
353
+ secret: bigint;
354
+ };
355
+ };
356
+ };
357
+
358
+ // PubKey = commitments, verifyingShares
359
+ // PrivKey = id, signingShare, commitment
360
+
361
+ const validateSigners = (signers: Signers) => {
362
+ if (!Number.isSafeInteger(signers.min) || !Number.isSafeInteger(signers.max))
363
+ throw new Error('Wrong signers info: min=' + signers.min + ' max=' + signers.max);
364
+ // Compatibility with frost-rs intentionally narrows RFC 9591's positive-nonzero threshold rule
365
+ // to `min >= 2`, even though the RFC text itself allows `MIN_PARTICIPANTS = 1`.
366
+ // This API is for actual threshold signing across participants; 1-of-n degenerates to ordinary
367
+ // single-signer mode, which does not need FROST's network/coordination machinery at all.
368
+ if (signers.min < 2 || signers.max < 2 || signers.min > signers.max)
369
+ throw new Error('Wrong signers info: min=' + signers.min + ' max=' + signers.max);
370
+ };
371
+ const validateCommitmentsNum = (signers: Signers, len: number) => {
372
+ // RFC 9591 Sections 5.2/5.3 require MIN_PARTICIPANTS <= NUM_PARTICIPANTS <= MAX_PARTICIPANTS.
373
+ if (len < signers.min || len > signers.max) throw new Error('Wrong number of commitments=' + len);
374
+ };
375
+
376
+ class AggErr extends Error {
377
+ // Empty means aggregation failed before per-share verification could attribute a signer.
378
+ public cheaters: Identifier[];
379
+ constructor(msg: string, cheaters: Identifier[]) {
380
+ super(msg);
381
+ this.cheaters = cheaters;
382
+ }
383
+ }
384
+
385
+ export function createFROST<P extends FROSTPoint<P>>(opts: FrostOpts<P>): TRet<FROST> {
386
+ validateObject(
387
+ opts,
388
+ {
389
+ name: 'string',
390
+ hash: 'function',
391
+ },
392
+ {
393
+ hashToScalar: 'function',
394
+ validatePoint: 'function',
395
+ parsePublicKey: 'function',
396
+ adjustScalar: 'function',
397
+ adjustPoint: 'function',
398
+ challenge: 'function',
399
+ adjustNonces: 'function',
400
+ adjustSecret: 'function',
401
+ adjustPublic: 'function',
402
+ adjustGroupCommitmentShare: 'function',
403
+ adjustDKG: 'function',
404
+ }
405
+ );
406
+ // Cheap constructor-surface sanity check only: this verifies the generic static hooks/fields that
407
+ // FROST consumes, but it does not certify point semantics like BASE/ZERO correctness.
408
+ validatePointCons(opts.Point);
409
+ const { Point } = opts;
410
+ const Fn = opts.Fn === undefined ? Point.Fn : opts.Fn;
411
+ // Hashes
412
+ const hashBytes = opts.hash;
413
+ const hashToScalar =
414
+ opts.hashToScalar === undefined
415
+ ? (msg: TArg<Uint8Array>, opts: TArg<H2CDSTOpts> = { DST: new Uint8Array() }) => {
416
+ const t = hashBytes(concatBytes(opts.DST as Uint8Array, msg));
417
+ return Fn.create(Fn.isLE ? bytesToNumberLE(t) : bytesToNumberBE(t));
418
+ }
419
+ : opts.hashToScalar;
420
+ const H1Prefix = utf8ToBytes(opts.H1 !== undefined ? opts.H1 : opts.name + 'rho');
421
+ const H2Prefix = utf8ToBytes(opts.H2 !== undefined ? opts.H2 : opts.name + 'chal');
422
+ const H3Prefix = utf8ToBytes(opts.H3 !== undefined ? opts.H3 : opts.name + 'nonce');
423
+ const H4Prefix = utf8ToBytes(opts.H4 !== undefined ? opts.H4 : opts.name + 'msg');
424
+ const H5Prefix = utf8ToBytes(opts.H5 !== undefined ? opts.H5 : opts.name + 'com');
425
+ const HDKGPrefix = utf8ToBytes(opts.HDKG !== undefined ? opts.HDKG : opts.name + 'dkg');
426
+ const HIDPrefix = utf8ToBytes(opts.HID !== undefined ? opts.HID : opts.name + 'id');
427
+ const H1 = (msg: TArg<Uint8Array>) => hashToScalar(msg, { DST: H1Prefix });
428
+ // Empty H2 still passes `{ DST: new Uint8Array() }` into custom hashToScalar hooks.
429
+ // The built-in fallback hashes that identically to omitted DST, which is how
430
+ // the Ed25519 suite models RFC 9591's undecorated H2 challenge hash.
431
+ const H2 = (msg: TArg<Uint8Array>) => hashToScalar(msg, { DST: H2Prefix });
432
+ const H3 = (msg: TArg<Uint8Array>) => hashToScalar(msg, { DST: H3Prefix });
433
+ const H4 = (msg: TArg<Uint8Array>) => hashBytes(concatBytes(H4Prefix, msg));
434
+ const H5 = (msg: TArg<Uint8Array>) => hashBytes(concatBytes(H5Prefix, msg));
435
+ const HDKG = (msg: TArg<Uint8Array>) => hashToScalar(msg, { DST: HDKGPrefix });
436
+ const HID = (msg: TArg<Uint8Array>) => hashToScalar(msg, { DST: HIDPrefix });
437
+ // /Hashes
438
+ const randomScalar = (rng: RNG = randomBytes) => {
439
+ // Intentional divergence from RFC 9591 §4.1 / §5.1: the RFC nonce_generate helper outputs a
440
+ // Scalar in [0, p-1], but round-one commit publishes ScalarBaseMult(nonce) values and §3.1
441
+ // requires SerializeElement / DeserializeElement to reject the identity element. Keep noble's
442
+ // mapHashToField generation here so round-one public nonce commitments stay in 1..n-1.
443
+ const t = mapHashToField(rng(getMinHashLength(Fn.ORDER)), Fn.ORDER, Fn.isLE);
444
+ // We cannot use Fn.fromBytes here because the field can have a different
445
+ // byte width, like ed448.
446
+ return Fn.isLE ? bytesToNumberLE(t) : bytesToNumberBE(t);
447
+ };
448
+ const serializePoint = (p: P) => p.toBytes();
449
+ const parsePoint = (bytes: TArg<Uint8Array>) => {
450
+ // RFC 9591 Section 3.1 requires DeserializeElement validation. Suite-specific validatePoint
451
+ // hooks tighten this further for ciphersuites in Section 6. Bare createFROST(...) only gets
452
+ // canonical point decoding unless the caller installs those extra subgroup / identity checks.
453
+ const p = Point.fromBytes(bytes);
454
+ if (opts.validatePoint) opts.validatePoint(p);
455
+ return p;
456
+ };
457
+ // RFC 9591 Sections 4.1/5.1 model each participant's round-one output as two public commitments.
458
+ const nonceCommitments = (identifier: Identifier, nonces: TArg<Nonces>): TRet<NonceCommitments> =>
459
+ ({
460
+ identifier,
461
+ hiding: serializePoint(Point.BASE.multiply(Fn.fromBytes(nonces.hiding))),
462
+ binding: serializePoint(Point.BASE.multiply(Fn.fromBytes(nonces.binding))),
463
+ }) as TRet<NonceCommitments>;
464
+ const adjustPoint = opts.adjustPoint === undefined ? (n: P) => n : opts.adjustPoint;
465
+ // We use hex to make it easier to use inside objects
466
+ const validateIdentifier = (n: bigint) => {
467
+ // Identifiers are canonical non-zero scalars. Custom / derived identifiers are allowed, so this
468
+ // is intentionally not bounded by the current signers.max slot count.
469
+ if (!Fn.isValid(n) || Fn.is0(n)) throw new Error('Invalid identifier ' + n);
470
+ return n;
471
+ };
472
+ const serializeIdentifier = (id: bigint) => bytesToHex(Fn.toBytes(validateIdentifier(id)));
473
+ const parseIdentifier = (id: string) => {
474
+ const n = validateIdentifier(Fn.fromBytes(hexToBytes(id)));
475
+ // Keep string-keyed maps stable by accepting only the canonical serialized form.
476
+ if (serializeIdentifier(n) !== id) throw new Error('expected canonical identifier hex');
477
+ return n;
478
+ };
479
+
480
+ const Signature = {
481
+ // RFC 9591 Appendix A encodes signatures canonically as
482
+ // SerializeElement(R) || SerializeScalar(z).
483
+ encode: (R: P, z: bigint): TRet<Signature> => {
484
+ let res: Uint8Array = concatBytes(serializePoint(R), Fn.toBytes(z));
485
+ if (opts.adjustTx) res = opts.adjustTx.encode(res);
486
+ return res as TRet<Signature>;
487
+ },
488
+ decode: (sig: TArg<Uint8Array>) => {
489
+ if (opts.adjustTx) sig = opts.adjustTx.decode(sig);
490
+ // We don't know size of point, but we know size of scalar
491
+ const R = parsePoint(sig.subarray(0, -Fn.BYTES));
492
+ const z = Fn.fromBytes(sig.subarray(-Fn.BYTES));
493
+ return { R, z };
494
+ },
495
+ };
496
+ // Generates pair of (scalar, point)
497
+ const genPointScalarPair = (rng: RNG = randomBytes) => {
498
+ let n = randomScalar(rng);
499
+ if (opts.adjustScalar) n = opts.adjustScalar(n);
500
+ let p = Point.BASE.multiply(n);
501
+ return { scalar: n, point: p };
502
+ };
503
+ // No roots here: root-based methods will throw.
504
+ // `poly` expects a structured roots-of-unity domain, but FROST uses an
505
+ // arbitrary domain and only needs the non-root operations below.
506
+ const nrErr = 'roots are unavailable in FROST polynomial mode';
507
+ const noRoots: RootsOfUnity = {
508
+ info: { G: Fn.ZERO, oddFactor: Fn.ZERO, powerOfTwo: 0 },
509
+ roots() {
510
+ throw new Error(nrErr);
511
+ },
512
+ brp() {
513
+ throw new Error(nrErr);
514
+ },
515
+ inverse() {
516
+ throw new Error(nrErr);
517
+ },
518
+ omega() {
519
+ throw new Error(nrErr);
520
+ },
521
+ clear() {},
522
+ };
523
+ const Poly = poly(Fn, noRoots);
524
+ const msm = (points: P[], scalars: bigint[]) => pippenger(Point, points, scalars);
525
+
526
+ // Internal stuff uses bigints & Points, external Uint8Arrays
527
+ const polynomialEvaluate = (x: bigint, coeffs: bigint[]): bigint => {
528
+ if (!coeffs.length) throw new Error('empty coefficients');
529
+ return Poly.monomial.eval(coeffs, x);
530
+ };
531
+ const deriveInterpolatingValue = (L: bigint[], xi: bigint): bigint => {
532
+ const err = 'invalid parameters';
533
+ // Generates lagrange coefficient
534
+ if (!L.some((x) => Fn.eql(x, xi))) throw new Error(err);
535
+ // Throws error if any x-coordinate is represented more than once in L.
536
+ const Lset = new Set(L);
537
+ if (Lset.size !== L.length) throw new Error(err);
538
+ // Or if xi is missing
539
+ if (!Lset.has(xi)) throw new Error(err);
540
+ let num = Fn.ONE;
541
+ let den = Fn.ONE;
542
+ for (const x of L) {
543
+ if (Fn.eql(x, xi)) continue;
544
+ num = Fn.mul(num, x); // num *= x
545
+ den = Fn.mul(den, Fn.sub(x, xi)); // RFC 9591 §4.2: denominator *= x_j - x_i
546
+ }
547
+ return Fn.div(num, den);
548
+ };
549
+ const evalutateVSS = (identifier: bigint, commitment: P[]) => {
550
+ // RFC 9591 Appendix C.2: S_i' = Σ_j ScalarMult(vss_commitment[j], i^j).
551
+ const monomial = Poly.monomial.basis(identifier, commitment.length);
552
+ return msm(commitment, monomial);
553
+ };
554
+ // High-level internal stuff
555
+ const generateSecretPolynomial = (
556
+ signers: Signers,
557
+ secret?: TArg<Uint8Array>,
558
+ coeffs?: bigint[],
559
+ rng: RNG = randomBytes
560
+ ) => {
561
+ validateSigners(signers);
562
+ // Dealer/DKG polynomial sampling reuses the same hardened scalar derivation as round-one
563
+ // nonces: overriding `rng` only swaps the entropy source, not the non-zero `1..n-1` policy.
564
+ const secretScalar = secret === undefined ? randomScalar(rng) : Fn.fromBytes(secret);
565
+ if (!coeffs) {
566
+ coeffs = [];
567
+ for (let i = 0; i < signers.min - 1; i++) coeffs.push(randomScalar(rng));
568
+ }
569
+ if (coeffs.length !== signers.min - 1) throw new Error('wrong coefficients length');
570
+ const coefficients: bigint[] = [secretScalar, ...coeffs];
571
+ // RFC 9591 Appendix C.2 commits to every polynomial coefficient with ScalarBaseMult.
572
+ const commitment = coefficients.map((i) => Point.BASE.multiply(i));
573
+ return { coefficients, commitment, secret: secretScalar };
574
+ };
575
+ // Pretty much sign+verify, same as basic
576
+ const ProofOfKnowledge = {
577
+ challenge: (id: bigint, verKey: P, R: P) =>
578
+ HDKG(concatBytes(Fn.toBytes(id), serializePoint(verKey), serializePoint(R))),
579
+ compute(id: bigint, coefficents: bigint[], commitments: P[], rng: RNG = randomBytes) {
580
+ if (coefficents.length < 1) throw new Error('coefficients should have at least one element');
581
+ const { point: R, scalar: k } = genPointScalarPair(rng);
582
+ const verKey = commitments[0]; // verify key is first one
583
+ const c = this.challenge(id, verKey, R);
584
+ const mu = Fn.add(k, Fn.mul(coefficents[0], c)); // mu = k + coeff[0] * c
585
+ return Signature.encode(R, mu);
586
+ },
587
+ validate(id: bigint, commitment: TArg<Commitment[]>, proof: TArg<Uint8Array>) {
588
+ if (commitment.length < 1) throw new Error('commitment should have at least one element');
589
+ const { R, z } = Signature.decode(proof);
590
+ const phi = parsePoint(commitment[0]);
591
+ const c = this.challenge(id, phi, R);
592
+ // R === z*G - phi*c
593
+ if (!R.equals(Point.BASE.multiply(z).subtract(phi.multiply(c))))
594
+ throw new Error('invalid proof of knowledge');
595
+ },
596
+ };
597
+ const Basic = {
598
+ challenge: (R: P, PK: P, msg: TArg<Uint8Array>) => {
599
+ if (opts.challenge) return opts.challenge(R, PK, msg);
600
+ return H2(concatBytes(serializePoint(R), serializePoint(PK), msg));
601
+ },
602
+ sign(msg: TArg<Uint8Array>, sk: bigint, rng: RNG = randomBytes): [P, bigint] {
603
+ const { point: R, scalar: r } = genPointScalarPair(rng);
604
+ const PK = Point.BASE.multiply(sk); // sk*G
605
+ const c = this.challenge(R, PK, msg);
606
+ const z = Fn.add(r, Fn.mul(c, sk)); // r + c * sk
607
+ return [R, z];
608
+ },
609
+ verify(msg: TArg<Uint8Array>, R: P, z: bigint, PK: P): boolean {
610
+ if (opts.adjustPoint) PK = opts.adjustPoint(PK);
611
+ if (opts.adjustPoint) R = opts.adjustPoint(R);
612
+ const c = this.challenge(R, PK, msg);
613
+ const zB = Point.BASE.multiply(z); // z*G
614
+ const cA = PK.multiply(c); // c*PK
615
+ let check = zB.subtract(cA).subtract(R); // zB - cA - R
616
+ // No clearCoffactor on ristretto
617
+ if (check.clearCofactor) check = check.clearCofactor();
618
+ return Point.ZERO.equals(check);
619
+ },
620
+ };
621
+ // === vssVerify
622
+ const validateSecretShare = (identifier: bigint, commitment: P[], signingShare: bigint) => {
623
+ // RFC 9591 Appendix C.2 `vss_verify(share_i, vss_commitment)` is purely algebraic.
624
+ // Public FROST packages still go through Section 3.1 element encoding,
625
+ // which rejects identity points, so a zero share or commitment does not
626
+ // become valid wire data just because VSS matches.
627
+ if (!Point.BASE.multiply(signingShare).equals(evalutateVSS(identifier, commitment)))
628
+ throw new Error('invalid secret share');
629
+ };
630
+ const Identifier = {
631
+ fromNumber(n: number): Identifier {
632
+ if (!Number.isSafeInteger(n)) throw new Error('expected safe interger');
633
+ return serializeIdentifier(BigInt(n));
634
+ },
635
+ // Not in spec, but in FROST implementation,
636
+ // seems useful and nice, no need to sync identifiers (would require more interactions)
637
+ derive(s: string): Identifier {
638
+ if (typeof s !== 'string') throw new Error('wrong identifier string: ' + s);
639
+ // Derived identifiers may land anywhere in the scalar field; they are not restricted to
640
+ // sequential `1..max_signers` values.
641
+ return serializeIdentifier(HID(utf8ToBytes(s)));
642
+ },
643
+ };
644
+ // RFC 9591 §4.1: nonce_generate() hashes 32 fresh RNG bytes with SerializeScalar(secret).
645
+ const generateNonce = (secret: bigint, rng: RNG = randomBytes) =>
646
+ H3(concatBytes(rng(32), Fn.toBytes(secret)));
647
+
648
+ const getGroupCommitment = (
649
+ GPK: P,
650
+ commitmentList: TArg<NonceCommitments[]>,
651
+ msg: TArg<Uint8Array>
652
+ ) => {
653
+ const CL = commitmentList.map((i) => [
654
+ i.identifier,
655
+ parseIdentifier(i.identifier),
656
+ parsePoint(i.hiding),
657
+ parsePoint(i.binding),
658
+ ]) as [Identifier, bigint, P, P][];
659
+ // RFC 9591 Sections 4.3/4.4/4.5 and 5.2/5.3 treat commitment_list as sorted by identifier.
660
+ CL.sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
661
+ // Encode commitment list
662
+ const Cbytes = [];
663
+ for (const [_, id, hC, bC] of CL)
664
+ Cbytes.push(Fn.toBytes(id), serializePoint(hC), serializePoint(bC));
665
+ const encodedCommitmentHash = H5(concatBytes(...Cbytes));
666
+ const rhoPrefix = concatBytes(serializePoint(GPK), H4(msg), encodedCommitmentHash);
667
+ // Compute binding factors
668
+ const bindingFactors: Record<Identifier, bigint> = {};
669
+ for (const [i, id] of CL) {
670
+ bindingFactors[i] = H1(concatBytes(rhoPrefix, Fn.toBytes(id)));
671
+ }
672
+ const points: P[] = [];
673
+ const scalars: bigint[] = [];
674
+ for (const [i, _, hC, bC] of CL) {
675
+ if (Point.ZERO.equals(hC) || Point.ZERO.equals(bC)) throw new Error('infinity commitment');
676
+ points.push(hC, bC);
677
+ scalars.push(Fn.ONE, bindingFactors[i]);
678
+ }
679
+ const groupCommitment = msm(points, scalars); // GC += hC + bC*bindingFactor
680
+ const identifiers = CL.map((i) => i[1]);
681
+ return { identifiers, groupCommitment, bindingFactors };
682
+ };
683
+ const prepareShare = (
684
+ PK: TArg<Uint8Array>,
685
+ commitmentList: TArg<NonceCommitments[]>,
686
+ msg: TArg<Uint8Array>,
687
+ identifier: Identifier
688
+ ) => {
689
+ // RFC 9591 Sections 4.4/4.5/4.6 feed directly into the Section 5.2 signer computation.
690
+ const GPK = adjustPoint(parsePoint(PK));
691
+ const id = parseIdentifier(identifier);
692
+ const { identifiers, groupCommitment, bindingFactors } = getGroupCommitment(
693
+ GPK,
694
+ commitmentList,
695
+ msg
696
+ );
697
+ const bindingFactor = bindingFactors[identifier];
698
+ const lambda = deriveInterpolatingValue(identifiers, id);
699
+ const challenge = Basic.challenge(groupCommitment, GPK, msg);
700
+ return { lambda, challenge, bindingFactor, groupCommitment };
701
+ };
702
+ Object.freeze(Identifier);
703
+ const frost = {
704
+ Identifier,
705
+ // DKG is Distributed Key Generation, not Trusted Dealer Key Generation.
706
+ DKG: Object.freeze({
707
+ // NOTE: we allow to pass secret scalar from user side,
708
+ // this way it can be derived, instead of random generation
709
+ round1: (
710
+ id: Identifier,
711
+ signers: Signers,
712
+ secret?: TArg<SecretKey>,
713
+ rng: RNG = randomBytes
714
+ ) => {
715
+ validateSigners(signers);
716
+ const idNum = parseIdentifier(id);
717
+ const { coefficients, commitment } = generateSecretPolynomial(
718
+ signers,
719
+ secret,
720
+ undefined,
721
+ rng
722
+ );
723
+ const proofOfKnowledge = ProofOfKnowledge.compute(idNum, coefficients, commitment, rng);
724
+ const commitmentBytes = commitment.map(serializePoint) as TRet<Commitment[]>;
725
+ const round1Public: DKG_Round1 = {
726
+ identifier: serializeIdentifier(idNum),
727
+ commitment: commitmentBytes,
728
+ proofOfKnowledge,
729
+ };
730
+ // store secret information for signing
731
+ const round1Secret: DKG_Secret = {
732
+ identifier: idNum,
733
+ coefficients,
734
+ commitment: commitment.map(serializePoint) as TRet<Point[]>,
735
+ // Copy threshold metadata instead of retaining the caller-owned object by reference.
736
+ signers: { min: signers.min, max: signers.max },
737
+ step: 1,
738
+ };
739
+ return { public: round1Public, secret: round1Secret };
740
+ },
741
+ round2: (
742
+ secret: TArg<DKG_Secret>,
743
+ others: TArg<DKG_Round1[]>
744
+ ): TRet<Record<string, DKG_Round2>> => {
745
+ if (others.length !== secret.signers.max - 1)
746
+ throw new Error('wrong number of round1 packages');
747
+ if (!secret.coefficients || secret.step === 3)
748
+ throw new Error('round3 package used in round2');
749
+ const res: Record<Identifier, DKG_Round2> = {};
750
+ for (const p of others) {
751
+ if (p.commitment.length !== secret.signers.min)
752
+ throw new Error('wrong number of commitments');
753
+ const id = parseIdentifier(p.identifier);
754
+ if (id === secret.identifier) throw new Error('duplicate id=' + serializeIdentifier(id));
755
+
756
+ ProofOfKnowledge.validate(id, p.commitment, p.proofOfKnowledge);
757
+ for (const c of p.commitment) parsePoint(c);
758
+ if (res[p.identifier]) throw new Error('Duplicate id=' + id);
759
+ const signingShare = Fn.toBytes(polynomialEvaluate(id, secret.coefficients));
760
+ res[p.identifier] = {
761
+ identifier: serializeIdentifier(secret.identifier),
762
+ signingShare: signingShare as TRet<Bytes>,
763
+ };
764
+ }
765
+ secret.step = 2;
766
+ return res as TRet<Record<string, DKG_Round2>>;
767
+ },
768
+ round3: (
769
+ secret: TArg<DKG_Secret>,
770
+ round1: TArg<DKG_Round1[]>,
771
+ round2: TArg<DKG_Round2[]>
772
+ ): TRet<Key> => {
773
+ // DKG is outside RFC 9591's signing flow; callers are expected to reuse the same
774
+ // remote round1 packages already accepted in round2, like frost-rs documents.
775
+ if (round1.length !== secret.signers.max - 1)
776
+ throw new Error('wrong length of round1 packages');
777
+ if (!secret.coefficients || secret.step !== 2)
778
+ throw new Error('round2 package used in round3');
779
+ if (round2.length !== round1.length) throw new Error('wrong length of round2 packages');
780
+ const merged: Record<Identifier, TArg<DKG_Round1> & { signingShare?: TArg<Bytes> }> = {};
781
+ for (const r1 of round1) {
782
+ if (!r1.identifier || !r1.commitment) throw new Error('wrong round1 share');
783
+ merged[r1.identifier] = { ...r1 };
784
+ }
785
+ for (const r2 of round2) {
786
+ if (!r2.identifier || !r2.signingShare) throw new Error('wrong round2 share');
787
+ if (!merged[r2.identifier])
788
+ throw new Error('round1 share for ' + r2.identifier + ' is missing');
789
+ merged[r2.identifier].signingShare = r2.signingShare;
790
+ }
791
+ if (Object.keys(merged).length !== round1.length)
792
+ throw new Error('mismatch identifiers between rounds');
793
+ let signingShare = Fn.ZERO;
794
+ if (secret.commitment.length !== secret.signers.min)
795
+ throw new Error('wrong commitments length');
796
+ const localCommitment = secret.commitment.map(parsePoint);
797
+ const localShare = polynomialEvaluate(secret.identifier, secret.coefficients);
798
+ validateSecretShare(secret.identifier, localCommitment, localShare);
799
+ const localCommitmentBytes = localCommitment.map(serializePoint);
800
+ const commitments: Record<Identifier, TArg<Commitment[]>> = {
801
+ [serializeIdentifier(secret.identifier)]: localCommitmentBytes,
802
+ };
803
+ for (const k in merged) {
804
+ const v = merged[k];
805
+ if (!v.signingShare || !v.commitment) throw new Error('mismatch identifiers');
806
+ const id = parseIdentifier(k); // from
807
+ const signingSharePart = Fn.fromBytes(v.signingShare);
808
+ const commitment = v.commitment.map(parsePoint);
809
+ validateSecretShare(secret.identifier, commitment, signingSharePart);
810
+ signingShare = Fn.add(signingShare, signingSharePart);
811
+ const idSer = serializeIdentifier(id);
812
+ if (commitments[idSer]) throw new Error('duplicated id=' + idSer);
813
+ commitments[idSer] = v.commitment;
814
+ }
815
+ signingShare = Fn.add(signingShare, localShare);
816
+ const mergedCommitment = new Array(secret.signers.min).fill(Point.ZERO);
817
+ for (const k in commitments) {
818
+ const v = commitments[k];
819
+ if (v.length !== secret.signers.min) throw new Error('wrong commitments length');
820
+ for (let i = 0; i < v.length; i++)
821
+ mergedCommitment[i] = mergedCommitment[i].add(parsePoint(v[i]));
822
+ }
823
+ const mergedCommitmentBytes = mergedCommitment.map(serializePoint) as TRet<Commitment[]>;
824
+ const verifyingShares: Record<Identifier, Uint8Array> = {};
825
+ for (const k in commitments)
826
+ verifyingShares[k] = serializePoint(evalutateVSS(parseIdentifier(k), mergedCommitment));
827
+ // This is enough to sign stuff
828
+ let res: TRet<Key> = {
829
+ public: {
830
+ signers: { min: secret.signers.min, max: secret.signers.max },
831
+ commitments: mergedCommitmentBytes,
832
+ verifyingShares: Object.fromEntries(
833
+ Object.entries(verifyingShares).map(([k, v]) => [k, v.slice()])
834
+ ),
835
+ },
836
+ secret: {
837
+ identifier: serializeIdentifier(secret.identifier),
838
+ signingShare: Fn.toBytes(signingShare) as TRet<Bytes>,
839
+ },
840
+ };
841
+ if (opts.adjustDKG) res = opts.adjustDKG(res);
842
+ for (let i = 0; i < secret.coefficients.length; i++)
843
+ secret.coefficients[i] -= secret.coefficients[i];
844
+ delete secret.coefficients;
845
+ secret.step = 3;
846
+ return res;
847
+ },
848
+ clean(secret: TArg<DKG_Secret>) {
849
+ // Instead of replacing secret bigint with another (zero?), we subtract it from itself
850
+ // in the hope that JIT will modify it inplace, instead of creating new value.
851
+ // This is unverified and may not work, but it is best we can do in regard of bigints.
852
+ secret.identifier -= secret.identifier;
853
+ if (secret.coefficients) {
854
+ for (let i = 0; i < secret.coefficients.length; i++)
855
+ secret.coefficients[i] -= secret.coefficients[i];
856
+ }
857
+ // for (const c of secret.commitment) c.fill(0);
858
+ secret.step = 3;
859
+ },
860
+ }),
861
+ // Trusted dealer setup
862
+ // Generates keys for all participants
863
+ trustedDealer(
864
+ signers: Signers,
865
+ identifiers?: Identifier[],
866
+ secret?: TArg<SecretKey>,
867
+ rng: RNG = randomBytes
868
+ ): TRet<DealerShares> {
869
+ // if no identifiers provided, we generated default identifiers
870
+ validateSigners(signers);
871
+ if (identifiers === undefined) {
872
+ identifiers = [];
873
+ for (let i = 1; i <= signers.max; i++) identifiers.push(Identifier.fromNumber(i));
874
+ } else {
875
+ if (!Array.isArray(identifiers) || identifiers.length !== signers.max)
876
+ throw new Error('identifiers should be array of ' + signers.max);
877
+ }
878
+ const identifierNums: Record<Identifier, bigint> = {};
879
+ for (const id of identifiers) {
880
+ const idNum = parseIdentifier(id);
881
+ if (id in identifierNums) throw new Error('duplicated id=' + id);
882
+ identifierNums[id] = idNum;
883
+ }
884
+ const sp = generateSecretPolynomial(signers, secret, undefined, rng);
885
+ const commitmentBytes = sp.commitment.map(serializePoint);
886
+ const secretShares: Record<Identifier, FrostSecret> = {};
887
+ const verifyingShares: Record<Identifier, Uint8Array> = {};
888
+ for (const id of identifiers) {
889
+ const signingShare = polynomialEvaluate(identifierNums[id], sp.coefficients);
890
+ verifyingShares[id] = serializePoint(Point.BASE.multiply(signingShare));
891
+ secretShares[id] = {
892
+ identifier: id,
893
+ signingShare: Fn.toBytes(signingShare) as TRet<Bytes>,
894
+ };
895
+ }
896
+ return {
897
+ public: {
898
+ signers: { min: signers.min, max: signers.max },
899
+ commitments: commitmentBytes,
900
+ verifyingShares,
901
+ },
902
+ secretShares,
903
+ } as TRet<DealerShares>;
904
+ },
905
+ // Validate secret (from trusted dealer or DKG)
906
+ validateSecret(secret: TArg<FrostSecret>, pub: TArg<FrostPublic>) {
907
+ const id = parseIdentifier(secret.identifier);
908
+ const commitment = pub.commitments.map(parsePoint);
909
+ const signingShare = Fn.fromBytes(secret.signingShare);
910
+ validateSecretShare(id, commitment, signingShare);
911
+ },
912
+ // Actual signing
913
+ // Round 1: each participant commit to nonces
914
+ // Nonces kept private, commitments sent to coordinator (or every other participant)
915
+ // NOTE: we don't need the message at this point, which lets a coordinator
916
+ // keep multiple nonce commitments per participant in advance and skip
917
+ // round1 for signing.
918
+ // But then each participant needs to remember generated shares
919
+ commit(secret: TArg<FrostSecret>, rng: RNG = randomBytes): TRet<GenNonce> {
920
+ const secretScalar = Fn.fromBytes(secret.signingShare);
921
+ const hiding = generateNonce(secretScalar, rng);
922
+ const binding = generateNonce(secretScalar, rng);
923
+ const nonces = { hiding: Fn.toBytes(hiding), binding: Fn.toBytes(binding) };
924
+ return { nonces, commitments: nonceCommitments(secret.identifier, nonces) } as TRet<GenNonce>;
925
+ },
926
+ // Round2: sign. Each participant creates a signature share from the secret
927
+ // and the selected nonce commitments.
928
+ signShare(
929
+ secret: TArg<FrostSecret>,
930
+ pub: TArg<FrostPublic>,
931
+ nonces: TArg<Nonces>,
932
+ commitmentList: TArg<NonceCommitments[]>,
933
+ msg: TArg<Uint8Array>
934
+ ): TRet<Uint8Array> {
935
+ validateCommitmentsNum(pub.signers, commitmentList.length);
936
+ const hidingNonce0 = Fn.fromBytes(nonces.hiding);
937
+ const bindingNonce0 = Fn.fromBytes(nonces.binding);
938
+ if (Fn.is0(hidingNonce0) || Fn.is0(bindingNonce0))
939
+ throw new Error('signing nonces already used');
940
+ // Reject a coordinator-assigned commitment pair that does not match the signer's own nonce
941
+ // pair. This must happen before suite-specific nonce adjustment; secp256k1-tr may negate the
942
+ // actual signing nonces later, but the coordinator still assigns the original commitments.
943
+ const expectedCommitment = {
944
+ identifier: secret.identifier,
945
+ hiding: serializePoint(Point.BASE.multiply(hidingNonce0)),
946
+ binding: serializePoint(Point.BASE.multiply(bindingNonce0)),
947
+ };
948
+ const commitment = commitmentList.find((i) => i.identifier === secret.identifier);
949
+ if (!commitment) throw new Error('missing signer commitment');
950
+ if (
951
+ bytesToHex(commitment.hiding) !== bytesToHex(expectedCommitment.hiding) ||
952
+ bytesToHex(commitment.binding) !== bytesToHex(expectedCommitment.binding)
953
+ )
954
+ throw new Error('incorrect signer commitment');
955
+ if (opts.adjustSecret) secret = opts.adjustSecret(secret, pub);
956
+ if (opts.adjustPublic) pub = opts.adjustPublic(pub);
957
+ const SK = Fn.fromBytes(secret.signingShare);
958
+ const { lambda, challenge, bindingFactor, groupCommitment } = prepareShare(
959
+ pub.commitments[0],
960
+ commitmentList,
961
+ msg,
962
+ secret.identifier
963
+ );
964
+ const N = opts.adjustNonces ? opts.adjustNonces(groupCommitment, nonces) : nonces;
965
+ const hidingNonce = opts.adjustNonces ? Fn.fromBytes(N.hiding) : hidingNonce0;
966
+ const bindingNonce = opts.adjustNonces ? Fn.fromBytes(N.binding) : bindingNonce0;
967
+ const t = Fn.mul(Fn.mul(lambda, SK), challenge); // challenge * lambda * SK
968
+ const t2 = Fn.mul(bindingNonce, bindingFactor); // bindingNonce * bindingFactor
969
+ const r = Fn.toBytes(Fn.add(Fn.add(hidingNonce, t2), t)); // t + t2 + hidingNonce
970
+ // RFC 9591 round-one commitments are one-time-use, and round two must use the nonce
971
+ // corresponding to the published commitment. This API returns mutable local nonce bytes,
972
+ // so consume them after a successful signShare() call: later all-zero reuse fails closed.
973
+ nonces.hiding.fill(0);
974
+ nonces.binding.fill(0);
975
+ return r as TRet<Uint8Array>;
976
+ },
977
+ // Each participant (or coordinator) can verify signatures from other participants
978
+ verifyShare(
979
+ pub: TArg<FrostPublic>,
980
+ commitmentList: TArg<NonceCommitments[]>,
981
+ msg: TArg<Uint8Array>,
982
+ identifier: Identifier,
983
+ sigShare: TArg<Uint8Array>
984
+ ) {
985
+ if (opts.adjustPublic) pub = opts.adjustPublic(pub);
986
+ const comm = commitmentList.find((i) => i.identifier === identifier);
987
+ if (!comm) throw new Error('cannot find identifier commitment');
988
+ const PK = parsePoint(pub.verifyingShares[identifier]);
989
+ const hidingNonceCommitment = parsePoint(comm.hiding);
990
+ const bindingNonceCommitment = parsePoint(comm.binding);
991
+ const { lambda, challenge, bindingFactor, groupCommitment } = prepareShare(
992
+ pub.commitments[0],
993
+ commitmentList,
994
+ msg,
995
+ identifier
996
+ );
997
+ // hC + bC * bF
998
+ let commShare = hidingNonceCommitment.add(bindingNonceCommitment.multiply(bindingFactor));
999
+ if (opts.adjustGroupCommitmentShare)
1000
+ commShare = opts.adjustGroupCommitmentShare(groupCommitment, commShare);
1001
+ const l = Point.BASE.multiply(Fn.fromBytes(sigShare)); // sigShare*G
1002
+ // commShare + PK * (challenge * lambda)
1003
+ const r = commShare.add(PK.multiply(Fn.mul(challenge, lambda)));
1004
+ return l.equals(r);
1005
+ },
1006
+ // Aggregate multiple signature shares into groupSignature
1007
+ aggregate(
1008
+ pub: TArg<FrostPublic>,
1009
+ commitmentList: TArg<NonceCommitments[]>,
1010
+ msg: TArg<Uint8Array>,
1011
+ sigShares: TArg<Record<Identifier, Uint8Array>>
1012
+ ): TRet<Uint8Array> {
1013
+ if (opts.adjustPublic) pub = opts.adjustPublic(pub);
1014
+ try {
1015
+ validateCommitmentsNum(pub.signers, commitmentList.length);
1016
+ } catch {
1017
+ throw new AggErr('aggregation failed', []);
1018
+ }
1019
+ const ids = commitmentList.map((i) => i.identifier);
1020
+ if (ids.length !== Object.keys(sigShares).length) throw new AggErr('aggregation failed', []);
1021
+ for (const id of ids) {
1022
+ if (!(id in sigShares) || !(id in pub.verifyingShares))
1023
+ throw new AggErr('aggregation failed', []);
1024
+ }
1025
+ const GPK = parsePoint(pub.commitments[0]);
1026
+ const { groupCommitment } = getGroupCommitment(GPK, commitmentList, msg);
1027
+ let z = Fn.ZERO;
1028
+ // RFC 9591 Section 5.3 aggregates by summing the validated signature shares.
1029
+ for (const id of ids) z = Fn.add(z, Fn.fromBytes(sigShares[id])); // z += zi
1030
+ if (!Basic.verify(msg, groupCommitment, z, GPK)) {
1031
+ const cheaters = [];
1032
+ for (const id of ids) {
1033
+ if (!this.verifyShare(pub, commitmentList, msg, id, sigShares[id])) cheaters.push(id);
1034
+ }
1035
+ throw new AggErr('aggregation failed', cheaters);
1036
+ }
1037
+ return Signature.encode(groupCommitment, z);
1038
+ },
1039
+ // Basic sign/verify using single key
1040
+ sign(msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> {
1041
+ let sk = Fn.fromBytes(secretKey);
1042
+ // Taproot single-key signing needs the same scalar normalization as threshold keys.
1043
+ if (opts.adjustScalar) sk = opts.adjustScalar(sk);
1044
+ const [R, z] = Basic.sign(msg, sk);
1045
+ return Signature.encode(R, z);
1046
+ },
1047
+ verify(sig: TArg<Signature>, msg: TArg<Uint8Array>, publicKey: TArg<Uint8Array>) {
1048
+ const PK = opts.parsePublicKey ? opts.parsePublicKey(publicKey) : parsePoint(publicKey);
1049
+ const { R, z } = Signature.decode(sig);
1050
+ return Basic.verify(msg, R, z, PK);
1051
+ },
1052
+ // Combine multiple secret shares to restore secret
1053
+ combineSecret(shares: TArg<FrostSecret[]>, signers: Signers): TRet<Uint8Array> {
1054
+ validateSigners(signers);
1055
+ if (!Array.isArray(shares) || shares.length < signers.min)
1056
+ throw new Error('wrong secret shares array');
1057
+ const points = [];
1058
+ const seen: Record<Identifier, boolean> = {};
1059
+ // Interpolate over the full provided share set and reject duplicate identifiers.
1060
+ for (const s of shares) {
1061
+ const idNum = parseIdentifier(s.identifier);
1062
+ const id = serializeIdentifier(idNum);
1063
+ if (seen[id]) throw new Error('duplicated id=' + id);
1064
+ seen[id] = true;
1065
+ points.push([idNum, Fn.fromBytes(s.signingShare)]);
1066
+ }
1067
+ const xCoords = points.map(([x]) => x);
1068
+ let res = Fn.ZERO;
1069
+ for (const [x, y] of points)
1070
+ res = Fn.add(res, Fn.mul(y, deriveInterpolatingValue(xCoords, x)));
1071
+ return Fn.toBytes(res) as TRet<Uint8Array>;
1072
+ },
1073
+ // Utils
1074
+ utils: Object.freeze({
1075
+ Fn, // NOTE: we re-export it here because it may be different from Point.Fn (ed448 is fun!)
1076
+ // Test RNG overrides still go through noble's non-zero scalar derivation; this is not a raw
1077
+ // "bytes become scalar" escape hatch.
1078
+ randomScalar: (rng: RNG = randomBytes) =>
1079
+ Fn.toBytes(genPointScalarPair(rng).scalar) as TRet<Uint8Array>,
1080
+ generateSecretPolynomial: (
1081
+ signers: Signers,
1082
+ secret?: TArg<Uint8Array>,
1083
+ coeffs?: bigint[],
1084
+ rng?: RNG
1085
+ ) => {
1086
+ const res = generateSecretPolynomial(signers, secret, coeffs, rng);
1087
+ return { ...res, commitment: res.commitment.map(serializePoint) as TRet<Point[]> };
1088
+ },
1089
+ }),
1090
+ };
1091
+ return Object.freeze(frost) as TRet<FROST>;
1092
+ }