diff --git a/package.json b/package.json index 44f226a..77a5b0c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "lib" ], "scripts": { - "bench": "node benchmark/index.js", + "bench": "cd benchmark; node index.js", "build": "tsc && tsc -p tsconfig.esm.json", "build:release": "rollup -c rollup.config.js", "lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'", diff --git a/src/abstract/edwards.ts b/src/abstract/edwards.ts index bc8d070..7acb010 100644 --- a/src/abstract/edwards.ts +++ b/src/abstract/edwards.ts @@ -68,10 +68,10 @@ export type AffinePoint = { // Instance of Extended Point with coordinates in X, Y, Z, T export interface ExtendedPointType extends Group { - readonly x: bigint; - readonly y: bigint; - readonly z: bigint; - readonly t: bigint; + readonly ex: bigint; + readonly ey: bigint; + readonly ez: bigint; + readonly et: bigint; multiply(scalar: bigint): ExtendedPointType; multiplyUnsafe(scalar: bigint): ExtendedPointType; isSmallOrder(): boolean; @@ -85,7 +85,6 @@ export interface ExtendedPointConstructor extends GroupConstructor(); /** * Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy). @@ -161,51 +165,68 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { * https://en.wikipedia.org/wiki/Twisted_Edwards_curve#Extended_coordinates */ class ExtendedPoint implements ExtendedPointType { - constructor(readonly x: bigint, readonly y: bigint, readonly z: bigint, readonly t: bigint) { - if (y == null || !ut.big(y)) throw new Error('y required'); - if (z == null || !ut.big(z)) throw new Error('z required'); - if (t == null || !ut.big(t)) throw new Error('t required'); - } - static BASE = new ExtendedPoint(CURVE.Gx, CURVE.Gy, _1n, modP(CURVE.Gx * CURVE.Gy)); static ZERO = new ExtendedPoint(_0n, _1n, _1n, _0n); // 0, 1, 1, 0 + constructor( + readonly ex: bigint, + readonly ey: bigint, + readonly ez: bigint, + readonly et: bigint + ) { + if (badc(ey)) throw new Error('y required'); + if (badc(ez)) throw new Error('z required'); + if (badc(et)) throw new Error('t required'); + } + + get x(): bigint { + return this.toAffine().x; + } + get y(): bigint { + return this.toAffine().y; + } + static fromAffine(p: AffinePoint): ExtendedPoint { const { x, y } = p || {}; if (p instanceof ExtendedPoint) throw new Error('fromAffine: extended point not allowed'); if (!ut.big(x) || !ut.big(y)) throw new Error('fromAffine: invalid affine point'); - if (p.x === _0n && p.y === _1n) return ExtendedPoint.ZERO; - return new ExtendedPoint(p.x, p.y, _1n, modP(x * y)); + return new ExtendedPoint(x, y, _1n, modP(x * y)); } - // Takes a bunch of Jacobian Points but executes only one - // invert on all of them. invert is very slow operation, - // so this improves performance massively. - static toAffineBatch(points: ExtendedPoint[]): AffinePoint[] { - const toInv = Fp.invertBatch(points.map((p) => p.z)); - return points.map((p, i) => p.toAffine(toInv[i])); + static normalizeZ(points: ExtendedPoint[]): ExtendedPoint[] { + const toInv = Fp.invertBatch(points.map((p) => p.ez)); + return points.map((p, i) => p.toAffine(toInv[i])).map(ExtendedPoint.fromAffine); } - static normalizeZ(denorm: ExtendedPoint[]): ExtendedPoint[] { - return ExtendedPoint.toAffineBatch(denorm).map(ExtendedPoint.fromAffine); + + // 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. + _WINDOW_SIZE?: number; + + // "Private method", don't use it directly + _setWindowSize(windowSize: number) { + this._WINDOW_SIZE = windowSize; + pointPrecomputes.delete(this); } // Compare one point to another. equals(other: ExtendedPoint): boolean { assertExtPoint(other); - const { x: X1, y: Y1, z: Z1 } = this; - const { x: X2, y: Y2, z: Z2 } = other; + const { ex: X1, ey: Y1, ez: Z1 } = this; + const { ex: X2, ey: Y2, ez: Z2 } = other; const X1Z2 = modP(X1 * Z2); const X2Z1 = modP(X2 * Z1); const Y1Z2 = modP(Y1 * Z2); const Y2Z1 = modP(Y2 * Z1); return X1Z2 === X2Z1 && Y1Z2 === Y2Z1; } + protected is0(): boolean { return this.equals(ExtendedPoint.ZERO); } // Inverses point to one corresponding to (x, -y) in Affine coordinates. negate(): ExtendedPoint { - return new ExtendedPoint(modP(-this.x), this.y, this.z, modP(-this.t)); + return new ExtendedPoint(modP(-this.ex), this.ey, this.ez, modP(-this.et)); } // Fast algo for doubling Extended Point. @@ -213,7 +234,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Cost: 4M + 4S + 1*a + 6add + 1*2. double(): ExtendedPoint { const { a } = CURVE; - const { x: X1, y: Y1, z: Z1 } = this; + const { ex: X1, ey: Y1, ez: Z1 } = this; const A = modP(X1 * X1); // A = X12 const B = modP(Y1 * Y1); // B = Y12 const C = modP(_2n * modP(Z1 * Z1)); // C = 2*Z12 @@ -236,8 +257,8 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { add(other: ExtendedPoint) { assertExtPoint(other); const { a, d } = CURVE; - const { x: X1, y: Y1, z: Z1, t: T1 } = this; - const { x: X2, y: Y2, z: Z2, t: T2 } = other; + const { ex: X1, ey: Y1, ez: Z1, et: T1 } = this; + const { ex: X2, ey: Y2, ez: Z2, et: T2 } = other; // Faster algo for adding 2 Extended Points when curve's a=-1. // http://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-4 // Cost: 8M + 8add + 2*2. @@ -278,11 +299,16 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { return this.add(other.negate()); } + private wNAF(n: bigint): { p: ExtendedPoint; f: ExtendedPoint } { + return wnaf.wNAFCached(this, pointPrecomputes, n, ExtendedPoint.normalizeZ); + } + // Constant time multiplication. // Uses wNAF method. Windowed method may be 10% faster, // but takes 2x longer to generate and consumes 2x memory. multiply(scalar: bigint): ExtendedPoint { - return wNAF_TMP_FN(this, assertGE(scalar)); + const { p, f } = this.wNAF(assertGE(scalar)); + return ExtendedPoint.normalizeZ([p, f])[0]; } // Non-constant-time multiplication. Uses double-and-add algorithm. @@ -292,7 +318,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { let n = assertGE0(scalar); if (n === _0n) return I; if (this.equals(I) || n === _1n) return this; - if (this.equals(G)) return wNAF_TMP_FN(this, n); + if (this.equals(G)) return this.wNAF(n).p; return wnaf.unsafeLadder(this, n); } @@ -313,7 +339,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Converts Extended point to default (x, y) coordinates. // Can accept precomputed Z^-1 - for example, from invertBatch. toAffine(iz?: bigint): AffinePoint { - const { x, y, z } = this; + const { ex: x, ey: y, ez: z } = this; const is0 = this.is0(); if (iz == null) iz = is0 ? _8n : (Fp.invert(z) as bigint); // 8 was chosen arbitrarily const ax = modP(x * iz); @@ -390,26 +416,8 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } } const { BASE: G, ZERO: I } = ExtendedPoint; - let Gpows: ExtendedPoint[] | undefined = undefined; // precomputes for base point G const wnaf = wNAF(ExtendedPoint, CURVE.nByteLength * 8); - function wNAF_TMP_FN(P: ExtendedPoint, n: bigint): ExtendedPoint { - if (P.equals(G)) { - const W = 8; - if (!Gpows) { - const denorm = wnaf.precomputeWindow(P, W) as ExtendedPoint[]; - const norm = ExtendedPoint.toAffineBatch(denorm).map(ExtendedPoint.fromAffine); - Gpows = norm; - } - const comp = Gpows; - const { p, f } = wnaf.wNAF(W, comp, n); - return ExtendedPoint.normalizeZ([p, f])[0]; - } - const W = 1; - const denorm = wnaf.precomputeWindow(P, W) as ExtendedPoint[]; - const norm = ExtendedPoint.toAffineBatch(denorm).map(ExtendedPoint.fromAffine); - const { p, f } = wnaf.wNAF(W, norm, n); - return ExtendedPoint.normalizeZ([p, f])[0]; - } + function assertExtPoint(other: unknown) { if (!(other instanceof ExtendedPoint)) throw new TypeError('ExtendedPoint expected'); } @@ -494,7 +502,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } // Enable precomputes. Slows down first publicKey computation by 20ms. - // G._setWindowSize(8); + G._setWindowSize(8); const utils = { getExtendedPublicKey, @@ -515,12 +523,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { * but allows to speed-up subsequent getPublicKey() calls up to 20x. * @param windowSize 2, 4, 8, 16 */ - precompute(windowSize = 8, point = G): ExtendedPoint { - return G.multiply(2n); - // const cached = point.equals(Point.BASE) ? point : new Point(point.x, point.y); - // cached._setWindowSize(windowSize); - // cached.multiply(_2n); - // return cached; + precompute(windowSize = 8, point = ExtendedPoint.BASE): typeof ExtendedPoint.BASE { + point._setWindowSize(windowSize); + point.multiply(BigInt(3)); + return point; }, }; diff --git a/src/abstract/group.ts b/src/abstract/group.ts index fc36403..60a10e6 100644 --- a/src/abstract/group.ts +++ b/src/abstract/group.ts @@ -16,6 +16,7 @@ export type GroupConstructor = { BASE: T; ZERO: T; }; +export type Mapper = (i: T[]) => T[]; // Not big, but pretty complex and it is easy to break stuff. To avoid too much copy paste export function wNAF>(c: GroupConstructor, bits: number) { const constTimeNegate = (condition: boolean, item: T): T => { @@ -125,5 +126,19 @@ export function wNAF>(c: GroupConstructor, bits: number) { // which makes it less const-time: around 1 bigint multiply. return { p, f }; }, + + wNAFCached(P: T, precomputesMap: Map, n: bigint, transform: Mapper): { p: T; f: T } { + // @ts-ignore + const W: number = '_WINDOW_SIZE' in P ? P._WINDOW_SIZE : 1; + // Calculate precomputes on a first run, reuse them after + let comp = precomputesMap.get(P); + if (!comp) { + comp = this.precomputeWindow(P, W) as T[]; + if (W !== 1) { + precomputesMap.set(P, transform(comp)); + } + } + return this.wNAF(W, comp, n); + }, }; } diff --git a/src/abstract/hash-to-curve.ts b/src/abstract/hash-to-curve.ts index 3827f4d..57706a2 100644 --- a/src/abstract/hash-to-curve.ts +++ b/src/abstract/hash-to-curve.ts @@ -178,19 +178,18 @@ export function isogenyMap>(field: F, map: [T[], T[], }; } +type AffinePoint = { x: T; y: T }; export interface H2CPoint extends Group> { - readonly x: T; - readonly y: T; add(rhs: H2CPoint): H2CPoint; - toAffine(iz?: bigint): { x: T; y: T }; + toAffine(iz?: bigint): AffinePoint; clearCofactor(): H2CPoint; } export interface H2CPointConstructor extends GroupConstructor> { - fromAffine(ap: { x: T; y: T }): H2CPoint; + fromAffine(ap: AffinePoint): H2CPoint; } -export type MapToCurve = (scalar: bigint[]) => { x: T; y: T }; +export type MapToCurve = (scalar: bigint[]) => AffinePoint; // Separated from initialization opts, so users won't accidentally change per-curve parameters (changing DST is ok!) export type htfBasicOpts = { diff --git a/src/abstract/modular.ts b/src/abstract/modular.ts index b677235..0d39f5a 100644 --- a/src/abstract/modular.ts +++ b/src/abstract/modular.ts @@ -332,7 +332,7 @@ export function Fp( isValid: (num) => { if (typeof num !== 'bigint') throw new Error(`Invalid field element: expected bigint, got ${typeof num}`); - return _0n <= num && num < ORDER; + return _0n <= num && num < ORDER; // 0 is valid element, but it's not invertible }, isZero: (num) => num === _0n, isOdd: (num) => (num & _1n) === _1n, @@ -360,7 +360,6 @@ export function Fp( cmov: (a, b, c) => (c ? b : a), toBytes: (num) => isLE ? utils.numberToBytesLE(num, BYTES) : utils.numberToBytesBE(num, BYTES), - fromBytes: (bytes) => { if (bytes.length !== BYTES) throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes.length}`); diff --git a/src/abstract/weierstrass.ts b/src/abstract/weierstrass.ts index 5dabedc..07f84a1 100644 --- a/src/abstract/weierstrass.ts +++ b/src/abstract/weierstrass.ts @@ -115,9 +115,9 @@ export type AffinePoint = { } & { z?: never }; // Instance for 3d XYZ points export interface ProjectivePointType extends Group> { - readonly x: T; - readonly y: T; - readonly z: T; + readonly px: T; + readonly py: T; + readonly pz: T; multiply(scalar: bigint): ProjectivePointType; multiplyUnsafe(scalar: bigint): ProjectivePointType; multiplyAndAddUnsafe( @@ -140,7 +140,6 @@ export interface ProjectiveConstructor extends GroupConstructor): ProjectivePointType; fromHex(hex: Hex): ProjectivePointType; fromPrivateKey(privateKey: PrivKey): ProjectivePointType; - toAffineBatch(points: ProjectivePointType[]): AffinePoint[]; normalizeZ(points: ProjectivePointType[]): ProjectivePointType[]; } @@ -212,9 +211,12 @@ export function weierstrassPoints(opts: CurvePointsType) { // Valid group elements reside in range 1..n-1 function isWithinCurveOrder(num: bigint): boolean { - return _0n < num && num < CURVE.n; + return typeof num === 'bigint' && _0n < num && num < CURVE.n; + } + function assertGE(num: bigint) { + if (!isWithinCurveOrder(num)) + throw new TypeError('Expected valid bigint: 0 < bigint < curve.n'); } - /** * Validates if a private key is valid and converts it to bigint form. * Supports two options, that are passed when CURVE is initialized: @@ -231,31 +233,21 @@ export function weierstrassPoints(opts: CurvePointsType) { } else if (ut.isPositiveInt(key)) { num = BigInt(key); } else if (typeof key === 'string') { - if (key.length !== 2 * groupLen) throw new Error(`Expected ${groupLen} bytes of private key`); + if (key.length !== 2 * groupLen) throw new Error(`Private key must be ${groupLen} bytes`); // Validates individual octets num = ut.hexToNumber(key); } else if (key instanceof Uint8Array) { - if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes of private key`); + if (key.length !== groupLen) throw new Error(`Private key must be ${groupLen} bytes`); num = ut.bytesToNumberBE(key); } else { - throw new TypeError('Expected valid private key'); + throw new TypeError('Private key was invalid'); } // Useful for curves with cofactor != 1 if (wrapPrivateKey) num = mod.mod(num, order); - if (!isWithinCurveOrder(num)) throw new Error('Expected private key: 0 < key < n'); + assertGE(num); return num; } - /** - * Validates if a scalar ("private number") is valid. - * Scalars are valid only if they are less than curve order. - */ - function normalizeScalar(num: bigint): bigint { - if (ut.isPositiveInt(num)) return BigInt(num); - if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num; - throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n'); - } - const pointPrecomputes = new Map(); /** * Projective Point works in 3d / projective (homogeneous) coordinates: (x, y, z) ∋ (x=x/z, y=y/z) @@ -263,13 +255,14 @@ export function weierstrassPoints(opts: CurvePointsType) { * We're doing calculations in projective, because its operations don't require costly inversion. */ class ProjectivePoint implements ProjectivePointType { - constructor(readonly x: T, readonly y: T, readonly z: T) { - if (y == null || !Fp.isValid(y)) throw new Error('ProjectivePoint: y required'); - if (z == null || !Fp.isValid(z)) throw new Error('ProjectivePoint: z required'); - } static readonly BASE = new ProjectivePoint(CURVE.Gx, CURVE.Gy, Fp.ONE); static readonly ZERO = new ProjectivePoint(Fp.ZERO, Fp.ONE, Fp.ZERO); + constructor(readonly px: T, readonly py: T, readonly pz: T) { + if (py == null || !Fp.isValid(py)) throw new Error('ProjectivePoint: y required'); + if (pz == null || !Fp.isValid(pz)) throw new Error('ProjectivePoint: z required'); + } + static fromAffine(p: AffinePoint): ProjectivePoint { const { x, y } = p || {}; if (!p || !Fp.isValid(x) || !Fp.isValid(y)) @@ -281,6 +274,39 @@ export function weierstrassPoints(opts: CurvePointsType) { return new ProjectivePoint(x, y, Fp.ONE); } + get x(): T { + return this.toAffine().x; + } + get y(): T { + return this.toAffine().y; + } + + /** + * 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. + */ + static normalizeZ(points: ProjectivePoint[]): ProjectivePoint[] { + const toInv = Fp.invertBatch(points.map((p) => p.pz)); + return points.map((p, i) => p.toAffine(toInv[i])).map(ProjectivePoint.fromAffine); + } + + /** + * Converts hash string or Uint8Array to Point. + * @param hex short/long ECDSA hex + */ + static fromHex(hex: Hex): ProjectivePoint { + const P = ProjectivePoint.fromAffine(CURVE.fromBytes(ut.ensureBytes(hex))); + P.assertValidity(); + return P; + } + + // Multiplies generator point by privateKey. + static fromPrivateKey(privateKey: PrivKey) { + return ProjectivePoint.BASE.multiply(normalizePrivateKey(privateKey)); + } + // 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. @@ -291,38 +317,27 @@ export function weierstrassPoints(opts: CurvePointsType) { this._WINDOW_SIZE = windowSize; pointPrecomputes.delete(this); } - protected is0() { - return this.equals(ProjectivePoint.ZERO); - } - private wNAF(n: bigint): { p: ProjectivePoint; f: ProjectivePoint } { - const W = this._WINDOW_SIZE || 1; - // Calculate precomputes on a first run, reuse them after - let comp = pointPrecomputes.get(this); - if (!comp) { - comp = wnaf.precomputeWindow(this, W) as ProjectivePoint[]; - if (W !== 1) { - comp = ProjectivePoint.normalizeZ(comp); - pointPrecomputes.set(this, comp); - } + + // A point on curve is valid if it conforms to equation. + assertValidity(): void { + // Zero is valid point too! + if (this.is0()) { + if (CURVE.allowInfinityPoint) return; + throw new Error('bad point: ZERO'); } - return wnaf.wNAF(W, comp, n); + // Some 3rd-party test vectors require different wording between here & `fromCompressedHex` + const { x, y } = this.toAffine(); + // Check if x, y are valid field elements + if (!Fp.isValid(x) || !Fp.isValid(y)) throw new Error('bad point: x or y not FE'); + const left = Fp.square(y); // y² + const right = weierstrassEquation(x); // x³ + ax + b + if (!Fp.equals(left, right)) throw new Error('bad point: equation left != right'); + if (!this.isTorsionFree()) throw new Error('bad point: not in prime-order subgroup'); } - - /** - * 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. - */ - static toAffineBatch(points: ProjectivePoint[]): AffinePoint[] { - const toInv = Fp.invertBatch(points.map((p) => p.z)); - return points.map((p, i) => p.toAffine(toInv[i])); - } - - /** - * Optimization: converts a list of projective points to a list of identical points with Z=1. - */ - static normalizeZ(points: ProjectivePoint[]): ProjectivePoint[] { - return ProjectivePoint.toAffineBatch(points).map(ProjectivePoint.fromAffine); + hasEvenY(): boolean { + const { y } = this.toAffine(); + if (Fp.isOdd) return !Fp.isOdd(y); + throw new Error("Field doesn't support isOdd"); } /** @@ -330,8 +345,8 @@ export function weierstrassPoints(opts: CurvePointsType) { */ equals(other: ProjectivePoint): boolean { assertPrjPoint(other); - const { x: X1, y: Y1, z: Z1 } = this; - const { x: X2, y: Y2, z: Z2 } = other; + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = other; const U1 = Fp.equals(Fp.mul(X1, Z2), Fp.mul(X2, Z1)); const U2 = Fp.equals(Fp.mul(Y1, Z2), Fp.mul(Y2, Z1)); return U1 && U2; @@ -341,7 +356,7 @@ export function weierstrassPoints(opts: CurvePointsType) { * Flips point to one corresponding to (x, -y) in Affine coordinates. */ negate(): ProjectivePoint { - return new ProjectivePoint(this.x, Fp.negate(this.y), this.z); + return new ProjectivePoint(this.px, Fp.negate(this.py), this.pz); } // Renes-Costello-Batina exception-free doubling formula. @@ -351,7 +366,7 @@ export function weierstrassPoints(opts: CurvePointsType) { double() { const { a, b } = CURVE; const b3 = Fp.mul(b, 3n); - const { x: X1, y: Y1, z: Z1 } = this; + const { px: X1, py: Y1, pz: Z1 } = this; let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; // prettier-ignore let t0 = Fp.mul(X1, X1); // step 1 let t1 = Fp.mul(Y1, Y1); @@ -393,8 +408,8 @@ export function weierstrassPoints(opts: CurvePointsType) { // Cost: 12M + 0S + 3*a + 3*b3 + 23add. add(other: ProjectivePoint): ProjectivePoint { assertPrjPoint(other); - const { x: X1, y: Y1, z: Z1 } = this; - const { x: X2, y: Y2, z: Z2 } = other; + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = other; let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; // prettier-ignore const a = CURVE.a; const b3 = Fp.mul(CURVE.b, 3n); @@ -445,24 +460,33 @@ export function weierstrassPoints(opts: CurvePointsType) { return this.add(other.negate()); } + private is0() { + return this.equals(ProjectivePoint.ZERO); + } + private wNAF(n: bigint): { p: ProjectivePoint; f: ProjectivePoint } { + return wnaf.wNAFCached(this, pointPrecomputes, n, (comp: ProjectivePoint[]) => { + const toInv = Fp.invertBatch(comp.map((p) => p.pz)); + return comp.map((p, i) => p.toAffine(toInv[i])).map(ProjectivePoint.fromAffine); + }); + } + /** * Non-constant-time multiplication. Uses double-and-add algorithm. * It's faster, but should only be used when you don't care about * an exposed private key e.g. sig verification, which works over *public* keys. */ - multiplyUnsafe(scalar: bigint): ProjectivePoint { - const P0 = ProjectivePoint.ZERO; - if (typeof scalar === 'bigint' && scalar === _0n) return P0; - // Will throw on 0 - let n = normalizeScalar(scalar); + multiplyUnsafe(n: bigint): ProjectivePoint { + const I = ProjectivePoint.ZERO; + if (n === _0n) return I; + assertGE(n); // Will throw on 0 if (n === _1n) return this; - - if (!CURVE.endo) return wnaf.unsafeLadder(this, n); + const { endo } = CURVE; + if (!endo) return wnaf.unsafeLadder(this, n); // Apply endomorphism - let { k1neg, k1, k2neg, k2 } = CURVE.endo.splitScalar(n); - let k1p = P0; - let k2p = P0; + let { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); + let k1p = I; + let k2p = I; let d: ProjectivePoint = this; while (k1 > _0n || k2 > _0n) { if (k1 & _1n) k1p = k1p.add(d); @@ -473,9 +497,10 @@ export function weierstrassPoints(opts: CurvePointsType) { } if (k1neg) k1p = k1p.negate(); if (k2neg) k2p = k2p.negate(); - k2p = new ProjectivePoint(Fp.mul(k2p.x, CURVE.endo.beta), k2p.y, k2p.z); + k2p = new ProjectivePoint(Fp.mul(k2p.px, endo.beta), k2p.py, k2p.pz); return k1p.add(k2p); } + /** * Constant time multiplication. * Uses wNAF method. Windowed method may be 10% faster, @@ -485,19 +510,17 @@ export function weierstrassPoints(opts: CurvePointsType) { * @returns New point */ multiply(scalar: bigint): ProjectivePoint { - let n = normalizeScalar(scalar); - - // Real point. - let point: ProjectivePoint; - // Fake point, we use it to achieve constant-time multiplication. - let fake: ProjectivePoint; - if (CURVE.endo) { - const { k1neg, k1, k2neg, k2 } = CURVE.endo.splitScalar(n); + assertGE(scalar); + let n = scalar; + let point: ProjectivePoint, fake: ProjectivePoint; // Fake point is used to const-time mult + const { endo } = CURVE; + if (endo) { + const { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); let { p: k1p, f: f1p } = this.wNAF(k1); let { p: k2p, f: f2p } = this.wNAF(k2); k1p = wnaf.constTimeNegate(k1neg, k1p); k2p = wnaf.constTimeNegate(k2neg, k2p); - k2p = new ProjectivePoint(Fp.mul(k2p.x, CURVE.endo.beta), k2p.y, k2p.z); + k2p = new ProjectivePoint(Fp.mul(k2p.px, endo.beta), k2p.py, k2p.pz); point = k1p.add(k2p); fake = f1p.add(f2p); } else { @@ -509,11 +532,25 @@ export function weierstrassPoints(opts: CurvePointsType) { return ProjectivePoint.normalizeZ([point, fake])[0]; } + /** + * Efficiently calculate `aP + bQ`. Unsafe, can expose private key, if used incorrectly. + * @returns non-zero affine point + */ + multiplyAndAddUnsafe(Q: ProjectivePoint, a: bigint, b: bigint): ProjectivePoint | undefined { + const G = ProjectivePoint.BASE; // No Strauss-Shamir trick: we have 10% faster G precomputes + const mul = ( + P: ProjectivePoint, + a: bigint // Select faster multiply() method + ) => (a === _0n || a === _1n || !P.equals(G) ? P.multiplyUnsafe(a) : P.multiply(a)); + const sum = mul(this, a).add(mul(Q, b)); + return sum.is0() ? undefined : sum; + } + // Converts Projective point to affine (x, y) coordinates. // Can accept precomputed Z^-1 - for example, from invertBatch. // (x, y, z) ∋ (x=x/z, y=y/z) toAffine(iz?: T): AffinePoint { - const { x, y, z } = this; + const { px: x, py: y, pz: z } = this; const is0 = this.is0(); // If invZ was 0, we return zero point. However we still want to execute // all operations, so we replace invZ with a random number, 1. @@ -537,45 +574,7 @@ export function weierstrassPoints(opts: CurvePointsType) { if (clearCofactor) return clearCofactor(ProjectivePoint, this) as ProjectivePoint; return this.multiplyUnsafe(CURVE.h); } - /** - * Efficiently calculate `aP + bQ`. - * Unsafe, can expose private key, if used incorrectly. - * TODO: Utilize Shamir's trick - * @returns non-zero affine point - */ - multiplyAndAddUnsafe(Q: ProjectivePoint, a: bigint, b: bigint): ProjectivePoint | undefined { - const P = this; - const aP = - a === _0n || a === _1n || !P.equals(ProjectivePoint.BASE) - ? P.multiplyUnsafe(a) - : P.multiply(a); - const bQ = Q.multiplyUnsafe(b); - const sum = aP.add(bQ); - return sum.is0() ? undefined : sum; - } - // A point on curve is valid if it conforms to equation. - assertValidity(): void { - const err = 'Point invalid:'; - // Zero is valid point too! - if (this.is0()) { - if (CURVE.allowInfinityPoint) return; - throw new Error(`${err} ZERO`); - } - // Some 3rd-party test vectors require different wording between here & `fromCompressedHex` - const { x, y } = this.toAffine(); - // Check if x, y are valid field elements - if (!Fp.isValid(x) || !Fp.isValid(y)) throw new Error(`${err} x or y not FE`); - const left = Fp.square(y); // y² - const right = weierstrassEquation(x); // x³ + ax + b - if (!Fp.equals(left, right)) throw new Error(`${err} equation left != right`); - if (!this.isTorsionFree()) throw new Error(`${err} not in prime-order subgroup`); - } - hasEvenY(): boolean { - const { y } = this.toAffine(); - if (Fp.isOdd) return !Fp.isOdd(y); - throw new Error("Field doesn't support isOdd"); - } toRawBytes(isCompressed = true): Uint8Array { this.assertValidity(); return CURVE.toBytes(ProjectivePoint, this, isCompressed); @@ -584,21 +583,6 @@ export function weierstrassPoints(opts: CurvePointsType) { toHex(isCompressed = true): string { return bytesToHex(this.toRawBytes(isCompressed)); } - - /** - * Converts hash string or Uint8Array to Point. - * @param hex short/long ECDSA hex - */ - static fromHex(hex: Hex): ProjectivePoint { - const P = ProjectivePoint.fromAffine(CURVE.fromBytes(ut.ensureBytes(hex))); - P.assertValidity(); - return P; - } - - // Multiplies generator point by privateKey. - static fromPrivateKey(privateKey: PrivKey) { - return ProjectivePoint.BASE.multiply(normalizePrivateKey(privateKey)); - } } const _bits = CURVE.nBitLength; const wnaf = wNAF(ProjectivePoint, CURVE.endo ? Math.ceil(_bits / 2) : _bits); @@ -620,7 +604,7 @@ export interface SignatureType { readonly s: bigint; readonly recovery?: number; assertValidity(): void; - copyWithRecoveryBit(recovery: number): SignatureType; + addRecoveryBit(recovery: number): SignatureType; hasHighS(): boolean; normalizeS(): SignatureType; recoverPublicKey(msgHash: Hex): ProjectivePointType; @@ -752,49 +736,53 @@ export function weierstrass(curveDef: CurveType): CurveFn { const uncompressedLen = 2 * Fp.BYTES + 1; // e.g. 65 for 32 function isValidFieldElement(num: bigint): boolean { - // 0 is disallowed by arbitrary reasons. Probably because infinity point? - return _0n < num && num < Fp.ORDER; + return _0n < num && num < Fp.ORDER; // 0 is banned since it's not invertible FE } - const { ProjectivePoint, normalizePrivateKey, weierstrassEquation, isWithinCurveOrder } = - weierstrassPoints({ - ...CURVE, - toBytes(c, point, isCompressed: boolean): Uint8Array { - const a = point.toAffine(); - const x = Fp.toBytes(a.x); - const cat = ut.concatBytes; - if (isCompressed) { - // TODO: hasEvenY - return cat(Uint8Array.from([point.hasEvenY() ? 0x02 : 0x03]), x); - } else { - return cat(Uint8Array.from([0x04]), x, Fp.toBytes(a.y)); - } - }, - fromBytes(bytes: Uint8Array) { - const len = bytes.length; - const header = bytes[0]; - // this.assertValidity() is done inside of fromHex - if (len === compressedLen && (header === 0x02 || header === 0x03)) { - const x = ut.bytesToNumberBE(bytes.subarray(1)); - if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); - const y2 = weierstrassEquation(x); // y² = x³ + ax + b - let y = Fp.sqrt(y2); // y = y² ^ (p+1)/4 - const isYOdd = (y & _1n) === _1n; - // ECDSA - const isFirstByteOdd = (bytes[0] & 1) === 1; - if (isFirstByteOdd !== isYOdd) y = Fp.negate(y); - return { x, y }; - } else if (len === uncompressedLen && header === 0x04) { - const x = Fp.fromBytes(bytes.subarray(1, Fp.BYTES + 1)); - const y = Fp.fromBytes(bytes.subarray(Fp.BYTES + 1, 2 * Fp.BYTES + 1)); - return { x, y }; - } else { - throw new Error( - `Point.fromHex: received invalid point. Expected ${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes, not ${len}` - ); - } - }, - }); + const { + ProjectivePoint: Point, + normalizePrivateKey, + weierstrassEquation, + isWithinCurveOrder, + } = weierstrassPoints({ + ...CURVE, + toBytes(c, point, isCompressed: boolean): Uint8Array { + const a = point.toAffine(); + const x = Fp.toBytes(a.x); + const cat = ut.concatBytes; + if (isCompressed) { + // TODO: hasEvenY + return cat(Uint8Array.from([point.hasEvenY() ? 0x02 : 0x03]), x); + } else { + return cat(Uint8Array.from([0x04]), x, Fp.toBytes(a.y)); + } + }, + fromBytes(bytes: Uint8Array) { + const len = bytes.length; + const head = bytes[0]; + const tail = bytes.subarray(1); + // this.assertValidity() is done inside of fromHex + if (len === compressedLen && (head === 0x02 || head === 0x03)) { + const x = ut.bytesToNumberBE(tail); + if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); + const y2 = weierstrassEquation(x); // y² = x³ + ax + b + let y = Fp.sqrt(y2); // y = y² ^ (p+1)/4 + const isYOdd = (y & _1n) === _1n; + // ECDSA + const isHeadOdd = (head & 1) === 1; + if (isHeadOdd !== isYOdd) y = Fp.negate(y); + return { x, y }; + } else if (len === uncompressedLen && head === 0x04) { + const x = Fp.fromBytes(tail.subarray(0, Fp.BYTES)); + const y = Fp.fromBytes(tail.subarray(Fp.BYTES, 2 * Fp.BYTES)); + return { x, y }; + } else { + throw new Error( + `Point.fromHex: received invalid point. Expected ${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes, not ${len}` + ); + } + }, + }); // type Point = typeof ProjectivePoint.BASE; // Do we need these functions at all? @@ -809,11 +797,11 @@ export function weierstrass(curveDef: CurveType): CurveFn { * Normalizes hex, bytes, Point to Point. Checks for curve equation. */ function normalizePublicKey(publicKey: PubKey): ProjectivePointType { - if (publicKey instanceof ProjectivePoint) { + if (publicKey instanceof Point) { publicKey.assertValidity(); return publicKey; } else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') { - return ProjectivePoint.fromHex(publicKey); + return Point.fromHex(publicKey); // This can happen because PointType can be instance of different class } else throw new Error(`Unknown type of public key: ${publicKey}`); } @@ -826,6 +814,8 @@ export function weierstrass(curveDef: CurveType): CurveFn { function normalizeS(s: bigint) { return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE_ORDER) : s; } + // slice bytes num + const slcNum = (b: Uint8Array, from: number, to: number) => ut.bytesToNumberBE(b.slice(from, to)); /** * ECDSA signature with its (r, s) properties. Supports DER & compact representations. @@ -837,15 +827,9 @@ export function weierstrass(curveDef: CurveType): CurveFn { // pair (bytes of r, bytes of s) static fromCompact(hex: Hex) { - const arr = hex instanceof Uint8Array; - const name = 'Signature.fromCompact'; - if (typeof hex !== 'string' && !arr) - throw new TypeError(`${name}: Expected string or Uint8Array`); - const str = arr ? bytesToHex(hex) : hex; - const gl = CURVE.nByteLength * 2; // group length in hex, not ui8a - if (str.length !== 2 * gl) throw new Error(`${name}: Expected ${gl / 2}-byte hex`); - const slice = (from: number, to: number) => ut.hexToNumber(str.slice(from, to)); - return new Signature(slice(0, gl), slice(gl, 2 * gl)); + const gl = CURVE.nByteLength; + hex = ut.ensureBytes(hex, gl * 2); + return new Signature(slcNum(hex, 0, gl), slcNum(hex, gl, 2 * gl)); } // DER encoded ECDSA signature @@ -859,12 +843,12 @@ export function weierstrass(curveDef: CurveType): CurveFn { } assertValidity(): void { - const { r, s } = this; - if (!isWithinCurveOrder(r)) throw new Error('Invalid Signature: r must be 0 < r < n'); - if (!isWithinCurveOrder(s)) throw new Error('Invalid Signature: s must be 0 < s < n'); + // can use assertGE here + if (!isWithinCurveOrder(this.r)) throw new Error('r must be 0 < r < n'); + if (!isWithinCurveOrder(this.s)) throw new Error('s must be 0 < s < n'); } - copyWithRecoveryBit(recovery: number) { + addRecoveryBit(recovery: number) { return new Signature(this.r, this.s, recovery); } @@ -883,23 +867,21 @@ export function weierstrass(curveDef: CurveType): CurveFn { * @param msgHash message hash * @returns Point corresponding to public key */ - recoverPublicKey(msgHash: Hex): typeof ProjectivePoint.BASE { - const { r, s, recovery } = this; - if (recovery == null) throw new Error('Cannot recover: recovery bit is not present'); - if (![0, 1, 2, 3].includes(recovery)) throw new Error('Cannot recover: invalid recovery bit'); + recoverPublicKey(msgHash: Hex): typeof Point.BASE { + const { n: N } = CURVE; + const { r, s, recovery: rec } = this; const h = bits2int_modN(ut.ensureBytes(msgHash)); - - const { n } = CURVE; - const radj = recovery === 2 || recovery === 3 ? r + n : r; - if (radj >= Fp.ORDER) throw new Error('Cannot recover: bit 2/3 is invalid with current r'); - const rinv = mod.invert(radj, n); - // Q = u1⋅G + u2⋅R - const u1 = mod.mod(-h * rinv, n); - const u2 = mod.mod(s * rinv, n); - const prefix = recovery & 1 ? '03' : '02'; - const R = ProjectivePoint.fromHex(prefix + numToFieldStr(radj)); - const Q = ProjectivePoint.BASE.multiplyAndAddUnsafe(R, u1, u2); // unsafe is fine: no priv data leaked - if (!Q) throw new Error('Cannot recover: point at infinify'); + if (rec == null || ![0, 1, 2, 3].includes(rec)) throw new Error('recovery id invalid'); + const radj = rec === 2 || rec === 3 ? r + N : r; + if (radj >= Fp.ORDER) throw new Error('recovery id 2 or 3 currently invalid'); + const prefix = (rec & 1) === 0 ? '02' : '03'; + const R = Point.fromHex(prefix + numToFieldStr(radj)); + const ir = mod.invert(radj, N); // r^-1 + const u1 = mod.mod(-h * ir, N); // -hr^-1 + const u2 = mod.mod(s * ir, N); // sr^-1 + // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) + const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); + if (!Q) throw new Error('point at infinify'); // unsafe is fine: no priv data leaked Q.assertValidity(); return Q; } @@ -981,8 +963,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { * @param windowSize 2, 4, 8, 16 * @returns cached point */ - precompute(windowSize = 8, point = ProjectivePoint.BASE): typeof ProjectivePoint.BASE { - // const cached = point === ProjectivePoint.BASE ? point : ProjectivePoint.fromAffine({x, y}); + precompute(windowSize = 8, point = Point.BASE): typeof Point.BASE { point._setWindowSize(windowSize); point.multiply(BigInt(3)); return point; @@ -996,7 +977,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { * @returns Public key, full when isCompressed=false; short when isCompressed=true */ function getPublicKey(privateKey: PrivKey, isCompressed = true): Uint8Array { - return ProjectivePoint.fromPrivateKey(privateKey).toRawBytes(isCompressed); + return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed); } /** @@ -1008,7 +989,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { const len = (arr || str) && (item as Hex).length; if (arr) return len === compressedLen || len === uncompressedLen; if (str) return len === 2 * compressedLen || len === 2 * uncompressedLen; - if (item instanceof ProjectivePoint) return true; + if (item instanceof Point) return true; return false; } @@ -1101,7 +1082,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { if (!isWithinCurveOrder(k)) return; // Important: all mod() calls in the function must be done over `n` const ik = mod.invert(k, n); - const q = ProjectivePoint.BASE.multiply(k).toAffine(); + const q = Point.BASE.multiply(k).toAffine(); // r = x mod n const r = mod.mod(q.x, n); if (r === _0n) return; @@ -1146,7 +1127,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { } // Enable precomputes. Slows down first publicKey computation by 20ms. - ProjectivePoint.BASE._setWindowSize(8); + Point.BASE._setWindowSize(8); // utils.precompute(8, ProjectivePoint.BASE) /** @@ -1203,9 +1184,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { const u1 = mod.mod(h * sinv, n); const u2 = mod.mod(r * sinv, n); - // Some implementations compare R.x in projective, without inversion. - // The speed-up is <5%, so we don't complicate the code. - const R = ProjectivePoint.BASE.multiplyAndAddUnsafe(P, u1, u2)?.toAffine(); + const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2)?.toAffine(); if (!R) return false; const v = mod.mod(R.x, n); return v === r; @@ -1218,7 +1197,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { signUnhashed, verify, // Point, - ProjectivePoint, + ProjectivePoint: Point, Signature, utils, }; diff --git a/src/bls12-381.ts b/src/bls12-381.ts index f9626bd..d3d6374 100644 --- a/src/bls12-381.ts +++ b/src/bls12-381.ts @@ -990,7 +990,7 @@ export const bls12_381: CurveFn = bls({ // φ endomorphism const cubicRootOfUnityModP = 0x5f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffen; - const phi = new c(Fp.mul(point.x, cubicRootOfUnityModP), point.y, point.z); + const phi = new c(Fp.mul(point.px, cubicRootOfUnityModP), point.py, point.pz); // todo: unroll const xP = point.multiplyUnsafe(bls12_381.CURVE.x).negate(); // [x]P diff --git a/src/ed25519.ts b/src/ed25519.ts index eef3edb..b71bd77 100644 --- a/src/ed25519.ts +++ b/src/ed25519.ts @@ -362,7 +362,7 @@ export class RistrettoPoint { * https://ristretto.group/formulas/encoding.html */ toRawBytes(): Uint8Array { - let { x, y, z, t } = this.ep; + let { ex: x, ey: y, ez: z, et: t } = this.ep; const P = ed25519.CURVE.Fp.ORDER; const mod = ed25519.CURVE.Fp.create; const u1 = mod(mod(z + y) * mod(z - y)); // 1 @@ -400,12 +400,12 @@ export class RistrettoPoint { // Compare one point to another. equals(other: RistrettoPoint): boolean { assertRstPoint(other); - const a = this.ep; - const b = other.ep; + const { ex: X1, ey: Y1 } = this.ep; + const { ex: X2, ey: Y2 } = this.ep; const mod = ed25519.CURVE.Fp.create; // (x1 * y2 == y1 * x2) | (y1 * y2 == x1 * x2) - const one = mod(a.x * b.y) === mod(a.y * b.x); - const two = mod(a.y * b.y) === mod(a.x * b.x); + const one = mod(X1 * Y2) === mod(Y1 * X2); + const two = mod(Y1 * Y2) === mod(X1 * X2); return one || two; } diff --git a/src/secp256k1.ts b/src/secp256k1.ts index afff496..430ea75 100644 --- a/src/secp256k1.ts +++ b/src/secp256k1.ts @@ -280,7 +280,7 @@ function schnorrSign( if (k0 === _0n) throw new Error('sign: Creation of signature failed. k is zero'); const { point: R, x: rx, scalar: k } = schnorrGetScalar(k0); const e = schnorrChallengeFinalize(tag(TAGS.challenge, rx, px, m)); - const sig = new SchnorrSignature(R.x, mod(k + e * d, secp256k1.CURVE.n)).toRawBytes(); + const sig = new SchnorrSignature(R.px, mod(k + e * d, secp256k1.CURVE.n)).toRawBytes(); if (!schnorrVerify(sig, m, px)) throw new Error('sign: Invalid signature produced'); return sig; } diff --git a/src/stark.ts b/src/stark.ts index 7da61b0..d7ac9d4 100644 --- a/src/stark.ts +++ b/src/stark.ts @@ -245,7 +245,7 @@ function pedersenSingle(point: ProjectivePoint, value: PedersenArg, constants: P let x = pedersenArg(value); for (let j = 0; j < 252; j++) { const pt = constants[j]; - if (pt.x === point.x) throw new Error('Same point'); + if (pt.px === point.px) throw new Error('Same point'); if ((x & 1n) !== 0n) point = point.add(pt); x >>= 1n; } diff --git a/test/stark/basic.test.js b/test/stark/basic.test.js index bbb9cea..f29e192 100644 --- a/test/stark/basic.test.js +++ b/test/stark/basic.test.js @@ -1,198 +1,199 @@ import { deepStrictEqual, throws } from 'assert'; -import { should } from 'micro-should'; +import { describe, should } from 'micro-should'; import * as starknet from '../../lib/esm/stark.js'; import { default as issue2 } from './fixtures/issue2.json' assert { type: 'json' }; - -should('Basic elliptic sanity check', () => { - const g1 = starknet.ProjectivePoint.BASE; - deepStrictEqual( - g1.toAffine().x.toString(16), - '1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca' - ); - deepStrictEqual( - g1.toAffine().y.toString(16), - '5668060aa49730b7be4801df46ec62de53ecd11abe43a32873000c36e8dc1f' - ); - const g2 = g1.double(); - deepStrictEqual( - g2.toAffine().x.toString(16), - '759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5' - ); - deepStrictEqual( - g2.toAffine().y.toString(16), - '6f524a3400e7708d5c01a28598ad272e7455aa88778b19f93b562d7a9646c41' - ); - const g3 = g2.add(g1); - deepStrictEqual( - g3.toAffine().x.toString(16), - '411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20' - ); - deepStrictEqual( - g3.toAffine().y.toString(16), - '7e1b3ebac08924d2c26f409549191fcf94f3bf6f301ed3553e22dfb802f0686' - ); - const g32 = g1.multiply(3); - deepStrictEqual( - g32.toAffine().x.toString(16), - '411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20' - ); - deepStrictEqual( - g32.toAffine().y.toString(16), - '7e1b3ebac08924d2c26f409549191fcf94f3bf6f301ed3553e22dfb802f0686' - ); - const minus1 = g1.multiply(starknet.CURVE.n - 1n); - deepStrictEqual( - minus1.toAffine().x.toString(16), - '1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca' - ); - deepStrictEqual( - minus1.toAffine().y.toString(16), - '7a997f9f55b68e04841b7fe20b9139d21ac132ee541bc5cd78cfff3c91723e2' - ); -}); - -should('Pedersen', () => { - deepStrictEqual( - starknet.pedersen(2, 3), - '0x5774fa77b3d843ae9167abd61cf80365a9b2b02218fc2f628494b5bdc9b33b8' - ); - deepStrictEqual( - starknet.pedersen(1, 2), - '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' - ); - deepStrictEqual( - starknet.pedersen(3, 4), - '0x262697b88544f733e5c6907c3e1763131e9f14c51ee7951258abbfb29415fbf' - ); -}); - -should('Hash chain', () => { - deepStrictEqual( - starknet.hashChain([1, 2, 3]), - '0x5d9d62d4040b977c3f8d2389d494e4e89a96a8b45c44b1368f1cc6ec5418915' - ); -}); - -should('Pedersen hash edgecases', () => { - // >>> pedersen_hash(0,0) - const zero = '0x49ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804'; - deepStrictEqual(starknet.pedersen(0, 0), zero); - deepStrictEqual(starknet.pedersen(0n, 0n), zero); - deepStrictEqual(starknet.pedersen('0', '0'), zero); - deepStrictEqual(starknet.pedersen('0x0', '0x0'), zero); - // >>> pedersen_hash(3618502788666131213697322783095070105623107215331596699973092056135872020475,3618502788666131213697322783095070105623107215331596699973092056135872020475) - // 3226051580231087455100099637526672350308978851161639703631919449959447036451 - const big = 3618502788666131213697322783095070105623107215331596699973092056135872020475n; - const bigExp = '0x721e167a36655994e88efa865e2ed8a0488d36db4d988fec043cda755728223'; - deepStrictEqual(starknet.pedersen(big, big), bigExp); - // >= FIELD - const big2 = 36185027886661312136973227830950701056231072153315966999730920561358720204751n; - throws(() => starknet.pedersen(big2, big2), 'big2'); - - // FIELD -1 - const big3 = 3618502788666131213697322783095070105623107215331596699973092056135872020480n; - const big3exp = '0x7258fccaf3371fad51b117471d9d888a1786c5694c3e6099160477b593a576e'; - deepStrictEqual(starknet.pedersen(big3, big3), big3exp, 'big3'); - // FIELD - const big4 = 3618502788666131213697322783095070105623107215331596699973092056135872020481n; - throws(() => starknet.pedersen(big4, big4), 'big4'); - throws(() => starknet.pedersen(-1, -1), 'neg'); - throws(() => starknet.pedersen(false, false), 'false'); - throws(() => starknet.pedersen(true, true), 'true'); - throws(() => starknet.pedersen(10.1, 10.1), 'float'); -}); - -should('hashChain edgecases', () => { - deepStrictEqual(starknet.hashChain([32312321312321312312312321n]), '0x1aba6672c014b4838cc201'); - deepStrictEqual( - starknet.hashChain([1n, 2n]), - '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' - ); - deepStrictEqual( - starknet.hashChain([1, 2]), - '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' - ); - throws(() => starknet.hashChain([])); - throws(() => starknet.hashChain('123')); - deepStrictEqual( - starknet.hashChain([1, 2]), - '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' - ); -}); - -should('Pedersen hash, issue #2', () => { - // Verified with starnet.js - deepStrictEqual( - starknet.computeHashOnElements(issue2), - '0x22064462ea33a6ce5272a295e0f551c5da3834f80d8444e7a4df68190b1bc42' - ); - deepStrictEqual( - starknet.computeHashOnElements([]), - '0x49ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804' - ); - deepStrictEqual( - starknet.computeHashOnElements([1]), - '0x78d74f61aeaa8286418fd34b3a12a610445eba11d00ecc82ecac2542d55f7a4' - ); -}); - import * as bip32 from '@scure/bip32'; import * as bip39 from '@scure/bip39'; -should('Seed derivation (example)', () => { - const layer = 'starkex'; - const application = 'starkdeployement'; - const mnemonic = - 'range mountain blast problem vibrant void vivid doctor cluster enough melody ' + - 'salt layer language laptop boat major space monkey unit glimpse pause change vibrant'; - const ethAddress = '0xa4864d977b944315389d1765ffa7e66F74ee8cd7'; - const hdKey = bip32.HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(mnemonic)).derive( - starknet.getAccountPath(layer, application, ethAddress, 0) - ); - deepStrictEqual( - starknet.grindKey(hdKey.privateKey), - '6cf0a8bf113352eb863157a45c5e5567abb34f8d32cddafd2c22aa803f4892c' - ); -}); +describe('starknet basic', () => { + should('Basic elliptic sanity check', () => { + const g1 = starknet.ProjectivePoint.BASE; + deepStrictEqual( + g1.toAffine().x.toString(16), + '1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca' + ); + deepStrictEqual( + g1.toAffine().y.toString(16), + '5668060aa49730b7be4801df46ec62de53ecd11abe43a32873000c36e8dc1f' + ); + const g2 = g1.double(); + deepStrictEqual( + g2.toAffine().x.toString(16), + '759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5' + ); + deepStrictEqual( + g2.toAffine().y.toString(16), + '6f524a3400e7708d5c01a28598ad272e7455aa88778b19f93b562d7a9646c41' + ); + const g3 = g2.add(g1); + deepStrictEqual( + g3.toAffine().x.toString(16), + '411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20' + ); + deepStrictEqual( + g3.toAffine().y.toString(16), + '7e1b3ebac08924d2c26f409549191fcf94f3bf6f301ed3553e22dfb802f0686' + ); + const g32 = g1.multiply(3n); + deepStrictEqual( + g32.toAffine().x.toString(16), + '411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20' + ); + deepStrictEqual( + g32.toAffine().y.toString(16), + '7e1b3ebac08924d2c26f409549191fcf94f3bf6f301ed3553e22dfb802f0686' + ); + const minus1 = g1.multiply(starknet.CURVE.n - 1n); + deepStrictEqual( + minus1.toAffine().x.toString(16), + '1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca' + ); + deepStrictEqual( + minus1.toAffine().y.toString(16), + '7a997f9f55b68e04841b7fe20b9139d21ac132ee541bc5cd78cfff3c91723e2' + ); + }); -should('Compressed keys', () => { - const G = starknet.ProjectivePoint.BASE; - const half = starknet.CURVE.n / 2n; - const last = starknet.CURVE.n; - const vectors = [ - 1, - 2, - 3, - 4, - 5, - half - 5n, - half - 4n, - half - 3n, - half - 2n, - half - 1n, - half, - half + 1n, - half + 2n, - half + 3n, - half + 4n, - half + 5n, - last - 5n, - last - 4n, - last - 3n, - last - 2n, - last - 1n, - ].map((i) => G.multiply(i)); - const fixPoint = (pt) => pt.toAffine(); - for (const v of vectors) { - const uncompressed = v.toHex(); - const compressed = v.toHex(true); - const exp = fixPoint(v); - deepStrictEqual(fixPoint(starknet.ProjectivePoint.fromHex(uncompressed)), exp); - deepStrictEqual(fixPoint(starknet.ProjectivePoint.fromHex(compressed)), exp); - deepStrictEqual(starknet.ProjectivePoint.fromHex(compressed).toHex(), uncompressed); - } -}); + should('Pedersen', () => { + deepStrictEqual( + starknet.pedersen(2, 3), + '0x5774fa77b3d843ae9167abd61cf80365a9b2b02218fc2f628494b5bdc9b33b8' + ); + deepStrictEqual( + starknet.pedersen(1, 2), + '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' + ); + deepStrictEqual( + starknet.pedersen(3, 4), + '0x262697b88544f733e5c6907c3e1763131e9f14c51ee7951258abbfb29415fbf' + ); + }); + should('Hash chain', () => { + deepStrictEqual( + starknet.hashChain([1, 2, 3]), + '0x5d9d62d4040b977c3f8d2389d494e4e89a96a8b45c44b1368f1cc6ec5418915' + ); + }); + + should('Pedersen hash edgecases', () => { + // >>> pedersen_hash(0,0) + const zero = '0x49ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804'; + deepStrictEqual(starknet.pedersen(0, 0), zero); + deepStrictEqual(starknet.pedersen(0n, 0n), zero); + deepStrictEqual(starknet.pedersen('0', '0'), zero); + deepStrictEqual(starknet.pedersen('0x0', '0x0'), zero); + // >>> pedersen_hash(3618502788666131213697322783095070105623107215331596699973092056135872020475,3618502788666131213697322783095070105623107215331596699973092056135872020475) + // 3226051580231087455100099637526672350308978851161639703631919449959447036451 + const big = 3618502788666131213697322783095070105623107215331596699973092056135872020475n; + const bigExp = '0x721e167a36655994e88efa865e2ed8a0488d36db4d988fec043cda755728223'; + deepStrictEqual(starknet.pedersen(big, big), bigExp); + // >= FIELD + const big2 = 36185027886661312136973227830950701056231072153315966999730920561358720204751n; + throws(() => starknet.pedersen(big2, big2), 'big2'); + + // FIELD -1 + const big3 = 3618502788666131213697322783095070105623107215331596699973092056135872020480n; + const big3exp = '0x7258fccaf3371fad51b117471d9d888a1786c5694c3e6099160477b593a576e'; + deepStrictEqual(starknet.pedersen(big3, big3), big3exp, 'big3'); + // FIELD + const big4 = 3618502788666131213697322783095070105623107215331596699973092056135872020481n; + throws(() => starknet.pedersen(big4, big4), 'big4'); + throws(() => starknet.pedersen(-1, -1), 'neg'); + throws(() => starknet.pedersen(false, false), 'false'); + throws(() => starknet.pedersen(true, true), 'true'); + throws(() => starknet.pedersen(10.1, 10.1), 'float'); + }); + + should('hashChain edgecases', () => { + deepStrictEqual(starknet.hashChain([32312321312321312312312321n]), '0x1aba6672c014b4838cc201'); + deepStrictEqual( + starknet.hashChain([1n, 2n]), + '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' + ); + deepStrictEqual( + starknet.hashChain([1, 2]), + '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' + ); + throws(() => starknet.hashChain([])); + throws(() => starknet.hashChain('123')); + deepStrictEqual( + starknet.hashChain([1, 2]), + '0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026' + ); + }); + + should('Pedersen hash, issue #2', () => { + // Verified with starnet.js + deepStrictEqual( + starknet.computeHashOnElements(issue2), + '0x22064462ea33a6ce5272a295e0f551c5da3834f80d8444e7a4df68190b1bc42' + ); + deepStrictEqual( + starknet.computeHashOnElements([]), + '0x49ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804' + ); + deepStrictEqual( + starknet.computeHashOnElements([1]), + '0x78d74f61aeaa8286418fd34b3a12a610445eba11d00ecc82ecac2542d55f7a4' + ); + }); + + + should('Seed derivation (example)', () => { + const layer = 'starkex'; + const application = 'starkdeployement'; + const mnemonic = + 'range mountain blast problem vibrant void vivid doctor cluster enough melody ' + + 'salt layer language laptop boat major space monkey unit glimpse pause change vibrant'; + const ethAddress = '0xa4864d977b944315389d1765ffa7e66F74ee8cd7'; + const hdKey = bip32.HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(mnemonic)).derive( + starknet.getAccountPath(layer, application, ethAddress, 0) + ); + deepStrictEqual( + starknet.grindKey(hdKey.privateKey), + '6cf0a8bf113352eb863157a45c5e5567abb34f8d32cddafd2c22aa803f4892c' + ); + }); + + should('Compressed keys', () => { + const G = starknet.ProjectivePoint.BASE; + const half = starknet.CURVE.n / 2n; + const last = starknet.CURVE.n; + const vectors = [ + 1n, + 2n, + 3n, + 4n, + 5n, + half - 5n, + half - 4n, + half - 3n, + half - 2n, + half - 1n, + half, + half + 1n, + half + 2n, + half + 3n, + half + 4n, + half + 5n, + last - 5n, + last - 4n, + last - 3n, + last - 2n, + last - 1n, + ].map((i) => G.multiply(i)); + const fixPoint = (pt) => pt.toAffine(); + for (const v of vectors) { + const uncompressed = v.toHex(); + const compressed = v.toHex(true); + const exp = fixPoint(v); + deepStrictEqual(fixPoint(starknet.ProjectivePoint.fromHex(uncompressed)), exp); + deepStrictEqual(fixPoint(starknet.ProjectivePoint.fromHex(compressed)), exp); + deepStrictEqual(starknet.ProjectivePoint.fromHex(compressed).toHex(), uncompressed); + } + }); +}); // ESM is broken. import url from 'url'; if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {