Skip to content

Commit

Permalink
feat(zupass-gatekeeper): added zupass gatekeeper support
Browse files Browse the repository at this point in the history
  • Loading branch information
Crisgarner committed Sep 24, 2024
1 parent c6906b0 commit 53023f1
Show file tree
Hide file tree
Showing 7 changed files with 696 additions and 5 deletions.
6 changes: 6 additions & 0 deletions packages/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"@hatsprotocol/sdk-v1-core": "^0.10.0",
"@hookform/resolvers": "^3.3.4",
"@nivo/line": "^0.84.0",
"@pcd/eddsa-pcd": "^0.6.5",
"@pcd/pcd-types": "^0.11.4",
"@pcd/util": "^0.5.4",
"@pcd/zk-eddsa-event-ticket-pcd": "^0.6.6",
"@pcd/zuauth": "^1.4.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@rainbow-me/rainbowkit": "^2.0.1",
Expand All @@ -37,6 +42,7 @@
"dotenv": "^16.4.1",
"ethers": "^6.13.1",
"graphql-request": "^6.1.0",
"js-sha256": "^0.11.0",
"lowdb": "^1.0.0",
"lucide-react": "^0.316.0",
"maci-cli": "^2.3.0",
Expand Down
67 changes: 66 additions & 1 deletion packages/interface/src/components/EligibilityDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
/* eslint-disable no-console */
import { decStringToBigIntToUuid } from "@pcd/util";
import { ZKEdDSAEventTicketPCDPackage } from "@pcd/zk-eddsa-event-ticket-pcd";
import { zuAuthPopup } from "@pcd/zuauth";
import { GatekeeperTrait, getZupassGatekeeperData } from "maci-cli/sdk";
import { useRouter } from "next/router";
import { useState, useCallback, useEffect } from "react";
import { toast } from "sonner";
import { useAccount, useDisconnect } from "wagmi";

import { zupass, config } from "~/config";
import { useMaci } from "~/contexts/Maci";
import { useEthersSigner } from "~/hooks/useEthersSigner";
import { useAppState } from "~/utils/state";
import { EAppState } from "~/utils/types";

import type { EdDSAPublicKey } from "@pcd/eddsa-pcd";

import { Dialog } from "./ui/Dialog";

export const EligibilityDialog = (): JSX.Element | null => {
const signer = useEthersSigner();
const { address } = useAccount();
const { disconnect } = useDisconnect();

const [openDialog, setOpenDialog] = useState<boolean>(!!address);
const { onSignup, isEligibleToVote, isRegistered, initialVoiceCredits, votingEndsAt } = useMaci();
const {
onSignup,
isEligibleToVote,
isRegistered,
initialVoiceCredits,
votingEndsAt,
gatekeeperTrait,
generateZupassProof,
} = useMaci();
const router = useRouter();

const appState = useAppState();
Expand All @@ -26,6 +44,38 @@ export const EligibilityDialog = (): JSX.Element | null => {
setOpenDialog(false);
}, [onSignup, onError, setOpenDialog]);

const handleZupassVerify = useCallback(async () => {
if (address !== undefined && signer) {
const zupassGatekeeperData = await getZupassGatekeeperData({ maciAddress: config.maciAddress!, signer });
const eventId = decStringToBigIntToUuid(zupassGatekeeperData.eventId);
const result = await zuAuthPopup({
fieldsToReveal: {
revealTicketId: true,
revealEventId: true,
},
watermark: address,
config: [
{
pcdType: zupass.pcdType,
publicKey: zupass.publicKey as EdDSAPublicKey,
eventId,
eventName: zupass.eventName,
},
],
});
if (result.type === "pcd") {
try {
// eslint-disable-next-line
const jsonPCD: string = JSON.parse(result.pcdStr).pcd;
const pcd = await ZKEdDSAEventTicketPCDPackage.deserialize(jsonPCD);
await generateZupassProof(pcd);
} catch (e) {
console.error("zupass error:", e);
}
}
}
}, [signer, setOpenDialog, address, generateZupassProof]);

useEffect(() => {
setOpenDialog(!!address);
}, [address, setOpenDialog]);
Expand Down Expand Up @@ -120,6 +170,21 @@ export const EligibilityDialog = (): JSX.Element | null => {
);
}

if (appState === EAppState.VOTING && !isEligibleToVote && gatekeeperTrait === GatekeeperTrait.Zupass) {
return (
<Dialog
button="secondary"
buttonAction={handleZupassVerify}
buttonName="Generate Proof"
description="To participate in this round, you need to generate a Proof with Zupass and then signup."
isOpen={openDialog}
size="sm"
title="Signup with Zupass"
onOpenChange={handleCloseDialog}
/>
);
}

