From 0266e94aea10e09db2f2189944b19154dd1b52f9 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 18 Dec 2025 12:28:50 +0100 Subject: [PATCH 1/7] refactor: cleanup crisp sdk worker hook --- .../{crispWorker.js => crispSDKWorker.js} | 3 +- .../voteManagement/VoteManagement.context.tsx | 8 ++--- .../voteManagement/VoteManagement.types.ts | 1 - .../useSDKWorker.tsx} | 30 ++++++++++++------- .../client/src/hooks/voting/useVoteCasting.ts | 13 ++++---- .../client/src/utils/handle-generic-error.ts | 1 - .../CRISP/packages/crisp-sdk/src/types.ts | 2 +- .../CRISP/packages/crisp-sdk/src/utils.ts | 4 +-- examples/CRISP/packages/crisp-sdk/src/vote.ts | 2 +- .../packages/crisp-sdk/tests/vote.test.ts | 1 - 10 files changed, 34 insertions(+), 31 deletions(-) rename examples/CRISP/client/libs/{crispWorker.js => crispSDKWorker.js} (96%) rename examples/CRISP/client/src/hooks/{wasm/useWebAssembly.tsx => voting/useSDKWorker.tsx} (71%) diff --git a/examples/CRISP/client/libs/crispWorker.js b/examples/CRISP/client/libs/crispSDKWorker.js similarity index 96% rename from examples/CRISP/client/libs/crispWorker.js rename to examples/CRISP/client/libs/crispSDKWorker.js index c563523096..a6f75705f5 100755 --- a/examples/CRISP/client/libs/crispWorker.js +++ b/examples/CRISP/client/libs/crispSDKWorker.js @@ -11,7 +11,7 @@ self.onmessage = async function (event) { switch (type) { case 'generate_proof': try { - const { voteId, publicKey, address, signature, previousCiphertext, messageHash } = data + const { voteId, publicKey, address, signature, previousCiphertext } = data // voteId is either 0 or 1, so we need to encode the vote accordingly. // We are adapting to the current CRISP application. @@ -32,7 +32,6 @@ self.onmessage = async function (event) { merkleLeaves, balance, previousCiphertext, - messageHash, }) const encodedProof = encodeSolidityProof(proof) diff --git a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx index fccf6e2391..24404fd6e8 100644 --- a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx +++ b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx @@ -6,7 +6,7 @@ import { createGenericContext } from '@/utils/create-generic-context' import { VoteManagementContextType, VoteManagementProviderProps, VoteStatus } from '@/context/voteManagement' -import { useWebAssemblyHook } from '@/hooks/wasm/useWebAssembly' +import { useSDKWorkerHook } from '@/hooks/voting/useSDKWorker' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useAccount } from 'wagmi' import { VoteStateLite, VotingRound } from '@/model/vote.model' @@ -59,7 +59,7 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => { /** * Voting Management Methods **/ - const { isLoading: wasmLoading, generateProof } = useWebAssemblyHook() + const { isLoading: workerLoading, generateProof } = useSDKWorkerHook() const { isLoading: enclaveLoading, getRoundStateLite: getRoundStateLiteRequest, @@ -167,11 +167,11 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => { } useEffect(() => { - if ([wasmLoading, enclaveLoading].includes(true)) { + if ([workerLoading, enclaveLoading].includes(true)) { return setIsLoading(true) } setIsLoading(false) - }, [wasmLoading, enclaveLoading]) + }, [workerLoading, enclaveLoading]) useEffect(() => { if (isConnected && address) { diff --git a/examples/CRISP/client/src/context/voteManagement/VoteManagement.types.ts b/examples/CRISP/client/src/context/voteManagement/VoteManagement.types.ts index e410272ebe..4dd7cfac3d 100644 --- a/examples/CRISP/client/src/context/voteManagement/VoteManagement.types.ts +++ b/examples/CRISP/client/src/context/voteManagement/VoteManagement.types.ts @@ -42,7 +42,6 @@ export type VoteManagementContextType = { publicKey: Uint8Array, address: string, signature: string, - messageHash: `0x${string}`, previousCiphertext?: Uint8Array, ) => Promise broadcastVote: (vote: BroadcastVoteRequest) => Promise diff --git a/examples/CRISP/client/src/hooks/wasm/useWebAssembly.tsx b/examples/CRISP/client/src/hooks/voting/useSDKWorker.tsx similarity index 71% rename from examples/CRISP/client/src/hooks/wasm/useWebAssembly.tsx rename to examples/CRISP/client/src/hooks/voting/useSDKWorker.tsx index dcb9f6897a..703b204a33 100644 --- a/examples/CRISP/client/src/hooks/wasm/useWebAssembly.tsx +++ b/examples/CRISP/client/src/hooks/voting/useSDKWorker.tsx @@ -4,20 +4,22 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { handleGenericError } from '@/utils/handle-generic-error' import { useNotificationAlertContext } from '@/context/NotificationAlert' -export const useWebAssemblyHook = () => { +export const useSDKWorkerHook = () => { const { showToast } = useNotificationAlertContext() const [isLoading, setIsLoading] = useState(false) - const [worker, setWorker] = useState(null) + const workerRef = useRef(null) useEffect(() => { - const newWorker = new Worker(new URL('libs/crispWorker.js', import.meta.url), { + const newWorker = new Worker(new URL('libs/crispSDKWorker.js', import.meta.url), { type: 'module', }) - setWorker(newWorker) + + workerRef.current = newWorker + return () => { newWorker.terminate() } @@ -28,19 +30,24 @@ export const useWebAssemblyHook = () => { publicKey: Uint8Array, address: string, signature: string, - messageHash: `0x${string}`, previousCiphertext?: Uint8Array, ): Promise => { - if (!worker) { - console.error('WebAssembly worker not initialized') + if (!workerRef.current) { + console.error('Worker not initialized') return } return new Promise((resolve, reject) => { setIsLoading(true) - worker.postMessage({ type: 'generate_proof', data: { voteId, publicKey, address, signature, messageHash, previousCiphertext } }) - worker.onmessage = async (event) => { + + workerRef.current!.postMessage({ + type: 'generate_proof', + data: { voteId, publicKey, address, signature, previousCiphertext }, + }) + + workerRef.current!.onmessage = async (event) => { const { type, success, encodedProof, error } = event.data + if (type === 'generate_proof') { if (success) { resolve(encodedProof) @@ -49,9 +56,12 @@ export const useWebAssemblyHook = () => { type: 'danger', message: error, }) + handleGenericError('generateProof', new Error(error)) + reject(new Error(error)) } + setIsLoading(false) } } diff --git a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts index 214868a140..c8cb321f44 100644 --- a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts +++ b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts @@ -6,7 +6,6 @@ import { useState, useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import { hashMessage } from 'viem' import { useSignMessage } from 'wagmi' import { useVoteManagementContext } from '@/context/voteManagement' @@ -14,7 +13,7 @@ import { useNotificationAlertContext } from '@/context/NotificationAlert/Notific import { Poll } from '@/model/poll.model' import { BroadcastVoteRequest, VoteStateLite, VotingRound } from '@/model/vote.model' -import { encryptVote } from '@crisp-e3/sdk' +import { encryptVote, SIGNATURE_MESSAGE } from '@crisp-e3/sdk' export type VotingStep = 'idle' | 'signing' | 'encrypting' | 'generating_proof' | 'broadcasting' | 'confirming' | 'complete' | 'error' @@ -68,9 +67,9 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo const [stepMessage, setStepMessage] = useState('') const handleProofGeneration = useCallback( - async (vote: Poll, address: string, signature: string, messageHash: `0x${string}`, previousCiphertext?: Uint8Array) => { + async (vote: Poll, address: string, signature: string, previousCiphertext?: Uint8Array) => { if (!votingRound) throw new Error('No voting round available for proof generation') - return generateProof(BigInt(vote.value), new Uint8Array(votingRound.pk_bytes), address, signature, messageHash, previousCiphertext) + return generateProof(BigInt(vote.value), new Uint8Array(votingRound.pk_bytes), address, signature, previousCiphertext) }, [generateProof, votingRound], ) @@ -108,12 +107,10 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo setVotingStep('signing') setLastActiveStep('signing') setStepMessage('Please sign the message in your wallet...') - const message = `Vote for round ${roundState.id}` - const messageHash = hashMessage(message) let signature: string try { - signature = await signMessageAsync({ message }) + signature = await signMessageAsync({ message: SIGNATURE_MESSAGE }) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (signError) { console.log('User rejected signature or signing failed') @@ -130,7 +127,7 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo // @todo get this from the contract or server const newEncryptionTemp = encryptVote({ yes: 0n, no: 0n }, new Uint8Array(votingRound!.pk_bytes)) const previousCiphertext = isVoteUpdate ? newEncryptionTemp : undefined - const encodedProof = await handleProofGeneration(pollSelected, user.address, signature, messageHash, previousCiphertext) + const encodedProof = await handleProofGeneration(pollSelected, user.address, signature, previousCiphertext) if (!encodedProof) { throw new Error('Failed to encrypt vote.') } diff --git a/examples/CRISP/client/src/utils/handle-generic-error.ts b/examples/CRISP/client/src/utils/handle-generic-error.ts index feda7c3809..e21f8d308c 100644 --- a/examples/CRISP/client/src/utils/handle-generic-error.ts +++ b/examples/CRISP/client/src/utils/handle-generic-error.ts @@ -6,5 +6,4 @@ export const handleGenericError = (functionName: string, error: Error) => { console.error(`[${functionName}] - ${error.message}`) - // throw new Error(`[${functionName}] - ${error.message}`) } diff --git a/examples/CRISP/packages/crisp-sdk/src/types.ts b/examples/CRISP/packages/crisp-sdk/src/types.ts index 5a63ddd2af..cdbe96b54d 100644 --- a/examples/CRISP/packages/crisp-sdk/src/types.ts +++ b/examples/CRISP/packages/crisp-sdk/src/types.ts @@ -195,5 +195,5 @@ export type VoteProofInputs = { vote: Vote signature: `0x${string}` previousCiphertext?: Uint8Array - messageHash: `0x${string}` + messageHash?: `0x${string}` } diff --git a/examples/CRISP/packages/crisp-sdk/src/utils.ts b/examples/CRISP/packages/crisp-sdk/src/utils.ts index e6ceefd597..c49980f724 100644 --- a/examples/CRISP/packages/crisp-sdk/src/utils.ts +++ b/examples/CRISP/packages/crisp-sdk/src/utils.ts @@ -117,8 +117,8 @@ export const extractSignatureComponents = async ( } } -export const getAddressFromSignature = async (signature: `0x${string}`, messageHash: `0x${string}`): Promise => { - const publicKey = await recoverPublicKey({ hash: messageHash, signature }) +export const getAddressFromSignature = async (signature: `0x${string}`, messageHash?: `0x${string}`): Promise => { + const publicKey = await recoverPublicKey({ hash: messageHash || SIGNATURE_MESSAGE_HASH, signature }) return publicKeyToAddress(publicKey) } diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index dd0a474eb8..2be24331a1 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -7,7 +7,7 @@ import { ZKInputsGenerator } from '@crisp-e3/zk-inputs' import { type CircuitInputs, type Vote, ExecuteCircuitResult, MaskVoteProofInputs, ProofInputs, VoteProofInputs } from './types' import { generateMerkleProof, toBinary, extractSignatureComponents, getAddressFromSignature, getOptimalThreadCount } from './utils' -import { MAXIMUM_VOTE_VALUE, MASK_SIGNATURE } from './constants' +import { MAXIMUM_VOTE_VALUE, MASK_SIGNATURE, SIGNATURE_MESSAGE_HASH } from './constants' import { Noir, type CompiledCircuit } from '@noir-lang/noir_js' import { UltraHonkBackend, type ProofData } from '@aztec/bb.js' import circuit from '../../../circuits/target/crisp_circuit.json' diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index 69b3efdc2f..846f81ef0e 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -186,7 +186,6 @@ describe('Vote', () => { signature, merkleLeaves: LEAVES, balance, - messageHash: SIGNATURE_MESSAGE_HASH, }) expect(proof).toBeDefined() From 71695ecb4b0f6f09bfa4c1da531ccc886aaa8f64 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 18 Dec 2025 12:30:23 +0100 Subject: [PATCH 2/7] chore: fix lint error --- examples/CRISP/packages/crisp-sdk/src/vote.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index 2be24331a1..dd0a474eb8 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -7,7 +7,7 @@ import { ZKInputsGenerator } from '@crisp-e3/zk-inputs' import { type CircuitInputs, type Vote, ExecuteCircuitResult, MaskVoteProofInputs, ProofInputs, VoteProofInputs } from './types' import { generateMerkleProof, toBinary, extractSignatureComponents, getAddressFromSignature, getOptimalThreadCount } from './utils' -import { MAXIMUM_VOTE_VALUE, MASK_SIGNATURE, SIGNATURE_MESSAGE_HASH } from './constants' +import { MAXIMUM_VOTE_VALUE, MASK_SIGNATURE } from './constants' import { Noir, type CompiledCircuit } from '@noir-lang/noir_js' import { UltraHonkBackend, type ProofData } from '@aztec/bb.js' import circuit from '../../../circuits/target/crisp_circuit.json' From 5ff7cd3297b7e4e749dda740b97665f2db5003b6 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 18 Dec 2025 12:38:28 +0100 Subject: [PATCH 3/7] refactor: remove dead code --- .../client/src/assets/icons/arrowRight.svg | 10 - .../client/src/assets/icons/calendarCheck.svg | 14 - .../client/src/assets/icons/caretCircle.svg | 11 - .../CRISP/client/src/assets/icons/check.svg | 10 - .../CRISP/client/src/assets/icons/close.svg | 4 - .../client/src/assets/icons/enclaveLogo.svg | 6 - .../client/src/assets/icons/fingerprint.svg | 15 -- .../src/assets/icons/fingerprintWhite.svg | 15 -- .../CRISP/client/src/assets/icons/logout.svg | 12 - .../client/src/assets/icons/notebook.svg | 13 - .../CRISP/client/src/assets/icons/paper.svg | 13 - .../client/src/assets/icons/paperPurple.svg | 13 - .../client/src/components/CodeTextDisplay.tsx | 67 ----- .../CRISP/client/src/components/Modal.tsx | 56 ---- examples/CRISP/client/src/globals.css | 3 - .../src/hooks/generic/useLocalStorage.tsx | 33 --- examples/CRISP/client/src/model/vote.model.ts | 8 - .../CRISP/client/src/pages/About/About.tsx | 1 - examples/CRISP/client/src/utils/network.ts | 10 - .../CRISP/client/src/utils/proof-encoding.ts | 15 -- examples/CRISP/client/src/utils/whitepaper.ts | 241 ------------------ 21 files changed, 570 deletions(-) delete mode 100644 examples/CRISP/client/src/assets/icons/arrowRight.svg delete mode 100644 examples/CRISP/client/src/assets/icons/calendarCheck.svg delete mode 100644 examples/CRISP/client/src/assets/icons/caretCircle.svg delete mode 100644 examples/CRISP/client/src/assets/icons/check.svg delete mode 100644 examples/CRISP/client/src/assets/icons/close.svg delete mode 100644 examples/CRISP/client/src/assets/icons/enclaveLogo.svg delete mode 100644 examples/CRISP/client/src/assets/icons/fingerprint.svg delete mode 100644 examples/CRISP/client/src/assets/icons/fingerprintWhite.svg delete mode 100644 examples/CRISP/client/src/assets/icons/logout.svg delete mode 100644 examples/CRISP/client/src/assets/icons/notebook.svg delete mode 100644 examples/CRISP/client/src/assets/icons/paper.svg delete mode 100644 examples/CRISP/client/src/assets/icons/paperPurple.svg delete mode 100644 examples/CRISP/client/src/components/CodeTextDisplay.tsx delete mode 100644 examples/CRISP/client/src/components/Modal.tsx delete mode 100644 examples/CRISP/client/src/hooks/generic/useLocalStorage.tsx delete mode 100644 examples/CRISP/client/src/utils/network.ts delete mode 100644 examples/CRISP/client/src/utils/proof-encoding.ts delete mode 100644 examples/CRISP/client/src/utils/whitepaper.ts diff --git a/examples/CRISP/client/src/assets/icons/arrowRight.svg b/examples/CRISP/client/src/assets/icons/arrowRight.svg deleted file mode 100644 index ffc15027d3..0000000000 --- a/examples/CRISP/client/src/assets/icons/arrowRight.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/calendarCheck.svg b/examples/CRISP/client/src/assets/icons/calendarCheck.svg deleted file mode 100644 index 9aee725bd8..0000000000 --- a/examples/CRISP/client/src/assets/icons/calendarCheck.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/caretCircle.svg b/examples/CRISP/client/src/assets/icons/caretCircle.svg deleted file mode 100644 index d824825cb6..0000000000 --- a/examples/CRISP/client/src/assets/icons/caretCircle.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/check.svg b/examples/CRISP/client/src/assets/icons/check.svg deleted file mode 100644 index c8c4161a93..0000000000 --- a/examples/CRISP/client/src/assets/icons/check.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/close.svg b/examples/CRISP/client/src/assets/icons/close.svg deleted file mode 100644 index 15160d6cce..0000000000 --- a/examples/CRISP/client/src/assets/icons/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/CRISP/client/src/assets/icons/enclaveLogo.svg b/examples/CRISP/client/src/assets/icons/enclaveLogo.svg deleted file mode 100644 index e846d302b5..0000000000 --- a/examples/CRISP/client/src/assets/icons/enclaveLogo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/fingerprint.svg b/examples/CRISP/client/src/assets/icons/fingerprint.svg deleted file mode 100644 index 1ffc71ebdf..0000000000 --- a/examples/CRISP/client/src/assets/icons/fingerprint.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/fingerprintWhite.svg b/examples/CRISP/client/src/assets/icons/fingerprintWhite.svg deleted file mode 100644 index b1b57dc37b..0000000000 --- a/examples/CRISP/client/src/assets/icons/fingerprintWhite.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/logout.svg b/examples/CRISP/client/src/assets/icons/logout.svg deleted file mode 100644 index b0dabed2c3..0000000000 --- a/examples/CRISP/client/src/assets/icons/logout.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/notebook.svg b/examples/CRISP/client/src/assets/icons/notebook.svg deleted file mode 100644 index 559075cc32..0000000000 --- a/examples/CRISP/client/src/assets/icons/notebook.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/paper.svg b/examples/CRISP/client/src/assets/icons/paper.svg deleted file mode 100644 index ef186ab16d..0000000000 --- a/examples/CRISP/client/src/assets/icons/paper.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/examples/CRISP/client/src/assets/icons/paperPurple.svg b/examples/CRISP/client/src/assets/icons/paperPurple.svg deleted file mode 100644 index c9847af824..0000000000 --- a/examples/CRISP/client/src/assets/icons/paperPurple.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/examples/CRISP/client/src/components/CodeTextDisplay.tsx b/examples/CRISP/client/src/components/CodeTextDisplay.tsx deleted file mode 100644 index eea6eb4f68..0000000000 --- a/examples/CRISP/client/src/components/CodeTextDisplay.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -import React, { useState } from 'react' -import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter' -import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism' -import PaperIcon from '@/assets/icons/paper.svg' -import PaperPurpleIcon from '@/assets/icons/paperPurple.svg' -import FingerprintIcon from '@/assets/icons/fingerprint.svg' -import FingerprintWhiteIcon from '@/assets/icons/fingerprintWhite.svg' - -const selectedClass = 'border-slate-600/80 flex space-x-2 rounded-lg border-2 bg-white px-4 py-2' -const unSelectedClass = 'flex space-x-2 rounded-lg border-2 border-slate-600/20 bg-[#B7BBC1] px-4 py-2' - -const CodeTextDisplay: React.FC = () => { - const text = `import React from 'react' - - interface CardContentProps { - children: React.ReactNode - } - - const CardContent: React.FC = ({ children }) => { - return ( -
- {children} -
- ) - } - - export default CardContent - ` - - const [isCipher, setIsCipher] = useState(true) - - return ( -
-
- - -
-
- {text ? ( - isCipher ? ( - - {text} - - ) : ( - text - ) - ) : ( - 'Loading...' - )} -
-
- ) -} - -export default CodeTextDisplay diff --git a/examples/CRISP/client/src/components/Modal.tsx b/examples/CRISP/client/src/components/Modal.tsx deleted file mode 100644 index 151391460b..0000000000 --- a/examples/CRISP/client/src/components/Modal.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -// Modal.tsx -import React, { useEffect, useRef, useCallback, FC } from 'react' - -interface ModalProps { - show: boolean - onClose: () => void - children: React.ReactNode - className?: string | undefined -} - -const Modal: FC = ({ show, onClose, children, className }) => { - const modalRef = useRef(null) - - const closeModal = (e: React.MouseEvent) => { - if (modalRef.current === e.target) { - onClose() - } - } - - const keyPress = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Escape' && show) { - onClose() - } - }, - [show, onClose], - ) - - useEffect(() => { - document.addEventListener('keydown', keyPress) - return () => document.removeEventListener('keydown', keyPress) - }, [keyPress]) - - if (!show) return null - - return ( -
-
- {children} - -
-
- ) -} - -export default Modal diff --git a/examples/CRISP/client/src/globals.css b/examples/CRISP/client/src/globals.css index f51899eaa6..b2a237bccd 100644 --- a/examples/CRISP/client/src/globals.css +++ b/examples/CRISP/client/src/globals.css @@ -179,9 +179,6 @@ footer { .external-icon { @apply icon h-[14px] w-[14px] bg-[url('./assets/icons/arrow.svg')]; } - .close-icon { - @apply icon h-[18px] w-[18px] bg-[url('./assets/icons/close.svg')]; - } /* Modals */ .modal-overlay { diff --git a/examples/CRISP/client/src/hooks/generic/useLocalStorage.tsx b/examples/CRISP/client/src/hooks/generic/useLocalStorage.tsx deleted file mode 100644 index fe9211ce8b..0000000000 --- a/examples/CRISP/client/src/hooks/generic/useLocalStorage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -import { useState } from 'react' - -function useLocalStorage(key: string, initialValue: T) { - const [storedValue, setStoredValue] = useState(() => { - try { - const item = window.localStorage.getItem(key) - return item ? JSON.parse(item) : initialValue - } catch (error) { - console.log(error) - return initialValue - } - }) - - const setValue = (value: T | ((val: T) => T)) => { - try { - const valueToStore = value instanceof Function ? value(storedValue) : value - setStoredValue(valueToStore) - window.localStorage.setItem(key, JSON.stringify(valueToStore)) - } catch (error) { - console.log(error) - } - } - - return [storedValue, setValue] as const -} - -export default useLocalStorage diff --git a/examples/CRISP/client/src/model/vote.model.ts b/examples/CRISP/client/src/model/vote.model.ts index e72c32df37..190bf2e106 100644 --- a/examples/CRISP/client/src/model/vote.model.ts +++ b/examples/CRISP/client/src/model/vote.model.ts @@ -4,14 +4,6 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -export interface VotingConfigRequest { - round_id: number - chain_id: number - voting_address: string - ciphernode_count: number - voter_count: number -} - export interface VotingRound { round_id: number pk_bytes: number[] diff --git a/examples/CRISP/client/src/pages/About/About.tsx b/examples/CRISP/client/src/pages/About/About.tsx index 82de4f197f..53f08068e4 100644 --- a/examples/CRISP/client/src/pages/About/About.tsx +++ b/examples/CRISP/client/src/pages/About/About.tsx @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import React from 'react' -// import CircleIcon from '@/assets/icons/caretCircle.svg' import CardContent from '@/components/Cards/CardContent' import CircularTiles from '@/components/CircularTiles' diff --git a/examples/CRISP/client/src/utils/network.ts b/examples/CRISP/client/src/utils/network.ts deleted file mode 100644 index f6dd6df6a6..0000000000 --- a/examples/CRISP/client/src/utils/network.ts +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -export enum Chain { - MAINNET = 1, - SEPOLIA = 11155111, -} diff --git a/examples/CRISP/client/src/utils/proof-encoding.ts b/examples/CRISP/client/src/utils/proof-encoding.ts deleted file mode 100644 index 825fa62549..0000000000 --- a/examples/CRISP/client/src/utils/proof-encoding.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -import { encodeAbiParameters, parseAbiParameters, bytesToHex } from 'viem' - -const crispAbi = parseAbiParameters('(bytes, bytes32[], bytes)') - -export const encodeCrispInputs = (noirProof: Uint8Array, noirPublicInputs: string[], encryptedVote: Uint8Array): string => { - return encodeAbiParameters(crispAbi, [ - [bytesToHex(noirProof), noirPublicInputs.map((input) => input as `0x${string}`), bytesToHex(encryptedVote)], - ]) -} diff --git a/examples/CRISP/client/src/utils/whitepaper.ts b/examples/CRISP/client/src/utils/whitepaper.ts deleted file mode 100644 index b1b615d40d..0000000000 --- a/examples/CRISP/client/src/utils/whitepaper.ts +++ /dev/null @@ -1,241 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -export const markdown = ` - # Enclave: Encrypted Execution Environments (E3) - ## White Paper - Authors: Auryn Macmillan, Nathan Ginnever, Marvin Lin - - ### Abstract - This white paper introduces Enclave, an open-source protocol for Encrypted Execution Environments (E3) enabling privacy-preserving applications. Enclave integrates Fully Homomorphic Encryption (FHE) with a unique combination of threshold cryptography, zero-knowledge proofs (ZKPs), and a committee of staked nodes to enable secure computations on encrypted data without compromising confidentiality. In addition to Enclave, the paper also details its flagship offering: an implementation of CRISP (Collusion-Resistant Impartial Selection Protocol), a Secret Ballot protocol aimed at preventing collusion and mitigating vulnerabilities in governance using Enclave. We conclude with notes on limitations and comparisons between CRISP and the Minimal Anti-Collusion Infrastructure (MACI). - - ### Introduction - In a digital landscape fraught with vulnerabilities and privacy concerns, safeguarding sensitive information while maximising its utility has become a paramount concern for individuals and organisations globally. The inability of existing systems to sufficiently address these vulnerabilities highlights the need for robust data privacy and computational integrity measures, which would allow valuable insights to be derived from data without exposing private information. It is within this context that we introduce Enclave: an open-source protocol for Encrypted Execution Environments (E3) enabling organisations to create the next generation of privacy-preserving applications. - - Enclave represents an advancement in the field of privacy-preserving technologies, offering comprehensive infrastructure for organisations to simultaneously safeguard and leverage private data. By integrating multiparty FHE with a unique combination of threshold cryptography, zero-knowledge proofs (ZKPs), and token economics, Enclave enables secure computations on encrypted data without exposing the underlying information. It is designed to accommodate any use case where multiple users encrypt private data to preserve confidentiality, with strong guarantees that the inputs and intermediate states cannot be revealed. - - At the heart of Enclave's operational model lies its approach to data-processing enclaves, secured by a committee of staked nodes. These nodes, entrusted with overseeing data-processing operations, form a decentralized network that ensures the integrity of computations and the confidentiality of private data through a consensus mechanism driven by stakeholder participation. By utilising staking mechanisms, proportional fees, and punitive measures, this framework not only enhances transparency and accountability in the data-processing environment, but also provides strong economic and cryptographic guarantees around privacy, liveness, and computational integrity. - - In this white paper, we delve into the core principles and functionalities of Enclave, including a detailing of our flagship offering: CRISP (Collusion-Resistant Impartial Selection Protocol), a Secret Ballot protocol capable of preventing collusion and mitigating vulnerabilities in governance and decision-making systems. By exploring the intricacies of how our technology works with CRISP, we demonstrate how the flexibility of Enclave can be leveraged by any organisation for their specific use cases and needs, upholding the principles of privacy, integrity, and trust in their data-driven initiatives while maximising the value derived from their data assets. - - ## Contents - - [Overview](#overview) - - [Preamble](#preamble) - - [Structure of Enclave](#structure-of-enclave) - - [Actors](#actors) - - [Phases](#phases) - - [Cancellation](#cancellation) - - [Staking](#staking) - - [Fees](#fees) - - [Penalties](#penalties) - - [Inactive Node](#inactive-node) - - [Failure To Report Outcome](#failure-to-report-outcome) - - [Failure To Decrypt](#failure-to-decrypt) - - [Non-Deterministic Computation](#non-deterministic-computation) - - [Intermediate Decryption](#intermediate-decryption) - - [Forced Decommissioning](#forced-decommissioning) - - [Governance](#governance) - - [Flagship Use Case: Secret Ballots](#flagship-use-case-secret-ballots) - - [Preamble](#preamble-1) - - [Structure of CRISP Secret Ballots](#structure-of-crisp-secret-ballots) - - [Setup](#setup) - - [Poll Creation](#poll-creation) - - [Voter Registration](#voter-registration) - - [Voting](#voting) - - [Key Switching](#key-switching) - - [Computing and Publishing Results](#computing-and-publishing-results) - - [Triggering Onchain Actions](#triggering-onchain-actions) - - [Mitigation of Common Attacks](#mitigation-of-common-attacks) - - [Limitations](#limitations) - - [Corrupt Registry](#corrupt-registry) - - [Comparison to MACI](#comparison-to-maci) - - [Contributors](#contributors) - - ### Overview - #### Preamble - Enclave is an open-source protocol for Encrypted Execution Environments (E3) that enables organisations to simultaneously safeguard and leverage private data in their applications. Through advanced cryptography, privacy-preserving mechanism design, and multiparty computation, Enclave provides strong economic and cryptographic guarantees around privacy, liveness, and computational integrity. - - These guarantees are due largely to the unique structure of Enclave, which utilises data-processing enclaves composed of five groups of actors overseeing the initiation, execution, and decryption of computations through a multiphase process. Additionally, by pairing cryptographic assurances with staking mechanisms, proportional fees, punitive measures, and built-in governance, Enclave is designed to be resilient and adaptable, offering a general solution to build privacy-preserving applications suitable for various sectors, use cases, and organisational needs. - - ### Structure of Enclave - #### Actors - There are five groups of actors in Enclave: - - Token Holders: As the top-level governance body, Enclave token holders are responsible for setting protocol parameters, overseeing protocol upgrades, and facilitating dispute resolution. - - Execution Modules: Enclave is a modular framework, allowing the choice of many different Execution Modules in which to run encrypted computations. Broadly, Execution Modules fall into two categories: (1) Provable (like RISC Zero’s virtual machine or Arbitrum’s WAVM) and (2) Oracle-based. The former provides cryptographic guarantees of correct execution, while the latter provides economic guarantees of correct execution. - - Cypher Nodes: Cypher Nodes are responsible for creating threshold public keys and decrypting the cyphertext output for each requested computation. Cypher Nodes can be registered by anyone staking Enclave tokens. - - Requesters: Anyone can request an E3 from the Enclave network by calling the corresponding smart contract entrypoint and depositing a bond proportional to the number, threshold, and duration of Cypher Nodes that they request. - - Data Providers: Individuals and systems providing inputs to a requested E3. Data Providers contribute data encrypted to the public threshold key that is created by and published on chain by the Cypher Nodes selected for a requested E3. - - #### Phases - Enclave leverages a multiphase process to facilitate the interaction between its various Actors, ultimately producing publicly verifiable outputs with strong economic guarantees around data privacy. Figure 1 shows a visual description of these interactions. - - ![CRISP Diagram](/#/crisp-diagram.webp) - - ### Figure 1 - 1. **Request & Bond** - Requesters can request an FHE computation from Enclave at any time. To request a computation, the following must be defined: - - The FHE computation to be performed - - The Execution Module for the computation, along with any additional parameters required - - The quantity Cypher Nodes to be selected - - The threshold for each type of node that must agree on the computed outputs - - The timestamp for the Input Deadline, after which no new inputs will be accepted - - The duration for which the nodes must be available - Along with the computation request, Requesters must also provide a proportional bond to ensure a minimum reward for the requested nodes for performing their duties. Anyone can add to this bond at any point prior to Decryption, the final phase in the process. - - 2. **Node Selection** - When a computation is requested, the required number of Cypher Nodes are selected from the pool of available nodes via sortition. The selected Cypher Nodes immediately generate and publish shared public keys with the requested thresholds. - - 3. **Input Window** - Once the selected Cypher Nodes have published their shared public key, Data Providers can create and publish their encrypted inputs to the computation up until the Input Deadline. - To publish an input, one must provide both the input and a corresponding zero-knowledge proof (ZKP) to ensure the input is valid for the requested computation. A ZKP is required to ensure the Data Provider knows the plaintext of the input, that the input is correctly formed, that the input cannot be used in any other context, and that the input passes any other validation logic required by the selected computation. - - 4. **Metering** - With inputs finalised, the fee for the requested computation can be calculated according to the Execution Module’s metering method. Computation will not proceed until the bond for the requested computation is equal to or greater than the published fee. See the Fees section for details on how fees are calculated. - - 5. **Computation** - Once the fee has been published, the selected Execution Module runs the computation and provides the cyphertext output. - - 6. **Decryption** - Once the selected Execution Module provides the cyphertext output of a requested computation, a threshold of the Cypher Nodes must collectively decrypt and publish the output of the computation. Once the output of the computation is revealed, Cypher Node duties are complete. Each node can then claim their proportional share of the bond and should dispose of the keys used for this committee, treating them as toxic waste. - - ### Cancellation - If a requested computation cannot be completed (for example, if the output turns out to be non-deterministic), then the selected Execution Module can cancel the computation. In this case, a portion of the bond is used to purchase and burn CRISP tokens, and the remainder is paid to the selected Cypher Nodes. - - ### Staking - Anyone can register a new Cypher Node by staking an amount of CRISP tokens. After registration, new nodes must wait for a registration delay period before they can be selected for Cypher duties. The registration delay period enables Requesters to reasonably predict the current Cypher Node set when requesting a computation. - Nodes can request to be decommissioned at any time. Prior to being decommissioned, nodes must remain active for a decommission delay period, after which they will no longer be selected for duties. Nodes must also remain active to complete any duties for which they have been selected. Once its decommission delay has passed and all assigned duties have been completed, a node may be decommissioned and its staking deposit returned. - - ### Fees - When requesting a computation from Enclave, Requesters must first deposit a bond proportional to the number and threshold of Cypher Nodes requested, the duration for which they are required to be available, and any additional costs required by the selected Execution Module. This deposit reserves the requested Cypher Nodes, providing an economic guarantee that they will be online to decrypt the output of the requested computation. However, the deposit does not pay for the computation itself. Different execution environments will have different fee structures. - - Cypher Nodes are subject to a penalty and forcefully decommissioned if they are proven to have provided their share of the decryption data from any input or intermediate states for a computation on which they were a committee member. To ensure there is a long-term disincentive for intermediate decryption, even for decommissioned Cypher Nodes, a portion of the Cypher Node rewards are paid out immediately after Decryption, with the remaining portion subject to a cliff and vesting schedule, along with slashing conditions. - - ### Penalties - There are numerous slashing conditions for Cypher Nodes, with penalties ranging from loss of rewards for a given duty round to forced decommissioning. The cases and penalties are detailed below. - - #### Inactive Node - Cypher Nodes that fail to provide their share of the data necessary for decrypting a requested computation forfeit their share of the decryption fee and are also subject to a small penalty used to cover the additional gas cost incurred due to their missing signature. - - #### Failure To Report Outcome - If the selected Execution Module does not provide the cyphertext output of the requested computation in a timely manner, the computation request is cancelled. In this case, a small portion of the bond is paid out to the selected Cypher Node committee, while the remainder of the bond for the computation is returned to the requester. - - #### Failure To Decrypt - If the selected Cypher Node committee does not provide the decrypted plaintext from the cyphertext output agreed on by the selected Execution Module in a timely manner, the computation request is cancelled. In this case, the bond for the computation is returned to the Requester. Any nodes that did not provide their share of the decryption data are subject to a penalty and are forcefully decommissioned. Penalties collected are split proportionally between the remaining members of the selected Cypher Node committee. - - #### Non-Deterministic Computation - In order for nodes to reach consensus on the output, a requested computation must be deterministic. If the selected Execution Module reports that the requested computation has a non-deterministic output, the bond for that output is partially distributed between the selected nodes and partially used to purchase and burn CRISP tokens. - - #### Intermediate Decryption - Maintaining the privacy of all encrypted inputs and intermediary states is a critical feature of Enclave. Attempts to decrypt anything encrypted to a Cypher Node committee, except the agreed upon output of a requested computation, is punishable by forced decommissioning, along with slashing a portion of the offending node’s stake. A portion of the slashed stake is burned, while the remaining portion is allocated to the account that notified the network of the slashable offence. - - #### Forced Decommissioning - If a node’s effective stake is ever reduced to half of the minimum stake, it is immediately decommissioned and will no longer be selected for duties. However, nodes must also remain active to complete any duties for which they had already been selected. Once all assigned duties have been completed, a forcefully decommissioned node may claim the remainder of its staking deposit. - - ### Governance - Enclave has several variables that may require periodic adjustments to maintain the protocol's fairness, performance, and responsiveness to evolving requirements. The setting of these variables is controlled by Enclave governance, which is responsible for protocol upgrades, dispute resolution, and the following parameter settings: - - cancellation burn percentage - - staking deposit amounts - - registration delay period - - decommission delay period - - inactive node penalty amounts - - intermediate slashing penalty and burn ratio - - ### Flagship Use Case: Secret Ballots - #### Preamble - Collusion-Resistant Impartial Selection Protocol (CRISP) is a strategic response to the persisting challenges in contemporary decision-making systems. Collusion, data breaches, and compromised privacy continue to undermine governance and decision-making, necessitating the development of an advanced protocol capable of mitigating potential vulnerabilities while also preventing forms of collusion. - - To address these threats, CRISP reconfigures current decision-making paradigms using Enclave. Serving as a modern embodiment of the secret (Australian) ballot, the protocol leverages Enclave to align economic incentives with the goals of fairness, transparency, and integrity, cultivating robust and equitable decision-making environments. - - #### Structure of CRISP Secret Ballots - This section details the components built atop Enclave’s general-purpose core in order to enable modern, collusion-resistant ballots. This includes smart contracts that mediate Requester and Data Provider interactions with Enclave, along with a vote-tallying computation to be run via Enclave. - - The CRISP implementation includes a Zodiac-compatible module that can be used to control any contract account that conforms to the Zodiac IAvatar interface; a Safe, for example. - - #### Setup - To enable a compatible account for control by CRISP secret ballots, one must deploy a CRISP module and activate it on the account to be controlled. This process involves specifying the following: - - The account to be controlled by the module. - - The ID of the designated vote-tallying computation. - - The designated execution environment, along with any additional parameters. - - The required number and threshold of Cypher Nodes. - - The duration for each poll. - - The address of a voter registry contract. - The specific steps for enabling a module on a smart contract account may differ between implementations and are not detailed in this document. Once the CRISP module is enabled on the account, it can be utilised to create polls that can ultimately trigger the account to make any arbitrary call. - - #### Poll Creation - Anyone can propose a transaction to be executed by a smart contract account with a CRISP module enabled. To do so, one must call a function on the CRISP module and provide the following: the hash of the proposal description, the hash of the transaction payload, and the bond for the computation required by the CRISP network. - - This will register the proposal in the CRISP module and request the computation from the CRISP network, using the threshold and duration parameters defined in the CRISP module’s setup. - - #### Voter Registration - In each poll, every voter must have a cryptographic keypair to cast their vote, which must be registered in a Voter Registry smart contract. This contract is responsible for: - - Specifying the necessary proof for validating messages in the FHE computation. - - Enforcing any voter eligibility criteria mandated by the poll. - - Determining the vote weight for each registered voter. - For example, a Voter Registry may require that voters hold a minimum amount of a token or be provably a member on a predefined list of eligible voters. Importantly, voting keys are distinct from the keypairs a voter might use to otherwise interact onchain; voting keys are single-use keys specific to the current poll, disposable once the poll concludes, and should not be reused for other polls or in other contexts. - - #### Voting - Votes are submitted directly to the CRISP smart contract as cyphertext encrypted to the shared key provided by the selected Cypher Nodes, along with a zero-knowledge proof that the encrypted message represents a valid vote format was signed by a registered voter, and cannot be re-used in any other proposal. This setup allows any account to submit a vote message on behalf of any user. - - Voters have the option to change their vote at any time prior to the Input Deadline by submitting an encrypted message to the CRISP smart contract. Only the latest message will be counted in the tallied results. - - #### Key Switching - Voters have the option to change their voting keys at any time prior to the Input Deadline by submitting a correctly formatted key change message to the CRISP smart contract, encrypted to the private key provided by the selected Cypher Nodes. Only messages signed by a voter’s most recent valid key will be counted in the tallied results. - - #### Computing and Publishing Results - Once the Input Deadline has passed, the selected Execution Module will compute the tally cyphertext, and the selected Cypher Nodes will both decrypt the output and post the hash of plaintext results onchain. - - #### Triggering Onchain Actions - Once the selected Cypher Nodes post the hash of the plaintext results onchain, anyone can call a function to execute the attached transaction payloads. This execution requires providing proof of the results supplied by the selected Cypher Nodes. - - #### Mitigation of Common Attacks - Our voting system built on CRISP is designed to be resilient against a host of practical and theoretical attacks. This section details several of these attack vectors, along with the corresponding mitigations in our voting implementation. - - In each scenario, Alice and Bob are both registered voters. Alice and Bob are both outspoken supporters of the Banana Party. Chuck runs the web service through which voters submit their votes in polls and is also a supporter of the Durian Party. Other characters may also be introduced. - - ##### Censorship - Knowing that Alice and Bob will each likely vote for the Banana Party, Chuck chooses to ignore Alice’s and Bob’s votes when they are submitted to his web service via the voting application, neglecting to post the votes on chain. - - As Chuck was unable to provide a valid transaction receipt to Alice and Bob showing that their votes had been submitted onchain, Alice and Bob can choose to directly post their votes to the CRISP contract without permission or censorship from any intermediaries, like Chuck. By submitting their votes directly, or via an alternate relaying service, Alice and Bob are able to circumvent Chuck’s attempt to censor their votes. - - Chuck could also attempt to mount a similar attack attempting to deny voter registration for any given poll, but the mitigation would be similar. - - ##### Receipt Sharing - Knowing that Alice and Bob will each likely vote for the Banana Party, Chuck decides to offer Alice a bribe to vote for the Durian Party, rather than her original preference. Chuck offers to pay the bribe to Alice on the condition that Alice can prove how they voted. - - Alice’s optimal behaviour is to accept the bribe from Chuck, vote in the same way they would have without the bribe, and simply supply Chuck with a fake proof that is indistinguishable from a legitimate vote. To do this, Alice could cast a vote for the Durian party, then submit a keychange message to change their voting key, invalidating their previous vote, and then finally cast a vote for the Banana party with the new key. Alice can share the receipt of the first vote with Chuck. However, Chuck has no way of guaranteeing whether the key used to cast the vote was Alice’s valid voting key. So Chuck must simply take Alice’s word for it. - - ##### Proxy Voting - Knowing that Alice and Bob will each likely vote for the Banana Party, Chuck decides to offer Alice a bribe to vote for the Durian Party, rather than her original preference. Chuck offers to pay the bribe to Alice on the condition that Chuck is granted permission to cast Alice’s vote. To comply, Alice must submit a key change message switching her voting key to one supplied by Chuck. - - As with the case of receipt sharing, Alice’s optimal behaviour is to accept Chuck’s bribe and to share the receipt of a corresponding key change message with Chuck, after having already switched voting keys to another key unknown to Chuck. Unfortunately for Chuck, there is no way to ensure that Alice has not previously registered or switched to a different voting key, and no way to determine if the message to change keys to Chuck’s voting key is in fact invalid. - - ##### Forced Abstention - Knowing that Alice and Bob will each likely vote for the Banana Party, Chuck decides to force both Alice and Bob to abstain from voting (either by bribery or more coercive forms of collusion). To comply with Chuck’s demand, Alice and Bob must not be caught registering for or casting a vote in the poll. - - Alice and Bob’s optimal behaviour is to vote as normal, while taking care to not leave any identifiable traces. When registering and submitting votes onchain, Alice and Bob can submit messages through a relayer other than the one controlled by Chuck or from a fresh address which cannot be linked to their identity. Registration involves providing a proof that the voter is on the registry, but does not require the voter to identify themselves in plaintext or submit the registration message from a specific account. Similarly, when casting a vote, nothing identifiable is published in plaintext, and no cyphertext aside from the result will ever be decrypted by the Cypher Nodes. - - ### Limitations - #### Corrupt Registry - Like any other voting implementation, CRISP is dependent on a functioning voter registry. If the voter registration process is compromised, allowing an attacker to either deny registration or take control of a voter’s account before they select their voter keys, then the attacker can successfully corrupt the system. - - The latter type of attack could be mitigated in a variety of ways, depending on the scope and trust assumptions appropriate for a given poll. However, a general rule of thumb is to ensure there is enough other value associated with each voter's account that they would be unwilling to share the credentials with a third party. At the very least, this makes such attacks more costly and less scalable. - - ### Comparison to MACI - CRISP’s design is heavily inspired by MACI, a protocol originally proposed by Vitalik Buterin for collusion-resistant voting leveraging zero-knowledge proofs (ZKPs). CRISP differs from MACI primarily by employing fully homomorphic encryption (FHE) and threshold cryptography. This approach allows CRISP to establish an arbitrarily large network of nodes for trust distribution, contrasting with MACI's reliance on a single trusted coordinator. - - While MACI relies on an honest coordinator assumption for privacy, meaning the coordinator has unrestricted access to all of the inputs and intermediate states and is trusted to not divulge them, CRISP provides strong economic guarantees around privacy, as Cypher Nodes are subject to slashing if anyone can prove that they’ve attempted to decrypt any input or intermediate cyphertext in a computation. - - ### Contributors - A special thank you to the following people for their early contributions to, and reviews of, the CRISP white paper. - - - Alex Espinosa - - Anthony Leutenegger - - Disruption Joe - - Koh Wei Jie - - Mike Chan - - Vitalik Buterin - - Yuet Loo Wong - ` From 80a3ff143827ed590c9b4e3ae02d3c5cbaa61102 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 18 Dec 2025 17:11:51 +0100 Subject: [PATCH 4/7] refactor: update expiration check --- examples/CRISP/client/src/App.tsx | 11 ++++++--- .../client/src/components/Cards/PollCard.tsx | 17 ++++++++------ .../pages/Landing/components/DailyPoll.tsx | 22 ++++++++++++++---- .../client/src/providers/Web3Provider.tsx | 23 ++++++++----------- examples/CRISP/client/src/utils/methods.ts | 14 ++++------- 5 files changed, 49 insertions(+), 38 deletions(-) diff --git a/examples/CRISP/client/src/App.tsx b/examples/CRISP/client/src/App.tsx index c8705fdaf8..512e796f83 100644 --- a/examples/CRISP/client/src/App.tsx +++ b/examples/CRISP/client/src/App.tsx @@ -8,6 +8,7 @@ import React, { Fragment, useEffect } from 'react' import { Routes, Route, Navigate } from 'react-router-dom' import Navbar from '@/components/Navbar' import Footer from '@/components/Footer' +import { useSwitchChain } from 'wagmi' //Pages import Landing from '@/pages/Landing/Landing' import DailyPoll from '@/pages/DailyPoll/DailyPoll' @@ -17,16 +18,20 @@ import PollResult from '@/pages/PollResult/PollResult' import RoundPoll from '@/pages/RoundPoll' import useScrollToTop from '@/hooks/generic/useScrollToTop' import { useVoteManagementContext } from '@/context/voteManagement' +import { getChain } from './utils/methods' const App: React.FC = () => { useScrollToTop() const { initialLoad } = useVoteManagementContext() + const { switchChain } = useSwitchChain() useEffect(() => { - async function loadWasm() { + ;(async () => { await initialLoad() - } - loadWasm() + + const chain = getChain() + switchChain({ chainId: chain.id }) + })() }, []) return ( diff --git a/examples/CRISP/client/src/components/Cards/PollCard.tsx b/examples/CRISP/client/src/components/Cards/PollCard.tsx index 927827c7c8..25713063e0 100644 --- a/examples/CRISP/client/src/components/Cards/PollCard.tsx +++ b/examples/CRISP/client/src/components/Cards/PollCard.tsx @@ -9,24 +9,27 @@ import { useNavigate } from 'react-router-dom' import { PollOption, PollResult } from '@/model/poll.model' import VotesBadge from '@/components/VotesBadge' import PollCardResult from '@/components/Cards/PollCardResult' -import { formatDate, hasPollEndedByTimestamp, markWinner } from '@/utils/methods' +import { formatDate, markWinner } from '@/utils/methods' import { useVoteManagementContext } from '@/context/voteManagement' +import { usePublicClient } from 'wagmi' const PollCard: React.FC = ({ roundId, options, totalVotes, date, endTime }) => { const navigate = useNavigate() const [results, setResults] = useState(options) - const [isActive, setIsActive] = useState(!hasPollEndedByTimestamp(endTime)) + const [isActive, setIsActive] = useState(true) const { roundState, setPollResult, currentRoundId } = useVoteManagementContext() + const client = usePublicClient() const isCurrentRound = roundId === currentRoundId const displayVoteCount = isCurrentRound && isActive ? (roundState?.vote_count ?? totalVotes) : totalVotes useEffect(() => { - if (!isActive) return + if (!isActive || !client) return - const checkPollStatus = () => { - const pollEnded = hasPollEndedByTimestamp(endTime) - if (pollEnded) { + const checkPollStatus = async () => { + const block = await client.getBlock() + + if (block.timestamp >= endTime) { setIsActive(false) } } @@ -35,7 +38,7 @@ const PollCard: React.FC = ({ roundId, options, totalVotes, date, en const interval = setInterval(checkPollStatus, 1000) return () => clearInterval(interval) - }, [endTime, isActive]) + }, [endTime, client, isActive]) useEffect(() => { const newPollOptions = markWinner(options) diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx index 8badae8ccb..ca16d45c30 100644 --- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx @@ -4,18 +4,18 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Poll } from '@/model/poll.model' import Card from '@/components/Cards/Card' import CircularTiles from '@/components/CircularTiles' import { useVoteManagementContext } from '@/context/voteManagement' import LoadingAnimation from '@/components/LoadingAnimation' -import { hasPollEnded } from '@/utils/methods' import CountdownTimer from '@/components/CountdownTime' import { useModal } from 'connectkit' import { useVoteCasting } from '@/hooks/voting/useVoteCasting' import VotingStepIndicator from '@/components/VotingStepIndicator' +import { usePublicClient } from 'wagmi' type DailyPollSectionProps = { loading?: boolean @@ -25,13 +25,25 @@ type DailyPollSectionProps = { const DailyPollSection: React.FC = ({ loading, endTime, title = 'Daily Poll' }) => { const { user, pollOptions, setPollOptions, roundState, hasVotedInCurrentRound, voteStatusLoading } = useVoteManagementContext() - const isEnded = roundState ? hasPollEnded(roundState?.duration, roundState?.start_time) : false + const client = usePublicClient() + const [isEnded, setIsEnded] = useState(false) const [pollSelected, setPollSelected] = useState(null) const [noPollSelected, setNoPollSelected] = useState(true) const { setOpen } = useModal() const { castVoteWithProof, isLoading: isCastingVote, votingStep, lastActiveStep, stepMessage } = useVoteCasting() - const statusClass = !isEnded ? 'lime' : 'red' + useEffect(() => { + ;(async () => { + if (!client) return + if (!roundState) return + + const block = await client.getBlock() + + if (block.timestamp > roundState.expiration) { + setIsEnded(true) + } + })() + }, [roundState, client]) const handleChecked = (selectedPoll: Poll) => { const isAlreadySelected = pollSelected?.value === selectedPoll.value @@ -77,7 +89,7 @@ const DailyPollSection: React.FC = ({ loading, endTime, t {roundState && (
{!isEnded ? 'Live' : 'Ended'}
diff --git a/examples/CRISP/client/src/providers/Web3Provider.tsx b/examples/CRISP/client/src/providers/Web3Provider.tsx index d1d6c6a1ce..3a54781f1e 100644 --- a/examples/CRISP/client/src/providers/Web3Provider.tsx +++ b/examples/CRISP/client/src/providers/Web3Provider.tsx @@ -4,38 +4,35 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { WagmiProvider, createConfig } from 'wagmi' +import { WagmiProvider, createConfig, http } from 'wagmi' import { sepolia, anvil } from 'wagmi/chains' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ConnectKitProvider, getDefaultConfig } from 'connectkit' import React from 'react' - -type ConnectkitOptions = React.ComponentProps['options'] +import { getChain } from '@/utils/methods' const walletConnectProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || '' if (!walletConnectProjectId) console.warn('VITE_WALLETCONNECT_PROJECT_ID is not set in .env file. WalletConnect will not function properly.') -const chains = import.meta.env.DEV ? ([sepolia, anvil] as const) : ([sepolia] as const) - const config = createConfig( getDefaultConfig({ appName: 'CRISP', enableFamily: false, - chains, + chains: [getChain()], + transports: { + [anvil.id]: http(anvil.rpcUrls.default.http[0]), + [sepolia.id]: http(sepolia.rpcUrls.default.http[0]), + }, walletConnectProjectId: walletConnectProjectId, }), ) const queryClient = new QueryClient() -const options = import.meta.env.DEV - ? ({ - initialChainId: 0, - } as ConnectkitOptions) - : ({ - initialChainId: sepolia.id, - } as ConnectkitOptions) +// NOTE: ConnectKit doesn’t ship a drop-in chain switcher UI. This just sets the initial chain. +// For a user-facing switcher, implement a small component that calls wagmi’s `useSwitchChain`. +const options = { initialChainId: getChain().id } export const Web3Provider = ({ children }: { children: React.ReactNode }) => { return ( diff --git a/examples/CRISP/client/src/utils/methods.ts b/examples/CRISP/client/src/utils/methods.ts index a606354a8c..652a5852d3 100644 --- a/examples/CRISP/client/src/utils/methods.ts +++ b/examples/CRISP/client/src/utils/methods.ts @@ -6,6 +6,8 @@ import { PollOption, PollRequestResult, PollResult } from '@/model/poll.model' import { VoteStateLite } from '@/model/vote.model' +import { Chain, sepolia } from 'viem/chains' +import { hardhat } from 'viem/chains' export const markWinner = (options: PollOption[]) => { const highestVoteCount = Math.max(...options.map((o) => o.votes)) @@ -21,16 +23,8 @@ export const convertTimestampToDate = (timestamp: number, secondsToAdd: number = return date } -export const hasPollEnded = (pollLength: number, startTime: number): boolean => { - const endTime = (startTime + pollLength) * 1000 - const currentTime = Date.now() - return currentTime >= endTime -} - -export const hasPollEndedByTimestamp = (endTime: number): boolean => { - const endTimeMillis = endTime * 1000 - const currentTime = Date.now() - return currentTime >= endTimeMillis +export const getChain = (): Chain => { + return import.meta.env.DEV ? hardhat : sepolia } export const formatDate = (isoDateString: string): string => { From c9d158e4c072d2e594816e79babeca3e7922ea66 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 18 Dec 2025 17:14:24 +0100 Subject: [PATCH 5/7] chore: reduce wait time for E3 activation --- examples/CRISP/server/.env.example | 4 ++-- examples/CRISP/test/crisp.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index ef360227e0..16e696e2bc 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -20,10 +20,10 @@ FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config # Defines the time window during which an e3 can be activated -E3_WINDOW_SIZE=40 +E3_WINDOW_SIZE=30 # Defines the time interval during which users can submit their inputs # After this interval, the computation phase starts automatically -E3_DURATION=60 +E3_DURATION=40 E3_THRESHOLD_MIN=2 E3_THRESHOLD_MAX=5 diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 69409c410d..239b93a421 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -44,7 +44,7 @@ async function checkE3Activated(e3id: number): Promise { } } -async function waitForE3Activation(e3id: number, maxWaitMs: number = 60000): Promise { +async function waitForE3Activation(e3id: number, maxWaitMs: number = 30000): Promise { const startTime = Date.now() while (Date.now() - startTime < maxWaitMs) { const isActivated = await checkE3Activated(e3id) @@ -110,7 +110,7 @@ test('CRISP smoke test', async ({ context, page, metamaskPage, extensionId }) => await page.locator('button:has-text("Cast Vote")').click() log(`confirming MetaMask signature request...`) await metamask.confirmSignature() - const WAIT = 150_000 + const WAIT = 60_000 log(`waiting for ${WAIT}ms...`) await page.waitForTimeout(WAIT) log(`clicking historic polls button...`) From c388d3d326abb55dca7d26106f267fed460d16e9 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 18 Dec 2025 17:32:20 +0100 Subject: [PATCH 6/7] chore: increase wait time for E3 activation --- examples/CRISP/server/.env.example | 2 +- examples/CRISP/test/crisp.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 16e696e2bc..7fcffc68b3 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -23,7 +23,7 @@ FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" E3_WINDOW_SIZE=30 # Defines the time interval during which users can submit their inputs # After this interval, the computation phase starts automatically -E3_DURATION=40 +E3_DURATION=60 E3_THRESHOLD_MIN=2 E3_THRESHOLD_MAX=5 diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 239b93a421..2aef522011 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -110,7 +110,7 @@ test('CRISP smoke test', async ({ context, page, metamaskPage, extensionId }) => await page.locator('button:has-text("Cast Vote")').click() log(`confirming MetaMask signature request...`) await metamask.confirmSignature() - const WAIT = 60_000 + const WAIT = 80_000 log(`waiting for ${WAIT}ms...`) await page.waitForTimeout(WAIT) log(`clicking historic polls button...`) From 6a92f620ea6f30c40fc7a1aa8b62617a55307b1d Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 19 Dec 2025 11:08:23 +0100 Subject: [PATCH 7/7] refactor: solve coderabbit issues --- examples/CRISP/client/src/App.tsx | 19 ++++++++++++++++--- .../pages/Landing/components/DailyPoll.tsx | 6 +++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/CRISP/client/src/App.tsx b/examples/CRISP/client/src/App.tsx index 512e796f83..335d3794d3 100644 --- a/examples/CRISP/client/src/App.tsx +++ b/examples/CRISP/client/src/App.tsx @@ -18,19 +18,32 @@ import PollResult from '@/pages/PollResult/PollResult' import RoundPoll from '@/pages/RoundPoll' import useScrollToTop from '@/hooks/generic/useScrollToTop' import { useVoteManagementContext } from '@/context/voteManagement' +import { useNotificationAlertContext } from '@/context/NotificationAlert' +import { handleGenericError } from '@/utils/handle-generic-error' import { getChain } from './utils/methods' const App: React.FC = () => { useScrollToTop() const { initialLoad } = useVoteManagementContext() const { switchChain } = useSwitchChain() + const { showToast } = useNotificationAlertContext() useEffect(() => { ;(async () => { - await initialLoad() + try { + await initialLoad() - const chain = getChain() - switchChain({ chainId: chain.id }) + const chain = getChain() + switchChain({ chainId: chain.id }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + handleGenericError('App initial load', error instanceof Error ? error : new Error(errorMessage)) + showToast({ + type: 'danger', + message: 'Failed to initialize application. Please refresh the page.', + persistent: true, + }) + } })() }, []) diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx index ca16d45c30..ea4271316c 100644 --- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx @@ -89,7 +89,11 @@ const DailyPollSection: React.FC = ({ loading, endTime, t {roundState && (
{!isEnded ? 'Live' : 'Ended'}