From 40483e78f8e6576b93e8d977bd812bbf5db380fc Mon Sep 17 00:00:00 2001 From: cedoor Date: Sun, 10 Dec 2023 18:07:03 +0000 Subject: [PATCH 1/2] feat(baby-jubjub): create new package --- packages/baby-jubjub/LICENSE | 21 +++ packages/baby-jubjub/README.md | 90 +++++++++++++ packages/baby-jubjub/build.tsconfig.json | 8 ++ packages/baby-jubjub/package.json | 44 +++++++ packages/baby-jubjub/rollup.config.ts | 32 +++++ packages/baby-jubjub/rollup.iife.config.ts | 47 +++++++ packages/baby-jubjub/src/baby-jubjub.ts | 142 +++++++++++++++++++++ packages/baby-jubjub/src/field.ts | 121 ++++++++++++++++++ packages/baby-jubjub/src/index.ts | 2 + packages/baby-jubjub/src/scalar.ts | 36 ++++++ packages/baby-jubjub/src/sqrt.ts | 56 ++++++++ packages/baby-jubjub/src/types/index.ts | 10 ++ packages/baby-jubjub/src/utils.ts | 128 +++++++++++++++++++ packages/baby-jubjub/tests/index.test.ts | 45 +++++++ packages/baby-jubjub/tsconfig.json | 4 + packages/baby-jubjub/typedoc.json | 3 + yarn.lock | 14 ++ 17 files changed, 803 insertions(+) create mode 100644 packages/baby-jubjub/LICENSE create mode 100644 packages/baby-jubjub/README.md create mode 100644 packages/baby-jubjub/build.tsconfig.json create mode 100644 packages/baby-jubjub/package.json create mode 100644 packages/baby-jubjub/rollup.config.ts create mode 100644 packages/baby-jubjub/rollup.iife.config.ts create mode 100644 packages/baby-jubjub/src/baby-jubjub.ts create mode 100644 packages/baby-jubjub/src/field.ts create mode 100644 packages/baby-jubjub/src/index.ts create mode 100644 packages/baby-jubjub/src/scalar.ts create mode 100644 packages/baby-jubjub/src/sqrt.ts create mode 100644 packages/baby-jubjub/src/types/index.ts create mode 100644 packages/baby-jubjub/src/utils.ts create mode 100644 packages/baby-jubjub/tests/index.test.ts create mode 100644 packages/baby-jubjub/tsconfig.json create mode 100644 packages/baby-jubjub/typedoc.json diff --git a/packages/baby-jubjub/LICENSE b/packages/baby-jubjub/LICENSE new file mode 100644 index 000000000..4377091ec --- /dev/null +++ b/packages/baby-jubjub/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ethereum Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/baby-jubjub/README.md b/packages/baby-jubjub/README.md new file mode 100644 index 000000000..8069e3b3c --- /dev/null +++ b/packages/baby-jubjub/README.md @@ -0,0 +1,90 @@ +

+

+ Baby Jubjub +

+

A JavaScript library for adding points to the Baby Jubjub curve.

+

+ +

+ + + + + NPM license + + + NPM version + + + Downloads + + + npm bundle size (scoped) + + + Linter eslint + + + Code style prettier + +

+ +
+

+ + 🗣️ Chat & Support + +   |   + + 📘 Docs + +

