diff --git a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol index 2fefe3d4b1..c43cf8898c 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol @@ -10,6 +10,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IE3Program} from "@enclave-e3/contracts/contracts/interfaces/IE3Program.sol"; import {IInputValidator} from "@enclave-e3/contracts/contracts/interfaces/IInputValidator.sol"; import {IEnclave} from "@enclave-e3/contracts/contracts/interfaces/IEnclave.sol"; +import {E3} from "@enclave-e3/contracts/contracts/interfaces/IE3.sol"; import {CRISPInputValidatorFactory} from "./CRISPInputValidatorFactory.sol"; import {HonkVerifier} from "./CRISPVerifier.sol"; @@ -24,6 +25,10 @@ contract CRISPProgram is IE3Program, Ownable { HonkVerifier private immutable HONK_VERIFIER; bytes32 public imageId; + /// @notice Half of the largest minimum degree used to fit votes + /// inside the plaintext polynomial + uint256 public constant HALF_LARGEST_MINIMUM_DEGREE = 28; + // Mappings mapping(address => bool) public authorizedContracts; mapping(uint256 e3Id => bytes32 paramsHash) public paramsHashes; @@ -55,10 +60,7 @@ contract CRISPProgram is IE3Program, Ownable { ) Ownable(msg.sender) { require(address(_enclave) != address(0), EnclaveAddressZero()); require(address(_verifier) != address(0), VerifierAddressZero()); - require( - address(_inputValidatorFactory) != address(0), - InvalidInputValidatorFactory() - ); + require(address(_inputValidatorFactory) != address(0), InvalidInputValidatorFactory()); require(address(_honkVerifier) != address(0), InvalidHonkVerifier()); enclave = _enclave; @@ -91,36 +93,68 @@ contract CRISPProgram is IE3Program, Ownable { /// @notice Validate the E3 program parameters /// @param e3Id The E3 program ID /// @param e3ProgramParams The E3 program parameters - function validate( - uint256 e3Id, - uint256, - bytes calldata e3ProgramParams, - bytes calldata - ) external returns (bytes32, IInputValidator inputValidator) { - require( - authorizedContracts[msg.sender] || msg.sender == owner(), - CallerNotAuthorized() - ); + function validate(uint256 e3Id, uint256, bytes calldata e3ProgramParams, bytes calldata) + external + returns (bytes32, IInputValidator inputValidator) + { + require(authorizedContracts[msg.sender] || msg.sender == owner(), CallerNotAuthorized()); require(paramsHashes[e3Id] == bytes32(0), E3AlreadyInitialized()); paramsHashes[e3Id] = keccak256(e3ProgramParams); // Deploy a new input validator - inputValidator = IInputValidator( - INPUT_VALIDATOR_FACTORY.deploy(address(HONK_VERIFIER), owner()) - ); + inputValidator = IInputValidator(INPUT_VALIDATOR_FACTORY.deploy(address(HONK_VERIFIER), owner())); return (ENCRYPTION_SCHEME_ID, inputValidator); } + /// @notice Decode the tally from the plaintext output + /// @param e3Id The E3 program ID + /// @return yes The number of yes votes + /// @return no The number of no votes + function decodeTally(uint256 e3Id) public view returns (uint256 yes, uint256 no) { + // 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[])); + + /// @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++) { + 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++) { + no += tally[i] * weight; + weight /= 2; + } + + return (yes, no); + } + /// @notice Verify the proof /// @param e3Id The E3 program ID /// @param ciphertextOutputHash The hash of the ciphertext output /// @param proof The proof to verify - function verify( - uint256 e3Id, - bytes32 ciphertextOutputHash, - bytes memory proof - ) external view override returns (bool) { + function verify(uint256 e3Id, bytes32 ciphertextOutputHash, bytes memory proof) + external + view + override + returns (bool) + { require(paramsHashes[e3Id] != bytes32(0), E3DoesNotExist()); bytes32 inputRoot = bytes32(enclave.getInputRoot(e3Id)); bytes memory journal = new bytes(396); // (32 + 1) * 4 * 3 @@ -137,11 +171,7 @@ contract CRISPProgram is IE3Program, Ownable { /// @param journal The journal to encode into /// @param startIndex The start index in the journal /// @param hashVal The hash value to encode - function encodeLengthPrefixAndHash( - bytes memory journal, - uint256 startIndex, - bytes32 hashVal - ) internal pure { + function encodeLengthPrefixAndHash(bytes memory journal, uint256 startIndex, bytes32 hashVal) internal pure { journal[startIndex] = 0x20; startIndex += 4; for (uint256 i = 0; i < 32; i++) { diff --git a/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol new file mode 100644 index 0000000000..1cefce8b2e --- /dev/null +++ b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol @@ -0,0 +1,39 @@ +// 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. +pragma solidity >=0.8.27; + +import {E3} from "@enclave-e3/contracts/contracts/interfaces/IE3.sol"; +import {IE3Program} from "@enclave-e3/contracts/contracts/interfaces/IE3Program.sol"; +import {IInputValidator} from "@enclave-e3/contracts/contracts/interfaces/IInputValidator.sol"; +import {IDecryptionVerifier} from "@enclave-e3/contracts/contracts/interfaces/IDecryptionVerifier.sol"; + +contract MockEnclave { + bytes public plaintextOutput; + + function setPlaintextOutput(uint256[] memory plaintext) external { + plaintextOutput = abi.encode(plaintext); + } + + function getE3(uint256 e3Id) external view returns (E3 memory) { + return E3({ + seed: 0, + threshold: [uint32(1), uint32(2)], + requestBlock: 0, + startWindow: [uint256(0), uint256(0)], + duration: 0, + expiration: 0, + encryptionSchemeId: bytes32(0), + e3Program: IE3Program(address(0)), + e3ProgramParams: bytes(""), + customParams: bytes(""), + inputValidator: IInputValidator(address(0)), + decryptionVerifier: IDecryptionVerifier(address(0)), + committeePublicKey: bytes32(0), + ciphertextOutput: bytes32(0), + plaintextOutput: plaintextOutput + }); + } +} diff --git a/examples/CRISP/packages/crisp-contracts/package.json b/examples/CRISP/packages/crisp-contracts/package.json index 74c2070efe..49029db6fb 100644 --- a/examples/CRISP/packages/crisp-contracts/package.json +++ b/examples/CRISP/packages/crisp-contracts/package.json @@ -33,11 +33,12 @@ "deploy:contracts": "hardhat run deploy/deploy.ts", "deploy:contracts:full": "export DEPLOY_ENCLAVE=true && pnpm deploy:contracts", "deploy:contracts:full:mock": "export DEPLOY_ENCLAVE=true && export USE_MOCK_VERIFIER=true && export USE_MOCK_INPUT_VALIDATOR=true && pnpm deploy:contracts", - "test": "hardhat test tests/crisp.contracts.test.ts --network localhost", + "test": "hardhat test mocha", "verify": "hardhat run deploy/verify.ts" }, "dependencies": { "@enclave-e3/contracts": "workspace:*", + "@crisp-e3/sdk": "workspace:*", "@excubiae/contracts": "^0.4.0", "@zk-kit/lean-imt.sol": "2.0.0", "poseidon-solidity": "^0.0.5", @@ -57,6 +58,7 @@ "@types/chai": "^4.2.0", "@types/mocha": ">=9.1.0", "@types/node": "^22.18.0", + "chai": "^6.2.0", "dotenv": "^16.4.5", "ethers": "^6.15.0", "forge-std": "github:foundry-rs/forge-std#v1.9.4", 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 10ccd79651..2f318ebf51 100644 --- a/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts +++ b/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts @@ -7,17 +7,15 @@ import { network } from "hardhat"; import { zeroAddress, zeroHash } from "viem"; -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - - -describe("CRISP Contracts", async function () { - const { ethers } = await network.connect(); +import { expect } from "chai"; +import { MockEnclave } from "../types"; +describe("CRISP Contracts", function () { const nonZeroAddress = "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d"; describe("deployment", () => { it("should deploy the contracts", async () => { + const { ethers } = await network.connect(); /* IEnclave _enclave, IRiscZeroVerifier _verifier, @@ -33,7 +31,44 @@ describe("CRISP Contracts", async function () { zeroHash ]) - assert(await program.getAddress() !== zeroAddress) + expect(await program.getAddress()).to.not.equal(zeroAddress) + }) + }) + + describe("decode tally", () => { + it("should decode different tallies correctly", async () => { + const { ethers } = await network.connect(); + const mockEnclave = await ethers.deployContract("MockEnclave") as MockEnclave; + + const program = await ethers.deployContract("CRISPProgram", [ + await mockEnclave.getAddress(), + nonZeroAddress, + nonZeroAddress, + nonZeroAddress, + zeroHash + ]) + + // 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]; + + await mockEnclave.setPlaintextOutput(tally1); + + const decodedTally1 = await program.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 program.decodeTally(0); + + expect(decodedTally2[0]).to.equal(8277n) + expect(decodedTally2[1]).to.equal(1218n) + }) }) }) diff --git a/examples/CRISP/packages/crisp-sdk/src/constants.ts b/examples/CRISP/packages/crisp-sdk/src/constants.ts index a50e703b8c..603bb29d12 100644 --- a/examples/CRISP/packages/crisp-sdk/src/constants.ts +++ b/examples/CRISP/packages/crisp-sdk/src/constants.ts @@ -10,12 +10,17 @@ import { BFVParams } from './types' export const CRISP_SERVER_TOKEN_TREE_ENDPOINT = 'state/token-holders' export const CRISP_SERVER_STATE_LITE_ENDPOINT = 'state/lite' +/** + * Half the minimum degree needed to support the maxium vote value + * If you change MAXIMUM_VOTE_VALUE, make sure to update this value too. + */ +export const HALF_LARGEST_MINIMUM_DEGREE = 28; + /** * This is the maximum value for a vote (Yes or No). This is 2^28 * The minimum degree that BFV should use is 56 (to accommodate both Yes and No votes) - * If you change this value, make sure to update the circuit too. */ -export const MAXIMUM_VOTE_VALUE = 268435456n +export const MAXIMUM_VOTE_VALUE = BigInt(Math.pow(2, HALF_LARGEST_MINIMUM_DEGREE)) /** * Default BFV parameters for the CRISP ZK inputs generator. diff --git a/examples/CRISP/packages/crisp-sdk/src/vote.ts b/examples/CRISP/packages/crisp-sdk/src/vote.ts index 43df33e882..3c4ba12f51 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 { BFVParams, type CRISPCircuitInputs, type EncryptVoteAndGenerateCRISPInputsParams, type IVote, VotingMode } from './types' import { toBinary } from './utils' -import { MAXIMUM_VOTE_VALUE, DEFAULT_BFV_PARAMS, MESSAGE } from './constants' +import { MAXIMUM_VOTE_VALUE, DEFAULT_BFV_PARAMS, HALF_LARGEST_MINIMUM_DEGREE, MESSAGE } from './constants' import { extractSignature } from './signature' import { Noir, type CompiledCircuit } from '@noir-lang/noir_js' import { UltraHonkBackend, type ProofData } from '@aztec/bb.js' @@ -97,31 +97,37 @@ export const encodeVote = (vote: IVote, votingMode: VotingMode, votingPower: big export const decodeTally = (tally: string[], votingMode: VotingMode): IVote => { switch (votingMode) { case VotingMode.GOVERNANCE: - const halfLength = tally.length / 2 + 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; - // Split the tally into two halves - const yesBinary = tally.slice(0, halfLength) - const noBinary = tally.slice(halfLength, tally.length) + // 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); - let yes = 0n - let no = 0n + let yes = 0n; + let no = 0n; - // Convert each half back to decimal - for (let i = 0; i < halfLength; i += 1) { - const weight = 2n ** BigInt(halfLength - 1 - i) + // Convert yes votes (from START_INDEX_Y to HALF_D) + for (let i = 0; i < yesBinary.length; i += 1) { + const weight = 2n ** BigInt(yesBinary.length - 1 - i); + yes += BigInt(yesBinary[i]) * weight; + } - yes += BigInt(yesBinary[i]) * weight - no += BigInt(noBinary[i]) * weight + // Convert no votes (from START_INDEX_N to D) + for (let i = 0; i < noBinary.length; i += 1) { + const weight = 2n ** BigInt(noBinary.length - 1 - i); + no += BigInt(noBinary[i]) * weight; } return { yes, no, - } + }; default: - throw new Error('Unsupported voting mode') + throw new Error('Unsupported voting mode'); } -} +}; /** * Validate whether a vote is valid for a given voting mode diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 026d9c258a..1a260fc20c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,6 +248,9 @@ importers: examples/CRISP/packages/crisp-contracts: dependencies: + '@crisp-e3/sdk': + specifier: workspace:* + version: link:../crisp-sdk '@enclave-e3/contracts': specifier: workspace:* version: link:../../../../packages/enclave-contracts @@ -303,6 +306,9 @@ importers: '@types/node': specifier: 22.7.5 version: 22.7.5 + chai: + specifier: ^6.2.0 + version: 6.2.0 dotenv: specifier: ^16.4.5 version: 16.6.1