Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@
url = https://github.com/risc0/risc0-ethereum
[submodule "templates/default/lib/risc0-ethereum"]
path = templates/default/lib/risc0-ethereum
<<<<<<< semaphore-noir-interface-fix
url = https://github.com/gnosisguild/risc0-ethereum
[submodule "examples/CRISP/lib/openzeppelin-contracts-upgradeable"]
path = examples/CRISP/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
=======
url = https://github.com/gnosisguild/risc0-ethereum
>>>>>>> hacknet
12 changes: 8 additions & 4 deletions examples/CRISP/apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@
"@aztec/bb.js": "^0.82.2",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.4",
"@noir-lang/acvm_js": "1.0.0-beta.3",
"@noir-lang/noir_js": "1.0.0-beta.3",
"@noir-lang/noirc_abi": "1.0.0-beta.3",
"@phosphor-icons/react": "^2.1.4",
"@semaphore-protocol/core": "^4.9.2",
"@semaphore-protocol/data": "^4.9.2",
"@svgr/rollup": "^8.1.0",
"@semaphore-protocol/core": "^4.0.3",
"@semaphore-protocol/data": "^4.0.3",
"@semaphore-protocol/group": "^4.0.3",
"@semaphore-protocol/identity": "^4.0.3",
"@semaphore-protocol/proof": "file:../../../../semaphore-noir/packages/proof",
"@zk-kit/artifacts": "github:hashcloak/snark-artifacts#main",
"@tanstack/react-query": "^5.74.3",
"axios": "^1.6.8",
"connectkit": "^1.9.0",
Expand All @@ -36,7 +41,6 @@
"react-syntax-highlighter": "^15.5.0",
"viem": "^2.30.6",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.3.2",
"wagmi": "^2.14.16"
},
Expand Down
109 changes: 64 additions & 45 deletions examples/CRISP/apps/client/src/hooks/voting/useVoteCasting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { useVoteManagementContext } from '@/context/voteManagement';
import { useNotificationAlertContext } from '@/context/NotificationAlert/NotificationAlert.context.tsx';
import { Poll } from '@/model/poll.model';
import { BroadcastVoteRequest } from '@/model/vote.model';
import { Group, generateProof, SemaphoreProof } from '@semaphore-protocol/core';
import { encodeSemaphoreProof } from '@/utils/proof-encoding';
import { Group } from '@semaphore-protocol/group';
import { generateNoirProof, SemaphoreNoirProof, initSemaphoreNoirBackend } from '@/utils/semaphoreNoirProof';
import { encodeSemaphoreNoirProof } from '@/utils/proof-encoding';
import { encodeScope } from '@/utils/scopeEncoding';

