@noble/curves 2.0.1 → 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 +82 -22
  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 +322 -10
  15. package/abstract/fft.d.ts.map +1 -1
  16. package/abstract/fft.js +154 -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 +125 -14
  59. package/ed25519.d.ts.map +1 -1
  60. package/ed25519.js +202 -40
  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 +11 -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 +350 -67
  82. package/src/abstract/curve.ts +327 -44
  83. package/src/abstract/edwards.ts +367 -143
  84. package/src/abstract/fft.ts +369 -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 +227 -55
  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
@@ -7,11 +7,13 @@
7
7
  * @module
8
8
  */
9
9
  /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
10
- import { asafenumber, bitGet, validateObject } from '../utils.ts';
10
+ import { asafenumber, bitGet, validateObject, type TArg, type TRet } from '../utils.ts';
11
11
  import { FpInvertBatch, FpPow, type IField, validateField } from './modular.ts';
12
12
 
13
13
  // Grain LFSR (Linear-Feedback Shift Register): https://eprint.iacr.org/2009/109.pdf
14
14
  function grainLFSR(state: number[]): () => boolean {
15
+ // Advances the caller-provided 80-entry state array in place; only the length
16
+ // is checked here, so entries are assumed to already be bits.
15
17
  let pos = 0;
16
18
  if (state.length !== 80) throw new Error('grainLFRS: wrong state length, should be 80 bits');
17
19
  const getBit = (): boolean => {
@@ -33,15 +35,21 @@ function grainLFSR(state: number[]): () => boolean {
33
35
  };
34
36
  }
35
37
 
38
+ /** Core Poseidon permutation parameters shared by all variants. */
36
39
  export type PoseidonBasicOpts = {
40
+ /** Prime field used by the permutation. */
37
41
  Fp: IField<bigint>;
38
- t: number; // t = rate + capacity
42
+ /** Poseidon width `t = rate + capacity`. */
43
+ t: number;
44
+ /** Number of full S-box rounds. */
39
45
  roundsFull: number;
46
+ /** Number of partial S-box rounds. */
40
47
  roundsPartial: number;
48
+ /** Whether to use the inverse S-box variant. */
41
49
  isSboxInverse?: boolean;
42
50
  };
43
51
 
44
- function assertValidPosOpts(opts: PoseidonBasicOpts) {
52
+ function assertValidPosOpts(opts: TArg<PoseidonBasicOpts>) {
45
53
  const { Fp, roundsFull } = opts;
46
54
  validateField(Fp);
47
55
  validateObject(
@@ -59,10 +67,11 @@ function assertValidPosOpts(opts: PoseidonBasicOpts) {
59
67
  asafenumber(opts[k], k);
60
68
  if (opts[k] < 1) throw new Error('invalid number ' + k);
61
69
  }
70
+ // Poseidon splits full rounds as `R_F / 2`, then partial rounds, then `R_F / 2` again.
62
71
  if (roundsFull & 1) throw new Error('roundsFull is not even' + roundsFull);
63
72
  }
64
73
 
65
- function poseidonGrain(opts: PoseidonBasicOpts) {
74
+ function poseidonGrain(opts: TArg<PoseidonBasicOpts>) {
66
75
  assertValidPosOpts(opts);
67
76
  const { Fp } = opts;
68
77
  const state = Array(80).fill(1);
@@ -72,6 +81,12 @@ function poseidonGrain(opts: PoseidonBasicOpts) {
72
81
  };
73
82
  const _0n = BigInt(0);
74
83
  const _1n = BigInt(1);
84
+ // The Grain seed layout is fixed-width: `Fp.BITS` and `t` use 12 bits,
85
+ // `roundsFull` and `roundsPartial` use 10, so larger values are truncated here.
86
+ // This is intentional for compatibility with snarkVM / arkworks PoseidonGrainLFSR:
87
+ // they write the same fixed-width seed fields without range checks, then still consume
88
+ // the LFSR using the caller-provided round count for ARK/MDS generation.
89
+ // Normalizing or rejecting here would diverge from those implementations.
75
90
  writeBits(_1n, 2); // prime field
76
91
  writeBits(opts.isSboxInverse ? _1n : _0n, 4); // b2..b5
77
92
  writeBits(BigInt(Fp.BITS), 12); // b6..b17
@@ -98,7 +113,9 @@ function poseidonGrain(opts: PoseidonBasicOpts) {
98
113
  };
99
114
  }
100
115
 
116
+ /** Poseidon settings used by the Grain-LFSR constant generator. */
101
117
  export type PoseidonGrainOpts = PoseidonBasicOpts & {
118
+ /** S-box power used while generating constants. */
102
119
  sboxPower?: number;
103
120
  };
104
121
 
@@ -106,9 +123,32 @@ type PoseidonConstants = { mds: bigint[][]; roundConstants: bigint[][] };
106
123
 
107
124
  // NOTE: this is not standard but used often for constant generation for poseidon
108
125
  // (grain LFRS-like structure)
109
- export function grainGenConstants(opts: PoseidonGrainOpts, skipMDS: number = 0): PoseidonConstants {
126
+ /**
127
+ * @param opts - Poseidon grain options. See {@link PoseidonGrainOpts}.
128
+ * @param skipMDS - Number of MDS samples to skip.
129
+ * @returns Generated constants.
130
+ * @throws If the generated MDS matrix contains a zero denominator. {@link Error}
131
+ * @example
132
+ * Generate Poseidon round constants and an MDS matrix from the Grain LFSR.
133
+ *
134
+ * ```ts
135
+ * import { grainGenConstants } from '@noble/curves/abstract/poseidon.js';
136
+ * import { Field } from '@noble/curves/abstract/modular.js';
137
+ * const Fp = Field(17n);
138
+ * const constants = grainGenConstants({ Fp, t: 2, roundsFull: 8, roundsPartial: 8 });
139
+ * ```
140
+ */
141
+ export function grainGenConstants(
142
+ opts: TArg<PoseidonGrainOpts>,
143
+ skipMDS: number = 0
144
+ ): PoseidonConstants {
110
145
  const { Fp, t, roundsFull, roundsPartial } = opts;
146
+ // `skipMDS` counts how many candidate matrices to discard before taking one.
147
+ asafenumber(skipMDS, 'skipMDS');
148
+ if (skipMDS < 0) throw new Error('invalid number skipMDS');
111
149
  const rounds = roundsFull + roundsPartial;
150
+ // `sboxPower` is carried in the opts shape for Poseidon compatibility, but
151
+ // Grain constant generation here only depends on field/size/round counts/inverse flag.
112
152
  const sample = poseidonGrain(opts);
113
153
  const roundConstants: bigint[][] = [];
114
154
  for (let r = 0; r < rounds; r++) roundConstants.push(sample(t, true));
@@ -131,24 +171,46 @@ export function grainGenConstants(opts: PoseidonGrainOpts, skipMDS: number = 0):
131
171
  return { roundConstants, mds };
132
172
  }
133
173
 
174
+ /** Fully specified Poseidon permutation options with explicit constants. */
134
175
  export type PoseidonOpts = PoseidonBasicOpts &
135
176
  PoseidonConstants & {
177
+ /** S-box power used by the permutation. */
136
178
  sboxPower?: number;
179
+ /** Whether to reverse the partial-round S-box index. */
137
180
  reversePartialPowIdx?: boolean; // Hack for stark
138
181
  };
139
182
 
140
- export function validateOpts(opts: PoseidonOpts): Readonly<{
141
- rounds: number;
142
- sboxFn: (n: bigint) => bigint;
143
- roundConstants: bigint[][];
144
- mds: bigint[][];
145
- Fp: IField<bigint>;
146
- t: number;
147
- roundsFull: number;
148
- roundsPartial: number;
149
- sboxPower?: number;
150
- reversePartialPowIdx?: boolean; // Hack for stark
151
- }> {
183
+ /**
184
+ * @param opts - Poseidon options. See {@link PoseidonOpts}.
185
+ * @returns Normalized poseidon options.
186
+ * @throws If the Poseidon options, constants, or MDS matrix are invalid. {@link Error}
187
+ * @example
188
+ * Validate generated constants before constructing a permutation.
189
+ *
190
+ * ```ts
191
+ * import { grainGenConstants, validateOpts } from '@noble/curves/abstract/poseidon.js';
192
+ * import { Field } from '@noble/curves/abstract/modular.js';
193
+ * const Fp = Field(17n);
194
+ * const constants = grainGenConstants({ Fp, t: 2, roundsFull: 8, roundsPartial: 8 });
195
+ * const opts = validateOpts({ ...constants, Fp, t: 2, roundsFull: 8, roundsPartial: 8, sboxPower: 3 });
196
+ * ```
197
+ */
198
+ export function validateOpts(opts: TArg<PoseidonOpts>): TRet<
199
+ Readonly<{
200
+ rounds: number;
201
+ sboxFn: (n: bigint) => bigint;
202
+ roundConstants: bigint[][];
203
+ mds: bigint[][];
204
+ Fp: IField<bigint>;
205
+ t: number;
206
+ roundsFull: number;
207
+ roundsPartial: number;
208
+ sboxPower?: number;
209
+ reversePartialPowIdx?: boolean; // Hack for stark
210
+ }>
211
+ > {
212
+ // This only normalizes shapes and field membership for the provided constants;
213
+ // it does not prove the stronger MDS/security criteria discussed in the specs.
152
214
  assertValidPosOpts(opts);
153
215
  const { Fp, mds, reversePartialPowIdx: rev, roundConstants: rc } = opts;
154
216
  const { roundsFull, roundsPartial, sboxPower, t } = opts;
@@ -160,6 +222,8 @@ export function validateOpts(opts: PoseidonOpts): Readonly<{
160
222
  throw new Error('invalid MDS matrix row: ' + mdsRow);
161
223
  return mdsRow.map((i) => {
162
224
  if (typeof i !== 'bigint') throw new Error('invalid MDS matrix bigint: ' + i);
225
+ // Hardcoded Poseidon MDS matrices often use signed entries like `-1`;
226
+ // accept bigint representatives here and reduce them into the field.
163
227
  return Fp.create(i);
164
228
  });
165
229
  });
@@ -179,6 +243,9 @@ export function validateOpts(opts: PoseidonOpts): Readonly<{
179
243
  return Fp.create(i);
180
244
  });
181
245
  });
246
+ // Freeze nested constants so exported handles cannot retune a live permutation instance.
247
+ const freezeRows = (rows: bigint[][]) =>
248
+ Object.freeze(rows.map((row) => Object.freeze(row))) as unknown as bigint[][];
182
249
 
183
250
  if (!sboxPower || ![3, 5, 7, 17].includes(sboxPower)) throw new Error('invalid sboxPower');
184
251
  const _sboxPower = BigInt(sboxPower);
@@ -187,16 +254,50 @@ export function validateOpts(opts: PoseidonOpts): Readonly<{
187
254
  if (sboxPower === 3) sboxFn = (n: bigint) => Fp.mul(Fp.sqrN(n), n);
188
255
  else if (sboxPower === 5) sboxFn = (n: bigint) => Fp.mul(Fp.sqrN(Fp.sqrN(n)), n);
189
256
 
190
- return Object.freeze({ ...opts, rounds, sboxFn, roundConstants, mds: _mds });
257
+ return Object.freeze({
258
+ ...opts,
259
+ rounds,
260
+ sboxFn,
261
+ roundConstants: freezeRows(roundConstants),
262
+ mds: freezeRows(_mds),
263
+ }) as TRet<
264
+ Readonly<{
265
+ rounds: number;
266
+ sboxFn: (n: bigint) => bigint;
267
+ roundConstants: bigint[][];
268
+ mds: bigint[][];
269
+ Fp: IField<bigint>;
270
+ t: number;
271
+ roundsFull: number;
272
+ roundsPartial: number;
273
+ sboxPower?: number;
274
+ reversePartialPowIdx?: boolean;
275
+ }>
276
+ >;
191
277
  }
192
278
 
279
+ /**
280
+ * @param rc - Flattened round constants.
281
+ * @param t - Poseidon width.
282
+ * @returns Constants grouped by round.
283
+ * @throws If the width or flattened constant array is invalid. {@link Error}
284
+ * @example
285
+ * Regroup a flat constant list into per-round chunks.
286
+ *
287
+ * ```ts
288
+ * const rounds = splitConstants([1n, 2n, 3n, 4n], 2);
289
+ * ```
290
+ */
193
291
  export function splitConstants(rc: bigint[], t: number): bigint[][] {
194
- if (typeof t !== 'number') throw new Error('poseidonSplitConstants: invalid t');
292
+ asafenumber(t, 't');
293
+ if (t < 1) throw new Error('poseidonSplitConstants: invalid t');
195
294
  if (!Array.isArray(rc) || rc.length % t) throw new Error('poseidonSplitConstants: invalid rc');
196
295
  const res = [];
197
296
  let tmp = [];
198
297
  for (let i = 0; i < rc.length; i++) {
199
- tmp.push(rc[i]);
298
+ const c = rc[i];
299
+ if (typeof c !== 'bigint') throw new Error('invalid bigint=' + c);
300
+ tmp.push(c);
200
301
  if (tmp.length === t) {
201
302
  res.push(tmp);
202
303
  tmp = [];
@@ -205,13 +306,34 @@ export function splitConstants(rc: bigint[], t: number): bigint[][] {
205
306
  return res;
206
307
  }
207
308
 
309
+ /**
310
+ * Poseidon permutation callable.
311
+ * @param values - Poseidon state vector. Non-canonical bigints are normalized with `Fp.create(...)`.
312
+ * @returns Permuted state vector.
313
+ */
208
314
  export type PoseidonFn = {
209
315
  (values: bigint[]): bigint[];
210
- // For verification in tests
316
+ /** Round constants captured by the permutation instance. */
211
317
  roundConstants: bigint[][];
212
318
  };
213
319
  /** Poseidon NTT-friendly hash. */
214
- export function poseidon(opts: PoseidonOpts): PoseidonFn {
320
+ /**
321
+ * @param opts - Poseidon options. See {@link PoseidonOpts}.
322
+ * @returns Poseidon permutation.
323
+ * @throws If the Poseidon options or state vector are invalid. {@link Error}
324
+ * @example
325
+ * Build a Poseidon permutation from validated parameters and constants.
326
+ *
327
+ * ```ts
328
+ * import { grainGenConstants, poseidon } from '@noble/curves/abstract/poseidon.js';
329
+ * import { Field } from '@noble/curves/abstract/modular.js';
330
+ * const Fp = Field(17n);
331
+ * const constants = grainGenConstants({ Fp, t: 2, roundsFull: 8, roundsPartial: 8 });
332
+ * const hash = poseidon({ ...constants, Fp, t: 2, roundsFull: 8, roundsPartial: 8, sboxPower: 3 });
333
+ * const state = hash([1n, 2n]);
334
+ * ```
335
+ */
336
+ export function poseidon(opts: TArg<PoseidonOpts>): PoseidonFn {
215
337
  const _opts = validateOpts(opts);
216
338
  const { Fp, mds, roundConstants, rounds: totalRounds, roundsPartial, sboxFn, t } = _opts;
217
339
  const halfRoundsFull = _opts.roundsFull / 2;
@@ -228,10 +350,13 @@ export function poseidon(opts: PoseidonOpts): PoseidonFn {
228
350
  const poseidonHash = function poseidonHash(values: bigint[]) {
229
351
  if (!Array.isArray(values) || values.length !== t)
230
352
  throw new Error('invalid values, expected array of bigints with length ' + t);
231
- values = values.map((i) => {
353
+ // `.map()` skips sparse holes, which would leak `undefined` into round math below.
354
+ values = values.slice();
355
+ for (let j = 0; j < values.length; j++) {
356
+ const i = values[j];
232
357
  if (typeof i !== 'bigint') throw new Error('invalid bigint=' + i);
233
- return Fp.create(i);
234
- });
358
+ values[j] = Fp.create(i);
359
+ }
235
360
  let lastRound = 0;
236
361
  // Apply r_f/2 full rounds.
237
362
  for (let i = 0; i < halfRoundsFull; i++) values = poseidonRound(values, true, lastRound++);
@@ -242,12 +367,34 @@ export function poseidon(opts: PoseidonOpts): PoseidonFn {
242
367
 
243
368
  if (lastRound !== totalRounds) throw new Error('invalid number of rounds');
244
369
  return values;
245
- };
370
+ } as PoseidonFn;
246
371
  // For verification in tests
247
- poseidonHash.roundConstants = roundConstants;
372
+ Object.defineProperty(poseidonHash, 'roundConstants', {
373
+ value: roundConstants,
374
+ enumerable: true,
375
+ });
248
376
  return poseidonHash;
249
377
  }
250
378
 
379
+ /**
380
+ * @param Fp - Field implementation.
381
+ * @param rate - Sponge rate.
382
+ * @param capacity - Sponge capacity.
383
+ * @param hash - Poseidon permutation.
384
+ * @example
385
+ * Wrap one Poseidon permutation in a sponge interface.
386
+ *
387
+ * ```ts
388
+ * import { PoseidonSponge, grainGenConstants, poseidon } from '@noble/curves/abstract/poseidon.js';
389
+ * import { Field } from '@noble/curves/abstract/modular.js';
390
+ * const Fp = Field(17n);
391
+ * const constants = grainGenConstants({ Fp, t: 2, roundsFull: 8, roundsPartial: 8 });
392
+ * const hash = poseidon({ ...constants, Fp, t: 2, roundsFull: 8, roundsPartial: 8, sboxPower: 3 });
393
+ * const sponge = new PoseidonSponge(Fp, 1, 1, hash);
394
+ * sponge.absorb([1n]);
395
+ * const out = sponge.squeeze(1);
396
+ * ```
397
+ */
251
398
  export class PoseidonSponge {
252
399
  private Fp: IField<bigint>;
253
400
  readonly rate: number;
@@ -258,14 +405,24 @@ export class PoseidonSponge {
258
405
  private isAbsorbing = true;
259
406
 
260
407
  constructor(Fp: IField<bigint>, rate: number, capacity: number, hash: PoseidonFn) {
408
+ const width = spongeShape(rate, capacity);
409
+ // The direct constructor accepts an arbitrary permutation hook, but callers still
410
+ // need to preserve the `PoseidonFn.roundConstants` width metadata. Reject width
411
+ // mismatches here instead of deferring them until the first `process()` call.
412
+ if (width !== hash.roundConstants[0]?.length)
413
+ throw new Error(
414
+ `invalid sponge width: expected ${hash.roundConstants[0]?.length}, got ${width}`
415
+ );
261
416
  this.Fp = Fp;
262
417
  this.hash = hash;
263
418
  this.rate = rate;
264
419
  this.capacity = capacity;
265
- this.state = new Array(rate + capacity);
420
+ this.state = new Array(width);
266
421
  this.clean();
267
422
  }
268
423
  private process(): void {
424
+ // The permutation is expected to return an owned state array. If callers inject a custom
425
+ // hook that reuses external storage, `clean()` will zero that shared buffer too.
269
426
  this.state = this.hash(this.state);
270
427
  }
271
428
  absorb(input: bigint[]): void {
@@ -285,6 +442,10 @@ export class PoseidonSponge {
285
442
  }
286
443
  }
287
444
  squeeze(count: number): bigint[] {
445
+ // Rust oracles use unsigned counts. In JS we keep `squeeze(0) => []` for
446
+ // compatibility, but still reject negative/fractional counts explicitly.
447
+ asafenumber(count, 'count');
448
+ if (count < 0) throw new Error('invalid number count');
288
449
  const res: bigint[] = [];
289
450
  while (res.length < count) {
290
451
  if (this.isAbsorbing || this.pos === this.rate) {
@@ -305,29 +466,68 @@ export class PoseidonSponge {
305
466
  clone(): PoseidonSponge {
306
467
  const c = new PoseidonSponge(this.Fp, this.rate, this.capacity, this.hash);
307
468
  c.pos = this.pos;
469
+ c.isAbsorbing = this.isAbsorbing;
308
470
  c.state = [...this.state];
309
471
  return c;
310
472
  }
311
473
  }
312
474
 
475
+ /** Options for the non-standard but commonly used Poseidon sponge wrapper. */
313
476
  export type PoseidonSpongeOpts = Omit<PoseidonOpts, 't'> & {
477
+ /** Sponge rate. */
314
478
  rate: number;
479
+ /** Sponge capacity. */
315
480
  capacity: number;
316
481
  };
317
482
 
483
+ const spongeShape = (rate: number, capacity: number) => {
484
+ asafenumber(rate, 'rate');
485
+ asafenumber(capacity, 'capacity');
486
+ // A sponge with zero rate cannot absorb or squeeze any field elements.
487
+ if (rate < 1) throw new Error('invalid number rate');
488
+ // Negative capacity can accidentally keep `rate + capacity` coherent while still
489
+ // producing a nonsensical sponge shape.
490
+ if (capacity < 0) throw new Error('invalid number capacity');
491
+ return rate + capacity;
492
+ };
493
+
318
494
  /**
319
495
  * The method is not defined in spec, but nevertheless used often.
320
496
  * Check carefully for compatibility: there are many edge cases, like absorbing an empty array.
321
497
  * We cross-test against:
322
- * - https://github.com/ProvableHQ/snarkVM/tree/staging/algorithms
323
- * - https://github.com/arkworks-rs/crypto-primitives/tree/main
498
+ * - {@link https://github.com/ProvableHQ/snarkVM/tree/staging/algorithms | snarkVM algorithms}
499
+ * - {@link https://github.com/arkworks-rs/crypto-primitives/tree/main | arkworks crypto-primitives}
500
+ * @param opts - Sponge options. See {@link PoseidonSpongeOpts}.
501
+ * @returns Factory for sponge instances.
502
+ * @throws If the sponge dimensions or backing permutation options are invalid. {@link Error}
503
+ * @example
504
+ * Use the sponge helper to absorb several field elements and squeeze one digest.
505
+ *
506
+ * ```ts
507
+ * import { grainGenConstants, poseidonSponge } from '@noble/curves/abstract/poseidon.js';
508
+ * import { Field } from '@noble/curves/abstract/modular.js';
509
+ * const Fp = Field(17n);
510
+ * const constants = grainGenConstants({ Fp, t: 2, roundsFull: 8, roundsPartial: 8 });
511
+ * const makeSponge = poseidonSponge({
512
+ * ...constants,
513
+ * Fp,
514
+ * rate: 1,
515
+ * capacity: 1,
516
+ * roundsFull: 8,
517
+ * roundsPartial: 8,
518
+ * sboxPower: 3,
519
+ * });
520
+ * const sponge = makeSponge();
521
+ * sponge.absorb([1n]);
522
+ * const out = sponge.squeeze(1);
523
+ * ```
324
524
  */
325
- export function poseidonSponge(opts: PoseidonSpongeOpts): () => PoseidonSponge {
326
- for (const k of ['rate', 'capacity'] as const) asafenumber(opts[k], k);
525
+ export function poseidonSponge(opts: TArg<PoseidonSpongeOpts>): TRet<() => PoseidonSponge> {
327
526
  const { rate, capacity } = opts;
328
- const t = opts.rate + opts.capacity;
329
- // Re-use hash instance between multiple instances
527
+ const t = spongeShape(rate, capacity);
528
+ // Re-use one hash instance between sponge instances; isolation depends on
529
+ // poseidon(...) itself staying immutable and not carrying mutable call state.
330
530
  const hash = poseidon({ ...opts, t });
331
531
  const { Fp } = opts;
332
- return () => new PoseidonSponge(Fp, rate, capacity, hash);
532
+ return (() => new PoseidonSponge(Fp, rate, capacity, hash)) as TRet<() => PoseidonSponge>;
333
533
  }