]> git.codecow.com Git - libnemo.git/commitdiff
Convert Point from class to factory function to resolve final class self-references.
authorChris Duncan <chris@zoso.dev>
Mon, 1 Dec 2025 06:14:16 +0000 (22:14 -0800)
committerChris Duncan <chris@zoso.dev>
Mon, 1 Dec 2025 06:14:16 +0000 (22:14 -0800)
src/lib/crypto/secp256k1.ts

index 8f5970ff43ab8cfd7dc04236bb84c76a5a4553fc..0c48cbcc6a5387e47fdb64c8af26d4f354d7f860 100644 (file)
  * Functionality wrapped in a class for embedding into a Web Worker.
  */
 
-type Point = InstanceType<typeof Secp256k1.Point>
+type Point = {
+       X: bigint
+       Y: bigint
+       Z: bigint
+       get x (): bigint
+       get y (): bigint
+       equals: (other: Point) => boolean
+       is0: () => boolean
+       negate: () => Point
+       double: () => Point
+       add: (other: Point) => Point
+       subtract: (other: Point) => Point
+       multiply: (n: bigint, safe?: boolean) => Point
+       multiplyUnsafe: (scalar: bigint) => Point
+       toAffine: () => AffinePoint
+       assertValidity: () => Point
+       toBytes: (isCompressed?: boolean) => Bytes
+       toHex: (isCompressed?: boolean) => string
+}
 type Signature = ReturnType<typeof Secp256k1.Signature>
 
 /** Alias to Uint8Array. */
@@ -260,140 +278,136 @@ export class Secp256k1 {
        }
 
        /** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */
