mirror of
https://github.com/Discreetly/server.git
synced 2026-01-10 13:27:58 -05:00
jubmoji verifier might work now
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"discreetly-claimcodes": "^1.1.5",
|
||||
"discreetly-interfaces": "^0.1.42",
|
||||
"dotenv": "^16.3.1",
|
||||
"elliptic": "^6.5.4",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"express": "^4.18.2",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
@@ -33,7 +34,8 @@
|
||||
"poseidon-lite": "^0.2.0",
|
||||
"rlnjs": "^3.2.0",
|
||||
"snarkjs": "^0.7.1",
|
||||
"socket.io": "^4.6.2"
|
||||
"socket.io": "^4.6.2",
|
||||
"uuidv4": "^6.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.6.2",
|
||||
@@ -2392,6 +2394,11 @@
|
||||
"@types/superagent": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
@@ -8637,6 +8644,23 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uuidv4": {
|
||||
"version": "6.2.13",
|
||||
"resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz",
|
||||
"integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==",
|
||||
"dependencies": {
|
||||
"@types/uuid": "8.3.4",
|
||||
"uuid": "8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"discreetly-claimcodes": "^1.1.5",
|
||||
"discreetly-interfaces": "^0.1.42",
|
||||
"dotenv": "^16.3.1",
|
||||
"elliptic": "^6.5.4",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"express": "^4.18.2",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
@@ -47,7 +48,8 @@
|
||||
"poseidon-lite": "^0.2.0",
|
||||
"rlnjs": "^3.2.0",
|
||||
"snarkjs": "^0.7.1",
|
||||
"socket.io": "^4.6.2"
|
||||
"socket.io": "^4.6.2",
|
||||
"uuidv4": "^6.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.6.2",
|
||||
|
||||
52
src/endpoints/gateways/jubmojis.ts
Normal file
52
src/endpoints/gateways/jubmojis.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import express from 'express';
|
||||
import type { Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { limiter } from '../middleware';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { addIdentityToIdentityListRooms } from '../../data/db';
|
||||
import { RoomI } from 'discreetly-interfaces';
|
||||
import { jubmojiVerifier } from '../../gateways/jubmojis/jubmoji';
|
||||
import { JubmojiRequestI } from '../../gateways/jubmojis/jubmoji.types';
|
||||
|
||||
const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* This code is the API route used to verify the proof submitted by the user.
|
||||
* It uses the SNARKProof type and idc from the request body to verify the proof. If it is valid,
|
||||
* it adds the identity to the room and returns the roomId. If it is invalid, it returns an error.
|
||||
* @param {SNARKProof} proof - The SNARKProof object from the user
|
||||
* @param {string} idc - The identity commitment of the user
|
||||
* @returns {void}
|
||||
*/
|
||||
router.post(
|
||||
'/join',
|
||||
limiter,
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { proof, idc } = req.body as { proof: JubmojiRequestI; idc: string };
|
||||
const isValid = await jubmojiVerifier(proof);
|
||||
if (isValid) {
|
||||
const room = (await prisma.rooms.findUnique({
|
||||
where: {
|
||||
roomId: process.env.JUBMOJI_ROOM_ID ? process.env.JUBMOJI_ROOM_ID : '10212131510919'
|
||||
}
|
||||
})) as RoomI;
|
||||
const addedRoom = await addIdentityToIdentityListRooms([room], idc);
|
||||
if (addedRoom.length === 0) {
|
||||
res.status(500).json({
|
||||
status: 'already-added',
|
||||
roomIds: []
|
||||
});
|
||||
} else {
|
||||
res.status(200).json({
|
||||
status: 'valid',
|
||||
roomIds: [room.roomId]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.status(500).json({ status: 'Invalid Proof' });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
181
src/gateways/jubmojis/babyJubjub.ts
Normal file
181
src/gateways/jubmojis/babyJubjub.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import elliptic from 'elliptic';
|
||||
import { ZqField } from 'ffjavascript';
|
||||
import * as hash from 'hash.js';
|
||||
import { BabyJubJub } from './jubmoji.types';
|
||||
import { bigIntToHex, hexToBigInt } from './utils';
|
||||
|
||||
// Define short Weierstrass parameters
|
||||
const curveOptions = {
|
||||
p: '30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001',
|
||||
prime: null,
|
||||
a: '10216f7ba065e00de81ac1e7808072c9b8114d6d7de87adb16a0a72f1a91f6a0',
|
||||
b: '23d885f647fed5743cad3d1ee4aba9c043b4ac0fc2766658a410efdeb21f706e',
|
||||
g: [
|
||||
'1fde0a3cac7cb46b36c79f4c0a7a732e38c2c7ee9ac41f44392a07b748a0869f',
|
||||
'203a710160811d5c07ebaeb8fe1d9ce201c66b970d66f18d0d2b264c195309aa'
|
||||
],
|
||||
gRed: false,
|
||||
n: '60c89ce5c263405370a08b6d0302b0bab3eedb83920ee0a677297dc392126f1',
|
||||
type: 'short'
|
||||
};
|
||||
|
||||
// Initialize Babyjubjub curve using short Weierstrass parameters
|
||||
const ShortWeierstrassCurve = elliptic.curve.short;
|
||||
const curve = new ShortWeierstrassCurve(curveOptions);
|
||||
const ec = new elliptic.ec({
|
||||
curve: { curve, g: curve.g, n: curve.n, hash: hash.sha256 }
|
||||
});
|
||||
const baseField = new ZqField(
|
||||
'21888242871839275222246405745257275088548364400416034343698204186575808495617'
|
||||
);
|
||||
const scalarField = new ZqField(
|
||||
'2736030358979909402780800718157159386076813972158567259200215660948447373041'
|
||||
);
|
||||
const cofactor = 8;
|
||||
const scalarFieldBitLength = 251;
|
||||
|
||||
export const babyjubjub: BabyJubJub = {
|
||||
ec,
|
||||
Fb: baseField,
|
||||
Fs: scalarField,
|
||||
cofactor,
|
||||
scalarFieldBitLength
|
||||
};
|
||||
|
||||
export interface CurvePoint {
|
||||
x: bigint;
|
||||
y: bigint;
|
||||
|
||||
equals(other: CurvePoint): boolean;
|
||||
|
||||
isInfinity(): boolean;
|
||||
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export class WeierstrassPoint implements CurvePoint {
|
||||
public x: bigint;
|
||||
public y: bigint;
|
||||
|
||||
constructor(x: bigint, y: bigint) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
equals(other: CurvePoint): boolean {
|
||||
return this.x.toString() === other.x.toString() && this.y.toString() === other.y.toString();
|
||||
}
|
||||
|
||||
static infinity(): WeierstrassPoint {
|
||||
return new WeierstrassPoint(BigInt(0), BigInt(0));
|
||||
}
|
||||
|
||||
isInfinity(): boolean {
|
||||
return this.x === BigInt(0) && this.y === BigInt(0);
|
||||
}
|
||||
|
||||
// Converts from an elliptic.js curve point to a WeierstrassPoint
|
||||
static fromEllipticPoint(point: any): WeierstrassPoint {
|
||||
if (point.isInfinity()) {
|
||||
return this.infinity();
|
||||
}
|
||||
|
||||
return new WeierstrassPoint(
|
||||
BigInt(point.getX().toString(10)),
|
||||
BigInt(point.getY().toString(10))
|
||||
);
|
||||
}
|
||||
|
||||
// Based on conversion formulae: https://www-fourier.univ-grenoble-alpes.fr/mphell/doc-v5/conversion_weierstrass_edwards.html
|
||||
toEdwards(): EdwardsPoint {
|
||||
if (this.isInfinity()) {
|
||||
return EdwardsPoint.infinity();
|
||||
}
|
||||
|
||||
const Fb = baseField;
|
||||
const malpha = Fb.div(BigInt(168698), BigInt(3));
|
||||
const mx = BigInt(Fb.sub(BigInt(this.x.toString()), malpha));
|
||||
const my = BigInt(this.y);
|
||||
|
||||
const ex = Fb.div(mx, my);
|
||||
const ey = Fb.div(Fb.sub(mx, BigInt(1)), Fb.add(mx, BigInt(1)));
|
||||
return new EdwardsPoint(ex, ey);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Weierstrass: (${this.x.toString()}, ${this.y.toString()})`;
|
||||
}
|
||||
|
||||
serialize(): string {
|
||||
return JSON.stringify({
|
||||
x: bigIntToHex(this.x),
|
||||
y: bigIntToHex(this.y)
|
||||
});
|
||||
}
|
||||
|
||||
static deserialize(serialized: string): WeierstrassPoint {
|
||||
const { x, y } = JSON.parse(serialized);
|
||||
return new WeierstrassPoint(hexToBigInt(x), hexToBigInt(y));
|
||||
}
|
||||
}
|
||||
|
||||
export class EdwardsPoint implements CurvePoint {
|
||||
public x: bigint;
|
||||
public y: bigint;
|
||||
|
||||
constructor(x: bigint, y: bigint) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
equals(other: CurvePoint): boolean {
|
||||
return this.x.toString() === other.x.toString() && this.y.toString() === other.y.toString();
|
||||
}
|
||||
|
||||
static infinity(): EdwardsPoint {
|
||||
return new EdwardsPoint(BigInt(0), BigInt(1));
|
||||
}
|
||||
|
||||
isInfinity(): boolean {
|
||||
return this.x === BigInt(0) && this.y === BigInt(1);
|
||||
}
|
||||
|
||||
// Based on conversion formulae: https://www-fourier.univ-grenoble-alpes.fr/mphell/doc-v5/conversion_weierstrass_edwards.html
|
||||
toWeierstrass(): WeierstrassPoint {
|
||||
if (this.isInfinity()) {
|
||||
return WeierstrassPoint.infinity();
|
||||
}
|
||||
|
||||
const Fb = baseField;
|
||||
const mA = BigInt(168698);
|
||||
const mB = BigInt(1);
|
||||
const mx = Fb.div(Fb.add(BigInt(1), this.y), Fb.sub(BigInt(1), this.y));
|
||||
const my = Fb.div(Fb.add(BigInt(1), this.y), Fb.mul(Fb.sub(BigInt(1), this.y), this.x));
|
||||
|
||||
const sx = Fb.div(Fb.add(mx, Fb.div(mA, BigInt(3))), mB);
|
||||
const sy = Fb.div(my, mB);
|
||||
|
||||
return new WeierstrassPoint(sx, sy);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Edwards: (${this.x.toString()}, ${this.y.toString()})`;
|
||||
}
|
||||
|
||||
serialize(): string {
|
||||
return JSON.stringify({
|
||||
x: bigIntToHex(this.x),
|
||||
y: bigIntToHex(this.y)
|
||||
});
|
||||
}
|
||||
|
||||
static deserialize(serialized: string): EdwardsPoint {
|
||||
const { x, y } = JSON.parse(serialized);
|
||||
return new EdwardsPoint(hexToBigInt(x), hexToBigInt(y));
|
||||
}
|
||||
}
|
||||
45
src/gateways/jubmojis/ecdsa.ts
Normal file
45
src/gateways/jubmojis/ecdsa.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { EdwardsPoint, WeierstrassPoint, babyjubjub } from './babyJubjub';
|
||||
|
||||
/**
|
||||
* Converts a private key to a public key on the baby jubjub curve
|
||||
* @param privKey - The private key to convert
|
||||
* @returns The public key in Short Weierstrass form
|
||||
*/
|
||||
export const privateKeyToPublicKey = (privKey: bigint): WeierstrassPoint => {
|
||||
const pubKeyPoint = babyjubjub.ec.g.mul(privKey.toString(16));
|
||||
|
||||
return WeierstrassPoint.fromEllipticPoint(pubKeyPoint);
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes public parameters T, U of the membership proof based on the provided R value
|
||||
* This ensures that T, U were generated appropriately
|
||||
* See: https://hackmd.io/HQZxucnhSGKT_VfNwB6wOw?view
|
||||
* @param R - The R value of the membership proof
|
||||
* @param msgHash - The hash of the message signed by the signature
|
||||
* @returns - The public parameters T, U
|
||||
*/
|
||||
export const computeTUFromR = (
|
||||
R: EdwardsPoint,
|
||||
msgHash: bigint
|
||||
): { T: EdwardsPoint; U: EdwardsPoint } => {
|
||||
const Fs = babyjubjub.Fs;
|
||||
|
||||
const shortR = R.toWeierstrass();
|
||||
const r = shortR.x % Fs.p;
|
||||
const rInv = Fs.inv(r);
|
||||
const ecR = babyjubjub.ec.curve.point(shortR.x.toString(16), shortR.y.toString(16));
|
||||
const ecT = ecR.mul(rInv.toString(16));
|
||||
const T = WeierstrassPoint.fromEllipticPoint(ecT);
|
||||
const G = babyjubjub.ec.curve.g;
|
||||
const rInvm = Fs.neg(Fs.mul(rInv, msgHash));
|
||||
const ecU = G.mul(rInvm.toString(16));
|
||||
const U = WeierstrassPoint.fromEllipticPoint(ecU);
|
||||
|
||||
return { T: T.toEdwards(), U: U.toEdwards() };
|
||||
};
|
||||
80
src/gateways/jubmojis/inputGen.ts
Normal file
80
src/gateways/jubmojis/inputGen.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { EdwardsPoint } from './babyJubjub';
|
||||
import { MerkleProof } from './jubmoji.types';
|
||||
import { hashEdwardsPublicKey, hexToBigInt } from './utils';
|
||||
import { poseidon2 } from 'poseidon-lite/poseidon2';
|
||||
|
||||
export const MERKLE_TREE_DEPTH = 8; // We used a fixed depth merkle tree for now
|
||||
// Precomputed hashes of zero for each layer of the merkle tree
|
||||
export const MERKLE_TREE_ZEROS = [
|
||||
'0',
|
||||
'14744269619966411208579211824598458697587494354926760081771325075741142829156',
|
||||
'7423237065226347324353380772367382631490014989348495481811164164159255474657',
|
||||
'11286972368698509976183087595462810875513684078608517520839298933882497716792',
|
||||
'3607627140608796879659380071776844901612302623152076817094415224584923813162',
|
||||
'19712377064642672829441595136074946683621277828620209496774504837737984048981',
|
||||
'20775607673010627194014556968476266066927294572720319469184847051418138353016',
|
||||
'3396914609616007258851405644437304192397291162432396347162513310381425243293'
|
||||
];
|
||||
|
||||
/**
|
||||
* Computes the merkle root based a list of public keys
|
||||
* Note that public keys must be in Twisted Edwards form
|
||||
* This is because we only ever use the Merkle Tree for in circuit verification,
|
||||
* and the circuit only ever uses Twisted Edwards points
|
||||
* @param pubKeys - The list of public keys to compute the merkle root of in Twisted Edwards form
|
||||
* @param hashFn - The hash function to use for the merkle tree. Defaults to Poseidon
|
||||
* @returns - The merkle root
|
||||
*/
|
||||
export const computeMerkleRoot = async (pubKeys: EdwardsPoint[]): Promise<bigint> => {
|
||||
const proof = await computeMerkleProof(pubKeys, 0);
|
||||
return proof.root;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a merkle proof for a given list of public keys and index
|
||||
* Once again, all public keys are represented in Twisted Edwards form
|
||||
* @param pubKeys - The list of public keys to generate the merkle proof for in Twisted Edwards form
|
||||
* @param index - The index of the public key to generate the merkle proof for
|
||||
* @param hashFn - The hash function to use for the merkle tree. Defaults to Poseidon
|
||||
* @returns - The merkle proof
|
||||
*/
|
||||
export const computeMerkleProof = async (
|
||||
pubKeys: EdwardsPoint[],
|
||||
index: number
|
||||
): Promise<MerkleProof> => {
|
||||
// All public keys are hashed before insertion into the tree
|
||||
const leaves = await Promise.all(pubKeys.map((pubKey) => hashEdwardsPublicKey(pubKey)));
|
||||
|
||||
let prevLayer: bigint[] = leaves;
|
||||
let nextLayer: bigint[] = [];
|
||||
let pathIndices: number[] = [];
|
||||
let siblings: bigint[] = [];
|
||||
|
||||
for (let i = 0; i < MERKLE_TREE_DEPTH; i++) {
|
||||
pathIndices.push(index % 2);
|
||||
const siblingIndex = index % 2 === 0 ? index + 1 : index - 1;
|
||||
const sibling =
|
||||
siblingIndex === prevLayer.length ? BigInt(MERKLE_TREE_ZEROS[i]) : prevLayer[siblingIndex];
|
||||
siblings.push(sibling);
|
||||
index = Math.floor(index / 2);
|
||||
|
||||
for (let j = 0; j < prevLayer.length; j += 2) {
|
||||
const secondNode =
|
||||
j + 1 === prevLayer.length ? BigInt(MERKLE_TREE_ZEROS[i]) : prevLayer[j + 1];
|
||||
const nextNode = poseidon2([prevLayer[j], secondNode]);
|
||||
nextLayer.push(hexToBigInt(nextNode.toString()));
|
||||
}
|
||||
|
||||
prevLayer = nextLayer;
|
||||
nextLayer = [];
|
||||
}
|
||||
const root = prevLayer[0];
|
||||
|
||||
return { root, pathIndices, siblings: siblings };
|
||||
};
|
||||
128
src/gateways/jubmojis/jubmoji.ts
Normal file
128
src/gateways/jubmojis/jubmoji.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import snarkjs from 'snarkjs';
|
||||
import { EdwardsPoint, WeierstrassPoint } from './babyJubjub';
|
||||
import { computeTUFromR } from './ecdsa';
|
||||
import { computeMerkleRoot } from './inputGen';
|
||||
import { VerificationResult, VerifyArgs, ZKP, ZKPPublicSignals } from './jubmoji.types';
|
||||
import { deserializeMembershipProof, hexToBigInt } from './utils';
|
||||
import vkey from './vkey';
|
||||
import { cardPubKeys } from './pubkeys';
|
||||
|
||||
export async function jubmojiVerifier(serializedMembershipProof): Promise<VerificationResult> {
|
||||
const merkleRoot = await getMerkleRootFromCache(collectionPubKeys);
|
||||
|
||||
return await verifyMembership({
|
||||
proof: deserializeMembershipProof(serializedMembershipProof),
|
||||
merkleRoot,
|
||||
sigNullifierRandomness: hexToBigInt(
|
||||
'6addd8ed78c6fb64157aa768c5a9477db536172929949dc22274e323ccb9'
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
const collectionPubKeys = cardPubKeys.map((card) => card.pubKeyJub);
|
||||
|
||||
const verifyMembership = async ({
|
||||
proof,
|
||||
merkleRoot,
|
||||
merkleRootArgs,
|
||||
sigNullifierRandomness
|
||||
}: VerifyArgs): Promise<VerificationResult> => {
|
||||
if (!merkleRoot && !merkleRootArgs) {
|
||||
throw new Error('Must provide either merkle root or merkle root args!');
|
||||
}
|
||||
|
||||
const publicSignals = getPublicSignalsFromMembershipZKP(proof.zkp);
|
||||
|
||||
let resolvedMerkleRoot;
|
||||
if (merkleRoot) {
|
||||
resolvedMerkleRoot = merkleRoot;
|
||||
} else {
|
||||
const { pubKeys } = merkleRootArgs!;
|
||||
const edwardsPubKeys = pubKeys.map((pubKey) => pubKey.toEdwards());
|
||||
resolvedMerkleRoot = await computeMerkleRoot(edwardsPubKeys);
|
||||
}
|
||||
if (resolvedMerkleRoot !== publicSignals.merkleRoot) {
|
||||
return { verified: false };
|
||||
}
|
||||
const { T, U } = computeTUFromR(proof.R, proof.msgHash);
|
||||
if (!T.equals(publicSignals.T) || !U.equals(publicSignals.U)) {
|
||||
return { verified: false };
|
||||
}
|
||||
if (sigNullifierRandomness !== publicSignals.sigNullifierRandomness) {
|
||||
return { verified: false };
|
||||
}
|
||||
// TODO! This is where we need to check and make sure someone can't join more than once
|
||||
// if (usedSigNullifiers && usedSigNullifiers.includes(publicSignals.sigNullifier)) {
|
||||
// return { verified: false };
|
||||
// }
|
||||
const verified = await verifyMembershipZKP(vkey, proof.zkp);
|
||||
if (!verified) {
|
||||
return { verified: false };
|
||||
}
|
||||
|
||||
return {
|
||||
verified: true,
|
||||
consumedSigNullifiers: [publicSignals.sigNullifier]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets public signals as typed arguments from a membership zkp
|
||||
* @param zkp - The membership zkp
|
||||
* @returns - Public signals of the membership zkp
|
||||
*/
|
||||
const getPublicSignalsFromMembershipZKP = (zkp: ZKP): ZKPPublicSignals => {
|
||||
const publicSignals = zkp.publicSignals;
|
||||
|
||||
return {
|
||||
merkleRoot: BigInt(publicSignals[3]),
|
||||
T: new EdwardsPoint(BigInt(publicSignals[4]), BigInt(publicSignals[5])),
|
||||
U: new EdwardsPoint(BigInt(publicSignals[6]), BigInt(publicSignals[7])),
|
||||
sigNullifier: BigInt(publicSignals[0]),
|
||||
sigNullifierRandomness: BigInt(publicSignals[8]),
|
||||
pubKeyNullifier: BigInt(publicSignals[1]),
|
||||
pubKeyNullifierRandomnessHash: BigInt(publicSignals[2])
|
||||
};
|
||||
};
|
||||
|
||||
const getMerkleRootFromCache = async (pubKeyList: string[]): Promise<bigint> => {
|
||||
const pubKeys = pubKeyList.map((pubKey) => publicKeyFromString(pubKey).toEdwards());
|
||||
|
||||
return await computeMerkleRoot(pubKeys);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a public key in hex form to a WeierstrassPoint
|
||||
* Reference for key format: https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
||||
* @param pubKey - The public key in hex form
|
||||
* @returns The public key in Weierstrass form
|
||||
*/
|
||||
const publicKeyFromString = (pubKey: string): WeierstrassPoint => {
|
||||
if (!pubKey.startsWith('04')) {
|
||||
throw new Error('Only handle uncompressed public keys for now');
|
||||
}
|
||||
|
||||
const pubKeyLength = pubKey.length - 2;
|
||||
const x = hexToBigInt(pubKey.slice(2, 2 + pubKeyLength / 2));
|
||||
const y = hexToBigInt(pubKey.slice(2 + pubKeyLength / 2));
|
||||
|
||||
return new WeierstrassPoint(x, y);
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies a zero knowledge proof for a membership proof
|
||||
* @param vkey - The verification key for the membership proof
|
||||
* @param proof - The zero knowledge proof to verify
|
||||
* @param publicInputs - The public inputs to the zero knowledge proof
|
||||
* @returns - A boolean indicating whether or not the proof is valid
|
||||
*/
|
||||
const verifyMembershipZKP = async (vkey: any, { proof, publicSignals }: ZKP): Promise<boolean> => {
|
||||
return await snarkjs.groth16.verify(vkey, publicSignals, proof);
|
||||
};
|
||||
284
src/gateways/jubmojis/jubmoji.types.ts
Normal file
284
src/gateways/jubmojis/jubmoji.types.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/* eslint-disable @typescript-eslint/prefer-function-type */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
import { ZqField } from 'ffjavascript';
|
||||
import { EdwardsPoint, WeierstrassPoint } from './babyJubjub';
|
||||
import { SNARKProof } from '../../types';
|
||||
|
||||
export interface JubmojiRequestI {
|
||||
R: string;
|
||||
msgHash: string;
|
||||
zkp: SNARKProof;
|
||||
}
|
||||
|
||||
export type BabyJubJub = {
|
||||
ec: any;
|
||||
Fb: ZqField;
|
||||
Fs: ZqField;
|
||||
cofactor: number;
|
||||
scalarFieldBitLength: number;
|
||||
};
|
||||
|
||||
export type Signature = {
|
||||
r: bigint;
|
||||
s: bigint;
|
||||
};
|
||||
|
||||
// Contents of a proof for demonstrating a valid BabyJubjub ECDSA
|
||||
// signature without revealing the signature's s value
|
||||
// Based on the Efficient ECDSA formulation: https://personaelabs.org/posts/efficient-ecdsa-1/
|
||||
export interface MembershipProof {
|
||||
R: EdwardsPoint;
|
||||
msgHash: bigint;
|
||||
zkp: ZKP;
|
||||
}
|
||||
|
||||
export interface PublicInputs {
|
||||
R: EdwardsPoint;
|
||||
T: EdwardsPoint;
|
||||
U: EdwardsPoint;
|
||||
}
|
||||
|
||||
// Arguments needed to compute a merkle proof
|
||||
export interface MerkleProofArgs {
|
||||
pubKeys: WeierstrassPoint[];
|
||||
index: number;
|
||||
hashFn?: any;
|
||||
}
|
||||
|
||||
// Arguments needed to batch compute merkle proofs
|
||||
export interface BatchMerkleProofArgs {
|
||||
pubKeys: WeierstrassPoint[];
|
||||
indices: number[];
|
||||
hashFn?: any;
|
||||
}
|
||||
|
||||
// Arguments needed to compute a merkle root
|
||||
export interface MerkleRootArgs {
|
||||
pubKeys: WeierstrassPoint[];
|
||||
hashFn?: any;
|
||||
}
|
||||
|
||||
// Arguments needed to generate a membership proof
|
||||
export interface ProveArgs {
|
||||
sig: Signature;
|
||||
msgHash: bigint;
|
||||
publicInputs?: PublicInputs;
|
||||
pubKey?: WeierstrassPoint;
|
||||
merkleProof?: MerkleProof;
|
||||
merkleProofArgs?: MerkleProofArgs;
|
||||
sigNullifierRandomness: bigint;
|
||||
pubKeyNullifierRandomness: bigint;
|
||||
pathToCircuits?: string;
|
||||
enableTiming?: boolean;
|
||||
}
|
||||
|
||||
// Arguments needed to batch generate membership proofs
|
||||
export interface BatchProveArgs {
|
||||
sigs: Signature[];
|
||||
msgHashes: bigint[];
|
||||
publicInputs?: PublicInputs[];
|
||||
pubKeys?: WeierstrassPoint[];
|
||||
merkleProofs?: MerkleProof[];
|
||||
merkleProofArgs?: BatchMerkleProofArgs;
|
||||
sigNullifierRandomness: bigint;
|
||||
pubKeyNullifierRandomness: bigint;
|
||||
pathToCircuits?: string;
|
||||
enableTiming?: boolean;
|
||||
}
|
||||
|
||||
// Arguments needed to verify a membership proof
|
||||
export interface VerifyArgs {
|
||||
proof: MembershipProof;
|
||||
merkleRoot?: bigint;
|
||||
merkleRootArgs?: MerkleRootArgs;
|
||||
sigNullifierRandomness: bigint;
|
||||
usedSigNullifiers?: bigint[];
|
||||
pathToCircuits?: string;
|
||||
enableTiming?: boolean;
|
||||
}
|
||||
|
||||
// Arguments needed to batch verify membership proofs
|
||||
export interface BatchVerifyArgs {
|
||||
proofs: MembershipProof[];
|
||||
merkleRoot?: bigint;
|
||||
merkleRootArgs?: MerkleRootArgs;
|
||||
sigNullifierRandomness: bigint;
|
||||
usedSigNullifiers?: bigint[];
|
||||
pathToCircuits?: string;
|
||||
enableTiming?: boolean;
|
||||
}
|
||||
|
||||
export type VerificationResult = {
|
||||
verified: boolean;
|
||||
consumedSigNullifiers?: bigint[];
|
||||
};
|
||||
|
||||
// Zero knowledge proof generated by snarkjs
|
||||
export type ZKP = { proof: any; publicSignals: string[] };
|
||||
|
||||
// Inputs to the membership proof circuit
|
||||
// Similar to inputs for Spartan-ecdsa membership circuit:
|
||||
// https://github.com/personaelabs/spartan-ecdsa/blob/main/packages/circuits/eff_ecdsa_membership/pubkey_membership.circom
|
||||
// Includes nullifierRandomness for generating unique nullifiers
|
||||
export type ZKPInputs = {
|
||||
s: bigint;
|
||||
root: bigint;
|
||||
Tx: bigint;
|
||||
Ty: bigint;
|
||||
Ux: bigint;
|
||||
Uy: bigint;
|
||||
pathIndices: number[];
|
||||
siblings: bigint[];
|
||||
sigNullifierRandomness: bigint;
|
||||
pubKeyNullifierRandomness: bigint;
|
||||
};
|
||||
|
||||
// Typed public signals for the membership proof circuit
|
||||
export type ZKPPublicSignals = {
|
||||
merkleRoot: bigint;
|
||||
T: EdwardsPoint;
|
||||
U: EdwardsPoint;
|
||||
sigNullifier: bigint;
|
||||
sigNullifierRandomness: bigint;
|
||||
pubKeyNullifier: bigint;
|
||||
pubKeyNullifierRandomnessHash: bigint;
|
||||
};
|
||||
|
||||
export interface MerkleProof {
|
||||
root: bigint;
|
||||
pathIndices: number[];
|
||||
siblings: bigint[];
|
||||
}
|
||||
|
||||
export type Jubmoji = {
|
||||
pubKeyIndex: number; // Index of the card's public key within the list of public keys
|
||||
sig: string; // DER-encoded signature
|
||||
// msgNonce and msgRand are the counter and randomness used to generate signatures
|
||||
// See: https://github.com/arx-research/libhalo/blob/master/docs/halo-command-set.md#command-sign_random
|
||||
msgNonce: number;
|
||||
msgRand: string;
|
||||
// R, T, U are serialized points on the BabyJubjub curve represented in Edwards form.
|
||||
// They are based on the Efficient ECDSA formulation: https://personaelabs.org/posts/efficient-ecdsa-1/
|
||||
R: string;
|
||||
T: string;
|
||||
U: string;
|
||||
};
|
||||
|
||||
export type NfcCardRawSignature = {
|
||||
r: string;
|
||||
s: string;
|
||||
v: 27 | 28;
|
||||
};
|
||||
|
||||
// Result of signing a message with an Arx card
|
||||
export type NfcCardSignMessageResult = {
|
||||
digest: string;
|
||||
rawSig: NfcCardRawSignature;
|
||||
pubKey: string;
|
||||
};
|
||||
|
||||
// Defines a class used to create and verify proofs
|
||||
export interface ProofClass<A, P> {
|
||||
prove(proofArgs: A): Promise<P>;
|
||||
|
||||
verify(proof: P): Promise<VerificationResult>;
|
||||
}
|
||||
|
||||
// Defines a constructor for a proof class
|
||||
// C: Arguments needed to construct the proof class. Should be JSON.stringify-able
|
||||
// A: Arguments needed to prove the proof
|
||||
// P: The proof. Should be JSON.stringify-able
|
||||
export interface ProofClassConstructor<C, A, P> {
|
||||
new (classArgs: C): ProofClass<A, P>;
|
||||
}
|
||||
|
||||
// Creates an instance of a proof class
|
||||
export function createProofInstance<C, A, P>(
|
||||
constructor: ProofClassConstructor<C, A, P>,
|
||||
args: C
|
||||
): ProofClass<A, P> {
|
||||
return new constructor(args);
|
||||
}
|
||||
|
||||
export interface JubmojiInCollectionClassArgs {
|
||||
collectionPubKeys: string[];
|
||||
sigNullifierRandomness: string;
|
||||
pathToCircuits?: string;
|
||||
}
|
||||
|
||||
export interface JubmojiInCollectionProofArgs {
|
||||
jubmoji: Jubmoji;
|
||||
}
|
||||
|
||||
export interface JubmojiInCollectionProof {
|
||||
serializedMembershipProof: string;
|
||||
usedSigNullifiers?: string[];
|
||||
}
|
||||
|
||||
export interface JubmojiInCollectionWithNonceClassArgs {
|
||||
collectionPubKeys: string[];
|
||||
sigNullifierRandomness: string;
|
||||
pathToCircuits?: string;
|
||||
}
|
||||
|
||||
export interface JubmojiInCollectionWithNonceProofArgs {
|
||||
jubmoji: Jubmoji;
|
||||
}
|
||||
|
||||
export interface JubmojiInCollectionWithNonceProof {
|
||||
serializedMembershipProof: string;
|
||||
msgNonce: number;
|
||||
msgRand: string;
|
||||
usedSigNullifiers?: string[];
|
||||
}
|
||||
|
||||
export interface NUniqueJubmojiInCollectionClassArgs {
|
||||
collectionPubKeys: string[];
|
||||
sigNullifierRandomness: string;
|
||||
N: number;
|
||||
pathToCircuits?: string;
|
||||
}
|
||||
|
||||
export interface NUniqueJubmojiInCollectionProofArgs {
|
||||
jubmojis: Jubmoji[];
|
||||
}
|
||||
|
||||
export interface NUniqueJubmojiInCollectionProof {
|
||||
serializedMembershipProofs: string[];
|
||||
usedSigNullifiers?: string[];
|
||||
}
|
||||
|
||||
export interface PublicMessageSignatureClassArgs {
|
||||
randStr?: string;
|
||||
}
|
||||
|
||||
export interface PublicMessageSignatureProofArgs {
|
||||
message: string;
|
||||
rawSig: NfcCardRawSignature;
|
||||
pubKeyIndex: number;
|
||||
}
|
||||
|
||||
export interface PublicMessageSignatureProof {
|
||||
message: string;
|
||||
rawSig: NfcCardRawSignature;
|
||||
pubKeyIndex: number;
|
||||
}
|
||||
|
||||
export interface TeamLeaderboardClassArgs {
|
||||
teamPubKeys: string[];
|
||||
collectionPubKeys: string[];
|
||||
sigNullifierRandomness: string;
|
||||
pathToCircuits?: string;
|
||||
}
|
||||
|
||||
export interface TeamLeaderboardProofArgs {
|
||||
teamJubmoji: Jubmoji;
|
||||
collectionJubmojis: Jubmoji[];
|
||||
}
|
||||
|
||||
export interface TeamLeaderboardProof {
|
||||
teamPubKeyIndex: number;
|
||||
serializedTeamMembershipProof: string;
|
||||
serializedCollectionMembershipProofs: string[];
|
||||
}
|
||||
2852
src/gateways/jubmojis/pubkeys.ts
Normal file
2852
src/gateways/jubmojis/pubkeys.ts
Normal file
File diff suppressed because it is too large
Load Diff
38
src/gateways/jubmojis/utils.ts
Normal file
38
src/gateways/jubmojis/utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { poseidon2 } from 'poseidon-lite/poseidon2';
|
||||
import { EdwardsPoint } from './babyJubjub';
|
||||
import { MembershipProof } from './jubmoji.types';
|
||||
|
||||
export const hexToBigInt = (hex: string): bigint => {
|
||||
return BigInt(`0x${hex}`);
|
||||
};
|
||||
|
||||
export const bigIntToHex = (bigInt: bigint): string => {
|
||||
return bigInt.toString(16);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hashes an EdwardsPoint to a bigint. Uses the Poseidon hash function
|
||||
* @param pubKey - The public key to hash
|
||||
* @param hashFn - Optional hash function to use. Defaults to Poseidon
|
||||
* @returns The hash of the public key
|
||||
*/
|
||||
export const hashEdwardsPublicKey = (pubKey: EdwardsPoint): bigint => {
|
||||
const hash = poseidon2([pubKey.x, pubKey.y]);
|
||||
|
||||
return hexToBigInt(hash.toString());
|
||||
};
|
||||
|
||||
export const deserializeMembershipProof = (serializedProof: string): MembershipProof => {
|
||||
const proof = JSON.parse(serializedProof);
|
||||
const R = EdwardsPoint.deserialize(proof.R);
|
||||
const msgHash = hexToBigInt(proof.msgHash);
|
||||
const zkp = proof.zkp;
|
||||
|
||||
return { R, msgHash, zkp };
|
||||
};
|
||||
127
src/gateways/jubmojis/vkey.ts
Normal file
127
src/gateways/jubmojis/vkey.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
const vkey = {
|
||||
protocol: 'groth16',
|
||||
curve: 'bn128',
|
||||
nPublic: 9,
|
||||
vk_alpha_1: [
|
||||
'20491192805390485299153009773594534940189261866228447918068658471970481763042',
|
||||
'9383485363053290200918347156157836566562967994039712273449902621266178545958',
|
||||
'1'
|
||||
],
|
||||
vk_beta_2: [
|
||||
[
|
||||
'6375614351688725206403948262868962793625744043794305715222011528459656738731',
|
||||
'4252822878758300859123897981450591353533073413197771768651442665752259397132'
|
||||
],
|
||||
[
|
||||
'10505242626370262277552901082094356697409835680220590971873171140371331206856',
|
||||
'21847035105528745403288232691147584728191162732299865338377159692350059136679'
|
||||
],
|
||||
['1', '0']
|
||||
],
|
||||
vk_gamma_2: [
|
||||
[
|
||||
'10857046999023057135944570762232829481370756359578518086990519993285655852781',
|
||||
'11559732032986387107991004021392285783925812861821192530917403151452391805634'
|
||||
],
|
||||
[
|
||||
'8495653923123431417604973247489272438418190587263600148770280649306958101930',
|
||||
'4082367875863433681332203403145435568316851327593401208105741076214120093531'
|
||||
],
|
||||
['1', '0']
|
||||
],
|
||||
vk_delta_2: [
|
||||
[
|
||||
'10857046999023057135944570762232829481370756359578518086990519993285655852781',
|
||||
'11559732032986387107991004021392285783925812861821192530917403151452391805634'
|
||||
],
|
||||
[
|
||||
'8495653923123431417604973247489272438418190587263600148770280649306958101930',
|
||||
'4082367875863433681332203403145435568316851327593401208105741076214120093531'
|
||||
],
|
||||
['1', '0']
|
||||
],
|
||||
vk_alphabeta_12: [
|
||||
[
|
||||
[
|
||||
'2029413683389138792403550203267699914886160938906632433982220835551125967885',
|
||||
'21072700047562757817161031222997517981543347628379360635925549008442030252106'
|
||||
],
|
||||
[
|
||||
'5940354580057074848093997050200682056184807770593307860589430076672439820312',
|
||||
'12156638873931618554171829126792193045421052652279363021382169897324752428276'
|
||||
],
|
||||
[
|
||||
'7898200236362823042373859371574133993780991612861777490112507062703164551277',
|
||||
'7074218545237549455313236346927434013100842096812539264420499035217050630853'
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
'7077479683546002997211712695946002074877511277312570035766170199895071832130',
|
||||
'10093483419865920389913245021038182291233451549023025229112148274109565435465'
|
||||
],
|
||||
[
|
||||
'4595479056700221319381530156280926371456704509942304414423590385166031118820',
|
||||
'19831328484489333784475432780421641293929726139240675179672856274388269393268'
|
||||
],
|
||||
[
|
||||
'11934129596455521040620786944827826205713621633706285934057045369193958244500',
|
||||
'8037395052364110730298837004334506829870972346962140206007064471173334027475'
|
||||
]
|
||||
]
|
||||
],
|
||||
IC: [
|
||||
[
|
||||
'5906523767025352576019539800575526703999785044312911592948973442389199126505',
|
||||
'5399516258430932355535174434944180731688115835606520831984100718735405595772',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'9790477188945059076442519292795257867957347672288790978218244312669794270325',
|
||||
'624485857042273351177690254178225395163543252084408929924426132565147437366',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'3479995447129150108900208244025267654247990557816586426180247323199495647349',
|
||||
'17304109161574863119413208339549874476577725501798123988351223506303640088143',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'3054209688638680605336774061629468549369097307440376949057827999544092296420',
|
||||
'5049000941597301571356507935915602177222642901014937911117181408531106793240',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'3768309033651830981078461634376711978666409179768543356075076042345846419822',
|
||||
'14232067096259050863334433214165468424370193127742466523583523730020905109183',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'5910156264669607430882933403494838472555501111573345279966258564796719456837',
|
||||
'10386898818693886233797834042450562283730425306753976504811645840997283281593',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'5848514880539808887001034866489273124040608404072979651845830442053600850472',
|
||||
'17995743802025070060480075195453000874769970519239751898841530116119494187156',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'19407199380583974063335319513369108311590348654020408498405117118250681465433',
|
||||
'10447798841946339789352050710608266132168605671188376395009160862498349470228',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'18746800065979837074894156943440632661656502322627312702866419198947336958616',
|
||||
'6057882634184253227568238012793960005434954857795973519013159759890297958086',
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'7369167653274471663233926616774246947916353554158573320045358175706593604250',
|
||||
'4271979081058356268893546335109696483068377530236220151699523589701419647715',
|
||||
'1'
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
export default vkey;
|
||||
Reference in New Issue
Block a user