UNPKG

micro-zk-proofs

Version:

Create & verify zero-knowledge SNARK proofs in parallel, using noble cryptography

138 lines (131 loc) 5.1 kB
/** * Pedersen Hash over babyjubjub elliptic curve, defined in * {@link https://eips.ethereum.org/EIPS/eip-2494 | EIP-2494}. * jubjub - edwards over bls12-381 scalar * babyjubjub - edwards over bn254 scalar * Using scalar as field allows to be used inside of zk-circuits. * @module */ import { type EdwardsPoint as ExtPointType } from '@noble/curves/abstract/edwards.js'; import { asciiToBytes } from '@noble/curves/utils.js'; import { babyjubjub } from '@noble/curves/misc.js'; import { blake256 } from '@noble/hashes/blake1.js'; import type { TArg, TRet } from '@noble/hashes/utils.js'; const Fp = babyjubjub.Point.Fp; const _0n = /* @__PURE__ */ BigInt(0); const _1n = /* @__PURE__ */ BigInt(1); type EdwardsPoint = typeof babyjubjub.Point.BASE; type PointCodec = { encode: (p: any) => Uint8Array; decode: (bytes: Uint8Array) => ExtPointType; }; // Seems like twistedEdwards fromBytes/toBytes, but with 'x > Fr.ORDER >> 1n' instead of oddity? // NOTE: we need to be as close as possible to original, otherwise hashes will change! /** * Pedersen point encoder/decoder for babyjubjub points. * @example * Encode a babyjubjub point to bytes and decode it back. * ```ts * const { babyjubjub } = await import('@noble/curves/misc.js'); * const point = babyjubjub.Point.BASE; * const encoded = Point.encode(point); * Point.decode(encoded); * ``` */ export const Point: TRet<PointCodec> = Object.freeze({ encode: (p: any): TRet<Uint8Array> => { const { x, y } = p.toAffine(); const bytes = Fp.toBytes(y); // Check highest bit instead of lowest in other twisted edwards if (x > Fp.ORDER >> _1n) bytes[31] |= 0x80; return bytes as TRet<Uint8Array>; }, // NOTE: decode doesn't check oddity of x before negate, which means this heavily depends on // formula and sqrt implementation. Other implementations may return different root first. // However it uses exactly same tonneli shanks as @noble/curves, but selects lower root // This is very fragile, but probably since used for hashes only decode: (bytes: TArg<Uint8Array>): TRet<ExtPointType> => { const sign = !!(bytes[31] & 0x80); // Clone before clearing the sign bit; callers may pass Buffer, whose slice aliases memory. bytes = Uint8Array.from(bytes); bytes[31] &= 0x7f; // clean sign bit const y = Fp.fromBytes(bytes); const y2 = Fp.sqr(y); let x = Fp.sqrt( Fp.div( Fp.sub(Fp.ONE, y2), Fp.sub(babyjubjub.Point.CURVE().a, Fp.mul(babyjubjub.Point.CURVE().d, y2)) ) ); // This forces lowest root (instead of isOdd in twisted edwards) if (x > Fp.ORDER >> _1n) x = Fp.neg(x); if (sign) x = Fp.neg(x); return babyjubjub.Point.fromAffine({ x, y }) as TRet<ExtPointType>; }, }) as unknown as TRet<PointCodec>; // We cannot do nice precomputes here since input can be unlimited in size let POINT_CACHE: EdwardsPoint[] = []; function basePoint(idx: number) { // pedersenHash() requests generators in ascending index order, // so sparse cache holes are not observed here. if (idx < POINT_CACHE.length) return POINT_CACHE[idx]; let p = undefined; for (let i = 0; !p; i++) { const s = `PedersenGenerator_${('' + idx).padStart(32, '0')}_${('' + i).padStart(32, '0')}`; const h = blake256(asciiToBytes(s)); h[31] = h[31] & 0b1011_1111; // clear 255 bit try { p = Point.decode(h); } catch {} } p = p.clearCofactor(); p.assertValidity(); POINT_CACHE[idx] = p; return p; } function getScalars(msg: TArg<Uint8Array>) { // noble-curves now exposes BabyJubJub's subgroup base and subgroup order directly. const SUBORDER = babyjubjub.Point.Fn.ORDER; const res: bigint[] = []; // Very fragile wNAF (4-bit) like structure to avoid zero points const window = (n: number) => { const sign = !!(n & 0b1000); // highest bit is sign n = (n & 0b0111) + 1; return BigInt(sign ? -n : n); }; // Process in chunks up to 25 bytes // 25 bytes -> 50 signed base-32 digits, which keeps each chunk below the subgroup order. const blockLen = 25; for (let pos = 0; pos < msg.length; pos += blockLen) { const cur = msg.subarray(pos, pos + blockLen); let scalar = _0n; let shift = _1n; for (const b of cur) { // NOTE: we need to use multiplication here, because of negative values scalar += window(b & 0xf) * shift; shift <<= BigInt(5); scalar += window((b >>> 4) & 0xf) * shift; shift <<= BigInt(5); } if (scalar < _0n) scalar = SUBORDER + scalar; res.push(scalar); } return res; } /** * Computes the Pedersen hash for the input bytes. * @param msg - Message bytes to hash. * @returns Encoded babyjubjub point bytes. * @example * Hash message bytes into an encoded babyjubjub point. * ```ts * const digest = pedersenHash(new Uint8Array([1, 2, 3])); * ``` */ export function pedersenHash(msg: TArg<Uint8Array>): TRet<Uint8Array> { const p = getScalars(msg).reduce( (acc, i, j) => acc.add(basePoint(j).multiply(i)), babyjubjub.Point.ZERO ); return Point.encode(p) as TRet<Uint8Array>; }