Files

611 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Methods for elliptic curve multiplication by scalars.
* Contains wNAF, pippenger.
* @module
*/
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { bitLen, bitMask, validateObject } from "../utils.js";
import { Field, FpInvertBatch, validateField } from "./modular.js";
const _0n = /* @__PURE__ */ BigInt(0);
const _1n = /* @__PURE__ */ BigInt(1);
/**
* Validates the static surface of a point constructor.
* This is only a cheap sanity check for the constructor hooks and fields consumed by generic
* factories; it does not certify `BASE`/`ZERO` semantics or prove the curve implementation itself.
* @param Point - Runtime point constructor.
* @throws On missing constructor hooks or malformed field metadata. {@link TypeError}
* @example
* Check that one point constructor exposes the static hooks generic helpers need.
*
* ```ts
* import { ed25519 } from '@noble/curves/ed25519.js';
* import { validatePointCons } from '@noble/curves/abstract/curve.js';
* validatePointCons(ed25519.Point);
* ```
*/
export function validatePointCons(Point) {
const pc = Point;
if (typeof pc !== 'function')
throw new TypeError('Point must be a constructor');
// validateObject only accepts plain objects, so copy the constructor statics into one bag first.
validateObject({
Fp: pc.Fp,
Fn: pc.Fn,
fromAffine: pc.fromAffine,
fromBytes: pc.fromBytes,
fromHex: pc.fromHex,
}, {
Fp: 'object',
Fn: 'object',
fromAffine: 'function',
fromBytes: 'function',
fromHex: 'function',
});
validateField(pc.Fp);
validateField(pc.Fn);
}
/**
* Computes both candidates first, but the final selection still branches on `condition`, so this
* is not a strict constant-time CMOV primitive.
* @param condition - Whether to negate the point.
* @param item - Point-like value.
* @returns Original or negated value.
* @example
* Keep the point or return its negation based on one boolean branch.
*
* ```ts
* import { negateCt } from '@noble/curves/abstract/curve.js';
* import { p256 } from '@noble/curves/nist.js';
* const maybeNegated = negateCt(true, p256.Point.BASE);
* ```
*/
export function negateCt(condition, item) {
const neg = item.negate();
return condition ? neg : item;
}
/**
* Takes a bunch of Projective Points but executes only one
* inversion on all of them. Inversion is very slow operation,
* so this improves performance massively.
* Optimization: converts a list of projective points to a list of identical points with Z=1.
* Input points are left unchanged; the normalized points are returned as fresh instances.
* @param c - Point constructor.
* @param points - Projective points.
* @returns Fresh projective points reconstructed from normalized affine coordinates.
* @example
* Batch-normalize projective points with a single shared inversion.
*
* ```ts
* import { normalizeZ } from '@noble/curves/abstract/curve.js';
* import { p256 } from '@noble/curves/nist.js';
* const points = normalizeZ(p256.Point, [p256.Point.BASE, p256.Point.BASE.double()]);
* ```
*/
export function normalizeZ(c, points) {
const invertedZs = FpInvertBatch(c.Fp, points.map((p) => p.Z));
return points.map((p, i) => c.fromAffine(p.toAffine(invertedZs[i])));
}
function validateW(W, bits) {
if (!Number.isSafeInteger(W) || W <= 0 || W > bits)
throw new Error('invalid window size, expected [1..' + bits + '], got W=' + W);
}
function calcWOpts(W, scalarBits) {
validateW(W, scalarBits);
const windows = Math.ceil(scalarBits / W) + 1; // W=8 33. Not 32, because we skip zero
const windowSize = 2 ** (W - 1); // W=8 128. Not 256, because we skip zero
const maxNumber = 2 ** W; // W=8 256
const mask = bitMask(W); // W=8 255 == mask 0b11111111
const shiftBy = BigInt(W); // W=8 8
return { windows, windowSize, mask, maxNumber, shiftBy };
}
function calcOffsets(n, window, wOpts) {
const { windowSize, mask, maxNumber, shiftBy } = wOpts;
let wbits = Number(n & mask); // extract W bits.
let nextN = n >> shiftBy; // shift number by W bits.
// What actually happens here:
// const highestBit = Number(mask ^ (mask >> 1n));
// let wbits2 = wbits - 1; // skip zero
// if (wbits2 & highestBit) { wbits2 ^= Number(mask); // (~);
// split if bits > max: +224 => 256-32
if (wbits > windowSize) {
// we skip zero, which means instead of `>= size-1`, we do `> size`
wbits -= maxNumber; // -32, can be maxNumber - wbits, but then we need to set isNeg here.
nextN += _1n; // +256 (carry)
}
const offsetStart = window * windowSize;
const offset = offsetStart + Math.abs(wbits) - 1; // -1 because we skip zero; ignore when isZero
const isZero = wbits === 0; // is current window slice a 0?
const isNeg = wbits < 0; // is current window slice negative?
const isNegF = window % 2 !== 0; // fake branch noise only
const offsetF = offsetStart; // fake branch noise only
return { nextN, offset, isZero, isNeg, isNegF, offsetF };
}
function validateMSMPoints(points, c) {
if (!Array.isArray(points))
throw new Error('array expected');
points.forEach((p, i) => {
if (!(p instanceof c))
throw new Error('invalid point at index ' + i);
});
}
function validateMSMScalars(scalars, field) {
if (!Array.isArray(scalars))
throw new Error('array of scalars expected');
scalars.forEach((s, i) => {
if (!field.isValid(s))
throw new Error('invalid scalar at index ' + i);
});
}
// Since points in different groups cannot be equal (different object constructor),
// we can have single place to store precomputes.
// Allows to make points frozen / immutable.
const pointPrecomputes = new WeakMap();
const pointWindowSizes = new WeakMap();
function getW(P) {
// To disable precomputes:
// return 1;
// `1` is also the uncached sentinel: use the ladder / non-precomputed path.
return pointWindowSizes.get(P) || 1;
}
function assert0(n) {
// Internal invariant: a non-zero remainder here means the wNAF window decomposition or loop
// count is inconsistent, not that the original caller provided a bad scalar.
if (n !== _0n)
throw new Error('invalid wNAF');
}
/**
* Elliptic curve multiplication of Point by scalar. Fragile.
* Table generation takes **30MB of ram and 10ms on high-end CPU**,
* but may take much longer on slow devices. Actual generation will happen on
* first call of `multiply()`. By default, `BASE` point is precomputed.
*
* Scalars should always be less than curve order: this should be checked inside of a curve itself.
* Creates precomputation tables for fast multiplication:
* - private scalar is split by fixed size windows of W bits
* - every window point is collected from window's table & added to accumulator
* - since windows are different, same point inside tables won't be accessed more than once per calc
* - each multiplication is 'Math.ceil(CURVE_ORDER / 𝑊) + 1' point additions (fixed for any scalar)
* - +1 window is neccessary for wNAF
* - wNAF reduces table size: 2x less memory + 2x faster generation, but 10% slower multiplication
*
* TODO: research returning a 2d JS array of windows instead of a single window.
* This would allow windows to be in different memory locations.
* @param Point - Point constructor.
* @param bits - Scalar bit length.
* @example
* Elliptic curve multiplication of Point by scalar.
*
* ```ts
* import { wNAF } from '@noble/curves/abstract/curve.js';
* import { p256 } from '@noble/curves/nist.js';
* const ladder = new wNAF(p256.Point, p256.Point.Fn.BITS);
* ```
*/
export class wNAF {
BASE;
ZERO;
Fn;
bits;
// Parametrized with a given Point class (not individual point)
constructor(Point, bits) {
this.BASE = Point.BASE;
this.ZERO = Point.ZERO;
this.Fn = Point.Fn;
this.bits = bits;
}
// non-const time multiplication ladder
_unsafeLadder(elm, n, p = this.ZERO) {
let d = elm;
while (n > _0n) {
if (n & _1n)
p = p.add(d);
d = d.double();
n >>= _1n;
}
return p;
}
/**
* Creates a wNAF precomputation window. Used for caching.
* Default window size is set by `utils.precompute()` and is equal to 8.
* Number of precomputed points depends on the curve size:
* 2^(𝑊−1) * (Math.ceil(𝑛 / 𝑊) + 1), where:
* - 𝑊 is the window size
* - 𝑛 is the bitlength of the curve order.
* For a 256-bit curve and window size 8, the number of precomputed points is 128 * 33 = 4224.
* @param point - Point instance
* @param W - window size
* @returns precomputed point tables flattened to a single array
*/
precomputeWindow(point, W) {
const { windows, windowSize } = calcWOpts(W, this.bits);
const points = [];
let p = point;
let base = p;
for (let window = 0; window < windows; window++) {
base = p;
points.push(base);
// i=1, bc we skip 0
for (let i = 1; i < windowSize; i++) {
base = base.add(p);
points.push(base);
}
p = base.double();
}
return points;
}
/**
* Implements ec multiplication using precomputed tables and w-ary non-adjacent form.
* More compact implementation:
* https://github.com/paulmillr/noble-secp256k1/blob/47cb1669b6e506ad66b35fe7d76132ae97465da2/index.ts#L502-L541
* @returns real and fake (for const-time) points
*/
wNAF(W, precomputes, n) {
// Scalar should be smaller than field order
if (!this.Fn.isValid(n))
throw new Error('invalid scalar');
// Accumulators
let p = this.ZERO;
let f = this.BASE;
// This code was first written with assumption that 'f' and 'p' will never be infinity point:
// since each addition is multiplied by 2 ** W, it cannot cancel each other. However,
// there is negate now: it is possible that negated element from low value
// would be the same as high element, which will create carry into next window.
// It's not obvious how this can fail, but still worth investigating later.
const wo = calcWOpts(W, this.bits);
for (let window = 0; window < wo.windows; window++) {
// (n === _0n) is handled and not early-exited. isEven and offsetF are used for noise
const { nextN, offset, isZero, isNeg, isNegF, offsetF } = calcOffsets(n, window, wo);
n = nextN;
if (isZero) {
// bits are 0: add garbage to fake point
// Important part for const-time getPublicKey: add random "noise" point to f.
f = f.add(negateCt(isNegF, precomputes[offsetF]));
}
else {
// bits are 1: add to result point
p = p.add(negateCt(isNeg, precomputes[offset]));
}
}
assert0(n);
// Return both real and fake points so JIT keeps the noise path alive.
// Known caveat: negate/carry interactions can still drive `f` to infinity even when `p` is not,
// which weakens the noise path and leaves this only "less const-time" by about one bigint mul.
return { p, f };
}
/**
* Implements unsafe EC multiplication using precomputed tables
* and w-ary non-adjacent form.
* @param acc - accumulator point to add result of multiplication
* @returns point
*/
wNAFUnsafe(W, precomputes, n, acc = this.ZERO) {
const wo = calcWOpts(W, this.bits);
for (let window = 0; window < wo.windows; window++) {
if (n === _0n)
break; // Early-exit, skip 0 value
const { nextN, offset, isZero, isNeg } = calcOffsets(n, window, wo);
n = nextN;
if (isZero) {
// Window bits are 0: skip processing.
// Move to next window.
continue;
}
else {
const item = precomputes[offset];
acc = acc.add(isNeg ? item.negate() : item); // Re-using acc allows to save adds in MSM
}
}
assert0(n);
return acc;
}
getPrecomputes(W, point, transform) {
// Cache key is only point identity plus the remembered window size; callers must not reuse the
// same point with incompatible `transform(...)` layouts and expect a separate cache entry.
let comp = pointPrecomputes.get(point);
if (!comp) {
comp = this.precomputeWindow(point, W);
if (W !== 1) {
// Doing transform outside of if brings 15% perf hit
if (typeof transform === 'function')
comp = transform(comp);
pointPrecomputes.set(point, comp);
}
}
return comp;
}
cached(point, scalar, transform) {
const W = getW(point);
return this.wNAF(W, this.getPrecomputes(W, point, transform), scalar);
}
unsafe(point, scalar, transform, prev) {
const W = getW(point);
if (W === 1)
return this._unsafeLadder(point, scalar, prev); // For W=1 ladder is ~x2 faster
return this.wNAFUnsafe(W, this.getPrecomputes(W, point, transform), scalar, prev);
}
// We calculate precomputes for elliptic curve point multiplication
// using windowed method. This specifies window size and
// stores precomputed values. Usually only base point would be precomputed.
createCache(P, W) {
validateW(W, this.bits);
pointWindowSizes.set(P, W);
pointPrecomputes.delete(P);
}
hasCache(elm) {
return getW(elm) !== 1;
}
}
/**
* Endomorphism-specific multiplication for Koblitz curves.
* Cost: 128 dbl, 0-256 adds.
* @param Point - Point constructor.
* @param point - Input point.
* @param k1 - First non-negative absolute scalar chunk.
* @param k2 - Second non-negative absolute scalar chunk.
* @returns Partial multiplication results.
* @example
* Endomorphism-specific multiplication for Koblitz curves.
*
* ```ts
* import { mulEndoUnsafe } from '@noble/curves/abstract/curve.js';
* import { secp256k1 } from '@noble/curves/secp256k1.js';
* const parts = mulEndoUnsafe(secp256k1.Point, secp256k1.Point.BASE, 3n, 5n);
* ```
*/
export function mulEndoUnsafe(Point, point, k1, k2) {
let acc = point;
let p1 = Point.ZERO;
let p2 = Point.ZERO;
while (k1 > _0n || k2 > _0n) {
if (k1 & _1n)
p1 = p1.add(acc);
if (k2 & _1n)
p2 = p2.add(acc);
acc = acc.double();
k1 >>= _1n;
k2 >>= _1n;
}
return { p1, p2 };
}
/**
* Pippenger algorithm for multi-scalar multiplication (MSM, Pa + Qb + Rc + ...).
* 30x faster vs naive addition on L=4096, 10x faster than precomputes.
* For N=254bit, L=1, it does: 1024 ADD + 254 DBL. For L=5: 1536 ADD + 254 DBL.
* Algorithmically constant-time (for same L), even when 1 point + scalar, or when scalar = 0.
* @param c - Curve Point constructor
* @param points - array of L curve points
* @param scalars - array of L scalars (aka secret keys / bigints)
* @returns MSM result point. Empty input is accepted and returns the identity.
* @throws If the point set, scalar set, or MSM sizing is invalid. {@link Error}
* @example
* Pippenger algorithm for multi-scalar multiplication (MSM, Pa + Qb + Rc + ...).
*
* ```ts
* import { pippenger } from '@noble/curves/abstract/curve.js';
* import { p256 } from '@noble/curves/nist.js';
* const point = pippenger(p256.Point, [p256.Point.BASE, p256.Point.BASE.double()], [2n, 3n]);
* ```
*/
export function pippenger(c, points, scalars) {
// If we split scalars by some window (let's say 8 bits), every chunk will only
// take 256 buckets even if there are 4096 scalars, also re-uses double.
// TODO:
// - https://eprint.iacr.org/2024/750.pdf
// - https://tches.iacr.org/index.php/TCHES/article/view/10287
// 0 is accepted in scalars
const fieldN = c.Fn;
validateMSMPoints(points, c);
validateMSMScalars(scalars, fieldN);
const plength = points.length;
const slength = scalars.length;
if (plength !== slength)
throw new Error('arrays of points and scalars must have equal length');
// if (plength === 0) throw new Error('array must be of length >= 2');
const zero = c.ZERO;
const wbits = bitLen(BigInt(plength));
let windowSize = 1; // bits
if (wbits > 12)
windowSize = wbits - 3;
else if (wbits > 4)
windowSize = wbits - 2;
else if (wbits > 0)
windowSize = 2;
const MASK = bitMask(windowSize);
const buckets = new Array(Number(MASK) + 1).fill(zero); // +1 for zero array
const lastBits = Math.floor((fieldN.BITS - 1) / windowSize) * windowSize;
let sum = zero;
for (let i = lastBits; i >= 0; i -= windowSize) {
buckets.fill(zero);
for (let j = 0; j < slength; j++) {
const scalar = scalars[j];
const wbits = Number((scalar >> BigInt(i)) & MASK);
buckets[wbits] = buckets[wbits].add(points[j]);
}
let resI = zero; // not using this will do small speed-up, but will lose ct
// Skip first bucket, because it is zero
for (let j = buckets.length - 1, sumI = zero; j > 0; j--) {
sumI = sumI.add(buckets[j]);
resI = resI.add(sumI);
}
sum = sum.add(resI);
if (i !== 0)
for (let j = 0; j < windowSize; j++)
sum = sum.double();
}
return sum;
}
/**
* Precomputed multi-scalar multiplication (MSM, Pa + Qb + Rc + ...).
* @param c - Curve Point constructor
* @param points - array of L curve points
* @param windowSize - Precompute window size.
* @returns Function which multiplies points with scalars. The closure accepts
* `scalars.length <= points.length`, and omitted trailing scalars are treated as zero.
* @throws If the point set or precompute window is invalid. {@link Error}
* @example
* Precomputed multi-scalar multiplication (MSM, Pa + Qb + Rc + ...).
*
* ```ts
* import { precomputeMSMUnsafe } from '@noble/curves/abstract/curve.js';
* import { p256 } from '@noble/curves/nist.js';
* const msm = precomputeMSMUnsafe(p256.Point, [p256.Point.BASE], 4);
* const point = msm([3n]);
* ```
*/
export function precomputeMSMUnsafe(c, points, windowSize) {
/**
* Performance Analysis of Window-based Precomputation
*
* Base Case (256-bit scalar, 8-bit window):
* - Standard precomputation requires:
* - 31 additions per scalar × 256 scalars = 7,936 ops
* - Plus 255 summary additions = 8,191 total ops
* Note: Summary additions can be optimized via accumulator
*
* Chunked Precomputation Analysis:
* - Using 32 chunks requires:
* - 255 additions per chunk
* - 256 doublings
* - Total: (255 × 32) + 256 = 8,416 ops
*
* Memory Usage Comparison:
* Window Size | Standard Points | Chunked Points
* ------------|-----------------|---------------
* 4-bit | 520 | 15
* 8-bit | 4,224 | 255
* 10-bit | 13,824 | 1,023
* 16-bit | 557,056 | 65,535
*
* Key Advantages:
* 1. Enables larger window sizes due to reduced memory overhead
* 2. More efficient for smaller scalar counts:
* - 16 chunks: (16 × 255) + 256 = 4,336 ops
* - ~2x faster than standard 8,191 ops
*
* Limitations:
* - Not suitable for plain precomputes (requires 256 constant doublings)
* - Performance degrades with larger scalar counts:
* - Optimal for ~256 scalars
* - Less efficient for 4096+ scalars (Pippenger preferred)
*/
const fieldN = c.Fn;
validateW(windowSize, fieldN.BITS);
validateMSMPoints(points, c);
const zero = c.ZERO;
const tableSize = 2 ** windowSize - 1; // table size (without zero)
const chunks = Math.ceil(fieldN.BITS / windowSize); // chunks of item
const MASK = bitMask(windowSize);
const tables = points.map((p) => {
const res = [];
for (let i = 0, acc = p; i < tableSize; i++) {
res.push(acc);
acc = acc.add(p);
}
return res;
});
return (scalars) => {
validateMSMScalars(scalars, fieldN);
if (scalars.length > points.length)
throw new Error('array of scalars must be smaller than array of points');
let res = zero;
for (let i = 0; i < chunks; i++) {
// No need to double if accumulator is still zero.
if (res !== zero)
for (let j = 0; j < windowSize; j++)
res = res.double();
const shiftBy = BigInt(chunks * windowSize - (i + 1) * windowSize);
for (let j = 0; j < scalars.length; j++) {
const n = scalars[j];
const curr = Number((n >> shiftBy) & MASK);
if (!curr)
continue; // skip zero scalars chunks
res = res.add(tables[j][curr - 1]);
}
}
return res;
};
}
function createField(order, field, isLE) {
if (field) {
// Reuse supplied field overrides as-is; `isLE` only affects freshly constructed fallback
// fields, and validateField() below only checks the arithmetic subset, not full byte/cmov
// behavior.
if (field.ORDER !== order)
throw new Error('Field.ORDER must match order: Fp == p, Fn == n');
validateField(field);
return field;
}
else {
return Field(order, { isLE });
}
}
/**
* Validates basic CURVE shape and field membership, then creates fields.
* This does not prove that the generator is on-curve, that subgroup/order data are consistent, or
* that the curve equation itself is otherwise sane.
* @param type - Curve family.
* @param CURVE - Curve parameters.
* @param curveOpts - Optional field overrides:
* - `Fp` (optional): Optional base-field override.
* - `Fn` (optional): Optional scalar-field override.
* @param FpFnLE - Whether field encoding is little-endian.
* @returns Frozen curve parameters and fields.
* @throws If the curve parameters or field overrides are invalid. {@link Error}
* @example
* Build curve fields from raw constants before constructing a curve instance.
*
* ```ts
* const curve = createCurveFields('weierstrass', {
* p: 17n,
* n: 19n,
* h: 1n,
* a: 2n,
* b: 2n,
* Gx: 5n,
* Gy: 1n,
* });
* ```
*/
export function createCurveFields(type, CURVE, curveOpts = {}, FpFnLE) {
if (FpFnLE === undefined)
FpFnLE = type === 'edwards';
if (!CURVE || typeof CURVE !== 'object')
throw new Error(`expected valid ${type} CURVE object`);
for (const p of ['p', 'n', 'h']) {
const val = CURVE[p];
if (!(typeof val === 'bigint' && val > _0n))
throw new Error(`CURVE.${p} must be positive bigint`);
}
const Fp = createField(CURVE.p, curveOpts.Fp, FpFnLE);
const Fn = createField(CURVE.n, curveOpts.Fn, FpFnLE);
const _b = type === 'weierstrass' ? 'b' : 'd';
const params = ['Gx', 'Gy', 'a', _b];
for (const p of params) {
// @ts-ignore
if (!Fp.isValid(CURVE[p]))
throw new Error(`CURVE.${p} must be valid field element of CURVE.Fp`);
}
CURVE = Object.freeze(Object.assign({}, CURVE));
return { CURVE, Fp, Fn };
}
/**
* @param randomSecretKey - Secret-key generator.
* @param getPublicKey - Public-key derivation helper.
* @returns Keypair generator.
* @example
* Build a `keygen()` helper from existing secret-key and public-key primitives.
*
* ```ts
* import { createKeygen } from '@noble/curves/abstract/curve.js';
* import { p256 } from '@noble/curves/nist.js';
* const keygen = createKeygen(p256.utils.randomSecretKey, p256.getPublicKey);
* const pair = keygen();
* ```
*/
export function createKeygen(randomSecretKey, getPublicKey) {
return function keygen(seed) {
const secretKey = randomSecretKey(seed);
return { secretKey, publicKey: getPublicKey(secretKey) };
};
}
//# sourceMappingURL=curve.js.map