Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions examples/CRISP/client/libs/crispSDKWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,49 @@
// without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.

import { hashLeaf, generateVoteProof, encodeSolidityProof } from '@crisp-e3/sdk'
import { hashLeaf, encodeSolidityProof, CrispSDK } from '@crisp-e3/sdk'

self.onmessage = async function (event) {
const { type, data } = event.data
switch (type) {
case 'generate_proof':
try {
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.
const balance = 1n
const vote = voteId === 0 ? { yes: 0n, no: balance } : { yes: balance, no: 0n }
const { e3Id, vote, publicKey, balance, address: slotAddress, signature, messageHash, isMasking, crispServer } = data

// todo: get the leaves from the server (pass them from the client).
const merkleLeaves = [
hashLeaf(address, balance),
hashLeaf(slotAddress, balance),
4720511075913887710172192848636076523165432993226978491435561065722130431597n,
14131255645332550266535358189863475289290770471998199141522479556687499890181n,
]

const proof = await generateVoteProof({
vote,
publicKey,
signature,
merkleLeaves,
balance,
previousCiphertext,
})
const sdk = new CrispSDK(crispServer)

let proof

if (isMasking) {
proof = await sdk.generateMaskVoteProof({
serverUrl: crispServer,
e3Id,
publicKey,
balance,
slotAddress,
merkleLeaves,
})
} else {
proof = await sdk.generateVoteProof({
serverUrl: crispServer,
vote,
e3Id,
publicKey,
signature,
merkleLeaves,
balance,
messageHash,
slotAddress,
})
}
Comment thread
ctrlc03 marked this conversation as resolved.

const encodedProof = encodeSolidityProof(proof)

self.postMessage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import type React from 'react'
import { ReactNode } from 'react'
import { BroadcastVoteRequest, BroadcastVoteResponse, VotingRound, VoteStateLite } from '@/model/vote.model'
import { BroadcastVoteRequest, BroadcastVoteResponse, VotingRound, VoteStateLite, Vote } from '@/model/vote.model'
import { Poll, PollRequestResult, PollResult } from '@/model/poll.model'

export type VoteStatus = {
Expand Down Expand Up @@ -38,11 +38,14 @@ export type VoteManagementContextType = {
setVotingRound: React.Dispatch<React.SetStateAction<VotingRound | null>>
setUser: React.Dispatch<React.SetStateAction<{ address: string } | null>>
generateProof: (
voteId: bigint,
e3Id: number,
vote: Vote,
publicKey: Uint8Array,
address: string,
balance: bigint,
signature: string,
previousCiphertext?: Uint8Array,
messageHash: `0x${string}`,
isMasking: boolean,
) => Promise<string | undefined>
broadcastVote: (vote: BroadcastVoteRequest) => Promise<BroadcastVoteResponse | undefined>
getRoundStateLite: (roundCount: number) => Promise<void>
Expand Down
13 changes: 9 additions & 4 deletions examples/CRISP/client/src/hooks/voting/useSDKWorker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import { useState, useEffect, useRef } from 'react'
import { handleGenericError } from '@/utils/handle-generic-error'
import { useNotificationAlertContext } from '@/context/NotificationAlert'
import { Vote } from '@/model/vote.model'

const ENCLAVE_API = import.meta.env.VITE_ENCLAVE_API
Comment thread
ctrlc03 marked this conversation as resolved.

export const useSDKWorkerHook = () => {
const { showToast } = useNotificationAlertContext()
Expand All @@ -26,11 +29,14 @@ export const useSDKWorkerHook = () => {
}, [])

const generateProof = async (
voteId: bigint,
e3Id: number,
vote: Vote,
publicKey: Uint8Array,
address: string,
balance: bigint,
signature: string,
previousCiphertext?: Uint8Array,
messageHash: `0x${string}`,
isMasking: boolean,
): Promise<string | undefined> => {
if (!workerRef.current) {
console.error('Worker not initialized')
Expand All @@ -42,9 +48,8 @@ export const useSDKWorkerHook = () => {

workerRef.current!.postMessage({
type: 'generate_proof',
data: { voteId, publicKey, address, signature, previousCiphertext },
data: { e3Id, vote, balance, publicKey, address, signature, messageHash, isMasking, crispServer: ENCLAVE_API },
})

workerRef.current!.onmessage = async (event) => {
const { type, success, encodedProof, error } = event.data

Expand Down
36 changes: 24 additions & 12 deletions examples/CRISP/client/src/hooks/voting/useVoteCasting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import { useSignMessage } from 'wagmi'
import { useVoteManagementContext } from '@/context/voteManagement'
import { useNotificationAlertContext } from '@/context/NotificationAlert/NotificationAlert.context.tsx'
import { Poll } from '@/model/poll.model'
import { BroadcastVoteRequest, VoteStateLite, VotingRound } from '@/model/vote.model'

import { encryptVote, SIGNATURE_MESSAGE } from '@crisp-e3/sdk'
import { BroadcastVoteRequest, Vote, VoteStateLite, VotingRound } from '@/model/vote.model'
import { hashMessage } from 'viem'

export type VotingStep = 'idle' | 'signing' | 'encrypting' | 'generating_proof' | 'broadcasting' | 'confirming' | 'complete' | 'error'

Expand Down Expand Up @@ -67,9 +66,18 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo
const [stepMessage, setStepMessage] = useState<string>('')

const handleProofGeneration = useCallback(
async (vote: Poll, address: string, signature: string, previousCiphertext?: Uint8Array) => {
async (vote: Vote, address: string, balance: bigint, signature: string, messageHash: `0x${string}`, isMasking: boolean) => {
if (!votingRound) throw new Error('No voting round available for proof generation')
return generateProof(BigInt(vote.value), new Uint8Array(votingRound.pk_bytes), address, signature, previousCiphertext)
return generateProof(
votingRound.round_id,
vote,
new Uint8Array(votingRound.pk_bytes),
address,
balance,
signature,
messageHash,
isMasking,
)
},
[generateProof, votingRound],
)
Expand All @@ -82,7 +90,7 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo
}, [])

const castVoteWithProof = useCallback(
async (pollSelected: Poll | null, isVoteUpdate: boolean = false) => {
async (pollSelected: Poll | null, isVoteUpdate: boolean = false, isMasking: boolean = false) => {
if (!pollSelected) {
console.log('Cannot cast vote: Poll option not selected.')
showToast({ type: 'danger', message: 'Please select a poll option first.' })
Expand All @@ -108,9 +116,12 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo
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_MESSAGE })
signature = await signMessageAsync({ message })
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (signError) {
console.log('User rejected signature or signing failed')
Expand All @@ -124,10 +135,12 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo
setLastActiveStep('encrypting')
setStepMessage('')

// @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, previousCiphertext)
// vote is either 0 or 1, so we need to encode the vote accordingly.
const balance = 1n
const vote = pollSelected.value === 0 ? { yes: balance, no: 0n } : { yes: 0n, no: balance }

const encodedProof = await handleProofGeneration(vote, user.address, balance, signature, messageHash, isMasking)

if (!encodedProof) {
throw new Error('Failed to encrypt vote.')
}
Expand Down Expand Up @@ -214,7 +227,6 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo
signMessageAsync,
markVotedInRound,
resetVotingState,
votingRound,
],
)

Expand Down
5 changes: 5 additions & 0 deletions examples/CRISP/client/src/model/vote.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@ export interface VoteStateLite {
committee_public_key: number[]
emojis: [string, string]
}

export interface Vote {
yes: bigint
no: bigint
}
84 changes: 73 additions & 11 deletions examples/CRISP/crates/evm_helpers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,25 @@ sol! {
#[sol(rpc)]
contract CRISPProgram {
function setMerkleRoot(uint256 e3_id, uint256 _root) external;
function getSlotIndex(uint256 e3_id, address slot_address) external view returns (uint256);
function isSlotEmptyByAddress(uint256 e3_id, address slot_address) external view returns (bool);
}
}

sol! {
event InputPublished(uint256 indexed e3Id, bytes vote, uint256 index);
}

/// Type alias for read-only provider (no wallet)
pub type CRISPReadProvider = FillProvider<
JoinFill<
Identity,
JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
>,
RootProvider<Ethereum>,
Ethereum,
>;

/// Type alias for write provider (same as EnclaveWriteProvider)
pub type CRISPWriteProvider = FillProvider<
JoinFill<
Expand All @@ -48,23 +60,18 @@ pub type CRISPWriteProvider = FillProvider<

/// CRISP contract instance for interacting with CRISPProgram
#[derive(Clone)]
pub struct CRISPContract {
provider: Arc<CRISPWriteProvider>,
pub struct CRISPContract<P = CRISPWriteProvider> {
provider: Arc<P>,
contract_address: Address,
}

impl CRISPContract {
/// Get the contract address
pub fn address(&self) -> &Address {
&self.contract_address
}

/// Create a new CRISP contract instance
impl CRISPContract<CRISPWriteProvider> {
/// Create a new CRISP contract instance with write capabilities
pub async fn new(
http_rpc_url: &str,
private_key: &str,
contract_address: &str,
) -> Result<CRISPContract> {
) -> Result<Self> {
let contract_address = contract_address.parse()?;
let signer: PrivateKeySigner = private_key.parse()?;
let wallet = EthereumWallet::from(signer);
Expand Down Expand Up @@ -97,6 +104,54 @@ impl CRISPContract {
}
}

impl CRISPContract<CRISPReadProvider> {
/// Create a read-only CRISP contract instance (no private key required)
pub async fn new_read_only(http_rpc_url: &str, contract_address: &str) -> Result<Self> {
let contract_address = contract_address.parse()?;
let provider = ProviderBuilder::new().connect(http_rpc_url).await?;

Ok(CRISPContract {
provider: Arc::new(provider),
contract_address,
})
}

/// Get the slot index from a given slot address
pub async fn get_slot_index_from_address(
&self,
e3_id: U256,
slot_address: Address,
) -> Result<U256> {
let contract = CRISPProgram::new(self.contract_address, self.provider.as_ref());

match contract.getSlotIndex(e3_id, slot_address).call().await {
Ok(slot_index) => Ok(slot_index),
Err(e) => Err(eyre::eyre!("Failed to get slot index: {}", e)),
}
}

/// Check if a slot is empty by its address
pub async fn get_is_slot_empty_by_address(
&self,
e3_id: U256,
slot_address: Address,
) -> Result<bool> {
let contract = CRISPProgram::new(self.contract_address, self.provider.as_ref());

match contract.isSlotEmptyByAddress(e3_id, slot_address).call().await {
Ok(is_empty) => Ok(is_empty),
Err(e) => Err(eyre::eyre!("Failed to check if slot is empty: {}", e)),
}
}
}

impl<P> CRISPContract<P> {
/// Get the contract address
pub fn address(&self) -> &Address {
&self.contract_address
}
}

/// Factory for creating CRISP contract instances
pub struct CRISPContractFactory;

Expand All @@ -106,7 +161,14 @@ impl CRISPContractFactory {
http_rpc_url: &str,
contract_address: &str,
private_key: &str,
) -> Result<CRISPContract> {
) -> Result<CRISPContract<CRISPWriteProvider>> {
CRISPContract::new(http_rpc_url, private_key, contract_address).await
}

pub async fn create_read(
http_rpc_url: &str,
contract_address: &str,
) -> Result<CRISPContract<CRISPReadProvider>> {
CRISPContract::new_read_only(http_rpc_url, contract_address).await
}
}
27 changes: 26 additions & 1 deletion examples/CRISP/crates/zk-inputs-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl ZKInputsGenerator {
Ok(ZKInputsGenerator { generator })
}

/// Generate a CRISP ZK inputs from JavaScript.
/// Generate CRISP ZK inputs from JavaScript.
#[wasm_bindgen(js_name = "generateInputs")]
pub fn generate_inputs(
&self,
Expand All @@ -72,6 +72,31 @@ impl ZKInputsGenerator {
}
}

/// Generate CRISP ZK inputs for a vote update (either from voter or as a masker) from JavaScript.
#[wasm_bindgen(js_name = "generateInputsForUpdate")]
pub fn generate_inputs_for_update(
&self,
prev_ciphertext: &[u8],
public_key: &[u8],
vote: Vec<i64>,
) -> Result<JsValue, JsValue> {
let vote_vec: Vec<u64> = vote.into_iter().map(|v| v as u64).collect();

match self
.generator
.generate_inputs_for_update(prev_ciphertext, public_key, vote_vec)
{
Ok(inputs_json) => {
// Parse the JSON string and return as JsValue.
match js_sys::JSON::parse(&inputs_json) {
Ok(js_value) => Ok(js_value),
Err(_) => Err(JsValue::from_str("Failed to parse inputs JSON")),
}
}
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}

/// Generate a public key from JavaScript.
#[wasm_bindgen(js_name = "generatePublicKey")]
pub fn generate_public_key(&self) -> Result<Vec<u8>, JsValue> {
Expand Down
Loading
Loading