]> git.codecow.com Git - libnemo.git/commitdiff
Convert arrow function properties to regular static methods. Fix scope of M in point...
authorChris Duncan <chris@zoso.dev>
Wed, 3 Dec 2025 22:41:01 +0000 (14:41 -0800)
committerChris Duncan <chris@zoso.dev>
Wed, 3 Dec 2025 22:41:01 +0000 (14:41 -0800)
src/lib/crypto/secp256k1.ts

index bce30a99eea6de52a55889e5decf9f43e66d70ee..c5c0560bb1f2616d9b6ea5f5114c9f362188bc2f 100644 (file)
@@ -100,6 +100,22 @@ type KeygenFn = (seed?: Bytes) => KeysSecPub
 
 type MaybePromise<T> = T | Promise<T>
 
+type EnsureNumber<A extends number> = A extends number ? A : never
+type Length<T extends any[]> = EnsureNumber<T['length']>
+type Tuple<N extends number, T, Accumulator extends T[] = []> = Length<Accumulator> extends N ? Accumulator : Tuple<N, T, [...Accumulator, T]>
+type Add<A extends number, B extends number> = Length<[...Tuple<A, any>, ...Tuple<B, any>]>
+type Subtract<A extends number, B extends number> = Tuple<A, any> extends [...Tuple<B, any>, ...infer Rest,] ? Length<Rest> : never
+type IsLessThanOrEqual<A extends number, B extends number> = Tuple<B, any> extends [...Tuple<A, any>, ...infer _Rest,] ? true : false
+type IsLessThan<A extends number, B extends number> = IsLessThanOrEqual<Add<A, 1>, B>
+type Multiply<A extends number, B extends number> = B extends 0 ? 0 : Add<Multiply<A, Subtract<B, 1>>, A>
+type Quotient<A extends number, B extends number, C extends number = 0> = B extends 0 ? never : B extends 1 ? A : A extends 0 ? 0 : A extends B ? 1 : IsLessThan<A, C> extends true ? never : Multiply<B, C> extends A ? C : Quotient<A, B, Add<C, 1>>
+type Secp256k1Lengths = {
+       publicKey: Add<typeof Secp256k1.L, 1>
+       publicKeyUncompressed: Add<typeof Secp256k1.L2, 1>
+       signature: typeof Secp256k1.L2
+       seed: Add<typeof Secp256k1.L, Quotient<typeof Secp256k1.L, 2>>
+}
+
 export class Secp256k1 {
        /**
         * Curve params. secp256k1 is short weierstrass / koblitz curve. Equation is y² == x³ + ax + b.
@@ -128,13 +144,13 @@ export class Secp256k1 {
                Gy: this.Gy,
        }
 
-       static L = 32 // field / group byte length
-       static L2 = 64
-       static lengths = {
-               publicKey: this.L + 1,
-               publicKeyUncompressed: this.L2 + 1,
-               signature: this.L2,
-               seed: this.L + this.L / 2,
+       static L: 32 = 32 // field / group byte length
+       static L2: 64 = 64
+       static lengths: Secp256k1Lengths = {
+               publicKey: 33,
+               publicKeyUncompressed: 65,
+               signature: 64,
+               seed: 48,
        }
 
        // Helpers and Precomputes sections are reused between libraries
@@ -149,7 +165,7 @@ export class Secp256k1 {
 
        /** Asserts something is Uint8Array. */
        static abytes (value: unknown, length?: number, title: string = ''): Bytes {
-               function isUint8Array (a: unknown): a is Uint8Array<ArrayBuffer> {
+               function isUint8Array (a: unknown): a is Bytes {
                        return (a instanceof Uint8Array && a.buffer instanceof ArrayBuffer) || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array')
                }
                const isBytes = isUint8Array(value)
@@ -219,7 +235,7 @@ export class Secp256k1 {
 
        /** Modular inversion using eucledian GCD (non-CT). No negative exponent for now. */
        // prettier-ignore
-       static invert = (num: bigint, md: bigint): bigint => {
+       static invert (num: bigint, md: bigint): bigint {
                if (num === 0n || md <= 0n) this.err('no inverse n=' + num + ' mod=' + md)
                let a = this.M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n
                while (a !== 0n) {
@@ -230,7 +246,7 @@ export class Secp256k1 {
                return b === 1n ? this.M(x, md) : this.err('no inverse') // b is gcd at this point
        }
 
-       static callHash = (name: string) => {
+       static callHash (name: string) {
                // @ts-ignore
                const fn = hashes[name]
                if (typeof fn !== 'function') this.err('hashes.' + name + ' not set')
@@ -252,7 +268,7 @@ export class Secp256k1 {
        static u8of = (n: number): Bytes => Uint8Array.of(n)
        static getPrefix = (y: bigint) => this.u8of(this.isEven(y) ? 0x02 : 0x03)
        /** lift_x from BIP340 calculates square root. Validates x, then validates root*root. */
-       static lift_x = (x: bigint) => {
+       static lift_x (x: bigint) {
                // Let c = x³ + 7 mod p. Fail if x ≥ p. (also fail if x < 1)
                const c = this.koblitz(this.FpIsValidNot0(x))
                // c = √y
@@ -269,7 +285,7 @@ export class Secp256k1 {
        }
 
        /** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */
-       static Point = (X: bigint, Y: bigint, Z: bigint): Point => {
+       static Point (X: bigint, Y: bigint, Z: bigint): Point {
                const secp256k1 = this
                return Object.freeze({
                        X: this.FpIsValid(X),
@@ -286,8 +302,8 @@ export class Secp256k1 {
                                return X1Z2 === X2Z1 && Y1Z2 === Y2Z1
                        },
                        /** Flip point over y coordinate. */
-                       negate: (): Point => {
-                               return this.Point(X, this.M(-Y), Z)
+                       negate (): Point {
+                               return secp256k1.Point(X, secp256k1.M(-Y), Z)
                        },
                        /** Point doubling: P+P, complete formula. */
                        double (): Point {
@@ -300,11 +316,11 @@ export class Secp256k1 {
                         */
                        // prettier-ignore
                        add (other: Point): Point {
-                               const { M, CURVE, Point } = secp256k1
+                               const M = (v: bigint): bigint => secp256k1.M(v)
                                const { X: X1, Y: Y1, Z: Z1 } = { X, Y, Z }
                                const { X: X2, Y: Y2, Z: Z2 } = other
                                const a = 0n
-                               const b = CURVE.b
+                               const b = secp256k1.CURVE.b
                                const b3 = M(b * 3n)
                                let X3 = 0n, Y3 = 0n, Z3 = 0n
                                let t0 = M(X1 * X2), t1 = M(Y1 * Y2), t2 = M(Z1 * Z2), t3 = M(X1 + Y1) // step 1
@@ -323,7 +339,7 @@ export class Secp256k1 {
                                t0 = M(t5 * t4) // step 35
                                X3 = M(t3 * X3); X3 = M(X3 - t0); t0 = M(t3 * t1); Z3 = M(t5 * Z3)
                                Z3 = M(Z3 + t0) // step 40
-                               return Point(X3, Y3, Z3)
+                               return secp256k1.Point(X3, Y3, Z3)
                        },
                        subtract (other: Point): Point {
                                return this.add(other.negate())
@@ -443,25 +459,25 @@ export class Secp256k1 {
                )
        }
        /** Normalize private key to scalar (bigint). Verifies scalar is in range 1<s<N */
-       static secretKeyToScalar = (secretKey: Bytes): bigint => {
+       static secretKeyToScalar (secretKey: Bytes): bigint {
                const num = this.bytesToBigint(this.abytes(secretKey, this.L, 'secret key'))
                return this.bigintInRange(num, 1n, this.N, 'invalid secret key: outside of range')
        }
        /** For Signature malleability, validates sig.s is bigger than N/2. */
        static highS = (n: bigint): boolean => n > this.N >> 1n
        /** Creates 33/65-byte public key from 32-byte private key. */
-       static getPublicKey = (privKey: Bytes, isCompressed = true): Bytes => {
+       static getPublicKey (privKey: Bytes, isCompressed = true): Bytes {
                return this.G.multiply(this.secretKeyToScalar(privKey)).toBytes(isCompressed)
        }
 
-       static isValidSecretKey = (secretKey: Bytes): boolean => {
+       static isValidSecretKey (secretKey: Bytes): boolean {
                try {
                        return !!this.secretKeyToScalar(secretKey)
                } catch (error) {
                        return false
                }
        }
-       static isValidPublicKey = (publicKey: Bytes, isCompressed?: boolean): boolean => {
+       static isValidPublicKey (publicKey: Bytes, isCompressed?: boolean): boolean {
                const { publicKey: comp, publicKeyUncompressed } = this.lengths
                try {
                        const l = publicKey.length
@@ -473,15 +489,15 @@ export class Secp256k1 {
                }
        }
 
-       static assertRecoveryBit = (recovery?: number) => {
+       static assertRecoveryBit (recovery?: number) {
                if (![0, 1, 2, 3].includes(recovery!)) this.err('recovery id must be valid and present')
        }
-       static assertSigFormat = (format?: ECDSASignatureFormat) => {
+       static assertSigFormat (format?: ECDSASignatureFormat) {
                if (format != null && !this.ALL_SIG.includes(format))
                        this.err(`Signature format must be one of: ${this.ALL_SIG.join(', ')}`)
                if (format === this.SIG_DER) this.err('Signature format "der" is not supported: switch to noble-curves')
        }
-       static assertSigLength = (sig: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT) => {
+       static assertSigLength (sig: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT) {
                this.assertSigFormat(format)
                const SL = this.lengths.signature
                const RL = SL + 1
@@ -513,7 +529,7 @@ export class Secp256k1 {
        static ALL_SIG = [this.SIG_COMPACT, this.SIG_RECOVERED, this.SIG_DER] as const
 
        /** ECDSA Signature class. Supports only compact 64-byte representation, not DER. */
-       static Signature = (r: bigint, s: bigint, recovery?: number) => {
+       static Signature (r: bigint, s: bigint, recovery?: number) {
                return Object.freeze({
                        r: this.FnIsValidNot0(r), // 1 <= r < N
                        s: this.FnIsValidNot0(s), // 1 <= s < N
@@ -553,7 +569,7 @@ export class Secp256k1 {
         * FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits,
         * which matches bits2int. bits2int can produce res>N.
         */
-       static bits2int = (bytes: Bytes): bigint => {
+       static bits2int (bytes: Bytes): bigint {
                const delta = bytes.length * 8 - 256
                if (delta > 1024) this.err('msg invalid') // our CUSTOM check, "just-in-case": prohibit long inputs
                const num = this.bytesToBigint(bytes)
@@ -592,7 +608,7 @@ export class Secp256k1 {
        static _maxDrbgIters = 1000
        static _drbgErr = 'drbg: tried max amount of iterations'
        // HMAC-DRBG from NIST 800-90. Minimal, non-full-spec - used for RFC6979 signatures.
-       static hmacDrbg = (seed: Bytes, pred: Pred<Bytes>): Bytes => {
+       static hmacDrbg (seed: Bytes, pred: Pred<Bytes>): Bytes {
                let v = new Uint8Array(this.L) // Steps B, C of RFC6979 3.2: set hashLen
                let k = new Uint8Array(this.L) // In our case, it's always equal to L
                let i = 0 // Iterations counter, will throw when over max
@@ -625,7 +641,7 @@ export class Secp256k1 {
        }
 
        // Identical to hmacDrbg, but async: uses built-in WebCrypto
-       static hmacDrbgAsync = async (seed: Bytes, pred: Pred<Bytes>): Promise<Bytes> => {
+       static async hmacDrbgAsync (seed: Bytes, pred: Pred<Bytes>): Promise<Bytes> {
                let v = new Uint8Array(this.L) // Steps B, C of RFC6979 3.2: set hashLen
                let k = new Uint8Array(this.L) // In our case, it's always equal to L
                let i = 0 // Iterations counter, will throw when over max
@@ -659,12 +675,7 @@ export class Secp256k1 {
 
        // RFC6979 signature generation, preparation step.
        // Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.2 & RFC6979.
-       static _sign = <T> (
-               messageHash: Bytes,
-               secretKey: Bytes,
-               opts: ECDSASignOpts,
-               hmacDrbg: (seed: Bytes, pred: Pred<Bytes>) => T
-       ): T => {
+       static _sign<T> (messageHash: Bytes, secretKey: Bytes, opts: ECDSASignOpts, hmacDrbg: (seed: Bytes, pred: Pred<Bytes>) => T): T {
                let { lowS, extraEntropy } = opts // generates low-s sigs by default
                // RFC6979 3.2: we skip step A
                const int2octets = this.bigintTo32Bytes // int to octets
@@ -714,7 +725,7 @@ export class Secp256k1 {
        }
 
        // Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.4.
-       static _verify = (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}) => {
+       static _verify (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): boolean {
                const { lowS, format } = opts
                if (sig instanceof this.Signature) this.err('Signature must be in Uint8Array, use .toBytes()')
                this.assertSigLength(sig, format)
@@ -736,7 +747,7 @@ export class Secp256k1 {
                }
        }
 
-       static setDefaults = (opts: ECDSASignOpts): Required<ECDSASignOpts> => {
+       static setDefaults (opts: ECDSASignOpts): Required<ECDSASignOpts> {
                const res: ECDSASignOpts = {}
                Object.keys(this.defaultSignOpts).forEach((k: string) => {
                        // @ts-ignore
@@ -758,7 +769,7 @@ export class Secp256k1 {
         * sign(msg, secretKey, { format: 'recovered' });
         * ```
         */
-       static sign = (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes => {
+       static sign (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes {
                opts = this.setDefaults(opts)
                message = this.prepMsg(message, opts, false) as Bytes
                return this._sign(message, secretKey, opts, this.hmacDrbg)
@@ -777,11 +788,7 @@ export class Secp256k1 {
         * await signAsync(msg, secretKey, { format: 'recovered' });
         * ```
         */
-       static signAsync = async (
-               message: Bytes,
-               secretKey: Bytes,
-               opts: ECDSASignOpts = {}
-       ): Promise<Bytes> => {
+       static async signAsync (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Promise<Bytes> {
                opts = this.setDefaults(opts)
                message = await this.prepMsg(message, opts, true)
                return this._sign(message, secretKey, opts, this.hmacDrbgAsync)
@@ -802,12 +809,7 @@ export class Secp256k1 {
         * verify(sigr, msg, publicKey, { format: 'recovered' });
         * ```
         */
-       static verify = (
-               signature: Bytes,
-               message: Bytes,
-               publicKey: Bytes,
-               opts: ECDSAVerifyOpts = {}
-       ): boolean => {
+       static verify (signature: Bytes, message: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): boolean {
                opts = this.setDefaults(opts)
                message = this.prepMsg(message, opts, false) as Bytes
                return this._verify(signature, message, publicKey, opts)
@@ -828,18 +830,13 @@ export class Secp256k1 {
         * verify(sigr, msg, publicKey, { format: 'recovered' });
         * ```
         */
-       static verifyAsync = async (
-               sig: Bytes,
-               message: Bytes,
-               publicKey: Bytes,
-               opts: ECDSAVerifyOpts = {}
-       ): Promise<boolean> => {
+       static async verifyAsync (sig: Bytes, message: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}): Promise<boolean> {
                opts = this.setDefaults(opts)
                message = await this.prepMsg(message, opts, true)
                return this._verify(sig, message, publicKey, opts)
        }
 
-       static _recover = (signature: Bytes, messageHash: Bytes) => {
+       static _recover (signature: Bytes, messageHash: Bytes): Bytes {
                const sig = this.signatureFromBytes(signature, 'recovered')
                const { r, s, recovery } = sig
                // 0 or 1 recovery id determines sign of "y" coordinate.
@@ -862,16 +859,12 @@ export class Secp256k1 {
         * ECDSA public key recovery. Requires msg hash and recovery id.
         * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.6.
         */
-       static recoverPublicKey = (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Bytes => {
+       static recoverPublicKey (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Bytes {
                message = this.prepMsg(message, this.setDefaults(opts), false) as Bytes
                return this._recover(signature, message)
        }
 
-       static recoverPublicKeyAsync = async (
-               signature: Bytes,
-               message: Bytes,
-               opts: ECDSARecoverOpts = {}
-       ): Promise<Bytes> => {
+       static async recoverPublicKeyAsync (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Promise<Bytes> {
                message = await this.prepMsg(message, this.setDefaults(opts), true)
                return this._recover(signature, message)
        }
@@ -882,13 +875,13 @@ export class Secp256k1 {
         * @param isCompressed 33-byte (true) or 65-byte (false) output
         * @returns public key C
         */
-       static getSharedSecret = (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes => {
+       static getSharedSecret (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes {
                return this.pointFromBytes(publicKeyB).multiply(this.secretKeyToScalar(secretKeyA)).toBytes(isCompressed)
        }
 
        // FIPS 186 B.4.1 compliant key generation produces private keys
        // with modulo bias being neglible. takes >N+16 bytes, returns (hash mod n-1)+1
-       static randomSecretKey (seed?: Bytes) {
+       static randomSecretKey (seed?: Bytes): Bytes {
                seed ??= crypto.getRandomValues(new Uint8Array(this.lengths.seed))
                this.abytes(seed)
                if (seed.length < this.lengths.seed || seed.length > 1024) this.err('expected 40-1024b')
@@ -928,12 +921,12 @@ export class Secp256k1 {
        static T_AUX = 'aux'
        static T_NONCE = 'nonce'
        static T_CHALLENGE = 'challenge'
-       static taggedHash = (tag: string, ...messages: Bytes[]): Bytes => {
+       static taggedHash (tag: string, ...messages: Bytes[]): Bytes {
                const fn = this.callHash('sha256')
                const tagH = fn(this.getTag(tag))
                return fn(this.concatBytes(tagH, tagH, ...messages))
        }
-       static taggedHashAsync = async (tag: string, ...messages: Bytes[]): Promise<Bytes> => {
+       static async taggedHashAsync (tag: string, ...messages: Bytes[]): Promise<Bytes> {
                const fn = this.hashes.sha256Async
                const tagH = await fn(this.getTag(tag))
                return await fn(this.concatBytes(tagH, tagH, ...messages))
@@ -941,7 +934,7 @@ export class Secp256k1 {
 
        // ECDSA compact points are 33-byte. Schnorr is 32: we strip first byte 0x02 or 0x03
        // Calculate point, scalar and bytes
-       static extpubSchnorr = (priv: Bytes) => {
+       static extpubSchnorr (priv: Bytes) {
                const d_ = this.secretKeyToScalar(priv)
                const p = this.G.multiply(d_) // P = d'⋅G; 0 < d' < n check is done inside
                const { x, y } = p.assertValidity().toAffine() // validate Point is not at infinity
@@ -958,19 +951,19 @@ export class Secp256k1 {
        /**
         * Schnorr public key is just `x` coordinate of Point as per BIP340.
         */
-       static pubSchnorr = (secretKey: Bytes): Bytes => {
+       static pubSchnorr (secretKey: Bytes): Bytes {
                return this.extpubSchnorr(secretKey).px // d'=int(sk). Fail if d'=0 or d'≥n. Ret bytes(d'⋅G)
        }
 
        static keygenSchnorr: KeygenFn = this.createKeygen(this.pubSchnorr)
 
        // Common preparation fn for both sync and async signing
-       static prepSigSchnorr = (message: Bytes, secretKey: Bytes, auxRand: Bytes) => {
+       static prepSigSchnorr (message: Bytes, secretKey: Bytes, auxRand: Bytes) {
                const { px, d } = this.extpubSchnorr(secretKey)
                return { m: this.abytes(message), px, d, a: this.abytes(auxRand, this.L) }
        }
 
-       static extractK = (rand: Bytes) => {
+       static extractK (rand: Bytes) {
                const k_ = this.bytesModN(rand) // Let k' = int(rand) mod n
                if (k_ === 0n) this.err('sign failed: k is zero') // Fail if k' = 0.
                const { px, d } = this.extpubSchnorr(this.bigintTo32Bytes(k_)) // Let R = k'⋅G.
@@ -978,7 +971,7 @@ export class Secp256k1 {
        }
 
        // Common signature creation helper
-       static createSigSchnorr = (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes => {
+       static createSigSchnorr (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes {
                return this.concatBytes(px, this.bigintTo32Bytes(this.modN(k + e * d)))
        }
 
@@ -987,7 +980,7 @@ export class Secp256k1 {
         * Creates Schnorr signature as per BIP340. Verifies itself before returning anything.
         * auxRand is optional and is not the sole source of k generation: bad CSPRNG won't be dangerous.
         */
-       static signSchnorr = (message: Bytes, secretKey: Bytes, auxRand?: Bytes): Bytes => {
+       static signSchnorr (message: Bytes, secretKey: Bytes, auxRand?: Bytes): Bytes {
                auxRand ??= crypto.getRandomValues(new Uint8Array(this.L))
                const { m, px, d, a } = this.prepSigSchnorr(message, secretKey, auxRand)
                const aux = this.taggedHash(this.T_AUX, a)
@@ -1027,12 +1020,7 @@ export class Secp256k1 {
                return res instanceof Promise ? res.then(later) : later(res)
        }
 
-       static _verifSchnorr (
-               signature: Bytes,
-               message: Bytes,
-               publicKey: Bytes,
-               challengeFn: (...args: Bytes[]) => bigint | Promise<bigint>
-       ): boolean | Promise<boolean> {
+       static _verifSchnorr (signature: Bytes, message: Bytes, publicKey: Bytes, challengeFn: (...args: Bytes[]) => bigint | Promise<bigint>): boolean | Promise<boolean> {
                const sig = this.abytes(signature, this.L2, 'signature')
                const msg = this.abytes(message, undefined, 'message')
                const pub = this.abytes(publicKey, this.L, 'publicKey')
@@ -1121,7 +1109,7 @@ export class Secp256k1 {
         *
         * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply().
         */
-       static wNAF = (n: bigint): { p: Point; f: Point } => {
+       static wNAF (n: bigint): { p: Point; f: Point } {
                const comp = this.Gpows || (this.Gpows = this.precompute())
                let p = this.I
                let f = this.G // f must be G, or could become I in the end