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
84 changes: 57 additions & 27 deletions examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
});
}
}
4 changes: 3 additions & 1 deletion examples/CRISP/packages/crisp-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

})
})
})
9 changes: 7 additions & 2 deletions examples/CRISP/packages/crisp-sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
ctrlc03 marked this conversation as resolved.

/**
* 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.
Expand Down
36 changes: 21 additions & 15 deletions examples/CRISP/packages/crisp-sdk/src/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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:
Comment thread
ctrlc03 marked this conversation as resolved.
throw new Error('Unsupported voting mode')
throw new Error('Unsupported voting mode');
}
}
};
Comment thread
ctrlc03 marked this conversation as resolved.

/**
* Validate whether a vote is valid for a given voting mode
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

Loading