export const useVoteCasting = () => {
const {
Expand All @@ -14,6 +16,7 @@ export const useVoteCasting = () => {
votingRound,
semaphoreIdentity,
currentGroupMembers,
currentSemaphoreGroupId,
fetchingMembers,
isRegisteredForCurrentRound,
encryptVote,
Expand Down Expand Up @@ -54,6 +57,11 @@ export const useVoteCasting = () => {
showToast({ type: 'danger', message: 'Could not load group members for this round.' });
return;
}
if (!currentSemaphoreGroupId) {
console.error("Cannot cast vote: No group ID available for this round.");
showToast({ type: 'danger', message: 'Group ID not available for this round.' });
return;
}

setIsLoading(true);
console.log("Processing vote...");
Expand All @@ -64,12 +72,39 @@ export const useVoteCasting = () => {
throw new Error("Failed to encrypt vote.");
}

// Initialize Noir backend for proof generation
const merkleTreeDepth = Math.ceil(Math.log2(currentGroupMembers.length)) || 10;
const backend = await initSemaphoreNoirBackend(merkleTreeDepth);

const group = new Group(currentGroupMembers);
const scope = String(roundState.id);

// CRITICAL FIX: Encode scope properly with address and group ID
// The scope encodes both the subject address and group ID to prevent front-running attacks:
// - Upper 160 bits: Subject address (20 bytes)
// - Lower 96 bits: Group ID (12 bytes)
const encodedScope = encodeScope(user.address, currentSemaphoreGroupId);
const scope = encodedScope.toString();
const message = String(pollSelected.value);
const fullProof: SemaphoreProof = await generateProof(semaphoreIdentity, group, message, scope);
console.log("Full generated proof object:", fullProof);
const proofBytes = encodeSemaphoreProof(fullProof);

console.log("Scope encoding details:", {
userAddress: user.address,
groupId: currentSemaphoreGroupId.toString(),
encodedScope: encodedScope.toString(),
scope
});

// Generate Noir proof
const fullProof: SemaphoreNoirProof = await generateNoirProof(
semaphoreIdentity as any,
group,
message,
scope,
backend,
true // Use keccak for Solidity verifier
);

console.log("Full generated Noir proof object:", fullProof);
const proofBytes = encodeSemaphoreNoirProof(fullProof);

const voteRequest: BroadcastVoteRequest = {
round_id: roundState.id,
Expand All @@ -80,60 +115,44 @@ export const useVoteCasting = () => {
proof_sem: Array.from(proofBytes)
};

const broadcastVoteResponse = await broadcastVote(voteRequest);
console.log('broadcastVoteResponse', broadcastVoteResponse)
console.log("Broadcasting vote to server...");
const result = await broadcastVote(voteRequest);
console.log("Vote broadcast result:", result);

if (broadcastVoteResponse) {
switch (broadcastVoteResponse.status) {
case 'success': {
const url = `https://sepolia.etherscan.io/tx/${broadcastVoteResponse.tx_hash}`;
setTxUrl(url);
showToast({
type: 'success',
message: broadcastVoteResponse.message || 'Successfully voted',
linkUrl: url,
});
navigate(`/result/${roundState.id}/confirmation`);
break;
}
case 'user_already_voted':
showToast({
type: 'danger',
message: broadcastVoteResponse.message || 'User has already voted',
});
break;
case 'failed_broadcast':
default:
showToast({
type: 'danger',
message: broadcastVoteResponse.message || 'Error broadcasting the vote',
});
break;
if (result && result.status === 'success') {
showToast({ type: 'success', message: 'Vote successfully submitted!' });
if (result.tx_hash) {
console.log('Transaction hash:', result.tx_hash);
setTxUrl(result.tx_hash);
}
navigate('/poll/result');
} else {
throw new Error('Received no response after broadcasting vote.');
throw new Error(result?.message || 'Failed to broadcast vote');
}
} catch (error) {
console.error("Vote processing failed:", error);
showToast({ type: 'danger', message: `Vote failed: ${error instanceof Error ? error.message : String(error)}` });
console.error("Error during vote casting:", error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
showToast({ type: 'danger', message: `Failed to cast vote: ${errorMessage}` });
} finally {
setIsLoading(false);
}
}, [
user,
isRegisteredForCurrentRound,
roundState,
votingRound,
semaphoreIdentity,
currentGroupMembers,
fetchingMembers,
isRegisteredForCurrentRound,
encryptVote,
currentGroupMembers,
currentSemaphoreGroupId,
handleVoteEncryption,
broadcastVote,
setTxUrl,
showToast,
setTxUrl,
navigate,
handleVoteEncryption
]);

return { castVoteWithProof, isLoading };
};
return {
castVoteWithProof,
isLoading,
};
};
20 changes: 20 additions & 0 deletions examples/CRISP/apps/client/src/utils/proof-encoding.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { hexToBytes, encodeAbiParameters, parseAbiParameters } from 'viem';
import { type SemaphoreProof } from '@semaphore-protocol/core';
import { type SemaphoreNoirProof } from '@/utils/semaphoreNoirProof';

const abi = parseAbiParameters(
'uint256,uint256,uint256,uint256,uint256,uint256[8]'
);

const noirAbi = parseAbiParameters(
'uint256,uint256,uint256,uint256,uint256,bytes'
);

type Tuple8<T> = readonly [T, T, T, T, T, T, T, T];

export function encodeSemaphoreProof(
Expand All @@ -25,3 +30,18 @@ export function encodeSemaphoreProof(

return hexToBytes(hex);
}

export function encodeSemaphoreNoirProof(
{ merkleTreeDepth, merkleTreeRoot, nullifier, message, scope, proofBytes }: SemaphoreNoirProof
): Uint8Array {
const hex = encodeAbiParameters(noirAbi, [
BigInt(merkleTreeDepth),
BigInt(merkleTreeRoot),
BigInt(nullifier),
BigInt(message),
BigInt(scope),
`0x${Buffer.from(proofBytes).toString('hex')}` as `0x${string}`,
]);

return hexToBytes(hex);
}
61 changes: 61 additions & 0 deletions examples/CRISP/apps/client/src/utils/scopeEncoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Utility functions for encoding and decoding scope values for CRISP contracts.
*
* The scope encodes both the subject address and group ID to prevent front-running attacks:
* - Upper 160 bits: Subject address (20 bytes)
* - Lower 96 bits: Group ID (12 bytes)
*/

/**
* Encodes an address and group ID into a scope value
* @param address The subject address (20 bytes, 160 bits)
* @param groupId The group ID (up to 96 bits)
* @returns The encoded scope as a BigInt
*/
export function encodeScope(address: string, groupId: bigint): bigint {
// Remove 0x prefix if present
const cleanAddress = address.startsWith('0x') ? address.slice(2) : address;

// Convert address to BigInt (160 bits)
const addressBigInt = BigInt('0x' + cleanAddress);

// Ensure groupId fits in 96 bits
const maxGroupId = (1n << 96n) - 1n;
if (groupId > maxGroupId) {
throw new Error(`Group ID ${groupId} exceeds maximum value of ${maxGroupId}`);
}

// Encode: (address << 96) | groupId
return (addressBigInt << 96n) | groupId;
}

/**
* Decodes a scope value into address and group ID
* @param scope The encoded scope value
* @returns Object with address and groupId
*/
export function decodeScope(scope: bigint): { address: string, groupId: bigint } {
// Extract group ID (lower 96 bits)
const groupId = scope & ((1n << 96n) - 1n);

// Extract address (upper 160 bits)
const addressBigInt = scope >> 96n;

// Convert back to hex address
const address = '0x' + addressBigInt.toString(16).padStart(40, '0');

return { address, groupId };
}

/**
* Validates that a scope contains the expected address and group ID
* @param scope The scope to validate
* @param expectedAddress The expected address
* @param expectedGroupId The expected group ID
* @returns True if scope matches expectations
*/
export function validateScope(scope: bigint, expectedAddress: string, expectedGroupId: bigint): boolean {
const decoded = decodeScope(scope);
return decoded.address.toLowerCase() === expectedAddress.toLowerCase() &&
decoded.groupId === expectedGroupId;
}
24 changes: 24 additions & 0 deletions examples/CRISP/apps/client/src/utils/semaphoreNoirProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Identity } from '@semaphore-protocol/identity';
import { Group } from '@semaphore-protocol/group';
import { generateNoirProof as generateSemaphoreNoirProof, initSemaphoreNoirBackend, SemaphoreNoirProof } from '@semaphore-protocol/proof';

export type { SemaphoreNoirProof };
export { initSemaphoreNoirBackend };

export async function generateNoirProof(
identity: Identity,
group: Group,
message: string,
scope: string,
backend: any,
useKeccak: boolean = true
): Promise<SemaphoreNoirProof> {
return generateSemaphoreNoirProof(
identity,
group,
message,
scope,
backend,
useKeccak
);
}
4 changes: 0 additions & 4 deletions examples/CRISP/apps/client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import viteTsconfigPaths from 'vite-tsconfig-paths'
import svgr from '@svgr/rollup'
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'
import path from 'path'
import { nodePolyfills } from 'vite-plugin-node-polyfills'

Expand All @@ -28,15 +26,13 @@ export default defineConfig({
plugins: [
// here is the main update
wasm(),
topLevelAwait(),
react({
jsxImportSource: '@emotion/react',
babel: {
plugins: ['@emotion/babel-plugin'],
},
}),
viteTsconfigPaths(),
svgr(),
nodePolyfills({ include: ['buffer'] }),
],
server: {
Expand Down
2 changes: 2 additions & 0 deletions examples/CRISP/apps/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name = "crisp"
version = "0.1.0"
edition = "2021"

[workspace]

[[bin]]
name = "server"
path = "src/server/main.rs"
Expand Down
Loading
Loading