@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.
- package/README.md +214 -122
- package/abstract/bls.d.ts +299 -16
- package/abstract/bls.d.ts.map +1 -1
- package/abstract/bls.js +89 -24
- package/abstract/bls.js.map +1 -1
- package/abstract/curve.d.ts +274 -27
- package/abstract/curve.d.ts.map +1 -1
- package/abstract/curve.js +177 -23
- package/abstract/curve.js.map +1 -1
- package/abstract/edwards.d.ts +166 -30
- package/abstract/edwards.d.ts.map +1 -1
- package/abstract/edwards.js +221 -86
- package/abstract/edwards.js.map +1 -1
- package/abstract/fft.d.ts +327 -10
- package/abstract/fft.d.ts.map +1 -1
- package/abstract/fft.js +155 -12
- package/abstract/fft.js.map +1 -1
- package/abstract/frost.d.ts +293 -0
- package/abstract/frost.d.ts.map +1 -0
- package/abstract/frost.js +704 -0
- package/abstract/frost.js.map +1 -0
- package/abstract/hash-to-curve.d.ts +173 -24
- package/abstract/hash-to-curve.d.ts.map +1 -1
- package/abstract/hash-to-curve.js +170 -31
- package/abstract/hash-to-curve.js.map +1 -1
- package/abstract/modular.d.ts +429 -37
- package/abstract/modular.d.ts.map +1 -1
- package/abstract/modular.js +414 -119
- package/abstract/modular.js.map +1 -1
- package/abstract/montgomery.d.ts +83 -12
- package/abstract/montgomery.d.ts.map +1 -1
- package/abstract/montgomery.js +32 -7
- package/abstract/montgomery.js.map +1 -1
- package/abstract/oprf.d.ts +164 -91
- package/abstract/oprf.d.ts.map +1 -1
- package/abstract/oprf.js +88 -29
- package/abstract/oprf.js.map +1 -1
- package/abstract/poseidon.d.ts +138 -7
- package/abstract/poseidon.d.ts.map +1 -1
- package/abstract/poseidon.js +178 -15
- package/abstract/poseidon.js.map +1 -1
- package/abstract/tower.d.ts +122 -3
- package/abstract/tower.d.ts.map +1 -1
- package/abstract/tower.js +323 -139
- package/abstract/tower.js.map +1 -1
- package/abstract/weierstrass.d.ts +339 -76
- package/abstract/weierstrass.d.ts.map +1 -1
- package/abstract/weierstrass.js +395 -205
- package/abstract/weierstrass.js.map +1 -1
- package/bls12-381.d.ts +16 -2
- package/bls12-381.d.ts.map +1 -1
- package/bls12-381.js +199 -209
- package/bls12-381.js.map +1 -1
- package/bn254.d.ts +11 -2
- package/bn254.d.ts.map +1 -1
- package/bn254.js +93 -38
- package/bn254.js.map +1 -1
- package/ed25519.d.ts +135 -14
- package/ed25519.d.ts.map +1 -1
- package/ed25519.js +207 -41
- package/ed25519.js.map +1 -1
- package/ed448.d.ts +108 -14
- package/ed448.d.ts.map +1 -1
- package/ed448.js +194 -42
- package/ed448.js.map +1 -1
- package/index.js +7 -1
- package/index.js.map +1 -1
- package/misc.d.ts +106 -7
- package/misc.d.ts.map +1 -1
- package/misc.js +141 -32
- package/misc.js.map +1 -1
- package/nist.d.ts +112 -11
- package/nist.d.ts.map +1 -1
- package/nist.js +139 -17
- package/nist.js.map +1 -1
- package/package.json +34 -6
- package/secp256k1.d.ts +92 -15
- package/secp256k1.d.ts.map +1 -1
- package/secp256k1.js +211 -28
- package/secp256k1.js.map +1 -1
- package/src/abstract/bls.ts +356 -69
- package/src/abstract/curve.ts +327 -44
- package/src/abstract/edwards.ts +367 -143
- package/src/abstract/fft.ts +371 -36
- package/src/abstract/frost.ts +1092 -0
- package/src/abstract/hash-to-curve.ts +255 -56
- package/src/abstract/modular.ts +591 -144
- package/src/abstract/montgomery.ts +114 -30
- package/src/abstract/oprf.ts +383 -194
- package/src/abstract/poseidon.ts +235 -35
- package/src/abstract/tower.ts +428 -159
- package/src/abstract/weierstrass.ts +710 -312
- package/src/bls12-381.ts +239 -236
- package/src/bn254.ts +107 -46
- package/src/ed25519.ts +234 -56
- package/src/ed448.ts +227 -57
- package/src/index.ts +7 -1
- package/src/misc.ts +154 -35
- package/src/nist.ts +143 -20
- package/src/secp256k1.ts +284 -41
- package/src/utils.ts +583 -81
- package/src/webcrypto.ts +302 -73
- package/utils.d.ts +457 -24
- package/utils.d.ts.map +1 -1
- package/utils.js +410 -53
- package/utils.js.map +1 -1
- package/webcrypto.d.ts +167 -25
- package/webcrypto.d.ts.map +1 -1
- package/webcrypto.js +165 -58
- package/webcrypto.js.map +1 -1
package/src/abstract/poseidon.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
+
/** Round constants captured by the permutation instance. */
|
|
211
317
|
roundConstants: bigint[][];
|
|
212
318
|
};
|
|
213
319
|
/** Poseidon NTT-friendly hash. */
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
329
|
-
// Re-use hash instance between
|
|
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
|
}
|