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 ae1355f commit 4063d09
Show file tree
Hide file tree
Showing 7 changed files with 687 additions and 5 deletions.
5 changes: 5 additions & 0 deletions packages/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
"@hatsprotocol/sdk-v1-core": "^0.10.0",
"@hookform/resolvers": "^3.3.4",
"@nivo/line": "^0.84.0",
"@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 +41,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
66 changes: 65 additions & 1 deletion packages/interface/src/components/EligibilityDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { useRouter } from "next/router";
import { useState, useCallback, useEffect } from "react";
import { toast } from "sonner";
Expand All @@ -8,13 +9,30 @@ import { useAppState } from "~/utils/state";
import { EAppState } from "~/utils/types";

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

Check failure on line 11 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

There should be at least one empty line between import groups
import { GatekeeperTrait, getZupassGatekeeperData } from "maci-cli/sdk";

Check failure on line 12 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

There should be at least one empty line between import groups

Check failure on line 12 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

`maci-cli/sdk` import should occur before import of `next/router`
import { useEthersSigner } from "~/hooks/useEthersSigner";

Check failure on line 13 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

There should be at least one empty line between import groups

Check failure on line 13 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

`~/hooks/useEthersSigner` import should occur before import of `~/utils/state`
import { zuAuthPopup } from "@pcd/zuauth";

Check failure on line 14 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

`@pcd/zuauth` import should occur before import of `next/router`
import { decStringToBigIntToUuid } from "@pcd/util";

Check failure on line 15 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

`@pcd/util` import should occur before import of `next/router`
import { ZKEdDSAEventTicketPCDPackage } from "@pcd/zk-eddsa-event-ticket-pcd";

Check failure on line 16 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

`@pcd/zk-eddsa-event-ticket-pcd` import should occur before import of `next/router`
import { add } from "date-fns";

Check failure on line 17 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

There should be at least one empty line between import groups

Check failure on line 17 in packages/interface/src/components/EligibilityDialog.tsx

View workflow job for this annotation

GitHub Actions / check (lint:ts)

`date-fns` import should occur before import of `next/router`
import { zupass, config } from "~/config";
import type { EdDSAPublicKey } from "@pcd/pcd-types";

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,37 @@ 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 {
const jsonPCD: string = JSON.parse(result.pcdStr).pcd;
const pcd = await ZKEdDSAEventTicketPCDPackage.deserialize(jsonPCD);
await generateZupassProof(pcd, onError);
} catch (e) {
console.error("zupass error:", e);
}
}
}
}, [signer, onError, setOpenDialog, address, generateZupassProof]);

useEffect(() => {
setOpenDialog(!!address);
}, [address, setOpenDialog]);
Expand Down Expand Up @@ -120,6 +169,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
28 changes: 26 additions & 2 deletions packages/interface/src/contexts/Maci.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
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 @@ -22,10 +22,12 @@ import { useEthersSigner } from "~/hooks/useEthersSigner";
import { api } from "~/utils/api";
import { getHatsClient } from "~/utils/hatsProtocol";
import { getSemaphoreProof } from "~/utils/semaphore";
import { generateWitness } from "~/utils/pcd";

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

export const MaciContext = createContext<MaciContextType | undefined>(undefined);

Expand All @@ -47,6 +49,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 +150,26 @@ 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);
const encodedProof = AbiCoder.defaultAbiCoder().encode(["uint256[2]", "uint256[2][2]", "uint256[2]", "uint256[38]"],
[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 +225,11 @@ 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 +322,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 +459,7 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
onSignup,
onVote,
gatekeeperTrait,
generateZupassProof,
}),
[
isLoading,
Expand All @@ -452,6 +475,7 @@ export const MaciProvider: React.FC<MaciProviderProps> = ({ children }: MaciProv
onSignup,
onVote,
gatekeeperTrait,
generateZupassProof,
],
);

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

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

export interface IVoteArgs {
voteOptionIndex: bigint;
Expand All @@ -21,6 +22,10 @@ export interface MaciContextType {
tallyData?: TallyData;
maciPubKey?: string;
gatekeeperTrait?: GatekeeperTrait;
generateZupassProof: (
args: PCD,
onError: () => void,
) => Promise<void>;
onSignup: (onError: () => void) => Promise<void>;
onVote: (
args: IVoteArgs[],
Expand Down
99 changes: 99 additions & 0 deletions packages/interface/src/utils/pcd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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 4063d09

Please sign in to comment.