-       static Point = class {
-               readonly X: bigint
-               readonly Y: bigint
-               readonly Z: bigint
-               constructor (X: bigint, Y: bigint, Z: bigint) {
-                       this.X = Secp256k1.FpIsValid(X)
-                       this.Y = Secp256k1.FpIsValidNot0(Y) // Y can't be 0 in Projective
-                       this.Z = Secp256k1.FpIsValid(Z)
-                       Object.freeze(this)
-               }
-               get x (): bigint {
-                       return this.toAffine().x
-               }
-               get y (): bigint {
-                       return this.toAffine().y
-               }
-               /** Equality check: compare points P&Q. */
-               equals (other: Point): boolean {
-                       const { X: X1, Y: Y1, Z: Z1 } = this
-                       const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other) // checks class equality
-                       const X1Z2 = Secp256k1.M(X1 * Z2)
-                       const X2Z1 = Secp256k1.M(X2 * Z1)
-                       const Y1Z2 = Secp256k1.M(Y1 * Z2)
-                       const Y2Z1 = Secp256k1.M(Y2 * Z1)
-                       return X1Z2 === X2Z1 && Y1Z2 === Y2Z1
-               }
-               is0 (): boolean {
-                       return this.equals(Secp256k1.I)
-               }
-               /** Flip point over y coordinate. */
-               negate (): Point {
-                       return new (this.constructor as typeof Secp256k1.Point)(this.X, Secp256k1.M(-this.Y), this.Z)
-               }
-               /** Point doubling: P+P, complete formula. */
-               double (): Point {
-                       return this.add(this)
-               }
-               /**
-                * Point addition: P+Q, complete, exception-free formula
-                * (Renes-Costello-Batina, algo 1 of [2015/1060](https://eprint.iacr.org/2015/1060)).
-                * Cost: `12M + 0S + 3*a + 3*b3 + 23add`.
-                */
-               // prettier-ignore
-               add (other: Point): Point {
-                       const { X: X1, Y: Y1, Z: Z1 } = this
-                       const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other)
-                       const a = 0n
-                       const b = Secp256k1._b
-                       let X3 = 0n, Y3 = 0n, Z3 = 0n
-                       const b3 = Secp256k1.M(b * 3n)
-                       let t0 = Secp256k1.M(X1 * X2), t1 = Secp256k1.M(Y1 * Y2), t2 = Secp256k1.M(Z1 * Z2), t3 = Secp256k1.M(X1 + Y1) // step 1
-                       let t4 = Secp256k1.M(X2 + Y2) // step 5
-                       t3 = Secp256k1.M(t3 * t4); t4 = Secp256k1.M(t0 + t1); t3 = Secp256k1.M(t3 - t4); t4 = Secp256k1.M(X1 + Z1)
-                       let t5 = Secp256k1.M(X2 + Z2) // step 10
-                       t4 = Secp256k1.M(t4 * t5); t5 = Secp256k1.M(t0 + t2); t4 = Secp256k1.M(t4 - t5); t5 = Secp256k1.M(Y1 + Z1)
-                       X3 = Secp256k1.M(Y2 + Z2) // step 15
-                       t5 = Secp256k1.M(t5 * X3); X3 = Secp256k1.M(t1 + t2); t5 = Secp256k1.M(t5 - X3); Z3 = Secp256k1.M(a * t4)
-                       X3 = Secp256k1.M(b3 * t2) // step 20
-                       Z3 = Secp256k1.M(X3 + Z3); X3 = Secp256k1.M(t1 - Z3); Z3 = Secp256k1.M(t1 + Z3); Y3 = Secp256k1.M(X3 * Z3)
-                       t1 = Secp256k1.M(t0 + t0) // step 25
-                       t1 = Secp256k1.M(t1 + t0); t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(b3 * t4); t1 = Secp256k1.M(t1 + t2)
-                       t2 = Secp256k1.M(t0 - t2) // step 30
-                       t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(t4 + t2); t0 = Secp256k1.M(t1 * t4); Y3 = Secp256k1.M(Y3 + t0)
-                       t0 = Secp256k1.M(t5 * t4) // step 35
-                       X3 = Secp256k1.M(t3 * X3); X3 = Secp256k1.M(X3 - t0); t0 = Secp256k1.M(t3 * t1); Z3 = Secp256k1.M(t5 * Z3)
-                       Z3 = Secp256k1.M(Z3 + t0) // step 40
-                       return new Secp256k1.Point(X3, Y3, Z3)
-               }
-               subtract (other: Point): Point {
-                       return this.add(Secp256k1.apoint(other).negate())
-               }
-               /**
-                * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.
-                * Uses {@link wNAF} for base point.
-                * Uses fake point to mitigate side-channel leakage.
-                * @param n scalar by which point is multiplied
-                * @param safe safe mode guards against timing attacks; unsafe mode is faster
-                */
-               multiply (n: bigint, safe = true): Point {
-                       if (!safe && n === 0n) return Secp256k1.I
-                       Secp256k1.FnIsValidNot0(n)
-                       if (n === 1n) return this
-                       if (this.equals(Secp256k1.G)) return Secp256k1.wNAF(n).p
-                       // init result point & fake point
-                       let p = Secp256k1.I
-                       let f = Secp256k1.G
-                       for (let d: Point = this; n > 0n; d = d.double(), n >>= 1n) {
-                               // if bit is present, add to point
-                               // if not present, add to fake, for timing safety
-                               if (n & 1n) p = p.add(d)
-                               else if (safe) f = f.add(d)
+       static Point = (X: bigint, Y: bigint, Z: bigint): Point => {
+               const secp256k1 = this
+               return Object.freeze({
+                       X: this.FpIsValid(X),
+                       Y: this.FpIsValidNot0(Y), // Y can't be 0 in Projective
+                       Z: this.FpIsValid(Z),
+                       get x (): bigint {
+                               return this.toAffine().x
+                       },
+                       get y (): bigint {
+                               return this.toAffine().y
+                       },
+                       /** Equality check: compare points P&Q. */
+                       equals (other: Point): boolean {
+                               const { X: X1, Y: Y1, Z: Z1 } = this
+                               const { X: X2, Y: Y2, Z: Z2 } = other
+                               const X1Z2 = secp256k1.M(X1 * Z2)
+                               const X2Z1 = secp256k1.M(X2 * Z1)
+                               const Y1Z2 = secp256k1.M(Y1 * Z2)
+                               const Y2Z1 = secp256k1.M(Y2 * Z1)
+                               return X1Z2 === X2Z1 && Y1Z2 === Y2Z1
+                       },
+                       is0 (): boolean {
+                               return this.equals(secp256k1.I)
+                       },
+                       /** Flip point over y coordinate. */
+                       negate: (): Point => {
+                               return this.Point(X, this.M(-Y), Z)
+                       },
+                       /** Point doubling: P+P, complete formula. */
+                       double (): Point {
+                               return this.add(this)
+                       },
+                       /**
+                        * Point addition: P+Q, complete, exception-free formula
+                        * (Renes-Costello-Batina, algo 1 of [2015/1060](https://eprint.iacr.org/2015/1060)).
+                        * Cost: `12M + 0S + 3*a + 3*b3 + 23add`.
+                        */
+                       // prettier-ignore
+                       add: (other: Point): Point => {
+                               const { X: X1, Y: Y1, Z: Z1 } = { X, Y, Z }
+                               const { X: X2, Y: Y2, Z: Z2 } = other
+                               const a = 0n
+                               const b = this._b
+                               let X3 = 0n, Y3 = 0n, Z3 = 0n
+                               const b3 = this.M(b * 3n)
+                               let t0 = this.M(X1 * X2), t1 = this.M(Y1 * Y2), t2 = this.M(Z1 * Z2), t3 = this.M(X1 + Y1) // step 1
+                               let t4 = this.M(X2 + Y2) // step 5
+                               t3 = this.M(t3 * t4); t4 = this.M(t0 + t1); t3 = this.M(t3 - t4); t4 = this.M(X1 + Z1)
+                               let t5 = this.M(X2 + Z2) // step 10
+                               t4 = this.M(t4 * t5); t5 = this.M(t0 + t2); t4 = this.M(t4 - t5); t5 = this.M(Y1 + Z1)
+                               X3 = this.M(Y2 + Z2) // step 15
+                               t5 = this.M(t5 * X3); X3 = this.M(t1 + t2); t5 = this.M(t5 - X3); Z3 = this.M(a * t4)
+                               X3 = this.M(b3 * t2) // step 20
+                               Z3 = this.M(X3 + Z3); X3 = this.M(t1 - Z3); Z3 = this.M(t1 + Z3); Y3 = this.M(X3 * Z3)
+                               t1 = this.M(t0 + t0) // step 25
+                               t1 = this.M(t1 + t0); t2 = this.M(a * t2); t4 = this.M(b3 * t4); t1 = this.M(t1 + t2)
+                               t2 = this.M(t0 - t2) // step 30
+                               t2 = this.M(a * t2); t4 = this.M(t4 + t2); t0 = this.M(t1 * t4); Y3 = this.M(Y3 + t0)
+                               t0 = this.M(t5 * t4) // step 35
+                               X3 = this.M(t3 * X3); X3 = this.M(X3 - t0); t0 = this.M(t3 * t1); Z3 = this.M(t5 * Z3)
+                               Z3 = this.M(Z3 + t0) // step 40
+                               return this.Point(X3, Y3, Z3)
+                       },
+                       subtract (other: Point): Point {
+                               return this.add(other.negate())
+                       },
+                       /**
+                        * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.
+                        * Uses {@link wNAF} for base point.
+                        * Uses fake point to mitigate side-channel leakage.
+                        * @param n scalar by which point is multiplied
+                        * @param safe safe mode guards against timing attacks; unsafe mode is faster
+                        */
+                       multiply (n: bigint, safe = true): Point {
+                               if (!safe && n === 0n) return secp256k1.I
+                               secp256k1.FnIsValidNot0(n)
+                               if (n === 1n) return this
+                               if (this.equals(secp256k1.G)) return secp256k1.wNAF(n).p
+                               // init result point & fake point
+                               let p = secp256k1.I
+                               let f = secp256k1.G
+                               for (let d: Point = this; n > 0n; d = d.double(), n >>= 1n) {
+                                       // if bit is present, add to point
+                                       // if not present, add to fake, for timing safety
+                                       if (n & 1n) p = p.add(d)
+                                       else if (safe) f = f.add(d)
+                               }
+                               return p
+                       },
+                       multiplyUnsafe (scalar: bigint): Point {
+                               return this.multiply(scalar, false)
+                       },
+                       /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */
+                       toAffine (): AffinePoint {
+                               const { X: x, Y: y, Z: z } = this
+                               // fast-paths for ZERO point OR Z=1
+                               if (this.equals(secp256k1.I)) return { x: 0n, y: 0n }
+                               if (z === 1n) return { x, y }
+                               const iz = secp256k1.invert(z, secp256k1.P)
+                               // (Z * Z^-1) must be 1, otherwise bad math
+                               if (secp256k1.M(z * iz) !== 1n) secp256k1.err('inverse invalid')
+                               // x = X*Z^-1; y = Y*Z^-1
+                               return { x: secp256k1.M(x * iz), y: secp256k1.M(y * iz) }
+                       },
+                       /** Checks if the point is valid and on-curve. */
+                       assertValidity (): Point {
+                               const { x, y } = this.toAffine() // convert to 2d xy affine point.
+                               secp256k1.FpIsValidNot0(x) // must be in range 1 <= x,y < P
+                               secp256k1.FpIsValidNot0(y)
+                               // y² == x³ + ax + b, equation sides must be equal
+                               return secp256k1.M(y * y) === secp256k1.koblitz(x) ? this : secp256k1.err('bad point: not on curve')
+                       },
+                       /** Converts point to 33/65-byte Uint8Array. */
+                       toBytes (isCompressed = true): Bytes {
+                               const { x, y } = this.assertValidity().toAffine()
+                               const x32b = secp256k1.numTo32b(x)
+                               if (isCompressed) return secp256k1.concatBytes(secp256k1.getPrefix(y), x32b)
+                               return secp256k1.concatBytes(secp256k1.u8of(0x04), x32b, secp256k1.numTo32b(y))
+                       },
+                       toHex (isCompressed?: boolean): string {
+                               return secp256k1.bytesToHex(this.toBytes(isCompressed))
                        }
-                       return p
-               }
-               multiplyUnsafe (scalar: bigint): Point {
-                       return this.multiply(scalar, false)
-               }
-               /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */
-               toAffine (): AffinePoint {
-                       const { X: x, Y: y, Z: z } = this
-                       // fast-paths for ZERO point OR Z=1
-                       if (this.equals(Secp256k1.I)) return { x: 0n, y: 0n }
-                       if (z === 1n) return { x, y }
-                       const iz = Secp256k1.invert(z, Secp256k1.P)
-                       // (Z * Z^-1) must be 1, otherwise bad math
-                       if (Secp256k1.M(z * iz) !== 1n) Secp256k1.err('inverse invalid')
-                       // x = X*Z^-1; y = Y*Z^-1
-                       return { x: Secp256k1.M(x * iz), y: Secp256k1.M(y * iz) }
-               }
-               /** Checks if the point is valid and on-curve. */
-               assertValidity (): Point {
-                       const { x, y } = this.toAffine() // convert to 2d xy affine point.
-                       Secp256k1.FpIsValidNot0(x) // must be in range 1 <= x,y < P
-                       Secp256k1.FpIsValidNot0(y)
-                       // y² == x³ + ax + b, equation sides must be equal
-                       return Secp256k1.M(y * y) === Secp256k1.koblitz(x) ? this : Secp256k1.err('bad point: not on curve')
-               }
-               /** Converts point to 33/65-byte Uint8Array. */
-               toBytes (isCompressed = true): Bytes {
-                       const { x, y } = this.assertValidity().toAffine()
-                       const x32b = Secp256k1.numTo32b(x)
-                       if (isCompressed) return Secp256k1.concatBytes(Secp256k1.getPrefix(y), x32b)
-                       return Secp256k1.concatBytes(Secp256k1.u8of(0x04), x32b, Secp256k1.numTo32b(y))
-               }
-
-               toHex (isCompressed?: boolean): string {
-                       return Secp256k1.bytesToHex(this.toBytes(isCompressed))
-               }
+               })
        }
 
        /** Create 3d xyz point from 2d xy. (0, 0) => (0, 1, 0), not (0, 0, 1) */
        static pointFromAffine (ap: AffinePoint): Point {
                const { x, y } = ap
-               return x === 0n && y === 0n ? this.I : new this.Point(x, y, 1n)
+               return x === 0n && y === 0n ? this.I : this.Point(x, y, 1n)
        }
        /** Convert Uint8Array or hex string to Point. */
        static pointFromBytes (bytes: Bytes): Point {
@@ -412,10 +426,10 @@ export class Secp256k1 {
                        const evenY = this.isEven(y)
                        const evenH = this.isEven(this.big(head))
                        if (evenH !== evenY) y = this.M(-y)
-                       p = new this.Point(x, y, 1n)
+                       p = this.Point(x, y, 1n)
                }
                // Uncompressed 65-byte point, 0x04 prefix
-               if (length === uncomp && head === 0x04) p = new this.Point(x, this.sliceBytesNumBE(tail, this.L, this.L2), 1n)
+               if (length === uncomp && head === 0x04) p = this.Point(x, this.sliceBytesNumBE(tail, this.L, this.L2), 1n)
                // Validate point
                return p ? p.assertValidity() : this.err('bad point: not on curve')
        }
@@ -424,9 +438,9 @@ export class Secp256k1 {
        }
 
        /** Generator / base point */
-       static G: Point = new this.Point(this.Gx, this.Gy, 1n)
+       static G: Point = this.Point(this.Gx, this.Gy, 1n)
        /** Identity / zero point */
-       static I: Point = new this.Point(0n, 1n, 0n)
+       static I: Point = this.Point(0n, 1n, 0n)
        /** `Q = u1⋅G + u2⋅R`. Verifies Q is not ZERO. Unsafe: non-CT. */
        static doubleScalarMulUns = (R: Point, u1: bigint, u2: bigint): Point => {
                return this.G.multiply(u1, false).add(R.multiply(u2, false)).assertValidity()
@@ -1042,7 +1056,7 @@ export class Secp256k1 {
                        const y_ = this.isEven(y) ? y : this.M(-y)
                        // Return the unique point P such that x(P) = x and
                        // y(P) = y if y mod 2 = 0 or y(P) = p-y otherwise.
-                       const P_ = new this.Point(x, y_, 1n).assertValidity()
+                       const P_ = this.Point(x, y_, 1n).assertValidity()
                        const px = this.numTo32b(P_.toAffine().x)
                        // P = lift_x(int(pk)); fail if that fails
                        const r = this.sliceBytesNumBE(sig, 0, this.L) // Let r = int(sig[0:32]); fail if r ≥ p.