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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
11 changes: 11 additions & 0 deletions examples/CRISP/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion examples/CRISP/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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 = [
Expand Down
14 changes: 14 additions & 0 deletions examples/CRISP/crates/crisp-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"

72 changes: 72 additions & 0 deletions examples/CRISP/crates/crisp-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<VoteCounts> {
// 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 })
}
Comment thread
cedoor marked this conversation as resolved.

#[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));
}
}
52 changes: 33 additions & 19 deletions examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -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;
Comment thread
cedoor marked this conversation as resolved.
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);
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})

Expand Down
2 changes: 1 addition & 1 deletion examples/CRISP/packages/crisp-sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 49 additions & 13 deletions examples/CRISP/packages/crisp-sdk/src/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Comment thread
cedoor marked this conversation as resolved.

/**
* 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
Expand Down Expand Up @@ -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')
}

Expand Down
Loading
Loading