diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f17d2a886..c4b0dc42d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -488,6 +488,9 @@ jobs: - name: Cache Rust dependencies uses: ./.github/actions/cache-dependencies + with: + cargo-lock-path: examples/CRISP/Cargo.lock + rust-target-path: examples/CRISP/target/ - name: Prepare test environment run: | diff --git a/examples/CRISP/Cargo.lock b/examples/CRISP/Cargo.lock index 082a9e465d..64ba384aa2 100644 --- a/examples/CRISP/Cargo.lock +++ b/examples/CRISP/Cargo.lock @@ -2031,6 +2031,7 @@ dependencies = [ "clap", "config", "crisp-constants", + "crisp-utils", "derivative", "dialoguer", "dotenvy", @@ -2063,6 +2064,16 @@ dependencies = [ "e3-sdk", ] +[[package]] +name = "crisp-utils" +version = "0.1.0" +dependencies = [ + "e3-sdk", + "eyre", + "hex", + "num-bigint", +] + [[package]] name = "crisp-zk-inputs" version = "0.1.0" diff --git a/examples/CRISP/Cargo.toml b/examples/CRISP/Cargo.toml index 82acc35d76..b75fa344e6 100644 --- a/examples/CRISP/Cargo.toml +++ b/examples/CRISP/Cargo.toml @@ -6,7 +6,8 @@ members = [ "crates/zk-inputs", "crates/zk-inputs-wasm", "crates/evm_helpers", - "crates/crisp-constants" + "crates/crisp-constants", + "crates/crisp-utils" ] resolver = "3" @@ -20,6 +21,7 @@ repository = "https://github.com/gnosisguild/enclave" [workspace.dependencies] e3-user-program = { path = "./program" } crisp-constants = { path = "./crates/crisp-constants" } +crisp-utils = { path = "./crates/crisp-utils" } alloy = { version = "=1.0.41", features = ["full", "rpc-types-eth"] } alloy-primitives = { version = "=1.3.0", default-features = false, features = [ diff --git a/examples/CRISP/crates/crisp-utils/Cargo.toml b/examples/CRISP/crates/crisp-utils/Cargo.toml new file mode 100644 index 0000000000..158925e92f --- /dev/null +++ b/examples/CRISP/crates/crisp-utils/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "crisp-utils" +version.workspace = true +edition.workspace = true +license.workspace = true +description.workspace = true +repository.workspace = true + +[dependencies] +e3-sdk = { workspace = true, default-features = false, features=["bfv"] } +eyre = { workspace = true } +hex = { workspace = true } +num-bigint = "=0.4.6" + diff --git a/examples/CRISP/crates/crisp-utils/src/lib.rs b/examples/CRISP/crates/crisp-utils/src/lib.rs new file mode 100644 index 0000000000..0979e47d91 --- /dev/null +++ b/examples/CRISP/crates/crisp-utils/src/lib.rs @@ -0,0 +1,72 @@ +// 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. + +use e3_sdk::bfv_helpers::decode_bytes_to_vec_u64; +use eyre::Result; +use num_bigint::BigUint; + +/// Represents decoded vote counts from a tally +#[derive(Debug, Clone)] +pub struct VoteCounts { + pub yes: BigUint, + pub no: BigUint, +} + +/// Decode an encoded tally into its decimal representation. +/// +/// The plaintext output from FHE computation contains the result as bytes. +/// Votes are encoded with yes votes in the first half and no votes in the second half, +/// right-aligned with leading zeros. +/// +/// # Arguments +/// +/// * `tally_bytes` - The encoded tally as bytes (little-endian format of u64s) +/// +/// # Returns +/// +/// A `VoteCounts` struct containing the decoded yes and no vote counts +pub fn decode_tally(tally_bytes: &[u8]) -> Result { + // Decode bytes to numbers array (little-endian, 8 bytes per value) + let decoded = decode_bytes_to_vec_u64(tally_bytes)?; + + // Votes are right-aligned with leading zeros, so we can use the entire halves + let half_d = decoded.len() / 2; + let yes_binary = &decoded[0..half_d]; + let no_binary = &decoded[half_d..decoded.len()]; + + // Convert yes votes (entire first half) + let mut yes = BigUint::from(0u64); + for (i, &value) in yes_binary.iter().enumerate() { + let weight = BigUint::from(2u64).pow((yes_binary.len() - 1 - i) as u32); + yes += BigUint::from(value) * weight; + } + + // Convert no votes (entire second half) + let mut no = BigUint::from(0u64); + for (i, &value) in no_binary.iter().enumerate() { + let weight = BigUint::from(2u64).pow((no_binary.len() - 1 - i) as u32); + no += BigUint::from(value) * weight; + } + + Ok(VoteCounts { yes, no }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_tally_fhe_output() { + // Expected: yes = 10000000000, no = 30000000000 + let tally_hex = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000100000000000000010000000000000001000000000000000100000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000300000000000000000000000000000003000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000030000000000000003000000000000000300000000000000030000000000000003000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + let bytes = hex::decode(tally_hex.strip_prefix("0x").unwrap_or(tally_hex)).unwrap(); + let result = decode_tally(&bytes).unwrap(); + + assert_eq!(result.yes, BigUint::from(10000000000u64)); + assert_eq!(result.no, BigUint::from(30000000000u64)); + } +} diff --git a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol index 786f7ccf5e..3c2a65f78f 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol @@ -29,8 +29,6 @@ contract CRISPProgram is IE3Program, Ownable { bytes32 public constant ENCRYPTION_SCHEME_ID = keccak256("fhe.rs:BFV"); /// @notice The depth of the input Merkle tree. uint8 public constant TREE_DEPTH = 20; - /// @notice Half of the largest minimum degree used to fit votes inside the plaintext polynomial. - uint256 public constant HALF_LARGEST_MINIMUM_DEGREE = 28; // static, hardcoded in the circuit. // State variables IEnclave public enclave; @@ -53,7 +51,7 @@ contract CRISPProgram is IE3Program, Ownable { error InvalidNoirProof(); error InvalidMerkleRoot(); error MerkleRootAlreadySet(); - + error InvalidTallyLength(); // Events event InputPublished(uint256 indexed e3Id, bytes vote, uint256 index); @@ -159,31 +157,21 @@ contract CRISPProgram is IE3Program, Ownable { // fetch from enclave E3 memory e3 = enclave.getE3(e3Id); - // abi decode it into an array of uint256 - uint256[] memory tally = abi.decode(e3.plaintextOutput, (uint256[])); + // decode it into an array of uint64 + uint64[] memory tally = _decodeBytesToUint64Array(e3.plaintextOutput); - /// @notice We want to completely ignore anything outside of the coefficients - /// we agreed to store out votes on. uint256 halfD = tally.length / 2; - uint256 START_INDEX_Y = halfD - HALF_LARGEST_MINIMUM_DEGREE; - uint256 START_INDEX_N = tally.length - HALF_LARGEST_MINIMUM_DEGREE; - - // first weight (we are converting back from bits to integer) - uint256 weight = 2 ** (HALF_LARGEST_MINIMUM_DEGREE - 1); // Convert yes votes - for (uint256 i = START_INDEX_Y; i < halfD; i++) { + for (uint256 i = 0; i < halfD; i++) { + uint256 weight = 2 ** (halfD - 1 - i); yes += tally[i] * weight; - weight /= 2; // Right shift equivalent } - // Reset weight for no votes - weight = 2 ** (HALF_LARGEST_MINIMUM_DEGREE - 1); - // Convert no votes - for (uint256 i = START_INDEX_N; i < tally.length; i++) { + for (uint256 i = halfD; i < tally.length; i++) { + uint256 weight = 2 ** (tally.length - 1 - i); no += tally[i] * weight; - weight /= 2; } return (yes, no); @@ -236,4 +224,30 @@ contract CRISPProgram is IE3Program, Ownable { journal[startIndex + i * 4] = hashVal[i]; } } + + /// @notice Decode bytes to uint64 array + /// @param data The bytes to decode (must be multiple of 8) + /// @return result Array of uint64 values + function _decodeBytesToUint64Array(bytes memory data) internal pure returns (uint64[] memory result) { + if (data.length % 8 != 0) { + revert InvalidTallyLength(); + } + + uint256 arrayLength = data.length / 8; + result = new uint64[](arrayLength); + + for (uint256 i = 0; i < arrayLength; i++) { + uint256 offset = i * 8; + uint64 value = 0; + + // Read 8 bytes in little-endian order + for (uint64 j = 0; j < 8; j++) { + value |= uint64(uint8(data[offset + j])) << (j * 8); + } + + result[i] = value; + } + + return result; + } } diff --git a/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol index 58cd6e65a6..605a756e85 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol @@ -12,8 +12,8 @@ import { IDecryptionVerifier } from "@enclave-e3/contracts/contracts/interfaces/ contract MockEnclave { bytes public plaintextOutput; - function setPlaintextOutput(uint256[] memory plaintext) external { - plaintextOutput = abi.encode(plaintext); + function setPlaintextOutput(bytes memory plaintext) external { + plaintextOutput = plaintext; } function getE3(uint256 e3Id) external view returns (E3 memory) { diff --git a/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts b/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts index 67e5a9827d..40a7597d51 100644 --- a/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts +++ b/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts @@ -22,36 +22,19 @@ let publicKey = generatePublicKey() describe('CRISP Contracts', function () { describe('decode tally', () => { - it('should decode different tallies correctly', async () => { + it('should decode a tally correctly', async () => { const mockEnclave = await deployMockEnclave() const crispProgram = await deployCRISPProgram({ mockEnclave }) - // 2 * 2 + 1 * 1 = 5 Y - // 2 * 1 + 0 * 1 = 2 N - const tally1 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, - ] + const tally = + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000100000000000000010000000000000001000000000000000100000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000300000000000000000000000000000003000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000030000000000000003000000000000000300000000000000030000000000000003000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' - await mockEnclave.setPlaintextOutput(tally1) + await mockEnclave.setPlaintextOutput(tally) const decodedTally1 = await crispProgram.decodeTally(0) - expect(decodedTally1[0]).to.equal(5n) - expect(decodedTally1[1]).to.equal(2n) - - // 1 * 1 + 2 * 2 + 5 * 16 + 8 * 1024 = 8277 - // 2 * 1 + 3 * 64 + 1024 = - const tally2 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 5, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, - ] - await mockEnclave.setPlaintextOutput(tally2) - - const decodedTally2 = await crispProgram.decodeTally(0) - - expect(decodedTally2[0]).to.equal(8277n) - expect(decodedTally2[1]).to.equal(1218n) + expect(decodedTally1[0]).to.equal(10000000000n) + expect(decodedTally1[1]).to.equal(30000000000n) }) }) diff --git a/examples/CRISP/packages/crisp-sdk/src/constants.ts b/examples/CRISP/packages/crisp-sdk/src/constants.ts index ef83a32cbc..6ae052f1f5 100644 --- a/examples/CRISP/packages/crisp-sdk/src/constants.ts +++ b/examples/CRISP/packages/crisp-sdk/src/constants.ts @@ -21,7 +21,7 @@ export const HALF_LARGEST_MINIMUM_DEGREE = 50 * This is the maximum value for a vote (Yes or No). This is 2^50 - 1 * The minimum degree that BFV should use is 100 (to accommodate both Yes and No votes) */ -export const MAXIMUM_VOTE_VALUE = BigInt(Math.pow(2, HALF_LARGEST_MINIMUM_DEGREE) - 1) +export const MAXIMUM_VOTE_VALUE = Math.pow(2, HALF_LARGEST_MINIMUM_DEGREE) - 1 /** * Message used by users to prove ownership of their Ethereum account diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index 5e9c984a9d..dd0a474eb8 100644 --- a/examples/CRISP/packages/crisp-sdk/src/vote.ts +++ b/examples/CRISP/packages/crisp-sdk/src/vote.ts @@ -7,11 +7,11 @@ 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, HALF_LARGEST_MINIMUM_DEGREE, MASK_SIGNATURE } 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' -import { bytesToHex, encodeAbiParameters, parseAbiParameters, numberToHex, getAddress } from 'viem/utils' +import { bytesToHex, encodeAbiParameters, parseAbiParameters, numberToHex, getAddress, hexToBytes } from 'viem/utils' import { Hex } from 'viem' // Initialize the ZKInputsGenerator. @@ -46,30 +46,65 @@ export const encodeVote = (vote: Vote): BigInt64Array => { return BigInt64Array.from(voteArray.map(BigInt)) } +/** + * Decode bytes to numbers array (little-endian, 8 bytes per value). + * @param data The bytes to decode (must be multiple of 8). + * @returns Array of numbers. + */ +const decodeBytesToNumbers = (data: Uint8Array): number[] => { + if (data.length % 8 !== 0) { + throw new Error('Data length must be multiple of 8') + } + + const arrayLength = data.length / 8 + const result: number[] = [] + + for (let i = 0; i < arrayLength; i++) { + const offset = i * 8 + let value = 0 + + // Read 8 bytes in little-endian order + for (let j = 0; j < 8; j++) { + const byteValue = data[offset + j] + value |= byteValue << (j * 8) + } + + result.push(value) + } + + return result +} + /** * Decode an encoded tally into its decimal representation. - * @param tally The encoded tally to decode. + * @param tallyBytes The encoded tally as a hex string (bytes). * @returns The decoded tally as an IVote. */ -export const decodeTally = (tally: string[]): Vote => { - const HALF_D = tally.length / 2 - const START_INDEX_Y = HALF_D - HALF_LARGEST_MINIMUM_DEGREE - const START_INDEX_N = tally.length - HALF_LARGEST_MINIMUM_DEGREE +export const decodeTally = (tallyBytes: string): Vote => { + // Convert hex string to bytes, handling both with and without 0x prefix + const hexString = tallyBytes.startsWith('0x') ? tallyBytes : `0x${tallyBytes}` + const bytes = hexToBytes(hexString as Hex) + + // Decode bytes to numbers array + const numbers = decodeBytesToNumbers(bytes) + + const HALF_D = numbers.length / 2 - // Extract only the relevant parts of the tally - const yesBinary = tally.slice(START_INDEX_Y, HALF_D) - const noBinary = tally.slice(START_INDEX_N, tally.length) + // Extract the first half for yes votes and second half for no votes + // Votes are right-aligned with leading zeros, so we can use the entire halves + const yesBinary = numbers.slice(0, HALF_D) + const noBinary = numbers.slice(HALF_D, numbers.length) let yes = 0n let no = 0n - // Convert yes votes (from START_INDEX_Y to HALF_D) + // Convert yes votes (entire first half) for (let i = 0; i < yesBinary.length; i += 1) { const weight = 2n ** BigInt(yesBinary.length - 1 - i) yes += BigInt(yesBinary[i]) * weight } - // Convert no votes (from START_INDEX_N to D) + // Convert no votes (entire second half) for (let i = 0; i < noBinary.length; i += 1) { const weight = 2n ** BigInt(noBinary.length - 1 - i) no += BigInt(noBinary[i]) * weight @@ -145,7 +180,8 @@ export const generateVoteProof = async (voteProofInputs: VoteProofInputs) => { throw new Error('Invalid vote: vote exceeds balance') } - if (voteProofInputs.vote.yes > MAXIMUM_VOTE_VALUE || voteProofInputs.vote.no > MAXIMUM_VOTE_VALUE) { + const maxVoteValue = BigInt(MAXIMUM_VOTE_VALUE) + if (voteProofInputs.vote.yes > maxVoteValue || voteProofInputs.vote.no > maxVoteValue) { throw new Error('Invalid vote: vote exceeds maximum allowed value') } diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index 2cfd5d5a33..69b3efdc2f 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -46,135 +46,36 @@ describe('Vote', () => { describe('decodeTally', () => { it('Should decode an encoded tally into its decimal representation', () => { - const tally = [ - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '5', - '0', - '2', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '1', - ] + const tally = + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000100000000000000010000000000000001000000000000000100000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000300000000000000000000000000000003000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000030000000000000003000000000000000300000000000000030000000000000003000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' const decoded = decodeTally(tally) - expect(decoded.yes).toBe(22n) - expect(decoded.no).toBe(1n) + expect(decoded.yes).toBe(10000000000n) + expect(decoded.no).toBe(30000000000n) }) }) describe('encodeVote', () => { - const decodeHalf = (encoded: BigInt64Array, isFirstHalf: boolean): bigint => { + const decodeHalf = (encoded: BigInt64Array, isFirstHalf: boolean): number => { const halfLength = encoded.length / 2 const half = Array.from(isFirstHalf ? encoded.slice(0, halfLength) : encoded.slice(halfLength)) const binaryString = half.map((b) => b.toString()).join('') const trimmedBinary = binaryString.replace(/^0+/, '') || '0' - return BigInt('0b' + trimmedBinary) + return parseInt(trimmedBinary, 2) } it('Should encode yes vote correctly in the first half', () => { - const encoded = encodeVote({ yes: 10n, no: 0n }) + const encoded = encodeVote({ yes: 10n, no: 2n }) - expect(decodeHalf(encoded, true)).toBe(10n) + expect(decodeHalf(encoded, true)).toBe(10) + expect(decodeHalf(encoded, false)).toBe(2) }) it('Should encode no vote correctly in the second half', () => { const encoded = encodeVote({ yes: 0n, no: 5n }) - expect(decodeHalf(encoded, false)).toBe(5n) + expect(decodeHalf(encoded, false)).toBe(5) }) it('Should only contain binary digits (0 or 1)', () => { diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 78ff8b0759..ef360227e0 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -19,10 +19,13 @@ E3_PROGRAM_ADDRESS="0x67d269191c92Caf3cD7723F116c85e6E9bf55933" # CRISPProgram C FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config +# Defines the time window during which an e3 can be activated E3_WINDOW_SIZE=40 +# Defines the time interval during which users can submit their inputs +# After this interval, the computation phase starts automatically +E3_DURATION=60 E3_THRESHOLD_MIN=2 E3_THRESHOLD_MAX=5 -E3_DURATION=160 # E3 Compute Provider Config E3_COMPUTE_PROVIDER_NAME="RISC0" diff --git a/examples/CRISP/server/Cargo.toml b/examples/CRISP/server/Cargo.toml index 88eda16571..7a06c4dfb3 100644 --- a/examples/CRISP/server/Cargo.toml +++ b/examples/CRISP/server/Cargo.toml @@ -46,6 +46,7 @@ e3-compute-provider.workspace = true e3-sdk = { workspace = true, default-features = false, features=["full"] } evm-helpers = { path = "../crates/evm_helpers" } crisp-constants.workspace = true +crisp-utils.workspace = true # CLI and user interaction dialoguer = { version = "=0.11.0", features = ["fuzzy-select"] } diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index 0706bc350a..ab1aaaf668 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -15,9 +15,9 @@ use crate::server::{ use alloy::providers::{Provider, ProviderBuilder}; use alloy::sol_types::{sol_data, SolType}; use alloy_primitives::{Address, U256}; +use crisp_utils::decode_tally; use e3_sdk::indexer::IndexerContext; use e3_sdk::{ - bfv_helpers::decode_bytes_to_vec_u64, evm_helpers::{ contracts::{EnclaveRead, EnclaveWrite, ReadWrite}, events::{ @@ -290,28 +290,19 @@ pub async fn register_plaintext_output_published( info!("[e3_id={}] Handling PlaintextOutputPublished", e3_id); // The plaintextOutput from the event contains the result of the FHE computation. - // The computation sums the encrypted votes: '0' for Option 1, '1' for Option 2. - // Thus, the decrypted sum directly represents the number of votes for Option 2. - // The output is expected to be a Vec in little endian format of u64s. - let decoded = decode_bytes_to_vec_u64(&event.plaintextOutput)?; + // Decode the tally using the utility function. + let vote_counts = decode_tally(&event.plaintextOutput)?; - // decoded[0] is the sum of all encrypted votes (0s and 1s). - // Since Option 1 votes are encrypted as '0' and Option 2 votes as '1', - // this sum is equivalent to the count of votes for Option 2. - let option_2 = decoded[0]; - - // Retrieve the total number of votes that were cast and recorded for this round. - let total_votes = repo.get_vote_count().await?; - - // The number of votes for Option 1 can be derived by subtracting - // the Option 2 votes (the sum from the FHE output) from the total votes. - let option_1 = total_votes - option_2; - - info!("[e3_id={}] Vote Count: {:?}", e3_id, total_votes); - info!("[e3_id={}] Votes Option 1: {:?}", e3_id, option_1); - info!("[e3_id={}] Votes Option 2: {:?}", e3_id, option_2); + info!( + "[e3_id={}] Votes Option 1 (Yes): {:?}", + e3_id, vote_counts.yes + ); + info!( + "[e3_id={}] Votes Option 2 (No): {:?}", + e3_id, vote_counts.no + ); - repo.set_votes(option_1, option_2).await?; + repo.set_votes(vote_counts.yes, vote_counts.no).await?; repo.update_status("Finished").await?; Ok(()) } diff --git a/examples/CRISP/server/src/server/models.rs b/examples/CRISP/server/src/server/models.rs index 9cc3fc8f0c..66cc8266d2 100644 --- a/examples/CRISP/server/src/server/models.rs +++ b/examples/CRISP/server/src/server/models.rs @@ -128,11 +128,11 @@ pub struct RoundRequest { #[derive(Debug, Deserialize, Serialize)] pub struct WebResultRequest { pub round_id: u64, - pub option_1_tally: u64, - pub option_2_tally: u64, - pub total_votes: u64, + pub option_1_tally: String, + pub option_2_tally: String, pub option_1_emoji: String, pub option_2_emoji: String, + pub total_votes: u64, pub end_time: u64, } @@ -168,8 +168,8 @@ pub struct E3 { pub status: String, pub has_voted: Vec, pub vote_count: u64, - pub votes_option_1: u64, - pub votes_option_2: u64, + pub votes_option_1: String, + pub votes_option_2: String, // Timing-related pub start_time: u64, @@ -198,8 +198,8 @@ pub struct E3Crisp { pub has_voted: Vec, pub start_time: u64, pub status: String, - pub votes_option_1: u64, - pub votes_option_2: u64, + pub votes_option_1: String, + pub votes_option_2: String, pub token_holder_hashes: Vec, pub token_address: String, pub balance_threshold: String, @@ -212,9 +212,9 @@ impl From for WebResultRequest { round_id: e3.id, option_1_tally: e3.votes_option_1, option_2_tally: e3.votes_option_2, - total_votes: e3.votes_option_1 + e3.votes_option_2, option_1_emoji: e3.emojis[0].clone(), option_2_emoji: e3.emojis[1].clone(), + total_votes: e3.vote_count, end_time: e3.expiration, } } diff --git a/examples/CRISP/server/src/server/repo.rs b/examples/CRISP/server/src/server/repo.rs index 264b7a296d..944326979a 100644 --- a/examples/CRISP/server/src/server/repo.rs +++ b/examples/CRISP/server/src/server/repo.rs @@ -11,6 +11,7 @@ use super::{ use e3_sdk::indexer::{models::E3 as EnclaveE3, DataStore, E3Repository, SharedStore}; use eyre::Result; use log::info; +use num_bigint::BigUint; pub struct CurrentRoundRepository { store: SharedStore, @@ -125,8 +126,8 @@ impl CrispE3Repository { has_voted: vec![], start_time: 0u64, status: "Requested".to_string(), - votes_option_1: 0, - votes_option_2: 0, + votes_option_1: "0".to_string(), + votes_option_2: "0".to_string(), emojis: generate_emoji(), token_holder_hashes: vec![], token_address, @@ -165,14 +166,14 @@ impl CrispE3Repository { Ok(()) } - pub async fn set_votes(&mut self, option_1: u64, option_2: u64) -> Result<()> { + pub async fn set_votes(&mut self, option_1: BigUint, option_2: BigUint) -> Result<()> { info!("set_votes(option_1:{} option_2:{})", option_1, option_2); let key = self.crisp_key(); self.store .modify(&key, |e3_obj: Option| { e3_obj.map(|mut e| { - e.votes_option_1 = option_1; - e.votes_option_2 = option_2; + e.votes_option_1 = option_1.to_string(); + e.votes_option_2 = option_2.to_string(); e }) }) @@ -198,10 +199,10 @@ impl CrispE3Repository { round_id: e3.id, option_1_tally: e3_crisp.votes_option_1, option_2_tally: e3_crisp.votes_option_2, - total_votes: e3_crisp.votes_option_1 + e3_crisp.votes_option_2, option_1_emoji: e3_crisp.emojis[0].clone(), option_2_emoji: e3_crisp.emojis[1].clone(), end_time: e3.expiration, + total_votes: self.get_vote_count().await?, }) } diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 601c278787..69409c410d 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 = 300000): Promise { +async function waitForE3Activation(e3id: number, maxWaitMs: number = 60000): 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 = 310_000 + const WAIT = 150_000 log(`waiting for ${WAIT}ms...`) await page.waitForTimeout(WAIT) log(`clicking historic polls button...`)