+
+ +| WIP | +| --- | + +- Super lightweight: [**~33kB**](https://bundlephobia.com/package/@zk-kit/baby-jubjub) (minified) +- Compatible with browsers and NodeJS +- TS type support +- Comprehensive code [documentation](https://zkkit.pse.dev/modules/_zk_kit_baby_jubjub.html) +- Full test coverage + +👾 Would you like to try it now? Explore it now on [Ceditor](https://ceditor.cedoor.dev/52787e4ad57d2f2076648d509efc3448)! + +> [!WARNING] +> This library has **not** been audited. + +## 🛠 Install + +### npm or yarn + +Install the `@zk-kit/baby-jubjub` package and its peer dependencies with npm: + +```bash +npm i @zk-kit/baby-jubjub +``` + +or yarn: + +```bash +yarn add @zk-kit/baby-jubjub +``` + +### CDN + +You can also load it using a `script` tag using [unpkg](https://unpkg.com/): + +```html + +``` + +or [JSDelivr](https://www.jsdelivr.com/): + +```html + +``` + +## 📜 Usage + +WIP diff --git a/packages/baby-jubjub/build.tsconfig.json b/packages/baby-jubjub/build.tsconfig.json new file mode 100644 index 000000000..2d4a1d6da --- /dev/null +++ b/packages/baby-jubjub/build.tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "declarationDir": "dist/types" + }, + "include": ["src"] +} diff --git a/packages/baby-jubjub/package.json b/packages/baby-jubjub/package.json new file mode 100644 index 000000000..c66a92617 --- /dev/null +++ b/packages/baby-jubjub/package.json @@ -0,0 +1,44 @@ +{ + "name": "@zk-kit/baby-jubjub", + "version": "0.1.0", + "description": "A JavaScript library for adding points to the curve.", + "license": "MIT", + "iife": "dist/index.js", + "unpkg": "dist/index.min.js", + "jsdelivr": "dist/index.min.js", + "main": "dist/index.node.js", + "exports": { + "import": "./dist/index.mjs", + "require": "./dist/index.node.js", + "types": "./dist/types/index.d.ts" + }, + "types": "dist/types/index.d.ts", + "files": [ + "dist/", + "src/", + "LICENSE", + "README.md" + ], + "repository": "https://github.com/privacy-scaling-explorations/zk-kit", + "homepage": "https://github.com/privacy-scaling-explorations/zk-kit/tree/main/packages/baby-jubjub", + "bugs": { + "url": "https://github.com/privacy-scaling-explorations/zk-kit.git/issues" + }, + "scripts": { + "build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript && yarn build:iife", + "build:iife": "rollup -c rollup.iife.config.ts --configPlugin typescript", + "prepublishOnly": "yarn build" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "circomlibjs": "0.0.8", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-polyfill-node": "^0.13.0", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.2" + } +} diff --git a/packages/baby-jubjub/rollup.config.ts b/packages/baby-jubjub/rollup.config.ts new file mode 100644 index 000000000..7d2994bd6 --- /dev/null +++ b/packages/baby-jubjub/rollup.config.ts @@ -0,0 +1,32 @@ +import commonjs from "@rollup/plugin-commonjs" +import { nodeResolve } from "@rollup/plugin-node-resolve" +import fs from "fs" +import cleanup from "rollup-plugin-cleanup" +import typescript from "rollup-plugin-typescript2" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation ${new Date().getFullYear()} + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +export default { + input: "src/index.ts", + output: [ + { file: pkg.exports.require, format: "cjs", banner }, + { file: pkg.exports.import, format: "es", banner } + ], + external: [], + plugins: [ + typescript({ tsconfig: "./build.tsconfig.json", useTsconfigDeclarationDir: true }), + commonjs(), + nodeResolve({ + preferBuiltins: true + }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/baby-jubjub/rollup.iife.config.ts b/packages/baby-jubjub/rollup.iife.config.ts new file mode 100644 index 000000000..f7936620a --- /dev/null +++ b/packages/baby-jubjub/rollup.iife.config.ts @@ -0,0 +1,47 @@ +import commonjs from "@rollup/plugin-commonjs" +import { nodeResolve } from "@rollup/plugin-node-resolve" +import fs from "fs" +import nodePolyfills from "rollup-plugin-polyfill-node" +import cleanup from "rollup-plugin-cleanup" +import { terser } from "rollup-plugin-terser" +import typescript from "rollup-plugin-typescript2" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation ${new Date().getFullYear()} + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +const name = pkg.name.split("/")[1].replace(/[-/]./g, (x: string) => x.toUpperCase()[1]) + +export default { + input: "src/index.ts", + output: [ + { + file: pkg.iife, + name, + format: "iife", + banner + }, + { + file: pkg.unpkg, + name, + format: "iife", + plugins: [terser({ output: { preamble: banner } })] + } + ], + external: [], + plugins: [ + typescript({ tsconfig: "./build.tsconfig.json", useTsconfigDeclarationDir: true }), + commonjs(), + nodeResolve({ + preferBuiltins: true + }), + nodePolyfills({ include: null }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/baby-jubjub/src/baby-jubjub.ts b/packages/baby-jubjub/src/baby-jubjub.ts new file mode 100644 index 000000000..5cb9e8f2f --- /dev/null +++ b/packages/baby-jubjub/src/baby-jubjub.ts @@ -0,0 +1,142 @@ +import Field from "./field" +import * as scalar from "./scalar" +import { Point } from "./types" +import * as utils from "./utils" +import * as sqrt from "./sqrt" + +// Spec: https://eips.ethereum.org/EIPS/eip-2494 + +// 'r' is the alt_bn128 prime order. +export const r = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617") + +// 'F' (F_r) is the prime finite field with r elements. +export const Fr = new Field(r) + +// Base8 is the base point used to generate other points on the curve. +export const Base8: Point = [ + Fr.e(BigInt("5299619240641551281634865583518297030282874472190772894086521144482721001553")), + Fr.e(BigInt("16950150798460657717958625567821834550301663161624707787222815936182638968203")) +] + +// Let E be the twisted Edwards elliptic curve defined over 'F_r' +// described by the equation 'ax^2 + y^2 = 1 + dx^2y^2'. + +// 'a' and 'd' are the parameters of the equation: +const a = Fr.e(BigInt("168700")) +const d = Fr.e(BigInt("168696")) + +// We call Baby Jubjub the curve 'E(F_r)', that is, the subgroup of 'F_r'-rational points of 'E'. + +// 'order' is order of the elliptic curve 'E'. +export const order = BigInt("21888242871839275222246405745257275088614511777268538073601725287587578984328") +export const subOrder = scalar.shiftRight(order, BigInt(3)) + +/** + * Performs point addition on the Baby Jubjub elliptic curve, + * calculating a third point from two given points. + * Let P1 = (x1, y1) and P2 = (x2, y2) be two arbitrary points of the curve. + * Then P1 + P2 = (x3, y3) is calculated in the following way: + * x3 = (x1*y2 + y1*x2)/(1 + d*x1*x2*y1*y2) + * y3 = (y1*y2 - a*x1*x2)/(1 - d*x1*x2*y1*y2) + * @param p1 - First point on the curve. + * @param p2 - Second point on the curve. + * @returns Resultant third point on the curve. + */ +export function addPoint(p1: Point, p2: Point): Point { + // beta = x1*y2 + const beta = Fr.mul(p1[0], p2[1]) + // gamma = y1*x2 + const gamma = Fr.mul(p1[1], p2[0]) + // delta = (y1-(a*x1))*(x2+y2) + const delta = Fr.mul(Fr.sub(p1[1], Fr.mul(a, p1[0])), Fr.add(p2[0], p2[1])) + + // x1*x2*y1*y2 + const tau = Fr.mul(beta, gamma) + // d*x1*x2*y1*y2 + const dtau = Fr.mul(d, tau) + + // x3 = (x1*y2 + y1*x2)/(1 + d*x1*x2*y1*y2) + const p3x = Fr.div(Fr.add(beta, gamma), Fr.add(Fr.one, dtau)) + // y3 = (y1*y2 - a*x1*x2)/(1 - d*x1*x2*y1*y2) + const p3y = Fr.div(Fr.add(delta, Fr.sub(Fr.mul(a, beta), gamma)), Fr.sub(Fr.one, dtau)) + + return [p3x, p3y] +} + +/** + * Performs a scalar multiplication by starting from the 'base' point and 'adding' + * it to itself 'e' times. + * @param base - The base point used as a starting point. + * @param e - A secret number representing the private key. + * @returns The resulting point representing the public key. + */ +export function mulPointEscalar(base: Point, e: bigint): Point { + let res: Point = [Fr.e(BigInt(0)), Fr.e(BigInt(1))] + let rem: bigint = Fr.e(e) + let exp: Point = base + + while (!scalar.isZero(rem)) { + if (scalar.isOdd(rem)) { + res = addPoint(res, exp) + } + + exp = addPoint(exp, exp) + rem = scalar.shiftRight(rem, BigInt(1)) + } + + return res +} + +export function inCurve(p: Point) { + const x1 = BigInt(p[0]) + const y1 = BigInt(p[1]) + + const x2 = Fr.square(x1) + const y2 = Fr.square(y1) + + return Fr.eq(Fr.add(Fr.mul(a, x2), y2), Fr.add(Fr.one, Fr.mul(Fr.mul(x2, y2), d))) +} + +export function packPoint(unpackedPoint: Point): bigint { + const buffer = utils.leInt2Buff(unpackedPoint[1]) + + if (Fr.lt(unpackedPoint[0], Fr.zero)) { + buffer[31] |= 0x80 + } + + return utils.buff2int(buffer) +} + +export function unpackPoint(packedPoint: bigint): Point | null { + const buffer = Buffer.from(utils.int2hex(packedPoint), "hex") + const unpackedPoint = new Array(2) + + let sign = false + + if (buffer[31] & 0x80) { + sign = true + buffer[31] &= 0x7f + } + + unpackedPoint[1] = utils.leBuff2int(buffer) + + if (scalar.gt(unpackedPoint[1], r)) { + return null + } + + const y2 = Fr.square(unpackedPoint[1]) + + let x = sqrt.tonelliShanks(Fr.div(Fr.sub(Fr.one, y2), Fr.sub(a, Fr.mul(d, y2)))) + + if (x == null) { + return null + } + + if (sign) { + x = Fr.neg(x) + } + + unpackedPoint[0] = x + + return unpackedPoint as Point +} diff --git a/packages/baby-jubjub/src/field.ts b/packages/baby-jubjub/src/field.ts new file mode 100644 index 000000000..4f322b6e2 --- /dev/null +++ b/packages/baby-jubjub/src/field.ts @@ -0,0 +1,121 @@ +import * as scalar from "./scalar" + +export default class Field { + one = BigInt(1) + zero = BigInt(0) + + _order: bigint + _half: bigint + _negone: bigint + + constructor(order: bigint) { + this._order = order + this._half = order >> this.one + this._negone = this._order - this.one + } + + e(res: bigint): bigint { + if (res < 0) { + let nres = -res + + if (nres >= this._order) { + nres %= this._order + } + + return this._order - nres + } + + return res >= this._order ? res % this._order : res + } + + mul(a: bigint, b: bigint): bigint { + return (a * b) % this._order + } + + sub(a: bigint, b: bigint): bigint { + return a >= b ? a - b : this._order - b + a + } + + add(a: bigint, b: bigint): bigint { + const res = a + b + + return res >= this._order ? res - this._order : res + } + + inv(a: bigint): bigint { + let t = this.zero + let r = this._order + let newt = this.one + let newr = a % this._order + + while (newr) { + const q = r / newr + ;[t, newt] = [newt, t - q * newt] + ;[r, newr] = [newr, r - q * newr] + } + + if (t < this.zero) { + t += this._order + } + + return t + } + + div(a: bigint, b: bigint): bigint { + return this.mul(a, this.inv(b)) + } + + eq(a: bigint, b: bigint): boolean { + return a === b + } + + square(a: bigint): bigint { + return (a * a) % this._order + } + + lt(a: bigint, b: bigint) { + const aa = a > this._half ? a - this._order : a + const bb = b > this._half ? b - this._order : b + + return aa < bb + } + + geq(a: bigint, b: bigint) { + const aa = a > this._half ? a - this._order : a + const bb = b > this._half ? b - this._order : b + + return aa >= bb + } + + neg(a: bigint) { + return a ? this._order - a : a + } + + isZero(a: bigint) { + return a === this.zero + } + + pow(base: bigint, e: bigint) { + if (scalar.isZero(e)) { + return this.one + } + + const n = scalar.bits(e) + + if (n.length === 0) { + return this.one + } + + let res = base + + for (let i = n.length - 2; i >= 0; i -= 1) { + res = this.square(res) + + if (n[i]) { + res = this.mul(res, base) + } + } + + return res + } +} diff --git a/packages/baby-jubjub/src/index.ts b/packages/baby-jubjub/src/index.ts new file mode 100644 index 000000000..66ce283fe --- /dev/null +++ b/packages/baby-jubjub/src/index.ts @@ -0,0 +1,2 @@ +export * from "./baby-jubjub" +export * from "./types" diff --git a/packages/baby-jubjub/src/scalar.ts b/packages/baby-jubjub/src/scalar.ts new file mode 100644 index 000000000..0f15bcae8 --- /dev/null +++ b/packages/baby-jubjub/src/scalar.ts @@ -0,0 +1,36 @@ +export function isZero(a: bigint): boolean { + return !a +} + +export function isOdd(a: bigint): boolean { + return (a & BigInt(1)) === BigInt(1) +} + +export function shiftRight(a: bigint, n: bigint): bigint { + return a >> n +} + +export function mul(a: bigint, b: bigint): bigint { + return a * b +} + +export function gt(a: bigint, b: bigint) { + return a > b +} + +export function bits(n: bigint): number[] { + const res = [] + let E = n + + while (E) { + if (E & BigInt(1)) { + res.push(1) + } else { + res.push(0) + } + + E >>= BigInt(1) + } + + return res +} diff --git a/packages/baby-jubjub/src/sqrt.ts b/packages/baby-jubjub/src/sqrt.ts new file mode 100644 index 000000000..5e3a05c6a --- /dev/null +++ b/packages/baby-jubjub/src/sqrt.ts @@ -0,0 +1,56 @@ +import { r } from "./baby-jubjub" +import Field from "./field" + +// Based on https://eprint.iacr.org/2012/685.pdf +// and https://github.com/iden3/ffjavascript/blob/6f37a93fabddf45100bf221de6a1399599497e5d/src/fsqrt.js#L38 + +/** + * The following is a sqrt function (i.e. tonelliShanks) with some pre-computed + * constants and it only works with 'r'. + * @param n - The number for which to calculate the square root. + * @returns The square root. + */ +// eslint-disable-next-line import/prefer-default-export +export function tonelliShanks(n: bigint) { + const Fr = new Field(r) + + const sqrt_s = 28 + const sqrt_z = BigInt("5978345932401256595026418116861078668372907927053715034645334559810731495452") + const sqrt_tm1d2 = BigInt("40770029410420498293352137776570907027550720424234931066070132305055") + + if (Fr.isZero(n)) return Fr.zero + + let w = Fr.pow(n, sqrt_tm1d2) + const a0 = Fr.pow(Fr.mul(Fr.square(w), n), BigInt(2 ** (sqrt_s - 1))) + + if (Fr.eq(a0, Fr._negone)) { + return null + } + + let v = sqrt_s + let x = Fr.mul(n, w) + let b = Fr.mul(x, w) + let z = sqrt_z + + while (!Fr.eq(b, Fr.one)) { + let b2k = Fr.square(b) + let k = 1 + while (!Fr.eq(b2k, Fr.one)) { + b2k = Fr.square(b2k) + k += 1 + } + + w = z + + for (let i = 0; i < v - k - 1; i += 1) { + w = Fr.square(w) + } + + z = Fr.square(w) + b = Fr.mul(b, z) + x = Fr.mul(x, w) + v = k + } + + return Fr.geq(x, Fr.zero) ? x : Fr.neg(x) +} diff --git a/packages/baby-jubjub/src/types/index.ts b/packages/baby-jubjub/src/types/index.ts new file mode 100644 index 000000000..eefb42be3 --- /dev/null +++ b/packages/baby-jubjub/src/types/index.ts @@ -0,0 +1,10 @@ +export type BigNumber = bigint | string + +export type BigNumberish = BigNumber | number | Buffer + +export type Point = [N, N] + +export type Signature = { + R8: Point + S: N +} diff --git a/packages/baby-jubjub/src/utils.ts b/packages/baby-jubjub/src/utils.ts new file mode 100644 index 000000000..3427a9b29 --- /dev/null +++ b/packages/baby-jubjub/src/utils.ts @@ -0,0 +1,128 @@ +import { BigNumber, BigNumberish, Point, Signature } from "./types" + +export function pruneBuffer(buff: Buffer): Buffer { + buff[0] &= 0xf8 + buff[31] &= 0x7f + buff[31] |= 0x40 + + return buff +} + +export function isStringifiedBigint(s: BigNumber | string): boolean { + try { + BigInt(s) + + return true + } catch (e) { + return false + } +} + +export function isHexadecimal(s: string) { + return /^(0x|0X)[0-9a-fA-F]+$/.test(s) +} + +export function isBigNumberish(value: BigNumberish): boolean { + return ( + typeof value === "number" || + typeof value === "bigint" || + (typeof value === "string" && isStringifiedBigint(value)) || + (typeof value === "string" && isHexadecimal(value)) || + Buffer.isBuffer(value) + ) +} + +export function isPoint(point: Point): boolean { + return Array.isArray(point) && point.length === 2 && isStringifiedBigint(point[0]) && isStringifiedBigint(point[1]) +} + +export function isSignature(signature: Signature): boolean { + return ( + typeof signature === "object" && + Object.prototype.hasOwnProperty.call(signature, "R8") && + Object.prototype.hasOwnProperty.call(signature, "S") && + isPoint(signature.R8) && + isStringifiedBigint(signature.S) + ) +} + +export function int2hex(n: bigint) { + let hex = n.toString(16) + + // Ensure even length. + if (hex.length % 2 !== 0) { + hex = `0${hex}` + } + + return hex +} + +export function bigNumberish2Buff(value: BigNumberish): Buffer { + if ( + typeof value === "number" || + typeof value === "bigint" || + (typeof value === "string" && isStringifiedBigint(value)) + ) { + const hex = int2hex(BigInt(value)) + + return Buffer.from(hex, "hex") + } + + return value as Buffer +} + +export function buff2int(buffer: Buffer): bigint { + return BigInt(`0x${buffer.toString("hex")}`) +} + +export function bigNumberish2BigNumber(value: BigNumberish): bigint { + if ( + typeof value === "number" || + typeof value === "bigint" || + (typeof value === "string" && isStringifiedBigint(value)) || + (typeof value === "string" && isHexadecimal(value)) + ) { + return BigInt(value) + } + + return buff2int(value as Buffer) +} + +export function leBuff2int(buffer: Buffer): bigint { + return BigInt(`0x${buffer.reverse().toString("hex")}`) +} + +export function leInt2Buff(n: bigint): Buffer { + const hex = int2hex(n) + + // Allocate buffer of the desired size, filled with zeros. + const buffer = Buffer.alloc(32, 0) + + Buffer.from(hex, "hex").reverse().copy(buffer) + + return buffer +} + +export function checkPrivateKey(privateKey: BigNumberish): Buffer { + if (isBigNumberish(privateKey)) { + return bigNumberish2Buff(privateKey) + } + + if (typeof privateKey !== "string") { + throw TypeError("Invalid private key type. Supported types: number, bigint, buffer, string.") + } + + return Buffer.from(privateKey) +} + +export function checkMessage(message: BigNumberish): bigint { + if (isBigNumberish(message)) { + return bigNumberish2BigNumber(message) + } + + if (typeof message !== "string") { + throw TypeError("Invalid message type. Supported types: number, bigint, buffer, string.") + } + + return buff2int(Buffer.from(message)) +} diff --git a/packages/baby-jubjub/tests/index.test.ts b/packages/baby-jubjub/tests/index.test.ts new file mode 100644 index 000000000..552a62516 --- /dev/null +++ b/packages/baby-jubjub/tests/index.test.ts @@ -0,0 +1,45 @@ +import { babyjub } from "circomlibjs" +import { addPoint, mulPointEscalar, Base8, inCurve, packPoint, unpackPoint, Point } from "../src" + +describe("BabyJubjub", () => { + const secretScalar = BigInt(2) + const p1: Point = [BigInt(0), BigInt(1)] + + it("Should add 1 point to the curve", async () => { + const newPoint = addPoint(p1, Base8) + const circomlibNewPoint = babyjub.addPoint(p1, Base8) + + expect(newPoint[0]).toBe(circomlibNewPoint[0]) + expect(newPoint[1]).toBe(circomlibNewPoint[1]) + }) + + it("Should derive a public key from a secret scalar", async () => { + const publicKey = mulPointEscalar(Base8, secretScalar) + + const circomlibPublicKey = babyjub.mulPointEscalar(Base8, secretScalar) + + expect(publicKey[0]).toBe(circomlibPublicKey[0]) + expect(publicKey[1]).toBe(circomlibPublicKey[1]) + }) + + it("Should check if a point is in the curve", async () => { + expect(inCurve(p1)).toBeTruthy() + }) + + it("Should pack a point", async () => { + const packedPoint = packPoint(p1) + + const expectedPackedPoint = babyjub.packPoint(p1) + + expect(packedPoint).toBe(BigInt(`0x${Buffer.from(expectedPackedPoint).toString("hex")}`)) + }) + + it("Should unpack a packed public key", async () => { + const packedPoint = packPoint(p1) as bigint + const unpackedPoint = unpackPoint(packedPoint) as Point + + expect(unpackedPoint).not.toBeNull() + expect(unpackedPoint[0]).toBe(p1[0]) + expect(unpackedPoint[1]).toBe(p1[1]) + }) +}) diff --git a/packages/baby-jubjub/tsconfig.json b/packages/baby-jubjub/tsconfig.json new file mode 100644 index 000000000..81e592a16 --- /dev/null +++ b/packages/baby-jubjub/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "tests", "rollup.config.ts", "rollup.iife.config.ts"] +} diff --git a/packages/baby-jubjub/typedoc.json b/packages/baby-jubjub/typedoc.json new file mode 100644 index 000000000..77a471c91 --- /dev/null +++ b/packages/baby-jubjub/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index dd0074a22..5a5b3e8fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4423,6 +4423,20 @@ __metadata: languageName: node linkType: hard +"@zk-kit/baby-jubjub@workspace:packages/baby-jubjub": + version: 0.0.0-use.local + resolution: "@zk-kit/baby-jubjub@workspace:packages/baby-jubjub" + dependencies: + "@rollup/plugin-commonjs": ^25.0.7 + "@rollup/plugin-node-resolve": ^15.2.3 + circomlibjs: 0.0.8 + rollup-plugin-cleanup: ^3.2.1 + rollup-plugin-polyfill-node: ^0.13.0 + rollup-plugin-terser: ^7.0.2 + rollup-plugin-typescript2: ^0.31.2 + languageName: unknown + linkType: soft + "@zk-kit/circuits@workspace:packages/circuits": version: 0.0.0-use.local resolution: "@zk-kit/circuits@workspace:packages/circuits" From afce4fede8b0d4860fe8d8faccb56fd0d01e8e3d Mon Sep 17 00:00:00 2001 From: cedoor Date: Wed, 20 Dec 2023 16:22:56 +0100 Subject: [PATCH 2/2] feat(utils): add new utils package --- README.md | 56 +++++++ jest.config.ts | 2 +- package.json | 2 +- packages/baby-jubjub/README.md | 72 +++++++-- packages/baby-jubjub/package.json | 3 + packages/baby-jubjub/rollup.config.ts | 8 +- packages/baby-jubjub/src/baby-jubjub.ts | 26 ++-- packages/baby-jubjub/src/sqrt.ts | 7 +- packages/baby-jubjub/src/types/index.ts | 9 +- packages/baby-jubjub/src/utils.ts | 128 ---------------- packages/baby-jubjub/tests/index.test.ts | 22 +-- packages/eddsa-poseidon/package.json | 4 + packages/eddsa-poseidon/rollup.config.ts | 2 +- packages/eddsa-poseidon/rollup.iife.config.ts | 2 +- packages/eddsa-poseidon/src/babyjub.ts | 142 ------------------ packages/eddsa-poseidon/src/eddsa-poseidon.ts | 66 +++++--- packages/eddsa-poseidon/src/field.ts | 121 --------------- packages/eddsa-poseidon/src/scalar.ts | 36 ----- packages/eddsa-poseidon/src/sqrt.ts | 56 ------- packages/eddsa-poseidon/src/types/index.ts | 7 +- packages/eddsa-poseidon/src/utils.ts | 98 ++---------- packages/imt/package.json | 7 +- packages/imt/rollup.config.ts | 3 +- packages/utils/LICENSE | 21 +++ packages/utils/README.md | 79 ++++++++++ packages/utils/build.tsconfig.json | 8 + packages/utils/package.json | 41 +++++ packages/utils/rollup.config.ts | 26 ++++ packages/utils/rollup.iife.config.ts | 41 +++++ .../src/field.ts => utils/src/f1-field.ts} | 2 +- packages/utils/src/index.ts | 7 + packages/utils/src/number-checks.ts | 25 +++ packages/utils/src/number-conversions.ts | 59 ++++++++ packages/{baby-jubjub => utils}/src/scalar.ts | 0 packages/utils/src/types/index.ts | 3 + packages/utils/tests/index.test.ts | 27 ++++ packages/utils/tsconfig.json | 4 + packages/utils/typedoc.json | 3 + yarn.lock | 16 +- 39 files changed, 578 insertions(+), 663 deletions(-) delete mode 100644 packages/baby-jubjub/src/utils.ts delete mode 100644 packages/eddsa-poseidon/src/babyjub.ts delete mode 100644 packages/eddsa-poseidon/src/field.ts delete mode 100644 packages/eddsa-poseidon/src/scalar.ts delete mode 100644 packages/eddsa-poseidon/src/sqrt.ts create mode 100644 packages/utils/LICENSE create mode 100644 packages/utils/README.md create mode 100644 packages/utils/build.tsconfig.json create mode 100644 packages/utils/package.json create mode 100644 packages/utils/rollup.config.ts create mode 100644 packages/utils/rollup.iife.config.ts rename packages/{baby-jubjub/src/field.ts => utils/src/f1-field.ts} (98%) create mode 100644 packages/utils/src/index.ts create mode 100644 packages/utils/src/number-checks.ts create mode 100644 packages/utils/src/number-conversions.ts rename packages/{baby-jubjub => utils}/src/scalar.ts (100%) create mode 100644 packages/utils/src/types/index.ts create mode 100644 packages/utils/tests/index.test.ts create mode 100644 packages/utils/tsconfig.json create mode 100644 packages/utils/typedoc.json diff --git a/README.md b/README.md index c70334b74..2b9c00ccf 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,62 @@ + + + + @zk-kit/baby-jubjub + + + (docs) + + + + + + NPM version + + + + + + Downloads + + + + + + npm bundle size (scoped) + + + + + + + @zk-kit/utils + + + (docs) + + + + + + NPM version + + + + + + Downloads + + + + + + npm bundle size (scoped) + + + diff --git a/jest.config.ts b/jest.config.ts index 60fc27f68..9b227f8d6 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,7 +1,7 @@ import fs from "fs" import type { Config } from "@jest/types" -const exclude = ["circuits", "imt.sol", "rollup-plugin-rust", "lazytower.sol", "lazytower.circom"] +const exclude = ["circuits", "imt.sol", "rollup-plugin-rust", "lazytower.sol", "lazytower.circom", "utils"] const projects: any = fs .readdirSync("./packages", { withFileTypes: true }) diff --git a/package.json b/package.json index 41b648dca..4c4004551 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "private": true, "scripts": { "build": "yarn build:libraries && yarn compile:contracts", - "build:libraries": "yarn workspaces foreach --no-private run build", + "build:libraries": "yarn workspaces foreach -t --no-private run build", "compile:contracts": "yarn workspaces foreach run compile", "test": "yarn test:libraries && yarn test:contracts && yarn test:circuits", "test:libraries": "jest --coverage", diff --git a/packages/baby-jubjub/README.md b/packages/baby-jubjub/README.md index 8069e3b3c..761030e53 100644 --- a/packages/baby-jubjub/README.md +++ b/packages/baby-jubjub/README.md @@ -41,17 +41,6 @@ -| WIP | -| --- | - -- Super lightweight: [**~33kB**](https://bundlephobia.com/package/@zk-kit/baby-jubjub) (minified) -- Compatible with browsers and NodeJS -- TS type support -- Comprehensive code [documentation](https://zkkit.pse.dev/modules/_zk_kit_baby_jubjub.html) -- Full test coverage - -👾 Would you like to try it now? Explore it now on [Ceditor](https://ceditor.cedoor.dev/52787e4ad57d2f2076648d509efc3448)! - > [!WARNING] > This library has **not** been audited. @@ -87,4 +76,63 @@ or [JSDelivr](https://www.jsdelivr.com/): ## 📜 Usage -WIP +\# **addPoint**(p1: _Point\_, p2: _Point\_): _bigint_ + +```typescript +import { addPoint } from "@zk-kit/baby-jubjub" + +const p1: Point = [BigInt(0), BigInt(1)] + +const newPoint = addPoint(p1, Base8) +``` + +\# **mulPointEscalar**(base: _Point\_, e: _bigint_): _Point\_ + +```typescript +import { Base8, mulPointEscalar } from "@zk-kit/baby-jubjub" + +const secretScalar = BigInt(324) + +const publicKey = mulPointEscalar(Base8, secretScalar) +``` + +\# **mulPointEscalar**(base: _Point\_, e: _bigint_): _Point\_ + +```typescript +import { inCurve, Base8, mulPointEscalar } from "@zk-kit/baby-jubjub" + +const secretScalar = BigInt(324) + +const publicKey = mulPointEscalar(Base8, secretScalar) + +const isInCurve = inCurve(publicKey) +``` + +\# **packPoint**(unpackedPoint: _Point\_): _bigint_ + +```typescript +import { packPoint, Base8, mulPointEscalar } from "@zk-kit/baby-jubjub" + +const secretScalar = BigInt(324) + +const publicKey = mulPointEscalar(Base8, secretScalar) + +const packedPoint = packPoint(publicKey) +``` + +\# **unpackPoint**(packedPoint: _bigint_): _Point\_ | _null_ + +```typescript +import { packPoint, unpackPoint, Base8, mulPointEscalar } from "@zk-kit/baby-jubjub" + +const secretScalar = BigInt(324) + +const publicKey = mulPointEscalar(Base8, secretScalar) + +const packedPoint = packPoint(publicKey) + +const unpackedPoint = unpackPoint(packedPoint) + +console.log(publicKey[0] === unpackedPoint[0]) // true +console.log(publicKey[1] === unpackedPoint[1]) // true +``` diff --git a/packages/baby-jubjub/package.json b/packages/baby-jubjub/package.json index c66a92617..432d963a0 100644 --- a/packages/baby-jubjub/package.json +++ b/packages/baby-jubjub/package.json @@ -40,5 +40,8 @@ "rollup-plugin-polyfill-node": "^0.13.0", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.2" + }, + "dependencies": { + "@zk-kit/utils": "0.1.0" } } diff --git a/packages/baby-jubjub/rollup.config.ts b/packages/baby-jubjub/rollup.config.ts index 7d2994bd6..4efe45a74 100644 --- a/packages/baby-jubjub/rollup.config.ts +++ b/packages/baby-jubjub/rollup.config.ts @@ -1,5 +1,3 @@ -import commonjs from "@rollup/plugin-commonjs" -import { nodeResolve } from "@rollup/plugin-node-resolve" import fs from "fs" import cleanup from "rollup-plugin-cleanup" import typescript from "rollup-plugin-typescript2" @@ -20,13 +18,9 @@ export default { { file: pkg.exports.require, format: "cjs", banner }, { file: pkg.exports.import, format: "es", banner } ], - external: [], + external: Object.keys(pkg.dependencies), plugins: [ typescript({ tsconfig: "./build.tsconfig.json", useTsconfigDeclarationDir: true }), - commonjs(), - nodeResolve({ - preferBuiltins: true - }), cleanup({ comments: "jsdoc" }) ] } diff --git a/packages/baby-jubjub/src/baby-jubjub.ts b/packages/baby-jubjub/src/baby-jubjub.ts index 5cb9e8f2f..75ebe1266 100644 --- a/packages/baby-jubjub/src/baby-jubjub.ts +++ b/packages/baby-jubjub/src/baby-jubjub.ts @@ -1,8 +1,6 @@ -import Field from "./field" -import * as scalar from "./scalar" -import { Point } from "./types" -import * as utils from "./utils" +import { F1Field, bigintToHexadecimal, bufferToBigint, leBigintToBuffer, leBufferToBigint, scalar } from "@zk-kit/utils" import * as sqrt from "./sqrt" +import { Point } from "./types" // Spec: https://eips.ethereum.org/EIPS/eip-2494 @@ -10,7 +8,7 @@ import * as sqrt from "./sqrt" export const r = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617") // 'F' (F_r) is the prime finite field with r elements. -export const Fr = new Field(r) +export const Fr = new F1Field(r) // Base8 is the base point used to generate other points on the curve. export const Base8: Point = [ @@ -72,7 +70,7 @@ export function addPoint(p1: Point, p2: Point): Point { */ export function mulPointEscalar(base: Point, e: bigint): Point { let res: Point = [Fr.e(BigInt(0)), Fr.e(BigInt(1))] - let rem: bigint = Fr.e(e) + let rem: bigint = e let exp: Point = base while (!scalar.isZero(rem)) { @@ -87,7 +85,7 @@ export function mulPointEscalar(base: Point, e: bigint): Point { return res } -export function inCurve(p: Point) { +export function inCurve(p: Point): boolean { const x1 = BigInt(p[0]) const y1 = BigInt(p[1]) @@ -98,17 +96,17 @@ export function inCurve(p: Point) { } export function packPoint(unpackedPoint: Point): bigint { - const buffer = utils.leInt2Buff(unpackedPoint[1]) + const buffer = leBigintToBuffer(unpackedPoint[1]) if (Fr.lt(unpackedPoint[0], Fr.zero)) { buffer[31] |= 0x80 } - return utils.buff2int(buffer) + return bufferToBigint(buffer) } -export function unpackPoint(packedPoint: bigint): Point | null { - const buffer = Buffer.from(utils.int2hex(packedPoint), "hex") +export function unpackPoint(packedPoint: bigint): Point | null { + const buffer = Buffer.from(bigintToHexadecimal(packedPoint), "hex") const unpackedPoint = new Array(2) let sign = false @@ -118,7 +116,7 @@ export function unpackPoint(packedPoint: bigint): Point | null { buffer[31] &= 0x7f } - unpackedPoint[1] = utils.leBuff2int(buffer) + unpackedPoint[1] = leBufferToBigint(buffer) if (scalar.gt(unpackedPoint[1], r)) { return null @@ -126,7 +124,7 @@ export function unpackPoint(packedPoint: bigint): Point | null { const y2 = Fr.square(unpackedPoint[1]) - let x = sqrt.tonelliShanks(Fr.div(Fr.sub(Fr.one, y2), Fr.sub(a, Fr.mul(d, y2)))) + let x = sqrt.tonelliShanks(Fr.div(Fr.sub(Fr.one, y2), Fr.sub(a, Fr.mul(d, y2))), r) if (x == null) { return null @@ -138,5 +136,5 @@ export function unpackPoint(packedPoint: bigint): Point | null { unpackedPoint[0] = x - return unpackedPoint as Point + return unpackedPoint as Point } diff --git a/packages/baby-jubjub/src/sqrt.ts b/packages/baby-jubjub/src/sqrt.ts index 5e3a05c6a..66844edc4 100644 --- a/packages/baby-jubjub/src/sqrt.ts +++ b/packages/baby-jubjub/src/sqrt.ts @@ -1,5 +1,4 @@ -import { r } from "./baby-jubjub" -import Field from "./field" +import { F1Field } from "@zk-kit/utils" // Based on https://eprint.iacr.org/2012/685.pdf // and https://github.com/iden3/ffjavascript/blob/6f37a93fabddf45100bf221de6a1399599497e5d/src/fsqrt.js#L38 @@ -11,8 +10,8 @@ import Field from "./field" * @returns The square root. */ // eslint-disable-next-line import/prefer-default-export -export function tonelliShanks(n: bigint) { - const Fr = new Field(r) +export function tonelliShanks(n: bigint, order: bigint) { + const Fr = new F1Field(order) const sqrt_s = 28 const sqrt_z = BigInt("5978345932401256595026418116861078668372907927053715034645334559810731495452") diff --git a/packages/baby-jubjub/src/types/index.ts b/packages/baby-jubjub/src/types/index.ts index eefb42be3..b4ce0aab3 100644 --- a/packages/baby-jubjub/src/types/index.ts +++ b/packages/baby-jubjub/src/types/index.ts @@ -1,10 +1,3 @@ -export type BigNumber = bigint | string - -export type BigNumberish = BigNumber | number | Buffer +import { BigNumber } from "@zk-kit/utils" export type Point = [N, N] - -export type Signature = { - R8: Point - S: N -} diff --git a/packages/baby-jubjub/src/utils.ts b/packages/baby-jubjub/src/utils.ts deleted file mode 100644 index 3427a9b29..000000000 --- a/packages/baby-jubjub/src/utils.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { BigNumber, BigNumberish, Point, Signature } from "./types" - -export function pruneBuffer(buff: Buffer): Buffer { - buff[0] &= 0xf8 - buff[31] &= 0x7f - buff[31] |= 0x40 - - return buff -} - -export function isStringifiedBigint(s: BigNumber | string): boolean { - try { - BigInt(s) - - return true - } catch (e) { - return false - } -} - -export function isHexadecimal(s: string) { - return /^(0x|0X)[0-9a-fA-F]+$/.test(s) -} - -export function isBigNumberish(value: BigNumberish): boolean { - return ( - typeof value === "number" || - typeof value === "bigint" || - (typeof value === "string" && isStringifiedBigint(value)) || - (typeof value === "string" && isHexadecimal(value)) || - Buffer.isBuffer(value) - ) -} - -export function isPoint(point: Point): boolean { - return Array.isArray(point) && point.length === 2 && isStringifiedBigint(point[0]) && isStringifiedBigint(point[1]) -} - -export function isSignature(signature: Signature): boolean { - return ( - typeof signature === "object" && - Object.prototype.hasOwnProperty.call(signature, "R8") && - Object.prototype.hasOwnProperty.call(signature, "S") && - isPoint(signature.R8) && - isStringifiedBigint(signature.S) - ) -} - -export function int2hex(n: bigint) { - let hex = n.toString(16) - - // Ensure even length. - if (hex.length % 2 !== 0) { - hex = `0${hex}` - } - - return hex -} - -export function bigNumberish2Buff(value: BigNumberish): Buffer { - if ( - typeof value === "number" || - typeof value === "bigint" || - (typeof value === "string" && isStringifiedBigint(value)) - ) { - const hex = int2hex(BigInt(value)) - - return Buffer.from(hex, "hex") - } - - return value as Buffer -} - -export function buff2int(buffer: Buffer): bigint { - return BigInt(`0x${buffer.toString("hex")}`) -} - -export function bigNumberish2BigNumber(value: BigNumberish): bigint { - if ( - typeof value === "number" || - typeof value === "bigint" || - (typeof value === "string" && isStringifiedBigint(value)) || - (typeof value === "string" && isHexadecimal(value)) - ) { - return BigInt(value) - } - - return buff2int(value as Buffer) -} - -export function leBuff2int(buffer: Buffer): bigint { - return BigInt(`0x${buffer.reverse().toString("hex")}`) -} - -export function leInt2Buff(n: bigint): Buffer { - const hex = int2hex(n) - - // Allocate buffer of the desired size, filled with zeros. - const buffer = Buffer.alloc(32, 0) - - Buffer.from(hex, "hex").reverse().copy(buffer) - - return buffer -} - -export function checkPrivateKey(privateKey: BigNumberish): Buffer { - if (isBigNumberish(privateKey)) { - return bigNumberish2Buff(privateKey) - } - - if (typeof privateKey !== "string") { - throw TypeError("Invalid private key type. Supported types: number, bigint, buffer, string.") - } - - return Buffer.from(privateKey) -} - -export function checkMessage(message: BigNumberish): bigint { - if (isBigNumberish(message)) { - return bigNumberish2BigNumber(message) - } - - if (typeof message !== "string") { - throw TypeError("Invalid message type. Supported types: number, bigint, buffer, string.") - } - - return buff2int(Buffer.from(message)) -} diff --git a/packages/baby-jubjub/tests/index.test.ts b/packages/baby-jubjub/tests/index.test.ts index 552a62516..00ccc0a6d 100644 --- a/packages/baby-jubjub/tests/index.test.ts +++ b/packages/baby-jubjub/tests/index.test.ts @@ -2,10 +2,13 @@ import { babyjub } from "circomlibjs" import { addPoint, mulPointEscalar, Base8, inCurve, packPoint, unpackPoint, Point } from "../src" describe("BabyJubjub", () => { - const secretScalar = BigInt(2) - const p1: Point = [BigInt(0), BigInt(1)] + const secretScalar = BigInt(324) + + let publicKey: Point it("Should add 1 point to the curve", async () => { + const p1: Point = [BigInt(0), BigInt(1)] + const newPoint = addPoint(p1, Base8) const circomlibNewPoint = babyjub.addPoint(p1, Base8) @@ -14,7 +17,7 @@ describe("BabyJubjub", () => { }) it("Should derive a public key from a secret scalar", async () => { - const publicKey = mulPointEscalar(Base8, secretScalar) + publicKey = mulPointEscalar(Base8, secretScalar) const circomlibPublicKey = babyjub.mulPointEscalar(Base8, secretScalar) @@ -23,23 +26,24 @@ describe("BabyJubjub", () => { }) it("Should check if a point is in the curve", async () => { - expect(inCurve(p1)).toBeTruthy() + expect(inCurve(publicKey)).toBeTruthy() }) it("Should pack a point", async () => { - const packedPoint = packPoint(p1) + const packedPoint = packPoint(publicKey) - const expectedPackedPoint = babyjub.packPoint(p1) + const expectedPackedPoint = babyjub.packPoint(publicKey) expect(packedPoint).toBe(BigInt(`0x${Buffer.from(expectedPackedPoint).toString("hex")}`)) }) it("Should unpack a packed public key", async () => { - const packedPoint = packPoint(p1) as bigint + const publicKey = mulPointEscalar(Base8, secretScalar) + const packedPoint = packPoint(publicKey) const unpackedPoint = unpackPoint(packedPoint) as Point expect(unpackedPoint).not.toBeNull() - expect(unpackedPoint[0]).toBe(p1[0]) - expect(unpackedPoint[1]).toBe(p1[1]) + expect(unpackedPoint[0]).toBe(publicKey[0]) + expect(unpackedPoint[1]).toBe(publicKey[1]) }) }) diff --git a/packages/eddsa-poseidon/package.json b/packages/eddsa-poseidon/package.json index 3689caf1e..40d0a7a8d 100644 --- a/packages/eddsa-poseidon/package.json +++ b/packages/eddsa-poseidon/package.json @@ -42,5 +42,9 @@ "rollup-plugin-polyfill-node": "^0.13.0", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.2" + }, + "dependencies": { + "@zk-kit/baby-jubjub": "0.1.0", + "@zk-kit/utils": "0.1.0" } } diff --git a/packages/eddsa-poseidon/rollup.config.ts b/packages/eddsa-poseidon/rollup.config.ts index 7d2994bd6..9f2d1f624 100644 --- a/packages/eddsa-poseidon/rollup.config.ts +++ b/packages/eddsa-poseidon/rollup.config.ts @@ -20,7 +20,7 @@ export default { { file: pkg.exports.require, format: "cjs", banner }, { file: pkg.exports.import, format: "es", banner } ], - external: [], + external: Object.keys(pkg.dependencies), plugins: [ typescript({ tsconfig: "./build.tsconfig.json", useTsconfigDeclarationDir: true }), commonjs(), diff --git a/packages/eddsa-poseidon/rollup.iife.config.ts b/packages/eddsa-poseidon/rollup.iife.config.ts index f7936620a..321cb34b5 100644 --- a/packages/eddsa-poseidon/rollup.iife.config.ts +++ b/packages/eddsa-poseidon/rollup.iife.config.ts @@ -1,8 +1,8 @@ import commonjs from "@rollup/plugin-commonjs" import { nodeResolve } from "@rollup/plugin-node-resolve" import fs from "fs" -import nodePolyfills from "rollup-plugin-polyfill-node" import cleanup from "rollup-plugin-cleanup" +import nodePolyfills from "rollup-plugin-polyfill-node" import { terser } from "rollup-plugin-terser" import typescript from "rollup-plugin-typescript2" diff --git a/packages/eddsa-poseidon/src/babyjub.ts b/packages/eddsa-poseidon/src/babyjub.ts deleted file mode 100644 index 007d07b7c..000000000 --- a/packages/eddsa-poseidon/src/babyjub.ts +++ /dev/null @@ -1,142 +0,0 @@ -import Field from "./field" -import * as scalar from "./scalar" -import { Point } from "./types" -import * as utils from "./utils" -import * as sqrt from "./sqrt" - -// Spec: https://eips.ethereum.org/EIPS/eip-2494 - -// 'r' is the alt_bn128 prime order. -export const r = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617") - -// 'F' (F_r) is the prime finite field with r elements. -export const Fr = new Field(r) - -// Base8 is the base point used to generate other points on the curve. -export const Base8: Point = [ - Fr.e(BigInt("5299619240641551281634865583518297030282874472190772894086521144482721001553")), - Fr.e(BigInt("16950150798460657717958625567821834550301663161624707787222815936182638968203")) -] - -// Let E be the twisted Edwards elliptic curve defined over 'F_r' -// described by the equation 'ax^2 + y^2 = 1 + dx^2y^2'. - -// 'a' and 'd' are the parameters of the equation: -const a = Fr.e(BigInt("168700")) -const d = Fr.e(BigInt("168696")) - -// We call Baby Jubjub the curve 'E(F_r)', that is, the subgroup of 'F_r'-rational points of 'E'. - -// 'order' is order of the elliptic curve 'E'. -export const order = BigInt("21888242871839275222246405745257275088614511777268538073601725287587578984328") -export const subOrder = scalar.shiftRight(order, BigInt(3)) - -/** - * Performs point addition on the Baby Jubjub elliptic curve, - * calculating a third point from two given points. - * Let P1 = (x1, y1) and P2 = (x2, y2) be two arbitrary points of the curve. - * Then P1 + P2 = (x3, y3) is calculated in the following way: - * x3 = (x1*y2 + y1*x2)/(1 + d*x1*x2*y1*y2) - * y3 = (y1*y2 - a*x1*x2)/(1 - d*x1*x2*y1*y2) - * @param p1 - First point on the curve. - * @param p2 - Second point on the curve. - * @returns Resultant third point on the curve. - */ -export function addPoint(p1: Point, p2: Point): Point { - // beta = x1*y2 - const beta = Fr.mul(p1[0], p2[1]) - // gamma = y1*x2 - const gamma = Fr.mul(p1[1], p2[0]) - // delta = (y1-(a*x1))*(x2+y2) - const delta = Fr.mul(Fr.sub(p1[1], Fr.mul(a, p1[0])), Fr.add(p2[0], p2[1])) - - // x1*x2*y1*y2 - const tau = Fr.mul(beta, gamma) - // d*x1*x2*y1*y2 - const dtau = Fr.mul(d, tau) - - // x3 = (x1*y2 + y1*x2)/(1 + d*x1*x2*y1*y2) - const p3x = Fr.div(Fr.add(beta, gamma), Fr.add(Fr.one, dtau)) - // y3 = (y1*y2 - a*x1*x2)/(1 - d*x1*x2*y1*y2) - const p3y = Fr.div(Fr.add(delta, Fr.sub(Fr.mul(a, beta), gamma)), Fr.sub(Fr.one, dtau)) - - return [p3x, p3y] -} - -/** - * Performs a scalar multiplication by starting from the 'base' point and 'adding' - * it to itself 'e' times. - * @param base - The base point used as a starting point. - * @param e - A secret number representing the private key. - * @returns The resulting point representing the public key. - */ -export function mulPointEscalar(base: Point, e: bigint): Point { - let res: Point = [Fr.e(BigInt(0)), Fr.e(BigInt(1))] - let rem: bigint = e - let exp: Point = base - - while (!scalar.isZero(rem)) { - if (scalar.isOdd(rem)) { - res = addPoint(res, exp) - } - - exp = addPoint(exp, exp) - rem = scalar.shiftRight(rem, BigInt(1)) - } - - return res -} - -export function inCurve(p: Point) { - const x1 = BigInt(p[0]) - const y1 = BigInt(p[1]) - - const x2 = Fr.square(x1) - const y2 = Fr.square(y1) - - return Fr.eq(Fr.add(Fr.mul(a, x2), y2), Fr.add(Fr.one, Fr.mul(Fr.mul(x2, y2), d))) -} - -export function packPoint(unpackedPoint: Point): bigint { - const buffer = utils.leInt2Buff(unpackedPoint[1]) - - if (Fr.lt(unpackedPoint[0], Fr.zero)) { - buffer[31] |= 0x80 - } - - return utils.buff2int(buffer) -} - -export function unpackPoint(packedPoint: bigint): Point | null { - const buffer = Buffer.from(utils.int2hex(packedPoint), "hex") - const unpackedPoint = new Array(2) - - let sign = false - - if (buffer[31] & 0x80) { - sign = true - buffer[31] &= 0x7f - } - - unpackedPoint[1] = utils.leBuff2int(buffer) - - if (scalar.gt(unpackedPoint[1], r)) { - return null - } - - const y2 = Fr.square(unpackedPoint[1]) - - let x = sqrt.tonelliShanks(Fr.div(Fr.sub(Fr.one, y2), Fr.sub(a, Fr.mul(d, y2)))) - - if (x == null) { - return null - } - - if (sign) { - x = Fr.neg(x) - } - - unpackedPoint[0] = x - - return unpackedPoint as Point -} diff --git a/packages/eddsa-poseidon/src/eddsa-poseidon.ts b/packages/eddsa-poseidon/src/eddsa-poseidon.ts index 1f5d9d7ef..4cc2d0a58 100644 --- a/packages/eddsa-poseidon/src/eddsa-poseidon.ts +++ b/packages/eddsa-poseidon/src/eddsa-poseidon.ts @@ -1,9 +1,27 @@ +import { + Base8, + Fr, + Point, + addPoint, + inCurve, + mulPointEscalar, + packPoint, + subOrder, + unpackPoint +} from "@zk-kit/baby-jubjub" +import { + BigNumber, + BigNumberish, + F1Field, + isHexadecimal, + isStringifiedBigint, + leBigintToBuffer, + leBufferToBigint, + scalar +} from "@zk-kit/utils" import { poseidon5 } from "poseidon-lite/poseidon5" -import * as babyjub from "./babyjub" import blake from "./blake" -import Field from "./field" -import * as scalar from "./scalar" -import { BigNumber, BigNumberish, Point, Signature } from "./types" +import { Signature } from "./types" import * as utils from "./utils" /** @@ -25,7 +43,7 @@ export function deriveSecretScalar(privateKey: BigNumberish): string { hash = hash.slice(0, 32) hash = utils.pruneBuffer(hash) - return scalar.shiftRight(utils.leBuff2int(hash), BigInt(3)).toString() + return scalar.shiftRight(leBufferToBigint(hash), BigInt(3)).toString() } /** @@ -40,7 +58,7 @@ export function deriveSecretScalar(privateKey: BigNumberish): string { export function derivePublicKey(privateKey: BigNumberish): Point { const s = deriveSecretScalar(privateKey) - const publicKey = babyjub.mulPointEscalar(babyjub.Base8, BigInt(s)) + const publicKey = mulPointEscalar(Base8, BigInt(s)) // Convert the public key values to strings so that it can easily be exported as a JSON. return [publicKey[0].toString(), publicKey[1].toString()] @@ -63,17 +81,17 @@ export function signMessage(privateKey: BigNumberish, message: BigNumberish): Si const hash = blake(privateKey) const sBuff = utils.pruneBuffer(hash.slice(0, 32)) - const s = utils.leBuff2int(sBuff) - const A = babyjub.mulPointEscalar(babyjub.Base8, scalar.shiftRight(s, BigInt(3))) + const s = leBufferToBigint(sBuff) + const A = mulPointEscalar(Base8, scalar.shiftRight(s, BigInt(3))) - const msgBuff = utils.leInt2Buff(message) + const msgBuff = leBigintToBuffer(message) const rBuff = blake(Buffer.concat([hash.slice(32, 64), msgBuff])) - const Fr = new Field(babyjub.subOrder) - const r = Fr.e(utils.leBuff2int(rBuff)) + const Fr = new F1Field(subOrder) + const r = Fr.e(leBufferToBigint(rBuff)) - const R8 = babyjub.mulPointEscalar(babyjub.Base8, r) + const R8 = mulPointEscalar(Base8, r) const hm = poseidon5([R8[0], R8[1], A[0], A[1], message]) const S = Fr.add(r, Fr.mul(hm, s)) @@ -95,9 +113,9 @@ export function verifySignature(message: BigNumberish, signature: Signature, pub if ( !utils.isPoint(publicKey) || !utils.isSignature(signature) || - !babyjub.inCurve(signature.R8) || - !babyjub.inCurve(publicKey) || - BigInt(signature.S) >= babyjub.subOrder + !inCurve(signature.R8) || + !inCurve(publicKey) || + BigInt(signature.S) >= subOrder ) { return false } @@ -115,24 +133,24 @@ export function verifySignature(message: BigNumberish, signature: Signature, pub const hm = poseidon5([signature.R8[0], signature.R8[1], publicKey[0], publicKey[1], message]) - const pLeft = babyjub.mulPointEscalar(babyjub.Base8, BigInt(signature.S)) - let pRight = babyjub.mulPointEscalar(_publicKey, scalar.mul(hm, BigInt(8))) + const pLeft = mulPointEscalar(Base8, BigInt(signature.S)) + let pRight = mulPointEscalar(_publicKey, scalar.mul(hm, BigInt(8))) - pRight = babyjub.addPoint(_signature.R8, pRight) + pRight = addPoint(_signature.R8, pRight) // Return true if the points match. - return babyjub.Fr.eq(BigInt(pLeft[0]), pRight[0]) && babyjub.Fr.eq(pLeft[1], pRight[1]) + return Fr.eq(BigInt(pLeft[0]), pRight[0]) && Fr.eq(pLeft[1], pRight[1]) } export function packPublicKey(publicKey: Point): string { - if (!utils.isPoint(publicKey) || !babyjub.inCurve(publicKey)) { + if (!utils.isPoint(publicKey) || !inCurve(publicKey)) { throw new Error("Invalid public key") } // Convert the public key values to big integers for calculations. const _publicKey: Point = [BigInt(publicKey[0]), BigInt(publicKey[1])] - const packedPublicKey = babyjub.packPoint(_publicKey) + const packedPublicKey = packPoint(_publicKey) if (packedPublicKey === null) { throw new Error("Invalid public key") @@ -144,13 +162,13 @@ export function packPublicKey(publicKey: Point): string { export function unpackPublicKey(publicKey: BigNumber): Point { if ( typeof publicKey !== "bigint" && - (typeof publicKey !== "string" || !utils.isStringifiedBigint(publicKey)) && - (typeof publicKey !== "string" || !utils.isHexadecimal(publicKey)) + (typeof publicKey !== "string" || !isStringifiedBigint(publicKey)) && + (typeof publicKey !== "string" || !isHexadecimal(publicKey)) ) { throw new TypeError("Invalid public key type") } - const unpackedPublicKey = babyjub.unpackPoint(BigInt(publicKey)) + const unpackedPublicKey = unpackPoint(BigInt(publicKey)) if (unpackedPublicKey === null) { throw new Error("Invalid public key") diff --git a/packages/eddsa-poseidon/src/field.ts b/packages/eddsa-poseidon/src/field.ts deleted file mode 100644 index 4f322b6e2..000000000 --- a/packages/eddsa-poseidon/src/field.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as scalar from "./scalar" - -export default class Field { - one = BigInt(1) - zero = BigInt(0) - - _order: bigint - _half: bigint - _negone: bigint - - constructor(order: bigint) { - this._order = order - this._half = order >> this.one - this._negone = this._order - this.one - } - - e(res: bigint): bigint { - if (res < 0) { - let nres = -res - - if (nres >= this._order) { - nres %= this._order - } - - return this._order - nres - } - - return res >= this._order ? res % this._order : res - } - - mul(a: bigint, b: bigint): bigint { - return (a * b) % this._order - } - - sub(a: bigint, b: bigint): bigint { - return a >= b ? a - b : this._order - b + a - } - - add(a: bigint, b: bigint): bigint { - const res = a + b - - return res >= this._order ? res - this._order : res - } - - inv(a: bigint): bigint { - let t = this.zero - let r = this._order - let newt = this.one - let newr = a % this._order - - while (newr) { - const q = r / newr - ;[t, newt] = [newt, t - q * newt] - ;[r, newr] = [newr, r - q * newr] - } - - if (t < this.zero) { - t += this._order - } - - return t - } - - div(a: bigint, b: bigint): bigint { - return this.mul(a, this.inv(b)) - } - - eq(a: bigint, b: bigint): boolean { - return a === b - } - - square(a: bigint): bigint { - return (a * a) % this._order - } - - lt(a: bigint, b: bigint) { - const aa = a > this._half ? a - this._order : a - const bb = b > this._half ? b - this._order : b - - return aa < bb - } - - geq(a: bigint, b: bigint) { - const aa = a > this._half ? a - this._order : a - const bb = b > this._half ? b - this._order : b - - return aa >= bb - } - - neg(a: bigint) { - return a ? this._order - a : a - } - - isZero(a: bigint) { - return a === this.zero - } - - pow(base: bigint, e: bigint) { - if (scalar.isZero(e)) { - return this.one - } - - const n = scalar.bits(e) - - if (n.length === 0) { - return this.one - } - - let res = base - - for (let i = n.length - 2; i >= 0; i -= 1) { - res = this.square(res) - - if (n[i]) { - res = this.mul(res, base) - } - } - - return res - } -} diff --git a/packages/eddsa-poseidon/src/scalar.ts b/packages/eddsa-poseidon/src/scalar.ts deleted file mode 100644 index 0f15bcae8..000000000 --- a/packages/eddsa-poseidon/src/scalar.ts +++ /dev/null @@ -1,36 +0,0 @@ -export function isZero(a: bigint): boolean { - return !a -} - -export function isOdd(a: bigint): boolean { - return (a & BigInt(1)) === BigInt(1) -} - -export function shiftRight(a: bigint, n: bigint): bigint { - return a >> n -} - -export function mul(a: bigint, b: bigint): bigint { - return a * b -} - -export function gt(a: bigint, b: bigint) { - return a > b -} - -export function bits(n: bigint): number[] { - const res = [] - let E = n - - while (E) { - if (E & BigInt(1)) { - res.push(1) - } else { - res.push(0) - } - - E >>= BigInt(1) - } - - return res -} diff --git a/packages/eddsa-poseidon/src/sqrt.ts b/packages/eddsa-poseidon/src/sqrt.ts deleted file mode 100644 index 099c59723..000000000 --- a/packages/eddsa-poseidon/src/sqrt.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { r } from "./babyjub" -import Field from "./field" - -// Based on https://eprint.iacr.org/2012/685.pdf -// and https://github.com/iden3/ffjavascript/blob/6f37a93fabddf45100bf221de6a1399599497e5d/src/fsqrt.js#L38 - -/** - * The following is a sqrt function (i.e. tonelliShanks) with some pre-computed - * constants and it only works with 'r'. - * @param n - The number for which to calculate the square root. - * @returns The square root. - */ -// eslint-disable-next-line import/prefer-default-export -export function tonelliShanks(n: bigint) { - const Fr = new Field(r) - - const sqrt_s = 28 - const sqrt_z = BigInt("5978345932401256595026418116861078668372907927053715034645334559810731495452") - const sqrt_tm1d2 = BigInt("40770029410420498293352137776570907027550720424234931066070132305055") - - if (Fr.isZero(n)) return Fr.zero - - let w = Fr.pow(n, sqrt_tm1d2) - const a0 = Fr.pow(Fr.mul(Fr.square(w), n), BigInt(2 ** (sqrt_s - 1))) - - if (Fr.eq(a0, Fr._negone)) { - return null - } - - let v = sqrt_s - let x = Fr.mul(n, w) - let b = Fr.mul(x, w) - let z = sqrt_z - - while (!Fr.eq(b, Fr.one)) { - let b2k = Fr.square(b) - let k = 1 - while (!Fr.eq(b2k, Fr.one)) { - b2k = Fr.square(b2k) - k += 1 - } - - w = z - - for (let i = 0; i < v - k - 1; i += 1) { - w = Fr.square(w) - } - - z = Fr.square(w) - b = Fr.mul(b, z) - x = Fr.mul(x, w) - v = k - } - - return Fr.geq(x, Fr.zero) ? x : Fr.neg(x) -} diff --git a/packages/eddsa-poseidon/src/types/index.ts b/packages/eddsa-poseidon/src/types/index.ts index eefb42be3..1cbbf41a5 100644 --- a/packages/eddsa-poseidon/src/types/index.ts +++ b/packages/eddsa-poseidon/src/types/index.ts @@ -1,8 +1,5 @@ -export type BigNumber = bigint | string - -export type BigNumberish = BigNumber | number | Buffer - -export type Point = [N, N] +import { Point } from "@zk-kit/baby-jubjub" +import { BigNumber } from "@zk-kit/utils" export type Signature = { R8: Point diff --git a/packages/eddsa-poseidon/src/utils.ts b/packages/eddsa-poseidon/src/utils.ts index 3427a9b29..65ddf4400 100644 --- a/packages/eddsa-poseidon/src/utils.ts +++ b/packages/eddsa-poseidon/src/utils.ts @@ -1,4 +1,13 @@ -import { BigNumber, BigNumberish, Point, Signature } from "./types" +import { Point } from "@zk-kit/baby-jubjub" +import { + BigNumberish, + bigNumberishToBigint, + bigNumberishToBuffer, + bufferToBigint, + isBigNumberish, + isStringifiedBigint +} from "@zk-kit/utils" +import { Signature } from "./types" export function pruneBuffer(buff: Buffer): Buffer { buff[0] &= 0xf8 @@ -8,30 +17,6 @@ export function pruneBuffer(buff: Buffer): Buffer { return buff } -export function isStringifiedBigint(s: BigNumber | string): boolean { - try { - BigInt(s) - - return true - } catch (e) { - return false - } -} - -export function isHexadecimal(s: string) { - return /^(0x|0X)[0-9a-fA-F]+$/.test(s) -} - -export function isBigNumberish(value: BigNumberish): boolean { - return ( - typeof value === "number" || - typeof value === "bigint" || - (typeof value === "string" && isStringifiedBigint(value)) || - (typeof value === "string" && isHexadecimal(value)) || - Buffer.isBuffer(value) - ) -} - export function isPoint(point: Point): boolean { return Array.isArray(point) && point.length === 2 && isStringifiedBigint(point[0]) && isStringifiedBigint(point[1]) } @@ -46,66 +31,9 @@ export function isSignature(signature: Signature): boolean { ) } -export function int2hex(n: bigint) { - let hex = n.toString(16) - - // Ensure even length. - if (hex.length % 2 !== 0) { - hex = `0${hex}` - } - - return hex -} - -export function bigNumberish2Buff(value: BigNumberish): Buffer { - if ( - typeof value === "number" || - typeof value === "bigint" || - (typeof value === "string" && isStringifiedBigint(value)) - ) { - const hex = int2hex(BigInt(value)) - - return Buffer.from(hex, "hex") - } - - return value as Buffer -} - -export function buff2int(buffer: Buffer): bigint { - return BigInt(`0x${buffer.toString("hex")}`) -} - -export function bigNumberish2BigNumber(value: BigNumberish): bigint { - if ( - typeof value === "number" || - typeof value === "bigint" || - (typeof value === "string" && isStringifiedBigint(value)) || - (typeof value === "string" && isHexadecimal(value)) - ) { - return BigInt(value) - } - - return buff2int(value as Buffer) -} - -export function leBuff2int(buffer: Buffer): bigint { - return BigInt(`0x${buffer.reverse().toString("hex")}`) -} - -export function leInt2Buff(n: bigint): Buffer { - const hex = int2hex(n) - - // Allocate buffer of the desired size, filled with zeros. - const buffer = Buffer.alloc(32, 0) - - Buffer.from(hex, "hex").reverse().copy(buffer) - - return buffer -} - export function checkPrivateKey(privateKey: BigNumberish): Buffer { if (isBigNumberish(privateKey)) { - return bigNumberish2Buff(privateKey) + return bigNumberishToBuffer(privateKey) } if (typeof privateKey !== "string") { @@ -117,12 +45,12 @@ export function checkPrivateKey(privateKey: BigNumberish): Buffer { export function checkMessage(message: BigNumberish): bigint { if (isBigNumberish(message)) { - return bigNumberish2BigNumber(message) + return bigNumberishToBigint(message) } if (typeof message !== "string") { throw TypeError("Invalid message type. Supported types: number, bigint, buffer, string.") } - return buff2int(Buffer.from(message)) + return bufferToBigint(Buffer.from(message)) } diff --git a/packages/imt/package.json b/packages/imt/package.json index ee90e56d3..14668d34a 100644 --- a/packages/imt/package.json +++ b/packages/imt/package.json @@ -21,13 +21,10 @@ ], "repository": "git@github.com:privacy-scaling-explorations/zk-kit.git", "homepage": "https://github.com/privacy-scaling-explorations/zk-kit/tree/main/packages/imt", - "author": { - "name": "Cedoor", - "email": "me@cedoor.dev", - "url": "https://cedoor.dev" + "bugs": { + "url": "https://github.com/privacy-scaling-explorations/zk-kit.git/issues" }, "scripts": { - "build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript", "build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript", "prepublishOnly": "yarn build" }, diff --git a/packages/imt/rollup.config.ts b/packages/imt/rollup.config.ts index da32af1a2..43545a0f0 100644 --- a/packages/imt/rollup.config.ts +++ b/packages/imt/rollup.config.ts @@ -8,10 +8,11 @@ const banner = `/** * @module ${pkg.name} * @version ${pkg.version} * @file ${pkg.description} - * @copyright ${pkg.author.name} ${new Date().getFullYear()} + * @copyright Ethereum Foundation ${new Date().getFullYear()} * @license ${pkg.license} * @see [Github]{@link ${pkg.homepage}} */` + const name = pkg.name.substr(1).replace(/[-/]./g, (x: string) => x.toUpperCase()[1]) export default { diff --git a/packages/utils/LICENSE b/packages/utils/LICENSE new file mode 100644 index 000000000..4377091ec --- /dev/null +++ b/packages/utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ethereum Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/utils/README.md b/packages/utils/README.md new file mode 100644 index 000000000..849a9fe56 --- /dev/null +++ b/packages/utils/README.md @@ -0,0 +1,79 @@ +

+

+ Utils +

+

Essential zero-knowledge utility library for JavaScript developers.

+

+ +

+ + + + + NPM license + + + NPM version + + + Downloads + + + npm bundle size (scoped) + + + Linter eslint + + + Code style prettier + +

+ + + +> [!WARNING] +> This library has **not** been audited. + +## 🛠 Install + +### npm or yarn + +Install the `@zk-kit/utils` package and its peer dependencies with npm: + +```bash +npm i @zk-kit/utils +``` + +or yarn: + +```bash +yarn add @zk-kit/utils +``` + +### CDN + +You can also load it using a `script` tag using [unpkg](https://unpkg.com/): + +```html + +``` + +or [JSDelivr](https://www.jsdelivr.com/): + +```html + +``` + +## 📜 Usage + +For more information on the functions provided by `@zk-kit/utils`, please refer to the [documentation](https://zkkit.pse.dev/modules/_zk_kit_utils.html). diff --git a/packages/utils/build.tsconfig.json b/packages/utils/build.tsconfig.json new file mode 100644 index 000000000..2d4a1d6da --- /dev/null +++ b/packages/utils/build.tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "declarationDir": "dist/types" + }, + "include": ["src"] +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..2818697f9 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,41 @@ +{ + "name": "@zk-kit/utils", + "version": "0.1.0", + "description": "Essential zero-knowledge utility library for JavaScript developers.", + "license": "MIT", + "iife": "dist/index.js", + "unpkg": "dist/index.min.js", + "jsdelivr": "dist/index.min.js", + "main": "dist/index.node.js", + "exports": { + "import": "./dist/index.mjs", + "require": "./dist/index.node.js", + "types": "./dist/types/index.d.ts" + }, + "types": "dist/types/index.d.ts", + "files": [ + "dist/", + "src/", + "LICENSE", + "README.md" + ], + "repository": "https://github.com/privacy-scaling-explorations/zk-kit", + "homepage": "https://github.com/privacy-scaling-explorations/zk-kit/tree/main/packages/utils", + "bugs": { + "url": "https://github.com/privacy-scaling-explorations/zk-kit.git/issues" + }, + "scripts": { + "build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript && yarn build:iife", + "build:iife": "rollup -c rollup.iife.config.ts --configPlugin typescript", + "prepublishOnly": "yarn build" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-polyfill-node": "^0.13.0", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.2" + } +} diff --git a/packages/utils/rollup.config.ts b/packages/utils/rollup.config.ts new file mode 100644 index 000000000..75b0a7bb4 --- /dev/null +++ b/packages/utils/rollup.config.ts @@ -0,0 +1,26 @@ +import fs from "fs" +import cleanup from "rollup-plugin-cleanup" +import typescript from "rollup-plugin-typescript2" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation ${new Date().getFullYear()} + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +export default { + input: "src/index.ts", + output: [ + { file: pkg.exports.require, format: "cjs", banner }, + { file: pkg.exports.import, format: "es", banner } + ], + external: [], + plugins: [ + typescript({ tsconfig: "./build.tsconfig.json", useTsconfigDeclarationDir: true }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/utils/rollup.iife.config.ts b/packages/utils/rollup.iife.config.ts new file mode 100644 index 000000000..002d7d9f5 --- /dev/null +++ b/packages/utils/rollup.iife.config.ts @@ -0,0 +1,41 @@ +import fs from "fs" +import cleanup from "rollup-plugin-cleanup" +import nodePolyfills from "rollup-plugin-polyfill-node" +import { terser } from "rollup-plugin-terser" +import typescript from "rollup-plugin-typescript2" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation ${new Date().getFullYear()} + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +const name = pkg.name.substr(1).replace(/[-/]./g, (x) => x.toUpperCase()[1]) + +export default { + input: "src/index.ts", + output: [ + { + file: pkg.iife, + name, + format: "iife", + banner + }, + { + file: pkg.unpkg, + name, + format: "iife", + plugins: [terser({ output: { preamble: banner } })] + } + ], + external: [], + plugins: [ + typescript({ tsconfig: "./build.tsconfig.json", useTsconfigDeclarationDir: true }), + nodePolyfills({ include: null }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/baby-jubjub/src/field.ts b/packages/utils/src/f1-field.ts similarity index 98% rename from packages/baby-jubjub/src/field.ts rename to packages/utils/src/f1-field.ts index 4f322b6e2..e4ef99db2 100644 --- a/packages/baby-jubjub/src/field.ts +++ b/packages/utils/src/f1-field.ts @@ -1,6 +1,6 @@ import * as scalar from "./scalar" -export default class Field { +export default class F1Field { one = BigInt(1) zero = BigInt(0) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 000000000..e5604aac3 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,7 @@ +import F1Field from "./f1-field" +import * as scalar from "./scalar" + +export { F1Field, scalar } +export * from "./types" +export * from "./number-checks" +export * from "./number-conversions" diff --git a/packages/utils/src/number-checks.ts b/packages/utils/src/number-checks.ts new file mode 100644 index 000000000..ac2e443f1 --- /dev/null +++ b/packages/utils/src/number-checks.ts @@ -0,0 +1,25 @@ +import { BigNumber, BigNumberish } from "./types" + +export function isStringifiedBigint(s: BigNumber | string): boolean { + try { + BigInt(s) + + return true + } catch (e) { + return false + } +} + +export function isHexadecimal(s: string) { + return /^(0x|0X)[0-9a-fA-F]+$/.test(s) +} + +export function isBigNumberish(value: BigNumberish): boolean { + return ( + typeof value === "number" || + typeof value === "bigint" || + (typeof value === "string" && isStringifiedBigint(value)) || + (typeof value === "string" && isHexadecimal(value)) || + Buffer.isBuffer(value) + ) +} diff --git a/packages/utils/src/number-conversions.ts b/packages/utils/src/number-conversions.ts new file mode 100644 index 000000000..dc1c1e730 --- /dev/null +++ b/packages/utils/src/number-conversions.ts @@ -0,0 +1,59 @@ +import { BigNumberish } from "./types" +import { isHexadecimal, isStringifiedBigint } from "./number-checks" + +export function bigintToHexadecimal(n: bigint) { + let hex = n.toString(16) + + // Ensure even length. + if (hex.length % 2 !== 0) { + hex = `0${hex}` + } + + return hex +} + +export function bigNumberishToBuffer(value: BigNumberish): Buffer { + if ( + typeof value === "number" || + typeof value === "bigint" || + (typeof value === "string" && isStringifiedBigint(value)) + ) { + const hex = bigintToHexadecimal(BigInt(value)) + + return Buffer.from(hex, "hex") + } + + return value as Buffer +} + +export function bufferToBigint(buffer: Buffer): bigint { + return BigInt(`0x${buffer.toString("hex")}`) +} + +export function bigNumberishToBigint(value: BigNumberish): bigint { + if ( + typeof value === "number" || + typeof value === "bigint" || + (typeof value === "string" && isStringifiedBigint(value)) || + (typeof value === "string" && isHexadecimal(value)) + ) { + return BigInt(value) + } + + return bufferToBigint(value as Buffer) +} + +export function leBufferToBigint(buffer: Buffer): bigint { + return BigInt(`0x${buffer.reverse().toString("hex")}`) +} + +export function leBigintToBuffer(n: bigint): Buffer { + const hex = bigintToHexadecimal(n) + + // Allocate buffer of the desired size, filled with zeros. + const buffer = Buffer.alloc(32, 0) + + Buffer.from(hex, "hex").reverse().copy(buffer) + + return buffer +} diff --git a/packages/baby-jubjub/src/scalar.ts b/packages/utils/src/scalar.ts similarity index 100% rename from packages/baby-jubjub/src/scalar.ts rename to packages/utils/src/scalar.ts diff --git a/packages/utils/src/types/index.ts b/packages/utils/src/types/index.ts new file mode 100644 index 000000000..327c62aad --- /dev/null +++ b/packages/utils/src/types/index.ts @@ -0,0 +1,3 @@ +export type BigNumber = bigint | string + +export type BigNumberish = BigNumber | number | Buffer diff --git a/packages/utils/tests/index.test.ts b/packages/utils/tests/index.test.ts new file mode 100644 index 000000000..73a625252 --- /dev/null +++ b/packages/utils/tests/index.test.ts @@ -0,0 +1,27 @@ +import { F1Field } from "../src" + +describe("Utils", () => { + describe("F1Field", () => { + let field: F1Field + + beforeEach(() => { + field = new F1Field(BigInt(12)) + }) + + it("Should create a finite field with a specific order", async () => { + expect(field.one).toBe(BigInt(1)) + expect(field.zero).toBe(BigInt(0)) + expect(field._order).toBe(BigInt(12)) + expect(field._half).toBe(BigInt(12) >> BigInt(1)) + expect(field._negone).toBe(BigInt(12) - BigInt(1)) + }) + + it("Should map the value back into the finite field", async () => { + const a = field.e(BigInt(24)) + const b = field.e(BigInt(-2)) + + expect(a).toBe(BigInt(0)) + expect(b).toBe(BigInt(10)) + }) + }) +}) diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 000000000..71510a096 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "tests", "rollup.config.ts"] +} diff --git a/packages/utils/typedoc.json b/packages/utils/typedoc.json new file mode 100644 index 000000000..77a471c91 --- /dev/null +++ b/packages/utils/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index 5a5b3e8fa..c273afd9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4423,12 +4423,13 @@ __metadata: languageName: node linkType: hard -"@zk-kit/baby-jubjub@workspace:packages/baby-jubjub": +"@zk-kit/baby-jubjub@0.1.0, @zk-kit/baby-jubjub@workspace:packages/baby-jubjub": version: 0.0.0-use.local resolution: "@zk-kit/baby-jubjub@workspace:packages/baby-jubjub" dependencies: "@rollup/plugin-commonjs": ^25.0.7 "@rollup/plugin-node-resolve": ^15.2.3 + "@zk-kit/utils": 0.1.0 circomlibjs: 0.0.8 rollup-plugin-cleanup: ^3.2.1 rollup-plugin-polyfill-node: ^0.13.0 @@ -4456,6 +4457,8 @@ __metadata: dependencies: "@rollup/plugin-commonjs": ^25.0.7 "@rollup/plugin-node-resolve": ^15.2.3 + "@zk-kit/baby-jubjub": 0.1.0 + "@zk-kit/utils": 0.1.0 blake-hash: 2.0.0 circomlibjs: 0.0.8 poseidon-lite: 0.2.0 @@ -4576,6 +4579,17 @@ __metadata: languageName: unknown linkType: soft +"@zk-kit/utils@0.1.0, @zk-kit/utils@workspace:packages/utils": + version: 0.0.0-use.local + resolution: "@zk-kit/utils@workspace:packages/utils" + dependencies: + rollup-plugin-cleanup: ^3.2.1 + rollup-plugin-polyfill-node: ^0.13.0 + rollup-plugin-terser: ^7.0.2 + rollup-plugin-typescript2: ^0.31.2 + languageName: unknown + linkType: soft + "JSONStream@npm:^1.0.4": version: 1.3.5 resolution: "JSONStream@npm:1.3.5"