UNPKG

@noble/curves

Version:

Audited & minimal JS implementation of elliptic curve cryptography

855 lines (829 loc) 30.3 kB
/** * Experimental implementation of NTT / FFT (Fast Fourier Transform) over finite fields. * API may change at any time. The code has not been audited. Feature requests are welcome. * @module */ import type { TArg } from '../utils.ts'; import type { IField } from './modular.ts'; /** Array-like coefficient storage that can be mutated in place. */ export interface MutableArrayLike<T> { /** Element access by numeric index. */ [index: number]: T; /** Current amount of stored coefficients. */ length: number; /** * Return a sliced copy using the same storage shape. * @param start - Inclusive start index. * @param end - Exclusive end index. * @returns Sliced copy. */ slice(start?: number, end?: number): this; /** * Iterate over stored coefficients in order. * @returns Coefficient iterator. */ [Symbol.iterator](): Iterator<T>; } /** * Concrete polynomial containers accepted by the high-level `poly(...)` helpers. * Lower-level FFT helpers can work with structural `MutableArrayLike`, but `poly(...)` * intentionally keeps runtime dispatch on plain arrays and typed-array views. */ export type PolyStorage<T> = T[] | (MutableArrayLike<T> & ArrayBufferView); function checkU32(n: number) { // 0xff_ff_ff_ff if (!Number.isSafeInteger(n) || n < 0 || n > 0xffffffff) throw new Error('wrong u32 integer:' + n); return n; } /** * Checks if integer is in form of `1 << X`. * @param x - Integer to inspect. * @returns `true` when the value is a power of two. * @throws If `x` is not a valid unsigned 32-bit integer. {@link Error} * @example * Validate that an FFT size is a power of two. * * ```ts * isPowerOfTwo(8); * ``` */ export function isPowerOfTwo(x: number): boolean { checkU32(x); return (x & (x - 1)) === 0 && x !== 0; } /** * @param n - Input value. * @returns Next power of two within the u32/array-length domain. * @throws If `n` is not a valid unsigned 32-bit integer. {@link Error} * @example * Round an integer up to the FFT size it needs. * * ```ts * nextPowerOfTwo(9); * ``` */ export function nextPowerOfTwo(n: number): number { checkU32(n); if (n <= 1) return 1; // FFT sizes here are used as JS array lengths, so `2^32` is not a meaningful result: // keep the fast u32 bit-twiddling path and fail explicitly instead of wrapping to 1. if (n > 0x8000_0000) throw new Error('nextPowerOfTwo overflow: result does not fit u32'); return (1 << (log2(n - 1) + 1)) >>> 0; } /** * @param n - Value to reverse. * @param bits - Number of bits to use. * @returns Bit-reversed integer. * @throws If `n` is not a valid unsigned 32-bit integer. {@link Error} * @example * Reverse the low `bits` bits of one index. * * ```ts * reverseBits(3, 3); * ``` */ export function reverseBits(n: number, bits: number): number { checkU32(n); if (!Number.isSafeInteger(bits) || bits < 0 || bits > 32) throw new Error(`expected integer 0 <= bits <= 32, got ${bits}`); let reversed = 0; for (let i = 0; i < bits; i++, n >>>= 1) reversed = (reversed << 1) | (n & 1); // JS bitwise ops are signed i32; cast back so 32-bit reversals stay in the unsigned u32 domain. return reversed >>> 0; } /** * Similar to `bitLen(x)-1` but much faster for small integers, like indices. * @param n - Input value. * @returns Base-2 logarithm. For `n = 0`, the current implementation returns `-1`. * @throws If `n` is not a valid unsigned 32-bit integer. {@link Error} * @example * Compute the radix-2 stage count for one transform size. * * ```ts * log2(8); * ``` */ export function log2(n: number): number { checkU32(n); return 31 - Math.clz32(n); } /** * Moves lowest bit to highest position, which at first step splits * array on even and odd indices, then it applied again to each part, * which is core of fft * @param values - Mutable coefficient array. * @returns Mutated input array. * @throws If the array length is not a positive power of two. {@link Error} * @example * Reorder coefficients into bit-reversed order in place. * * ```ts * const values = Uint8Array.from([0, 1, 2, 3]); * bitReversalInplace(values); * ``` */ export function bitReversalInplace<T extends MutableArrayLike<any>>(values: T): T { const n = values.length; // Size-1 FFT is the identity, so bit-reversal must stay a no-op there instead of rejecting it. if (!isPowerOfTwo(n)) throw new Error('expected positive power-of-two length, got ' + n); const bits = log2(n); for (let i = 0; i < n; i++) { const j = reverseBits(i, bits); if (i < j) { const tmp = values[i]; values[i] = values[j]; values[j] = tmp; } } return values; } /** * @param values - Input values. * @returns Reordered copy. * @throws If the array length is not a positive power of two. {@link Error} * @example * Return a reordered copy instead of mutating the input in place. * * ```ts * const reordered = bitReversalPermutation([0, 1, 2, 3]); * ``` */ export function bitReversalPermutation<T>(values: T[]): T[] { return bitReversalInplace(values.slice()) as T[]; } const _1n = /** @__PURE__ */ BigInt(1); function findGenerator(field: TArg<IField<bigint>>) { let G = BigInt(2); for (; field.eql(field.pow(G, field.ORDER >> _1n), field.ONE); G++); return G; } /** Cached roots-of-unity tables derived from one finite field. */ export type RootsOfUnity = { /** Generator and 2-adicity metadata for the cached field. */ info: { G: bigint; oddFactor: bigint; powerOfTwo: number }; /** * Return the natural-order roots of unity for one radix-2 size. * @param bits - Transform size as `log2(N)`. * @returns Natural-order roots for that size. */ roots: (bits: number) => bigint[]; /** * Return the bit-reversal permutation of the roots for one radix-2 size. * @param bits - Transform size as `log2(N)`. * @returns Bit-reversed roots. */ brp(bits: number): bigint[]; /** * Return the inverse roots of unity for one radix-2 size. * @param bits - Transform size as `log2(N)`. * @returns Inverse roots. */ inverse(bits: number): bigint[]; /** * Return one primitive root used by a radix-2 stage. * @param bits - Transform size as `log2(N)`. * @returns Primitive root for that stage. */ omega: (bits: number) => bigint; /** * Drop all cached root tables. * @returns Nothing. */ clear: () => void; }; /** * We limit roots up to 2**31, which is a lot: 2-billion polynomimal should be rare. * @param field - Field implementation. * @param generator - Optional generator override. * @returns Roots-of-unity cache. * @example * Cache roots once, then ask for the omega table of one FFT size. * * ```ts * import { rootsOfUnity } from '@noble/curves/abstract/fft.js'; * import { Field } from '@noble/curves/abstract/modular.js'; * const roots = rootsOfUnity(Field(17n)); * const omega = roots.omega(4); * ``` */ export function rootsOfUnity(field: TArg<IField<bigint>>, generator?: bigint): RootsOfUnity { // Factor field.ORDER-1 as oddFactor * 2^powerOfTwo let oddFactor = field.ORDER - _1n; let powerOfTwo = 0; for (; (oddFactor & _1n) !== _1n; powerOfTwo++, oddFactor >>= _1n); // Find non quadratic residue let G = generator !== undefined ? BigInt(generator) : findGenerator(field); // Powers of generator const omegas: bigint[] = new Array(powerOfTwo + 1); omegas[powerOfTwo] = field.pow(G, oddFactor); for (let i = powerOfTwo; i > 0; i--) omegas[i - 1] = field.sqr(omegas[i]); // Compute all roots of unity for powers up to maxPower const rootsCache: bigint[][] = []; const checkBits = (bits: number) => { checkU32(bits); if (bits > 31 || bits > powerOfTwo) throw new Error('rootsOfUnity: wrong bits ' + bits + ' powerOfTwo=' + powerOfTwo); return bits; }; const precomputeRoots = (maxPower: number) => { checkBits(maxPower); for (let power = maxPower; power >= 0; power--) { if (rootsCache[power]) continue; // Skip if we've already computed roots for this power const rootsAtPower: bigint[] = []; for (let j = 0, cur = field.ONE; j < 2 ** power; j++, cur = field.mul(cur, omegas[power])) rootsAtPower.push(cur); rootsCache[power] = rootsAtPower; } return rootsCache[maxPower]; }; const brpCache = new Map<number, bigint[]>(); const inverseCache = new Map<number, bigint[]>(); // roots()/brp()/inverse() expose shared cached arrays by reference for speed; callers must treat them as read-only. // NOTE: we use bits instead of power, because power = 2**bits, // but power is not neccesary isPowerOfTwo(power)! return { info: { G, powerOfTwo, oddFactor }, roots: (bits: number): bigint[] => { const b = checkBits(bits); return precomputeRoots(b); }, brp(bits: number): bigint[] { const b = checkBits(bits); if (brpCache.has(b)) return brpCache.get(b)!; else { const res = bitReversalPermutation(this.roots(b)); brpCache.set(b, res); return res; } }, inverse(bits: number): bigint[] { const b = checkBits(bits); if (inverseCache.has(b)) return inverseCache.get(b)!; else { const res = field.invertBatch(this.roots(b)); inverseCache.set(b, res); return res; } }, omega: (bits: number): bigint => omegas[checkBits(bits)], clear: (): void => { rootsCache.splice(0, rootsCache.length); brpCache.clear(); inverseCache.clear(); }, }; } /** Polynomial coefficient container used by the FFT helpers. */ export type Polynomial<T> = MutableArrayLike<T>; /** * Arithmetic operations used by the generic FFT implementation. * * Maps great to Field<bigint>, but not to Group (EC points): * - inv from scalar field * - we need multiplyUnsafe here, instead of multiply for speed * - multiplyUnsafe is safe in the context: we do mul(rootsOfUnity), which are public and sparse */ export type FFTOpts<T, R> = { /** * Add two coefficients. * @param a - Left coefficient. * @param b - Right coefficient. * @returns Sum coefficient. */ add: (a: T, b: T) => T; /** * Subtract two coefficients. * @param a - Left coefficient. * @param b - Right coefficient. * @returns Difference coefficient. */ sub: (a: T, b: T) => T; /** * Multiply one coefficient by a scalar/root factor. * @param a - Coefficient value. * @param scalar - Scalar/root factor. * @returns Scaled coefficient. */ mul: (a: T, scalar: R) => T; /** * Invert one scalar/root factor. * @param a - Scalar/root factor. * @returns Inverse factor. */ inv: (a: R) => R; }; /** Configuration for one low-level FFT loop. */ export type FFTCoreOpts<R> = { /** Transform size. Must be a power of two. */ N: number; /** Stage roots for the selected transform size. */ roots: Polynomial<R>; /** Whether to run the DIT variant instead of DIF. */ dit: boolean; /** Whether to invert butterfly placement for decode-oriented layouts. */ invertButterflies?: boolean; /** Number of initial stages to skip. */ skipStages?: number; /** Whether to apply bit-reversal permutation at the boundary. */ brp?: boolean; }; /** * Callable low-level FFT loop over one polynomial storage shape. * @param values - Polynomial coefficients to transform in place. * @returns The mutated input polynomial. */ export type FFTCoreLoop<T> = <P extends Polynomial<T>>(values: P) => P; /** * Constructs different flavors of FFT. radix2 implementation of low level mutating API. Flavors: * * - DIT (Decimation-in-Time): Bottom-Up (leaves to root), Cool-Turkey * - DIF (Decimation-in-Frequency): Top-Down (root to leaves), Gentleman-Sande * * DIT takes brp input, returns natural output. * DIF takes natural input, returns brp output. * * The output is actually identical. Time / frequence distinction is not meaningful * for Polynomial multiplication in fields. * Which means if protocol supports/needs brp output/inputs, then we can skip this step. * * Cyclic NTT: Rq = Zq[x]/(x^n-1). butterfly_DIT+loop_DIT OR butterfly_DIF+loop_DIT, roots are omega * Negacyclic NTT: Rq = Zq[x]/(x^n+1). butterfly_DIT+loop_DIF, at least for mlkem / mldsa * @param F - Field operations. * @param coreOpts - FFT configuration: * - `N`: Transform size. Must be a power of two. * - `roots`: Stage roots for the selected transform size. * - `dit`: Whether to run the DIT variant instead of DIF. * - `invertButterflies` (optional): Whether to invert butterfly placement. * - `skipStages` (optional): Number of initial stages to skip. * - `brp` (optional): Whether to apply bit-reversal permutation at the boundary. * @returns Low-level FFT loop. * @throws If the FFT options or cached roots are invalid for the requested size. {@link Error} * @example * Constructs different flavors of FFT. * * ```ts * import { FFTCore, rootsOfUnity } from '@noble/curves/abstract/fft.js'; * import { Field } from '@noble/curves/abstract/modular.js'; * const Fp = Field(17n); * const roots = rootsOfUnity(Fp).roots(2); * const loop = FFTCore(Fp, { N: 4, roots, dit: true }); * const values = loop([1n, 2n, 3n, 4n]); * ``` */ export const FFTCore = <T, R>(F: FFTOpts<T, R>, coreOpts: FFTCoreOpts<R>): FFTCoreLoop<T> => { const { N, roots, dit, invertButterflies = false, skipStages = 0, brp = true } = coreOpts; const bits = log2(N); if (!isPowerOfTwo(N)) throw new Error('FFT: Polynomial size should be power of two'); // Wrong-sized root tables can stay in-bounds for some loop shapes and silently compute nonsense. if (roots.length !== N) throw new Error(`FFT: wrong roots length: expected ${N}, got ${roots.length}`); const isDit = dit !== invertButterflies; isDit; return <P extends Polynomial<T>>(values: P): P => { if (values.length !== N) throw new Error('FFT: wrong Polynomial length'); if (dit && brp) bitReversalInplace(values); for (let i = 0, g = 1; i < bits - skipStages; i++) { // For each stage s (sub-FFT length m = 2^s) const s = dit ? i + 1 + skipStages : bits - i; const m = 1 << s; const m2 = m >> 1; const stride = N >> s; // Loop over each subarray of length m for (let k = 0; k < N; k += m) { // Loop over each butterfly within the subarray for (let j = 0, grp = g++; j < m2; j++) { const rootPos = invertButterflies ? (dit ? N - grp : grp) : j * stride; const i0 = k + j; const i1 = k + j + m2; const omega = roots[rootPos]; const b = values[i1]; const a = values[i0]; // Inlining gives us 10% perf in kyber vs functions if (isDit) { const t = F.mul(b, omega); // Standard DIT butterfly values[i0] = F.add(a, t); values[i1] = F.sub(a, t); } else if (invertButterflies) { values[i0] = F.add(b, a); // DIT loop + inverted butterflies (Kyber decode) values[i1] = F.mul(F.sub(b, a), omega); } else { values[i0] = F.add(a, b); // Standard DIF butterfly values[i1] = F.mul(F.sub(a, b), omega); } } } } if (!dit && brp) bitReversalInplace(values); return values; }; }; /** Forward and inverse FFT helpers for one coefficient domain. */ export type FFTMethods<T> = { /** * Apply the forward transform. * @param values - Polynomial coefficients to transform. * @param brpInput - Whether the input is already bit-reversed. * @param brpOutput - Whether to keep the output bit-reversed. * @returns Transformed copy. */ direct<P extends Polynomial<T>>(values: P, brpInput?: boolean, brpOutput?: boolean): P; /** * Apply the inverse transform. * @param values - Polynomial coefficients to transform. * @param brpInput - Whether the input is already bit-reversed. * @param brpOutput - Whether to keep the output bit-reversed. * @returns Inverse-transformed copy. */ inverse<P extends Polynomial<T>>(values: P, brpInput?: boolean, brpOutput?: boolean): P; }; /** * NTT aka FFT over finite field (NOT over complex numbers). * Naming mirrors other libraries. * @param roots - Roots-of-unity cache. * @param opts - Field operations. See {@link FFTOpts}. * @returns Forward and inverse FFT helpers. * @example * NTT aka FFT over finite field (NOT over complex numbers). * * ```ts * import { FFT, rootsOfUnity } from '@noble/curves/abstract/fft.js'; * import { Field } from '@noble/curves/abstract/modular.js'; * const Fp = Field(17n); * const fft = FFT(rootsOfUnity(Fp), Fp); * const values = fft.direct([1n, 2n, 3n, 4n]); * ``` */ export function FFT<T>(roots: RootsOfUnity, opts: FFTOpts<T, bigint>): FFTMethods<T> { const getLoop = ( N: number, roots: Polynomial<bigint>, brpInput = false, brpOutput = false ): (<P extends Polynomial<T>>(values: P) => P) => { if (brpInput && brpOutput) { // we cannot optimize this case, but lets support it anyway return (values) => FFTCore(opts, { N, roots, dit: false, brp: false })(bitReversalInplace(values)); } if (brpInput) return FFTCore(opts, { N, roots, dit: true, brp: false }); if (brpOutput) return FFTCore(opts, { N, roots, dit: false, brp: false }); return FFTCore(opts, { N, roots, dit: true, brp: true }); // all natural }; return { direct<P extends Polynomial<T>>(values: P, brpInput = false, brpOutput = false): P { const N = values.length; if (!isPowerOfTwo(N)) throw new Error('FFT: Polynomial size should be power of two'); const bits = log2(N); return getLoop(N, roots.roots(bits), brpInput, brpOutput)<P>(values.slice()); }, inverse<P extends Polynomial<T>>(values: P, brpInput = false, brpOutput = false): P { const N = values.length; if (!isPowerOfTwo(N)) throw new Error('FFT: Polynomial size should be power of two'); const bits = log2(N); const res = getLoop(N, roots.inverse(bits), brpInput, brpOutput)(values.slice()); const ivm = opts.inv(BigInt(values.length)); // scale // we can get brp output if we use dif instead of dit! for (let i = 0; i < res.length; i++) res[i] = opts.mul(res[i], ivm); // Allows to re-use non-inverted roots, but is VERY fragile // return [res[0]].concat(res.slice(1).reverse()); // inverse calculated as pow(-1), which transforms into ω^{-kn} (-> reverses indices) return res; }, }; } /** * Factory that allocates one polynomial storage container. * Callers must ensure `_create(len)` returns field-zero-filled storage when `elm` is omitted, * because the quadratic `mul()` / `convolve()` paths and the Kronecker-δ shortcut in * `lagrange.basis()` rely on that default instead of always passing `field.ZERO` explicitly. * @param len - Requested amount of coefficients. * @param elm - Optional fill value. * @returns Newly allocated polynomial container. */ export type CreatePolyFn<P extends PolyStorage<T>, T> = (len: number, elm?: T) => P; /** High-level polynomial helpers layered on top of FFT and field arithmetic. */ export type PolyFn<P extends PolyStorage<T>, T> = { /** Roots-of-unity cache used by the helper namespace. */ roots: RootsOfUnity; /** Factory used to allocate new polynomial containers. */ create: CreatePolyFn<P, T>; /** Optional enforced polynomial length. */ length?: number; /** * Compute the polynomial degree. * @param a - Polynomial coefficients. * @returns Polynomial degree. */ degree: (a: P) => number; /** * Extend or truncate one polynomial to a requested length. * @param a - Polynomial coefficients. * @param len - Target length. * @returns Resized polynomial. */ extend: (a: P, len: number) => P; /** * Add two polynomials coefficient-wise. * @param a - Left polynomial. * @param b - Right polynomial. * @returns Sum polynomial. */ add: (a: P, b: P) => P; /** * Subtract two polynomials coefficient-wise. * @param a - Left polynomial. * @param b - Right polynomial. * @returns Difference polynomial. */ sub: (a: P, b: P) => P; /** * Multiply by another polynomial or by one scalar. * @param a - Left polynomial. * @param b - Right polynomial or scalar. * @returns Product polynomial. */ mul: (a: P, b: P | T) => P; /** * Multiply coefficients point-wise. * @param a - Left polynomial. * @param b - Right polynomial. * @returns Point-wise product polynomial. */ dot: (a: P, b: P) => P; /** * Multiply two polynomials with convolution. * @param a - Left polynomial. * @param b - Right polynomial. * @returns Convolution product. */ convolve: (a: P, b: P) => P; /** * Apply a point-wise coefficient shift by powers of one factor. * @param p - Polynomial coefficients. * @param factor - Shift factor. * @returns Shifted polynomial. */ shift: (p: P, factor: bigint) => P; /** * Clone one polynomial container. * @param a - Polynomial coefficients. * @returns Cloned polynomial. */ clone: (a: P) => P; /** * Evaluate one polynomial on a basis vector. * @param a - Polynomial coefficients. * @param basis - Basis vector. * @returns Evaluated field element. */ eval: (a: P, basis: P) => T; /** Helpers for monomial-basis polynomials. */ monomial: { /** Build the monomial basis vector for one evaluation point. */ basis: (x: T, n: number) => P; /** Evaluate a polynomial in the monomial basis. */ eval: (a: P, x: T) => T; }; /** Helpers for Lagrange-basis polynomials. */ lagrange: { /** Build the Lagrange basis vector for one evaluation point. */ basis: (x: T, n: number, brp?: boolean) => P; /** Evaluate a polynomial in the Lagrange basis. */ eval: (a: P, x: T, brp?: boolean) => T; }; /** * Build the vanishing polynomial for a root set. * @param roots - Root set. * @returns Vanishing polynomial. */ vanishing: (roots: P) => P; }; /** * Poly wants a cracker. * * Polynomials are functions like `y=f(x)`, which means when we multiply two polynomials, result is * function `f3(x) = f1(x) * f2(x)`, we don't multiply values. Key takeaways: * * - **Polynomial** is an array of coefficients: `f(x) = sum(coeff[i] * basis[i](x))` * - **Basis** is array of functions * - **Monominal** is Polynomial where `basis[i](x) == x**i` (powers) * - **Array size** is domain size * - **Lattice** is matrix (Polynomial of Polynomials) * @param field - Field implementation. * @param roots - Roots-of-unity cache. * @param create - Optional polynomial factory. Runtime input validation accepts only plain `Array` * and typed-array polynomial containers; arbitrary structural wrappers are intentionally rejected. * @param fft - Optional FFT implementation. * @param length - Optional fixed polynomial length. * @returns Polynomial helper namespace. * @example * Build polynomial helpers, then convolve two coefficient arrays. * * ```ts * import { poly, rootsOfUnity } from '@noble/curves/abstract/fft.js'; * import { Field } from '@noble/curves/abstract/modular.js'; * const Fp = Field(17n); * const poly17 = poly(Fp, rootsOfUnity(Fp)); * const product = poly17.convolve([1n, 2n], [3n, 4n]); * ``` */ export function poly<T>( field: TArg<IField<T>>, roots: RootsOfUnity, create?: undefined, fft?: FFTMethods<T>, length?: number ): PolyFn<T[], T>; export function poly<T, P extends PolyStorage<T>>( field: TArg<IField<T>>, roots: RootsOfUnity, create: CreatePolyFn<P, T>, fft?: FFTMethods<T>, length?: number ): PolyFn<P, T>; export function poly<T, P extends PolyStorage<T>>( field: TArg<IField<T>>, roots: RootsOfUnity, create?: CreatePolyFn<P, T>, fft?: FFTMethods<T>, length?: number ): PolyFn<any, T> { const F = field as IField<T>; const _create = create || (((len: number, elm?: T): T[] => new Array(len).fill(elm ?? F.ZERO)) as CreatePolyFn<P, T>); // `poly.mul(a, b)` distinguishes polynomial-vs-scalar at runtime, so keep accepted // polynomial containers concrete instead of trying to support arbitrary wrappers. const isPoly = (x: any): x is P => { if (Array.isArray(x)) return true; if (!ArrayBuffer.isView(x)) return false; const v = x as unknown as ArrayLike<unknown> & { slice?: unknown; [Symbol.iterator]?: unknown }; return ( typeof v.length === 'number' && typeof v.slice === 'function' && typeof v[Symbol.iterator] === 'function' ); }; const checkLength = (...lst: P[]): number => { if (!lst.length) return 0; for (const i of lst) if (!isPoly(i)) throw new Error('poly: not polynomial: ' + i); const L = lst[0].length; for (let i = 1; i < lst.length; i++) if (lst[i].length !== L) throw new Error(`poly: mismatched lengths ${L} vs ${lst[i].length}`); if (length !== undefined && L !== length) throw new Error(`poly: expected fixed length ${length}, got ${L}`); return L; }; function findOmegaIndex(x: T, n: number, brp = false): number { const bits = log2(n); const omega = brp ? roots.brp(bits) : roots.roots(bits); for (let i = 0; i < n; i++) if (F.eql(x, omega[i] as T)) return i; return -1; } // TODO: mutating versions for mlkem/mldsa return { roots, create: _create, length, extend: (a: P, len: number): P => { checkLength(a); const out = _create(len, F.ZERO); // Plain arrays grow when writing past `out.length`, so cap the copy explicitly to keep // `extend()` consistent with typed arrays and with its documented truncate behavior. for (let i = 0; i < Math.min(a.length, len); i++) out[i] = a[i]; return out; }, degree: (a: P): number => { checkLength(a); for (let i = a.length - 1; i >= 0; i--) if (!F.is0(a[i])) return i; return -1; }, add: (a: P, b: P): P => { const len = checkLength(a, b); const out = _create(len); for (let i = 0; i < len; i++) out[i] = F.add(a[i], b[i]); return out; }, sub: (a: P, b: P): P => { const len = checkLength(a, b); const out = _create(len); for (let i = 0; i < len; i++) out[i] = F.sub(a[i], b[i]); return out; }, dot: (a: P, b: P): P => { const len = checkLength(a, b); const out = _create(len); for (let i = 0; i < len; i++) out[i] = F.mul(a[i], b[i]); return out; }, mul: (a: P, b: P | T): P => { if (isPoly(b)) { const len = checkLength(a, b); if (fft) { const A = fft.direct(a, false, true); const B = fft.direct(b, false, true); for (let i = 0; i < A.length; i++) A[i] = F.mul(A[i], B[i]); return fft.inverse(A, true, false) as P; } else { // NOTE: this is quadratic and mostly for compat tests with FFT const res = _create(len); for (let i = 0; i < len; i++) { for (let j = 0; j < len; j++) { const k = (i + j) % len; // wrap mod length res[k] = F.add(res[k], F.mul(a[i], b[j])); } } return res; } } else { const out = _create(checkLength(a)); for (let i = 0; i < out.length; i++) out[i] = F.mul(a[i], b); return out; } }, convolve(a: P, b: P): P { const len = nextPowerOfTwo(a.length + b.length - 1); return this.mul(this.extend(a, len), this.extend(b, len)); }, shift(p: P, factor: bigint): P { const out = _create(checkLength(p)); out[0] = p[0]; for (let i = 1, power = F.ONE; i < p.length; i++) { power = F.mul(power, factor); out[i] = F.mul(p[i], power); } return out; }, clone: (a: P): P => { checkLength(a); const out = _create(a.length); for (let i = 0; i < a.length; i++) out[i] = a[i]; return out; }, eval: (a: P, basis: P): T => { checkLength(a, basis); let acc = F.ZERO; for (let i = 0; i < a.length; i++) acc = F.add(acc, F.mul(a[i], basis[i])); return acc; }, monomial: { basis: (x: T, n: number): P => { const out = _create(n); let pow = F.ONE; for (let i = 0; i < n; i++) { out[i] = pow; pow = F.mul(pow, x); } return out; }, eval: (a: P, x: T): T => { checkLength(a); // Same as eval(a, monomialBasis(x, a.length)), but it is faster this way let acc = F.ZERO; for (let i = a.length - 1; i >= 0; i--) acc = F.add(F.mul(acc, x), a[i]); return acc; }, }, lagrange: { basis: (x: T, n: number, brp = false, weights?: P): P => { const bits = log2(n); const cache = weights || (brp ? roots.brp(bits) : roots.roots(bits)); // [ω⁰, ω¹, ..., ωⁿ⁻¹] const out = _create(n); // Fast Kronecker-δ shortcut const idx = findOmegaIndex(x, n, brp); if (idx !== -1) { out[idx] = F.ONE; return out; } const tm = F.pow(x, BigInt(n)); const c = F.mul(F.sub(tm, F.ONE), F.inv(BigInt(n) as T)); // c = (xⁿ - 1)/n const denom = _create(n); for (let i = 0; i < n; i++) denom[i] = F.sub(x, cache[i] as T); const inv = F.invertBatch(denom as any as T[]); for (let i = 0; i < n; i++) out[i] = F.mul(c, F.mul(cache[i] as T, inv[i])); return out; }, eval(a: P, x: T, brp = false): T { checkLength(a); const idx = findOmegaIndex(x, a.length, brp); if (idx !== -1) return a[idx]; // fast path const L = this.basis(x, a.length, brp); // Lᵢ(x) let acc = F.ZERO; for (let i = 0; i < a.length; i++) if (!F.is0(a[i])) acc = F.add(acc, F.mul(a[i], L[i])); return acc; }, }, vanishing(roots: P): P { checkLength(roots); const out = _create(roots.length + 1, F.ZERO); out[0] = F.ONE; for (const r of roots) { const neg = F.neg(r); for (let j = out.length - 1; j > 0; j--) out[j] = F.add(F.mul(out[j], neg), out[j - 1]); out[0] = F.mul(out[0], neg); } return out; }, }; }