if (appState === EAppState.VOTING && !isEligibleToVote) {
return (
<Dialog
Expand Down
9 changes: 9 additions & 0 deletions packages/interface/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export const eas = {
},
};

export const zupass = {
pcdType: "eddsa-ticket-pcd",
publicKey: [
"1ebfb986fbac5113f8e2c72286fe9362f8e7d211dbc68227a468d7b919e75003",
"10ec38f11baacad5535525bbe8e343074a483c051aa1616266f3b1df3fb7d204",
],
eventName: process.env.NEXT_PUBLIC_ZUPASS_EVENT_NAME!,
} as const;

export const impactCategories = {
ETHEREUM_INFRASTRUCTURE: { label: "Ethereum Infrastructure" },
OPEN_SOURCE: { label: "Web3 Open Source Software" },
Expand Down
34 changes: 32 additions & 2 deletions packages/interface/src/contexts/Maci.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable no-console */
import { type ZKEdDSAEventTicketPCD } from "@pcd/zk-eddsa-event-ticket-pcd/ZKEdDSAEventTicketPCD";
import { Identity } from "@semaphore-protocol/core";
import { isAfter } from "date-fns";
import { type Signer, BrowserProvider } from "ethers";
import { type Signer, BrowserProvider, AbiCoder } from "ethers";
import {
signup,
isRegisteredUser,
Expand All @@ -21,9 +22,11 @@ import { config } from "~/config";
import { useEthersSigner } from "~/hooks/useEthersSigner";
import { api } from "~/utils/api";
import { getHatsClient } from "~/utils/hatsProtocol";
import { generateWitness } from "~/utils/pcd";
import { getSemaphoreProof } from "~/utils/semaphore";

import type { IVoteArgs, MaciContextType, MaciProviderProps } from "./types";
import type { PCD } from "@pcd/pcd-types";
import type { EIP1193Provider } from "viem";
import type { Attestation } from "~/utils/types";

Expand All @@ -47,6 +50,7 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
const [tallyData, setTallyData] = useState<TallyData>();

const [semaphoreIdentity, setSemaphoreIdentity] = useState<Identity | undefined>();
const [zupassProof, setZupassProof] = useState<PCD>();
const [maciPrivKey, setMaciPrivKey] = useState<string | undefined>();
const [maciPubKey, setMaciPubKey] = useState<string | undefined>();

Expand Down Expand Up @@ -147,13 +151,29 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
});
setIsLoading(false);
break;
case GatekeeperTrait.Zupass:
if (!signer) {
setIsLoading(false);
return;
}
if (zupassProof) {
const proof = generateWitness(zupassProof as ZKEdDSAEventTicketPCD);
const encodedProof = AbiCoder.defaultAbiCoder().encode(
["uint256[2]", "uint256[2][2]", "uint256[2]", "uint256[38]"],
// eslint-disable-next-line no-underscore-dangle
[proof._pA, proof._pB, proof._pC, proof._pubSignals],
);
setSgData(encodedProof);
}
setIsLoading(false);
break;
case GatekeeperTrait.FreeForAll:
setIsLoading(false);
break;
default:
break;
}
}, [gatekeeperTrait, attestationId, semaphoreIdentity, signer]);
}, [gatekeeperTrait, attestationId, semaphoreIdentity, signer, zupassProof]);

// a user is eligible to vote if they pass certain conditions
// with gatekeepers like EAS it is possible to determine whether you are allowed
Expand Down Expand Up @@ -209,6 +229,13 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
setSemaphoreIdentity(newSemaphoreIdentity);
}, [address, signatureMessage, signMessageAsync, setMaciPrivKey, setMaciPubKey, setSemaphoreIdentity]);

const generateZupassProof = useCallback(
(proof: PCD) => {
setZupassProof(proof);
},
[setZupassProof],
);

// memo to calculate the voting end date
const votingEndsAt = useMemo(
() =>
Expand Down Expand Up @@ -301,6 +328,7 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
localStorage.removeItem("maciPrivKey");
localStorage.removeItem("maciPubKey");
localStorage.removeItem("semaphoreIdentity");
localStorage.removeItem("zupassProof");
}
}, [isDisconnected]);

Expand Down Expand Up @@ -437,6 +465,7 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
onSignup,
onVote,
gatekeeperTrait,
generateZupassProof,
}),
[
isLoading,
Expand All @@ -452,6 +481,7 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
onSignup,
onVote,
gatekeeperTrait,
generateZupassProof,
],
);

Expand Down
2 changes: 2 additions & 0 deletions packages/interface/src/contexts/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type TallyData, type IGetPollData, type GatekeeperTrait } from "maci-cli/sdk";
import { type ReactNode } from "react";

import type { PCD } from "@pcd/pcd-types";
import type { Ballot, Vote } from "~/features/ballot/types";

