UNPKG

as-pedersen

Version:

A pedersen hash implementation in AssemblyScript

447 lines (402 loc) 13.9 kB
import { BigInt } from './bigint'; // Convert a string representation of a number to a BigInt object function BN(n: string): BigInt { return BigInt.fromString(n); } // https://docs.starkware.co/starkex/stark-curve.html // Params: a, b export const CURVE_A = BN('1'); export const CURVE_B = BN( '3141592653589793238462643383279502884197169399375105820974944592307816406665' ); // Field over which we'll do calculations. Verify with: // NOTE: there is no efficient sqrt for field (P%4==1) export const CURVE_P = BN('2') .pow(251) .add(BN('17').mul(BN('2').pow(192))) .add(BN('1')); // Field over which we'll do calculations. Verify with: // NOTE: there is no efficient sqrt for field (P%4==1) // P: BN('2').pow(251).add(BN('17').mul(BN('2').pow(192)).add(BN('1'))), // Curve order, total count of valid points in the field. Verify with: export const CURVE_N = BN( '3618502788666131213697322783095070105526743751716087489154079457884512865583' ); export const CURVE_N_BITS = 252; // len(bin(N).replace('0b','')) // Base point (x, y) aka generator point export const CURVE_GX = BN( '874739451078007766457464989774322083649278607533249481151382481072868806602' ); export const CURVE_GY = BN( '152666792071518830868575557812948353041420400780739481342941381225525861407' ); /** * Jacobian Point works in 3d / jacobi coordinates: (x, y, z) ∋ (x=x/z², y=y/z³) * Default Point works in 2d / affine coordinates: (x, y) * We're doing calculations in jacobi, because its operations don't require costly inversion. */ class JacobianPoint { constructor(readonly x: BigInt, readonly y: BigInt, readonly z: BigInt) {} static readonly BASE: JacobianPoint = new JacobianPoint( CURVE_GX, CURVE_GY, BN('1') ); static readonly ZERO: JacobianPoint = new JacobianPoint( BN('0'), BN('1'), BN('0') ); static fromAffine(p: Point): JacobianPoint { if (!(p instanceof Point)) { throw new TypeError('JacobianPoint#fromAffine: expected Point'); } // fromAffine(x:0, y:0) would produce (x:0, y:0, z:1), but we need (x:0, y:1, z:0) if (p.equals(Point.ZERO)) return JacobianPoint.ZERO; return new JacobianPoint(p.x, p.y, BN('1')); } /** * Compare one point to another. */ equals(other: JacobianPoint): boolean { if (!(other instanceof JacobianPoint)) throw new TypeError('JacobianPoint expected'); const X1 = this.x; const Y1 = this.y; const Z1 = this.z; const X2 = other.x; const Y2 = other.y; const Z2 = other.z; const Z1Z1 = mod(Z1.mul(Z1)); const Z2Z2 = mod(Z2.mul(Z2)); const U1 = mod(X1.mul(Z2Z2)); const U2 = mod(X2.mul(Z1Z1)); const S1 = mod(mod(Y1.mul(Z2)).mul(Z2Z2)); const S2 = mod(mod(Y2.mul(Z1)).mul(Z1Z1)); return U1.eq(U2) && S1.eq(S2); } /** * Flips point to one corresponding to (x, -y) in Affine coordinates. */ negate(): JacobianPoint { return new JacobianPoint(this.x, mod(this.y.opposite()), this.z); } // Fast algo for doubling 2 Jacobian Points. // From: http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl // Cost: 1M + 8S + 1*a + 10add + 2*2 + 1*3 + 1*8.. double(): JacobianPoint { const X1 = this.x; const Y1 = this.y; const Z1 = this.z; const XX = mod(X1.mul(X1)); // XX = X1^2 const YY = mod(Y1.mul(Y1)); // YY = Y1^2 const YYYY = mod(YY.mul(YY)); // YYYY = YY^2 const ZZ = mod(Z1.mul(Z1)); // ZZ = Z1^2 const tmp1 = mod(X1.add(YY)); // (X1+YY) const tmp2 = mod(tmp1.mul(tmp1)); // (X1+YY)^2 const S = mod(BN('2').mul(tmp2.sub(XX).sub(YYYY))); // 2*((X1+YY)^2-XX-YYYY) const ZZZZ = mod(ZZ.mul(ZZ)); // ZZ^2 const M = mod(BN('3').mul(XX).add(CURVE_A.mul(ZZZZ))); // 3*XX+a*ZZ^2 const MM = mod(M.mul(M)); // M^2 const T = mod(MM.sub(BN('2').mul(S))); // M^2-2*S const X3 = T; const Y3 = mod(M.mul(S.sub(T)).sub(BN('8').mul(YYYY))); // M*(S-T)-8*YYYY const Y1Z1 = mod(Y1.add(Z1)); // (Y1+Z1) const tmp3 = mod(Y1Z1.mul(Y1Z1)); // (Y1+Z1)^2 const Z3 = mod(tmp3.sub(YY).sub(ZZ)); // (Y1+Z1)^2-YY-ZZ return new JacobianPoint(X3, Y3, Z3); } // Fast algo for adding 2 Jacobian Points. // http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2 // Cost: 12M + 4S + 6add + 1*2 // Note: 2007 Bernstein-Lange (11M + 5S + 9add + 4*2) is actually 10% slower. add(other: JacobianPoint): JacobianPoint { if (this.equals(JacobianPoint.ZERO)) return other; if (!(other instanceof JacobianPoint)) throw new TypeError('JacobianPoint expected'); const X1 = this.x; const Y1 = this.y; const Z1 = this.z; const X2 = other.x; const Y2 = other.y; const Z2 = other.z; if (X2.eq(BN('0')) || Y2.eq(BN('0'))) return this; if (X1.eq(BN('0')) || Y1.eq(BN('0'))) return other; // We're using same code in equals() const Z1Z1 = mod(Z1.mul(Z1)); // Z1Z1 = Z1^2 const Z2Z2 = mod(Z2.mul(Z2)); // Z2Z2 = Z2^2; const U1 = mod(X1.mul(Z2Z2)); // X1 * Z2Z2 const U2 = mod(X2.mul(Z1Z1)); // X2 * Z1Z1 const S1 = mod(mod(Y1.mul(Z2)).mul(Z2Z2)); // Y1 * Z2 * Z2Z2 const S2 = mod(mod(Y2.mul(Z1)).mul(Z1Z1)); // Y2 * Z1 * Z1Z1 const H = mod(U2.sub(U1)); // H = U2 - U1 const r = mod(S2.sub(S1)); // S2 - S1 // H = 0 meaning it's the same point. if (H.eq(BN('0'))) { if (r.eq(BN('0'))) { return this.double(); } else { return JacobianPoint.ZERO; } } const HH = mod(H.mul(H)); // HH = H2 const HHH = mod(H.mul(HH)); // HHH = H * HH const V = mod(U1.mul(HH)); // V = U1 * HH const X3 = mod(r.mul(r).sub(HHH).sub(BN('2').mul(V))); // X3 = r^2 - HHH - 2 * V; const Y3 = mod(r.mul(V.sub(X3)).sub(S1.mul(HHH))); // Y3 = r * (V - X3) - S1 * HHH; const Z3 = mod(Z1.mul(Z2).mul(H)); // Z3 = Z1 * Z2 * H; return new JacobianPoint(X3, Y3, Z3); } subtract(other: JacobianPoint): JacobianPoint { return this.add(other.negate()); } // Converts Jacobian point to affine (x, y) coordinates. // Can accept precomputed Z^-1 - for example, from invertBatch. // (x, y, z) ∋ (x=x/z², y=y/z³) // http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#scaling-z toAffine(): Point { const is0 = this.equals(JacobianPoint.ZERO); const invZ = is0 ? BN('8') : invert(this.z, CURVE_P); // 8 was chosen arbitrarily const iz1 = invZ; // A const iz2 = mod(iz1.mul(iz1)); // AA = A^2 const iz3 = mod(iz2.mul(iz1)); // AAA = A^2 * A = A^3 const ax = mod(this.x.mul(iz2)); // X3 = X1*AA const ay = mod(this.y.mul(iz3)); // Y3 = Y1*AA*A const zz = mod(this.z.mul(iz1)); if (is0) return Point.ZERO; if (!zz.eq(BN('1'))) throw new Error('invZ was invalid'); return new Point(ax, ay); } } /** * Default Point works in default aka affine coordinates: (x, y) */ class Point { /** * Base point aka generator. public_key = Point.BASE * private_key */ static BASE: Point = new Point(CURVE_GX, CURVE_GY); /** * Identity point aka point at infinity. point = point + zero_point */ static ZERO: Point = new Point(BN('0'), BN('0')); constructor(readonly x: BigInt, readonly y: BigInt) {} toHex(): string { return `04${numTo32bStr(this.x)}${numTo32bStr(this.y)}`; } toHexX(): string { return numTo32bStr(this.x); } toRawX(): Uint8Array { return hexToBytes(this.toHexX()); } equals(other: Point): boolean { return this.x.eq(other.x) && this.y.eq(other.y); } // Returns the same point with inverted `y` negate(): Point { return new Point(this.x, mod(this.y.opposite())); } // Adds point to itself double(): Point { return JacobianPoint.fromAffine(this).double().toAffine(); } // Adds point to other point add(other: Point): Point { return JacobianPoint.fromAffine(this) .add(JacobianPoint.fromAffine(other)) .toAffine(); } // Subtracts other point from the point subtract(other: Point): Point { return this.add(other.negate()); } } // Convert between types // --------------------- const POW_2_256 = BN( '0x10000000000000000000000000000000000000000000000000000000000000000' ); function numTo32bStr(num: BigInt): string { if (!(num instanceof BigInt)) throw new Error('Expected BigInt'); if (!(BN('0').lte(num) && num.lt(POW_2_256))) throw new Error('Expected number < 2^256'); return num.toString(16).padStart(64, '0'); } function strip0x(hex: string): string { return hex.replace('0x', ''); } // Caching slows it down 2-3x function hexToBytes(hex: string): Uint8Array { // Stakware has eth-like hexes hex = strip0x(hex); if (hex.length & 1) hex = `0${hex}`; // padding if (typeof hex !== 'string') { throw new TypeError('hexToBytes: expected string, got ' + typeof hex); } if (hex.length % 2) throw new Error(`hexToBytes: received invalid unpadded hex ${hex.length}`); const array = new Uint8Array(hex.length / 2); for (let i = 0; i < array.length; i++) { const j = i * 2; const hexByte = hex.slice(j, j + 2); const byte = Number.parseInt(hexByte, 16); if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence'); array[i] = <u32>byte; } return array; } const hexes = new Array<number>(256).map<string>((_: number, i: i32) => i.toString(16).padStart(2, '0') ); function bytesToHex(uint8a: Uint8Array): string { if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); // pre-caching improves the speed 6x let hex = ''; for (let i = 0; i < uint8a.length; i++) { hex += hexes[uint8a[i]]; } return hex; } // Regex is not supported const stripLeadingZeros = (s: string): string => { return BigInt.fromString(s, 16).toString(16); }; const bytesToHexEth = (uint8a: Uint8Array): string => `0x${stripLeadingZeros(bytesToHex(uint8a))}`; // ------------------------- // Calculates a modulo b function mod(a: BigInt, b: BigInt = CURVE_P): BigInt { const result = a.mod(b); return result.gte(BN('0')) ? result : b.add(result); } // Inverses number over modulo function invert(number: BigInt, modulo: BigInt): BigInt { if (number.eq(BN('0')) || modulo.lte(BN('0'))) { throw new Error( `invert: expected positive integers, got n=${number} mod=${modulo}` ); } // Eucledian GCD https://brilliant.org/wiki/extended-euclidean-algorithm/ let a = mod(number, modulo); let b = modulo; // prettier-ignore let x = BN('0'), y = BN('1'), u = BN('1'), v = BN('0'); while (!a.eq(BN('0'))) { const q = b.div(a); const r = b.mod(a); const m = x.sub(u.mul(q)); const n = y.sub(v.mul(q)); (b = a), (a = r), (x = u), (y = v), (u = m), (v = n); } const gcd = b; if (!gcd.eq(BN('1'))) throw new Error('invert: does not exist'); return mod(x, modulo); } // https://docs.starkware.co/starkex/pedersen-hash-function.html const PEDERSEN_POINTS: Point[] = [ new Point( BN( '2089986280348253421170679821480865132823066470938446095505822317253594081284' ), BN( '1713931329540660377023406109199410414810705867260802078187082345529207694986' ) ), new Point( BN( '996781205833008774514500082376783249102396023663454813447423147977397232763' ), BN( '1668503676786377725805489344771023921079126552019160156920634619255970485781' ) ), new Point( BN( '2251563274489750535117886426533222435294046428347329203627021249169616184184' ), BN( '1798716007562728905295480679789526322175868328062420237419143593021674992973' ) ), new Point( BN( '2138414695194151160943305727036575959195309218611738193261179310511854807447' ), BN( '113410276730064486255102093846540133784865286929052426931474106396135072156' ) ), new Point( BN( '2379962749567351885752724891227938183011949129833673362440656643086021394946' ), BN( '776496453633298175483985398648758586525933812536653089401905292063708816422' ) ), ]; const PEDERSEN_POINTS_JACOBIAN: JacobianPoint[] = PEDERSEN_POINTS.map<JacobianPoint>((p: Point) => JacobianPoint.fromAffine(p)); function pedersenPrecompute( p1: JacobianPoint, p2: JacobianPoint ): JacobianPoint[] { const out: JacobianPoint[] = []; let p = p1; for (let i = 0; i < 248; i++) { out.push(p); p = p.double(); } p = p2; for (let i = 0; i < 4; i++) { out.push(p); p = p.double(); } return out; } const PEDERSEN_POINTS1: JacobianPoint[] = pedersenPrecompute( PEDERSEN_POINTS_JACOBIAN[1], PEDERSEN_POINTS_JACOBIAN[2] ); const PEDERSEN_POINTS2: JacobianPoint[] = pedersenPrecompute( PEDERSEN_POINTS_JACOBIAN[3], PEDERSEN_POINTS_JACOBIAN[4] ); function pedersenArg(value: BigInt): BigInt { // [0..Fp) if (BN('0').gt(value) || value.gte(CURVE_P)) { throw new Error(`value should be 0<=ARG<CURVE.P: ${value}`); } return value; } function pedersenSingle( point: JacobianPoint, value: BigInt, constants: JacobianPoint[] ): JacobianPoint { let x = pedersenArg(value); for (let j = 0; j < 252; j++) { const pt = constants[j]; if (pt.x.eq(point.x)) throw new Error('Same point'); // Hacky fix for bigint comparision issue if (BigInt.from(x.bitwiseAnd(BN('1')).toString()).ne(BN('0'))) { point = point.add(pt); } x = x.rightShift(1); } return point; } // shift_point + x_low * P_0 + x_high * P1 + y_low * P2 + y_high * P3 export function pedersen(x: string, y: string): string { let point = PEDERSEN_POINTS_JACOBIAN[0]; point = pedersenSingle(point, BN(x), PEDERSEN_POINTS1); point = pedersenSingle(point, BN(y), PEDERSEN_POINTS2); return bytesToHexEth(point.toAffine().toRawX()); } export function computeHashOnElements(data: string[]): string { return data.concat([data.length.toString()]).reduce((x, y) => { return pedersen(x, y); }, '0'); }