diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index e244b2ebb..cebe9fc5d 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -35,10 +35,10 @@ jobs: run: yarn - name: Compile contracts - run: yarn compile:sol + run: yarn compile:contracts - name: Build libraries - run: yarn build:js + run: yarn build:libraries - name: Run Prettier run: yarn prettier diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 0c002c078..c6c6c2e17 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -33,10 +33,10 @@ jobs: run: yarn - name: Compile contracts - run: yarn compile:sol + run: yarn compile:contracts - name: Build libraries - run: yarn build:js + run: yarn build:libraries - name: Run Prettier run: yarn prettier diff --git a/README.md b/README.md index f99200140..48c4b0979 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,34 @@ + + + + @zk-kit/eddsa-poseidon + + + (docs) + + + + + + NPM version + + + + + + Downloads + + + + + + npm bundle size (scoped) + + + diff --git a/package.json b/package.json index 5c7f00fad..a1216580c 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "bugs": "https://github.com/privacy-scaling-explorations/zk-kit/issues", "private": true, "scripts": { - "build": "yarn build:js && yarn compile:sol", - "build:js": "yarn workspaces foreach --no-private run build", - "compile:sol": "yarn workspaces foreach run compile", + "build": "yarn build:libraries && yarn compile:contracts", + "build:libraries": "yarn workspaces foreach --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", "test:circuits": "yarn workspace @zk-kit/circuits test", diff --git a/packages/eddsa-poseidon/LICENSE b/packages/eddsa-poseidon/LICENSE new file mode 100644 index 000000000..4377091ec --- /dev/null +++ b/packages/eddsa-poseidon/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/eddsa-poseidon/README.md b/packages/eddsa-poseidon/README.md new file mode 100644 index 000000000..c18b6d8f1 --- /dev/null +++ b/packages/eddsa-poseidon/README.md @@ -0,0 +1,144 @@ +

+

+ EdDSA Poseidon +

+

A JavaScript EdDSA library for secure signing and verification using Poseidon and the Baby Jubjub elliptic curve.

+

+ +

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

+ +
+

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

+
+ +| This package offers a simplified JavaScript codebase essential for creating and validating digital signatures using EdDSA and Poseidon. It's built upon the Baby Jubjub elliptic curve, ensuring seamless integration with [Circom](https://github.com/iden3/circom) and enhancing the developer experience. | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | + +- Super lightweight: [**~33kB**](https://bundlephobia.com/package/@zk-kit/eddsa-poseidon) (minified) +- Compatible with browsers and NodeJS +- TS type support +- Comprehensive code [documentation](https://zkkit.pse.dev/modules/_zk_kit_eddsa_poseidon.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/eddsa-poseidon` package and its peer dependencies with npm: + +```bash +npm i @zk-kit/eddsa-poseidon +``` + +or yarn: + +```bash +yarn add @zk-kit/eddsa-poseidon +``` + +### CDN + +You can also load it using a `script` tag using [unpkg](https://unpkg.com/): + +```html + +``` + +or [JSDelivr](https://www.jsdelivr.com/): + +```html + +``` + +## 📜 Usage + +\# **derivePublicKey**(privateKey: _BigNumberish_): _Point\_ + +```typescript +import { derivePublicKey } from "@zk-kit/eddsa-poseidon" + +const privateKey = "secret" +const publicKey = derivePublicKey(privateKey) + +console.log(publicKey) +/* +[ + '17191193026255111087474416516591393721975640005415762645730433950079177536248', + '13751717961795090314625781035919035073474308127816403910435238282697898234143' +] +*/ +``` + +\# **signMessage**(privateKey: _BigNumberish_, message: _BigNumberish_): _Signature\_ + +```typescript +import { derivePublicKey, signMessage } from "@zk-kit/eddsa-poseidon" + +const privateKey = "secret" +const publicKey = derivePublicKey(privateKey) + +const message = "message" +const signature = signMessage(privateKey, message) + +console.log(signature) +/* +{ + R8: [ + '12949573675545142400102669657964360005184873166024880859462384824349649539693', + '18253636630408169174294927826710424418689461166073329946402765380454102840608' + ], + S: '701803947557694254685424075312408605924670918868054593580245088593184746870' +} +*/ +``` + +\# **verifySignature**(message: _BigNumberish_, signature: _Signature_, publicKey: _Point_): _boolean_ + +```typescript +import { derivePublicKey, signMessage, verifySignature } from "@zk-kit/eddsa-poseidon" + +const privateKey = "secret" +const publicKey = derivePublicKey(privateKey) + +const message = "message" +const signature = signMessage(privateKey, message) + +const response = verifySignature(message, signature, publicKey) + +console.log(response) // true +``` diff --git a/packages/eddsa-poseidon/build.tsconfig.json b/packages/eddsa-poseidon/build.tsconfig.json new file mode 100644 index 000000000..2d4a1d6da --- /dev/null +++ b/packages/eddsa-poseidon/build.tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "declarationDir": "dist/types" + }, + "include": ["src"] +} diff --git a/packages/eddsa-poseidon/package.json b/packages/eddsa-poseidon/package.json new file mode 100644 index 000000000..65ec53e8b --- /dev/null +++ b/packages/eddsa-poseidon/package.json @@ -0,0 +1,46 @@ +{ + "name": "@zk-kit/eddsa-poseidon", + "version": "0.2.0", + "description": "A JavaScript EdDSA library for secure signing and verification using Poseidon the Baby Jubjub elliptic 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/eddsa-poseidon", + "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", + "blake-hash": "2.0.0", + "circomlibjs": "0.0.8", + "poseidon-lite": "0.2.0", + "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/eddsa-poseidon/rollup.config.ts b/packages/eddsa-poseidon/rollup.config.ts new file mode 100644 index 000000000..7d2994bd6 --- /dev/null +++ b/packages/eddsa-poseidon/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/eddsa-poseidon/rollup.iife.config.ts b/packages/eddsa-poseidon/rollup.iife.config.ts new file mode 100644 index 000000000..f7936620a --- /dev/null +++ b/packages/eddsa-poseidon/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/eddsa-poseidon/src/babyjub.ts b/packages/eddsa-poseidon/src/babyjub.ts new file mode 100644 index 000000000..749fb3ee5 --- /dev/null +++ b/packages/eddsa-poseidon/src/babyjub.ts @@ -0,0 +1,96 @@ +import Field from "./field" +import * as scalar from "./scalar" +import { Point } from "./types" + +// 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) { + p[0] = BigInt(p[0]) + p[1] = BigInt(p[1]) + + const x2 = Fr.square(p[0]) + const y2 = Fr.square(p[1]) + + return Fr.eq(Fr.add(Fr.mul(a, x2), y2), Fr.add(Fr.one, Fr.mul(Fr.mul(x2, y2), d))) +} diff --git a/packages/eddsa-poseidon/src/blake.ts b/packages/eddsa-poseidon/src/blake.ts new file mode 100644 index 000000000..9b258ea8e --- /dev/null +++ b/packages/eddsa-poseidon/src/blake.ts @@ -0,0 +1,10 @@ +// @ts-ignore +import { Blake512 } from "blake-hash/lib" + +export default function hash(message: Buffer): Buffer { + const engine = new Blake512() + + engine.update(message) + + return engine.digest() +} diff --git a/packages/eddsa-poseidon/src/eddsa-poseidon.ts b/packages/eddsa-poseidon/src/eddsa-poseidon.ts new file mode 100644 index 000000000..cc85c4625 --- /dev/null +++ b/packages/eddsa-poseidon/src/eddsa-poseidon.ts @@ -0,0 +1,108 @@ +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 { BigNumberish, Point, Signature } from "./types" +import * as utils from "./utils" + +/** + * Derives a public key from a given private key using the + * {@link https://eips.ethereum.org/EIPS/eip-2494|Baby Jubjub} elliptic curve. + * This function utilizes the Baby Jubjub elliptic curve for cryptographic operations. + * The private key should be securely stored and managed, and it should never be exposed + * or transmitted in an unsecured manner. + * @param privateKey - The private key used for generating the public key. + * @returns The derived public key. + */ +export function derivePublicKey(privateKey: BigNumberish): Point { + // Convert the private key to buffer. + privateKey = utils.checkPrivateKey(privateKey) + + const hash = blake(privateKey) + + const s = utils.leBuff2int(utils.pruneBuffer(hash.slice(0, 32))) + + const publicKey = babyjub.mulPointEscalar(babyjub.Base8, scalar.shiftRight(s, BigInt(3))) + + // Convert the public key values to strings so that it can easily be exported as a JSON. + return [publicKey[0].toString(), publicKey[1].toString()] +} + +/** + * Signs a message using the provided private key, employing Poseidon hashing and + * EdDSA with the Baby Jubjub elliptic curve. + * @param privateKey - The private key used to sign the message. + * @param message - The message to be signed. + * @returns The signature object, containing properties relevant to EdDSA signatures, such as 'R8' and 'S' values. + */ +export function signMessage(privateKey: BigNumberish, message: BigNumberish): Signature { + // Convert the private key to buffer. + privateKey = utils.checkPrivateKey(privateKey) + + // Convert the message to big integer. + message = utils.checkMessage(message) + + 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 msgBuff = utils.leInt2Buff(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 R8 = babyjub.mulPointEscalar(babyjub.Base8, r) + const hm = poseidon5([R8[0], R8[1], A[0], A[1], message]) + const S = Fr.add(r, Fr.mul(hm, s)) + + // Convert the signature values to strings so that it can easily be exported as a JSON. + return { + R8: [R8[0].toString(), R8[1].toString()], + S: S.toString() + } +} + +/** + * Verifies an EdDSA signature using the Baby Jubjub elliptic curve and Poseidon hash function. + * @param message - The original message that was be signed. + * @param signature - The EdDSA signature to be verified. + * @param publicKey - The public key associated with the private key used to sign the message. + * @returns Returns true if the signature is valid and corresponds to the message and public key, false otherwise. + */ +export function verifySignature(message: BigNumberish, signature: Signature, publicKey: Point): boolean { + if ( + !utils.isPoint(publicKey) || + !utils.isSignature(signature) || + !babyjub.inCurve(signature.R8) || + !babyjub.inCurve(publicKey) || + BigInt(signature.S) >= babyjub.subOrder + ) { + return false + } + + // Convert the message to big integer. + message = utils.checkMessage(message) + + // Convert the signature values to big integers for calculations. + const _signature: Signature = { + R8: [BigInt(signature.R8[0]), BigInt(signature.R8[1])], + S: BigInt(signature.S) + } + // Convert the public key values to big integers for calculations. + const _publicKey: Point = [BigInt(publicKey[0]), BigInt(publicKey[1])] + + 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))) + + pRight = babyjub.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]) +} diff --git a/packages/eddsa-poseidon/src/field.ts b/packages/eddsa-poseidon/src/field.ts new file mode 100644 index 000000000..660bdcd74 --- /dev/null +++ b/packages/eddsa-poseidon/src/field.ts @@ -0,0 +1,59 @@ +export default class Field { + one = BigInt(1) + zero = BigInt(0) + + _order: bigint + + constructor(order: bigint) { + this._order = order + } + + e(res: bigint): bigint { + 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 + } +} diff --git a/packages/eddsa-poseidon/src/index.ts b/packages/eddsa-poseidon/src/index.ts new file mode 100644 index 000000000..1ce366761 --- /dev/null +++ b/packages/eddsa-poseidon/src/index.ts @@ -0,0 +1,2 @@ +export * from "./eddsa-poseidon" +export * from "./types" diff --git a/packages/eddsa-poseidon/src/scalar.ts b/packages/eddsa-poseidon/src/scalar.ts new file mode 100644 index 000000000..e77d7f02d --- /dev/null +++ b/packages/eddsa-poseidon/src/scalar.ts @@ -0,0 +1,15 @@ +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 +} diff --git a/packages/eddsa-poseidon/src/types/index.ts b/packages/eddsa-poseidon/src/types/index.ts new file mode 100644 index 000000000..eefb42be3 --- /dev/null +++ b/packages/eddsa-poseidon/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/eddsa-poseidon/src/utils.ts b/packages/eddsa-poseidon/src/utils.ts new file mode 100644 index 000000000..9e652ecc9 --- /dev/null +++ b/packages/eddsa-poseidon/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 +} + +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/eddsa-poseidon/tests/index.test.ts b/packages/eddsa-poseidon/tests/index.test.ts new file mode 100644 index 000000000..ccd178705 --- /dev/null +++ b/packages/eddsa-poseidon/tests/index.test.ts @@ -0,0 +1,200 @@ +import { eddsa } from "circomlibjs" +import crypto from "crypto" +import { derivePublicKey, signMessage, verifySignature } from "../src" + +describe("EdDSAPoseidon", () => { + const privateKey = "secret" + const message = BigInt(2) + + it("Should derive a public key from a private key (string)", async () => { + const publicKey = derivePublicKey(privateKey) + + const circomlibPublicKey = eddsa.prv2pub(privateKey) + + expect(publicKey[0]).toBe(circomlibPublicKey[0].toString()) + expect(publicKey[1]).toBe(circomlibPublicKey[1].toString()) + }) + + it("Should derive a public key from a private key (hexadecimal)", async () => { + const privateKey = "0x12" + + const publicKey = derivePublicKey(privateKey) + + const circomlibPublicKey = eddsa.prv2pub(Buffer.from(privateKey.slice(2), "hex")) + + expect(publicKey[0]).toBe(circomlibPublicKey[0].toString()) + expect(publicKey[1]).toBe(circomlibPublicKey[1].toString()) + }) + + it("Should derive a public key from a private key (buffer)", async () => { + const privateKey = Buffer.from("secret") + + const publicKey = derivePublicKey(privateKey) + + const circomlibPublicKey = eddsa.prv2pub(privateKey) + + expect(publicKey[0]).toBe(circomlibPublicKey[0].toString()) + expect(publicKey[1]).toBe(circomlibPublicKey[1].toString()) + }) + + it("Should derive a public key from a private key (bigint)", async () => { + const privateKey = BigInt(22) + + const publicKey = derivePublicKey(privateKey) + + const circomlibPublicKey = eddsa.prv2pub(Buffer.from(privateKey.toString(16), "hex")) + + expect(publicKey[0]).toBe(circomlibPublicKey[0].toString()) + expect(publicKey[1]).toBe(circomlibPublicKey[1].toString()) + }) + + it("Should derive a public key from a private key (number)", async () => { + const privateKey = 22 + + const publicKey = derivePublicKey(privateKey) + + const circomlibPublicKey = eddsa.prv2pub(Buffer.from(privateKey.toString(16), "hex")) + + expect(publicKey[0]).toBe(circomlibPublicKey[0].toString()) + expect(publicKey[1]).toBe(circomlibPublicKey[1].toString()) + }) + + it("Should throw an error if the secret type is not supported", async () => { + const privateKey = true + + const fun = () => derivePublicKey(privateKey as any) + + expect(fun).toThrow("Invalid private key type.") + }) + + it("Should sign a message (bigint)", async () => { + const signature = signMessage(privateKey, message) + + const circomlibSignature = eddsa.signPoseidon(privateKey, message) + + expect(signature.R8[0]).toBe(circomlibSignature.R8[0].toString()) + expect(signature.R8[1]).toBe(circomlibSignature.R8[1].toString()) + expect(signature.S).toBe(circomlibSignature.S.toString()) + }) + + it("Should sign a message (number)", async () => { + const message = 22 + + const signature = signMessage(privateKey, message) + + const circomlibSignature = eddsa.signPoseidon(privateKey, BigInt(message)) + + expect(signature.R8[0]).toBe(circomlibSignature.R8[0].toString()) + expect(signature.R8[1]).toBe(circomlibSignature.R8[1].toString()) + expect(signature.S).toBe(circomlibSignature.S.toString()) + }) + + it("Should sign a message (hexadecimal)", async () => { + const message = "0x12" + + const signature = signMessage(privateKey, message) + + const circomlibSignature = eddsa.signPoseidon(privateKey, BigInt(message)) + + expect(signature.R8[0]).toBe(circomlibSignature.R8[0].toString()) + expect(signature.R8[1]).toBe(circomlibSignature.R8[1].toString()) + expect(signature.S).toBe(circomlibSignature.S.toString()) + }) + + it("Should sign a message (buffer)", async () => { + const message = Buffer.from("message") + + const signature = signMessage(privateKey, message) + + const circomlibSignature = eddsa.signPoseidon(privateKey, BigInt(`0x${message.toString("hex")}`)) + + expect(signature.R8[0]).toBe(circomlibSignature.R8[0].toString()) + expect(signature.R8[1]).toBe(circomlibSignature.R8[1].toString()) + expect(signature.S).toBe(circomlibSignature.S.toString()) + }) + + it("Should sign a message (string)", async () => { + const message = "message" + + const signature = signMessage(privateKey, message) + + const circomlibSignature = eddsa.signPoseidon(privateKey, BigInt(`0x${Buffer.from(message).toString("hex")}`)) + + expect(signature.R8[0]).toBe(circomlibSignature.R8[0].toString()) + expect(signature.R8[1]).toBe(circomlibSignature.R8[1].toString()) + expect(signature.S).toBe(circomlibSignature.S.toString()) + }) + + it("Should throw an error if the message type is not supported", async () => { + const message = true + + const fun = () => signMessage(privateKey, message as any) + + expect(fun).toThrow("Invalid message type.") + }) + + it("Should verify a signature", async () => { + const publicKey = derivePublicKey(privateKey) + const signature = signMessage(privateKey, message) + + expect(verifySignature(message, signature, publicKey)).toBeTruthy() + }) + + it("Should not verify a signature if the public key is malformed", async () => { + const publicKey = derivePublicKey(privateKey) + const signature = signMessage(privateKey, message) + + publicKey[1] = 3 as any + + expect(verifySignature(message, signature, publicKey)).toBeFalsy() + }) + + it("Should not verify a signature if the signature is malformed", async () => { + const publicKey = derivePublicKey(privateKey) + const signature = signMessage(privateKey, message) + + signature.S = 3 as any + + expect(verifySignature(message, signature, publicKey)).toBeFalsy() + }) + + it("Should not verify a signature if the signature is not on the curve", async () => { + const publicKey = derivePublicKey(privateKey) + const signature = signMessage(privateKey, message) + + signature.R8[1] = BigInt(3).toString() + + expect(verifySignature(message, signature, publicKey)).toBeFalsy() + }) + + it("Should not verify a signature if the public key is not on the curve", async () => { + const publicKey = derivePublicKey(privateKey) + const signature = signMessage(privateKey, message) + + publicKey[1] = BigInt(3).toString() + + expect(verifySignature(message, signature, publicKey)).toBeFalsy() + }) + + it("Should not verify a signature S value exceeds the predefined sub order", async () => { + const publicKey = derivePublicKey(privateKey) + const signature = signMessage(privateKey, message) + + signature.S = "3421888242871839275222246405745257275088614511777268538073601725287587578984328" + + expect(verifySignature(message, signature, publicKey)).toBeFalsy() + }) + + it("Should derive a public key from N random private keys", async () => { + for (let i = 0, len = 10; i < len; i += 1) { + const privateKey = crypto.randomBytes(32) + + const publicKey = derivePublicKey(privateKey) + + const circomlibPublicKey = eddsa.prv2pub(privateKey) + + expect(publicKey[0]).toBe(circomlibPublicKey[0].toString()) + expect(publicKey[1]).toBe(circomlibPublicKey[1].toString()) + } + }) +}) diff --git a/packages/eddsa-poseidon/tsconfig.json b/packages/eddsa-poseidon/tsconfig.json new file mode 100644 index 000000000..81e592a16 --- /dev/null +++ b/packages/eddsa-poseidon/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "tests", "rollup.config.ts", "rollup.iife.config.ts"] +} diff --git a/packages/eddsa-poseidon/typedoc.json b/packages/eddsa-poseidon/typedoc.json new file mode 100644 index 000000000..77a471c91 --- /dev/null +++ b/packages/eddsa-poseidon/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index 614d0c04c..710c2ee35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2779,7 +2779,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 @@ -3305,6 +3305,41 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-commonjs@npm:^25.0.7": + version: 25.0.7 + resolution: "@rollup/plugin-commonjs@npm:25.0.7" + dependencies: + "@rollup/pluginutils": ^5.0.1 + commondir: ^1.0.1 + estree-walker: ^2.0.2 + glob: ^8.0.3 + is-reference: 1.2.1 + magic-string: ^0.30.3 + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 052e11839a9edc556eda5dcc759ab816dcc57e9f0f905a1e6e14fff954eaa6b1e2d0d544f5bd18d863993c5eba43d8ac9c19d9bb53b1c3b1213f32cfc9d50b2e + languageName: node + linkType: hard + +"@rollup/plugin-inject@npm:^5.0.4": + version: 5.0.5 + resolution: "@rollup/plugin-inject@npm:5.0.5" + dependencies: + "@rollup/pluginutils": ^5.0.1 + estree-walker: ^2.0.2 + magic-string: ^0.30.3 + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 22cb772fd6f7178308b2ece95cdde5f8615f6257197832166294552a7e4c0d3976dc996cbfa6470af3151d8b86c00091aa93da5f4db6ec563f11b6db29fd1b63 + languageName: node + linkType: hard + "@rollup/plugin-json@npm:^5.0.1": version: 5.0.2 resolution: "@rollup/plugin-json@npm:5.0.2" @@ -3319,7 +3354,7 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-node-resolve@npm:^15.0.2": +"@rollup/plugin-node-resolve@npm:^15.0.2, @rollup/plugin-node-resolve@npm:^15.2.3": version: 15.2.3 resolution: "@rollup/plugin-node-resolve@npm:15.2.3" dependencies: @@ -4401,6 +4436,22 @@ __metadata: languageName: unknown linkType: soft +"@zk-kit/eddsa-poseidon@workspace:packages/eddsa-poseidon": + version: 0.0.0-use.local + resolution: "@zk-kit/eddsa-poseidon@workspace:packages/eddsa-poseidon" + dependencies: + "@rollup/plugin-commonjs": ^25.0.7 + "@rollup/plugin-node-resolve": ^15.2.3 + blake-hash: 2.0.0 + circomlibjs: 0.0.8 + poseidon-lite: 0.2.0 + 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/groth16@0.4.0, @zk-kit/groth16@workspace:packages/groth16": version: 0.0.0-use.local resolution: "@zk-kit/groth16@workspace:packages/groth16" @@ -5493,6 +5544,18 @@ __metadata: languageName: node linkType: hard +"blake-hash@npm:2.0.0, blake-hash@npm:^2.0.0": + version: 2.0.0 + resolution: "blake-hash@npm:2.0.0" + dependencies: + node-addon-api: ^3.0.0 + node-gyp: latest + node-gyp-build: ^4.2.2 + readable-stream: ^3.6.0 + checksum: a0d9a8f3953b986d3b30a741a6c000dedcc9a03b1318f52cc01ae62d18829ba6cb1a4d8cbe74785abfdc952a21db410984523bd457764aca716162cfd3ca8ea4 + languageName: node + linkType: hard + "blake-hash@npm:^1.1.0": version: 1.1.1 resolution: "blake-hash@npm:1.1.1" @@ -5505,18 +5568,6 @@ __metadata: languageName: node linkType: hard -"blake-hash@npm:^2.0.0": - version: 2.0.0 - resolution: "blake-hash@npm:2.0.0" - dependencies: - node-addon-api: ^3.0.0 - node-gyp: latest - node-gyp-build: ^4.2.2 - readable-stream: ^3.6.0 - checksum: a0d9a8f3953b986d3b30a741a6c000dedcc9a03b1318f52cc01ae62d18829ba6cb1a4d8cbe74785abfdc952a21db410984523bd457764aca716162cfd3ca8ea4 - languageName: node - linkType: hard - "blake2b-wasm@git+https://github.com/jbaylina/blake2b-wasm.git": version: 2.1.0 resolution: "blake2b-wasm@https://github.com/jbaylina/blake2b-wasm.git#commit=0d5f024b212429c7f50a7f533aa3a2406b5b42b3" @@ -6450,7 +6501,7 @@ __metadata: languageName: node linkType: hard -"circomlibjs@npm:^0.0.8": +"circomlibjs@npm:0.0.8, circomlibjs@npm:^0.0.8": version: 0.0.8 resolution: "circomlibjs@npm:0.0.8" dependencies: @@ -13458,6 +13509,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.3": + version: 0.30.5 + resolution: "magic-string@npm:0.30.5" + dependencies: + "@jridgewell/sourcemap-codec": ^1.4.15 + checksum: da10fecff0c0a7d3faf756913ce62bd6d5e7b0402be48c3b27bfd651b90e29677e279069a63b764bcdc1b8ecdcdb898f29a5c5ec510f2323e8d62ee057a6eb18 + languageName: node + linkType: hard + "make-dir@npm:^1.0.0": version: 1.3.0 resolution: "make-dir@npm:1.3.0" @@ -15193,7 +15253,7 @@ __metadata: languageName: node linkType: hard -"poseidon-lite@npm:^0.2.0": +"poseidon-lite@npm:0.2.0, poseidon-lite@npm:^0.2.0": version: 0.2.0 resolution: "poseidon-lite@npm:0.2.0" checksum: c47c6fd0a29a78ca1f7cf6ccb8b0c4f4e72930d944e63425e36f60c15d37fb0aeca30b8a22a30640ed68d631142282c0b8308da83b1a2b2bb92b87f5a2432c93 @@ -16091,6 +16151,17 @@ __metadata: languageName: node linkType: hard +"rollup-plugin-polyfill-node@npm:^0.13.0": + version: 0.13.0 + resolution: "rollup-plugin-polyfill-node@npm:0.13.0" + dependencies: + "@rollup/plugin-inject": ^5.0.4 + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + checksum: 73c5b9086955afa108c940c13205fab4cece149d020a3faa696c5711bbb391d11aecd4c913ad2cc5ac24f9d43a4969ad8d087d085dd8d423dece45b6be4039bb + languageName: node + linkType: hard + "rollup-plugin-terser@npm:^7.0.2": version: 7.0.2 resolution: "rollup-plugin-terser@npm:7.0.2"