export interface IVoteArgs {
Expand All @@ -21,6 +22,7 @@ export interface MaciContextType {
tallyData?: TallyData;
maciPubKey?: string;
gatekeeperTrait?: GatekeeperTrait;
generateZupassProof: (args: PCD) => Promise<void>;
onSignup: (onError: () => void) => Promise<void>;
onVote: (
args: IVoteArgs[],
Expand Down
100 changes: 100 additions & 0 deletions packages/interface/src/utils/pcd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* eslint-disable */
import { booleanToBigInt, hexToBigInt, numberToBigInt, uuidToBigInt } from "@pcd/util";
import { ZKEdDSAEventTicketPCD, ZKEdDSAEventTicketPCDClaim } from "@pcd/zk-eddsa-event-ticket-pcd";
import { sha256 } from "js-sha256";

function convertStringArrayToBigIntArray(arr: string[]): bigint[] {
return arr.map((x) => BigInt(x));
}

/**
* Encoding of -1 in a Baby Jubjub field element (as p-1).
*/
export const BABY_JUB_NEGATIVE_ONE = BigInt(
"21888242871839275222246405745257275088548364400416034343698204186575808495616",
);

/**
* Max supported size of validEventIds field in ZKEdDSAEventTicketPCDArgs.
*/
export const VALID_EVENT_IDS_MAX_LEN = 20;

export function generateSnarkMessageHash(signal: string): bigint {
// right shift to fit into a field element, which is 254 bits long
// shift by 8 ensures we have a 253 bit element
return BigInt(`0x${sha256(signal)}`) >> BigInt(8);
}

export const STATIC_TICKET_PCD_NULLIFIER = generateSnarkMessageHash("dummy-nullifier-for-eddsa-event-ticket-pcds");

export function snarkInputForValidEventIds(validEventIds?: string[]): string[] {
if (validEventIds === undefined) {
validEventIds = [];
}
if (validEventIds.length > VALID_EVENT_IDS_MAX_LEN) {
throw new Error(
`validEventIds for a ZKEdDSAEventTicketPCD can have up to 100 entries. ${validEventIds.length} given.`,
);
}
const snarkIds = new Array<string>(VALID_EVENT_IDS_MAX_LEN);
let i = 0;
for (const validId of validEventIds) {
snarkIds[i] = uuidToBigInt(validId).toString();
++i;
}
for (; i < VALID_EVENT_IDS_MAX_LEN; ++i) {
snarkIds[i] = BABY_JUB_NEGATIVE_ONE.toString();
}
return snarkIds;
}

export function publicSignalsFromClaim(claim: ZKEdDSAEventTicketPCDClaim): string[] {
const t = claim.partialTicket;
const ret: string[] = [];

const negOne = BABY_JUB_NEGATIVE_ONE.toString();

// Outputs appear in public signals first
ret.push(t.ticketId === undefined ? negOne : uuidToBigInt(t.ticketId).toString());
ret.push(t.eventId === undefined ? negOne : uuidToBigInt(t.eventId).toString());
ret.push(t.productId === undefined ? negOne : uuidToBigInt(t.productId).toString());
ret.push(t.timestampConsumed === undefined ? negOne : t.timestampConsumed.toString());
ret.push(t.timestampSigned === undefined ? negOne : t.timestampSigned.toString());
ret.push(t.attendeeSemaphoreId || negOne);
ret.push(t.isConsumed === undefined ? negOne : booleanToBigInt(t.isConsumed).toString());
ret.push(t.isRevoked === undefined ? negOne : booleanToBigInt(t.isRevoked).toString());
ret.push(t.ticketCategory === undefined ? negOne : numberToBigInt(t.ticketCategory).toString());
ret.push(t.attendeeEmail === undefined ? negOne : generateSnarkMessageHash(t.attendeeEmail).toString());
ret.push(t.attendeeName === undefined ? negOne : generateSnarkMessageHash(t.attendeeName).toString());

// Placeholder for reserved field
ret.push(negOne);

ret.push(claim.nullifierHash || negOne);

// Public inputs appear in public signals in declaration order
ret.push(hexToBigInt(claim.signer[0]).toString());
ret.push(hexToBigInt(claim.signer[1]).toString());

for (const eventId of snarkInputForValidEventIds(claim.validEventIds)) {
ret.push(eventId);
}
ret.push(claim.validEventIds !== undefined ? "1" : "0"); // checkValidEventIds

ret.push(claim.externalNullifier?.toString() || STATIC_TICKET_PCD_NULLIFIER.toString());

ret.push(claim.watermark);

return ret;
}

// uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[38] calldata _pubSignals
export const generateWitness = (pcd: ZKEdDSAEventTicketPCD) => {
const _pA = pcd.proof.pi_a.slice(0, 2);
const _pB = [pcd.proof.pi_b[0].slice(0).reverse(), pcd.proof.pi_b[1].slice(0).reverse()];
const _pC = pcd.proof.pi_c.slice(0, 2);

const _pubSignals = convertStringArrayToBigIntArray(publicSignalsFromClaim(pcd.claim));

return { _pA, _pB, _pC, _pubSignals };
};
Loading

0 comments on commit 53023f1

Please sign in to comment.