Skip to content

feat: allow to encode multiple options in CRISP votes [skip-line-limit]#1262

Merged
ctrlc03 merged 9 commits into
mainfrom
feat/generic-encoding-crisp
Feb 5, 2026
Merged

feat: allow to encode multiple options in CRISP votes [skip-line-limit]#1262
ctrlc03 merged 9 commits into
mainfrom
feat/generic-encoding-crisp

Conversation

@ctrlc03

@ctrlc03 ctrlc03 commented Feb 4, 2026

Copy link
Copy Markdown
Collaborator

fix #1255

Summary by CodeRabbit

  • New Features

    • Multi-option voting with configurable option count and per-option tallies; SDK helpers to compute max per-choice value and zero-vote arrays.
  • Improvements

    • Vote/tally format switched to index-based arrays; num_options propagated across client, SDK, contracts, server, worker, and verifier inputs.
    • Added configurable bit-width/option limits and tightened per-option and aggregate validations.
  • Tests

    • Expanded coverage for 2+, multi-option, invalid, and edge-case scenarios.

@vercel

vercel Bot commented Feb 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
crisp Skipped Skipped Feb 5, 2026 9:35am
enclave-docs Skipped Skipped Feb 5, 2026 9:35am

Request Review

@coderabbitai

coderabbitai Bot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Generalizes CRISP from binary to multi-option voting: introduces num_options, MAX_OPTIONS, and MAX_VOTE_BITS; updates Noir circuits, SDK, Solidity contracts, client, and server to use array-shaped per-option votes and multi-option encoding/decoding and validation.

Changes

Cohort / File(s) Summary
Noir constants
examples/CRISP/circuits/src/constants.nr
Removed HALF_LARGEST_MINIMUM_DEGREE; added MAX_OPTIONS and MAX_VOTE_BITS.
Noir circuits & utils
examples/CRISP/circuits/src/main.nr, examples/CRISP/circuits/src/utils.nr
Added public num_options param; generalized coefficient checks to per-option segmented validation; updated mask/actual vote validation to use num_options.
SDK types & constants
examples/CRISP/packages/crisp-sdk/src/types.ts, .../src/constants.ts
Changed Vote to bigint[]; added num_options/numOptions fields; replaced old vote-size constants with MAX_VOTE_BITS/MAX_VOTE_OPTIONS.
SDK logic & tests
examples/CRISP/packages/crisp-sdk/src/vote.ts, .../src/utils.ts, .../src/index.ts, .../tests/vote.test.ts
Rewrote encode/decode for N options, added validateVote, getZeroVote, getMaxVoteValue; include num_options in circuit inputs; updated tests for multi-option cases.
Solidity contracts & mocks & tests
examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol, .../contracts/Mocks/MockEnclave.sol, .../tests/crisp.contracts.test.ts, examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol
Persist numOptions from customParams into RoundData; add MAX_VOTE_BITS; decodeTally returns uint256[]; verifier VK and public input count updated; mock enclave request() and tests updated.
Client model, hooks & worker
examples/CRISP/client/src/model/vote.model.ts, examples/CRISP/client/src/hooks/voting/useVoteCasting.ts, examples/CRISP/client/libs/crispSDKWorker.js, examples/CRISP/client/src/utils/constants.ts, examples/CRISP/client/package.json
Vote shape changed to array bigint[]; hook/worker pass numOptions/NUM_OPTIONS for mask proofs; added NUM_OPTIONS; package dependency bumped.
Server models & repo & indexer & CLI
examples/CRISP/server/src/server/models.rs, .../repo.rs, .../indexer.rs, .../cli/commands.rs
Propagated num_options through CustomParams, E3Crisp, E3StateLite; decoding and initialize_round now include num_options.
Enclave & interface ABI
packages/enclave-contracts/contracts/Enclave.sol, packages/enclave-contracts/contracts/interfaces/IE3Program.sol, packages/enclave-contracts/contracts/test/MockE3Program.sol
Extended IE3Program.validate ABI to accept customParams; callers updated to pass additional encoded custom params.
Artifacts / verifier
examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol, packages/enclave-contracts/artifacts/.../IEnclave.json
Verification key and public input count updated to match new circuit layout; artifact metadata updated.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Client as SDK/Client
    participant Worker as SDK Worker
    participant Circuit as Noir Circuit
    participant Contract as CRISP Contract
    participant Tally as Tally Decoder

    User->>Client: select option(s) & balance
    Client->>Worker: generateMaskVoteProof / generateVoteProof (vote: bigint[], numOptions)
    Worker->>Circuit: prove(encoded_vote, num_options)
    Circuit->>Circuit: validate per-option segments & bounds
    Circuit-->>Worker: proof + ciphertext
    Worker->>Contract: submit proof + ciphertext + numOptions
    Contract->>Tally: decodeTally(e3Id)
    Tally->>Tally: split plaintext into segments by numOptions and MAX_VOTE_BITS
    Tally-->>Contract: per-option counts array
    Contract-->>User: result/tally
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • cedoor
  • 0xjei

Poem

🐰
I nibbled halves and hopped through bits,
Now ballots bloom in tidy splits,
From two to many, choices sing,
Segments hum and numbers ring.
Hooray — more voices in the spring!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: enabling CRISP vote encoding to handle multiple voting options instead of being limited to binary voting.
Linked Issues check ✅ Passed All code changes align with the objective to generalize CRISP encoding/decoding for configurable number of options, including updates to constants, circuit logic, encoding functions, type definitions, and test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to generalizing CRISP vote encoding to support multiple options; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/generic-encoding-crisp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ctrlc03 ctrlc03 changed the title feat: allow to encode multiple options in CRISP votes feat: allow to encode multiple options in CRISP votes [skip-line-limit] Feb 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/CRISP/packages/crisp-sdk/src/vote.ts (1)

369-372: ⚠️ Potential issue | 🔴 Critical

Update ciphertext commitment index after adding num_options.
With num_options now a public input in the circuit, the public inputs array structure has shifted. Currently, publicInputs[5] points to num_options (a u32); the actual ciphertext commitment (the circuit's return value) is now at publicInputs[6].

Fix
-  const encryptedVoteCommitment = publicInputs[5] as `0x${string}`
+  const encryptedVoteCommitment = publicInputs[6] as `0x${string}`
🤖 Fix all issues with AI agents
In `@examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol`:
- Around line 175-178: Add a defensive validation to ensure numOptions is not
zero before computing segmentSize: check the value retrieved from
e3Data[e3Id].numOptions (and optionally that tally.length is divisible by
numOptions) and revert with a clear message if invalid; place this guard just
after reading numOptions and decoding tally in the same scope (where numOptions,
e3Data, e3Id, tally, and segmentSize are used) so the division by zero cannot
occur.

In `@examples/CRISP/packages/crisp-sdk/src/utils.ts`:
- Around line 161-166: The getMaxVoteValue function does not validate numChoices
and can produce NaN/Infinity when numChoices <= 0; update getMaxVoteValue to
validate that numChoices is a positive integer (e.g., numChoices > 0 and
Number.isInteger(numChoices)) before calling
ZKInputsGenerator.withDefaults().getBFVParams() and computing segmentSize, and
if invalid throw a clear RangeError (or return a safe default) so segmentSize,
effectiveBits, and the bigint calculation cannot produce invalid results.
🧹 Nitpick comments (3)
examples/CRISP/server/src/cli/commands.rs (1)

102-105: Hardcoded num_options limits configurability.

The num_options is hardcoded to 2, which doesn't allow CLI users to configure the number of voting options. The PR objective mentions "configurable number of options" - consider adding it as a function parameter or CLI input to initialize_crisp_round.

If hardcoding is intentional for demo purposes, this is acceptable, but consider adding a comment explaining the choice.

💡 Suggested approach to make num_options configurable
 pub async fn initialize_crisp_round(
     token_address: &str,
     balance_threshold: &str,
+    num_options: u64,
 ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
     // ...
     let token_address: Address = token_address.parse()?;
     let balance_threshold = U256::from_str_radix(&balance_threshold, 10)?;
-    let num_options = U256::from(2);
+    let num_options = U256::from(num_options);
examples/CRISP/packages/crisp-sdk/src/utils.ts (1)

173-175: Add input validation for numChoices.

getZeroVote(0) returns an empty array, and negative values would throw. Consider adding validation for consistency with getMaxVoteValue:

♻️ Proposed validation
 export const getZeroVote = (numChoices: number): bigint[] => {
+  if (numChoices <= 0) {
+    throw new Error('numChoices must be a positive integer')
+  }
   return Array(numChoices).fill(0n)
 }
examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol (1)

52-71: getE3() ignores e3Id and doesn't use the e3s mapping.

The function always returns a hardcoded E3 struct instead of reading from the e3s mapping populated by request(). This may cause issues if tests rely on retrieving specific E3 data (e.g., plaintextOutput, committeePublicKey set via other setters).

For a more accurate mock:

♻️ Proposed fix to use the mapping
-  function getE3(uint256) 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: abi.encode(address(0), 0, 2),
-        decryptionVerifier: IDecryptionVerifier(address(0)),
-        committeePublicKey: committeePublicKey,
-        ciphertextOutput: bytes32(0),
-        plaintextOutput: plaintextOutput,
-        requester: address(0)
-      });
+  function getE3(uint256 e3Id) external view returns (E3 memory) {
+    E3 storage stored = e3s[e3Id];
+    // Return stored E3 with dynamic fields from contract state
+    return E3({
+      seed: stored.seed,
+      threshold: stored.threshold,
+      requestBlock: stored.requestBlock,
+      startWindow: stored.startWindow,
+      duration: stored.duration,
+      expiration: stored.expiration,
+      encryptionSchemeId: stored.encryptionSchemeId,
+      e3Program: stored.e3Program,
+      e3ProgramParams: stored.e3ProgramParams,
+      customParams: stored.customParams,
+      decryptionVerifier: stored.decryptionVerifier,
+      committeePublicKey: committeePublicKey,  // Use current state
+      ciphertextOutput: stored.ciphertextOutput,
+      plaintextOutput: plaintextOutput,  // Use current state
+      requester: stored.requester
+    });
   }

Comment thread examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol
Comment thread examples/CRISP/packages/crisp-sdk/src/utils.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol (1)

52-71: ⚠️ Potential issue | 🟡 Minor

getE3 ignores the stored E3 data.

The request function stores E3 data in the e3s mapping (line 21-37), but getE3 ignores this stored data and always returns a hardcoded struct. This means tests using request() to set up state won't get the expected values back from getE3().

🐛 Proposed fix to use stored data
-  function getE3(uint256) 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: abi.encode(address(0), 0, 2),
-        decryptionVerifier: IDecryptionVerifier(address(0)),
-        committeePublicKey: committeePublicKey,
-        ciphertextOutput: bytes32(0),
-        plaintextOutput: plaintextOutput,
-        requester: address(0)
-      });
+  function getE3(uint256 e3Id) external view returns (E3 memory) {
+    E3 storage stored = e3s[e3Id];
+    // Return stored data if it exists, otherwise return defaults
+    if (address(stored.e3Program) != address(0)) {
+      return stored;
+    }
+    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: abi.encode(address(0), 0, 2),
+      decryptionVerifier: IDecryptionVerifier(address(0)),
+      committeePublicKey: committeePublicKey,
+      ciphertextOutput: bytes32(0),
+      plaintextOutput: plaintextOutput,
+      requester: address(0)
+    });
   }
examples/CRISP/packages/crisp-sdk/src/vote.ts (1)

81-103: ⚠️ Potential issue | 🔴 Critical

JavaScript bitwise operations are limited to 32-bit integers.

The left shift byteValue << (j * 8) wraps modulo 32 in JavaScript. When j >= 4, shifts of 32, 40, 48, 56 bits become 0, 8, 16, 24 bits respectively, causing bytes 4-7 to overwrite bytes 0-3. Based on learnings, the decoded u64 values can be any 64-bit value, so this will produce incorrect results.

🔧 Fix using BigInt for 64-bit arithmetic
-const decodeBytesToNumbers = (data: Uint8Array): number[] => {
+const decodeBytesToNumbers = (data: Uint8Array): bigint[] => {
   if (data.length % 8 !== 0) {
     throw new Error('Data length must be multiple of 8')
   }

   const arrayLength = data.length / 8
-  const result: number[] = []
+  const result: bigint[] = []

   for (let i = 0; i < arrayLength; i++) {
     const offset = i * 8
-    let value = 0
+    let value = 0n

     // Read 8 bytes in little-endian order
     for (let j = 0; j < 8; j++) {
-      const byteValue = data[offset + j]
-      value |= byteValue << (j * 8)
+      const byteValue = BigInt(data[offset + j])
+      value |= byteValue << BigInt(j * 8)
     }

     result.push(value)
   }

   return result
 }

Note: This change will require updating decodeTally to work with bigint[] instead of number[] (line 131-133 already uses BigInt conversions, so the impact is minimal).

🤖 Fix all issues with AI agents
In `@examples/CRISP/client/src/hooks/voting/useVoteCasting.ts`:
- Around line 132-142: The code hardcodes 2-element vote vectors which breaks
multi-option polls; update the VoteStateLite type to include num_options
(matching the SDK), update the Poll interface to expose either num_options or an
options array, and then replace every fixed [0n,0n], [balance,0n], and
[0n,balance] construction inside useVoteCasting (and its catch returns) with a
dynamic builder: create const numOptions = voteState.num_options ||
poll.num_options || poll.options.length and use Array(numOptions).fill(0n) for
the base vector, and when setting a selected choice assign
vote[pollSelected.value] = balance (or similar) so the vote vector length
matches the poll option count; ensure all places that returned [0n,0n]
(including initial returns and error branches) use this dynamic construction.

In `@examples/CRISP/packages/crisp-sdk/src/vote.ts`:
- Around line 266-272: The check inside validateVote is tautological because
numChoices is set to vote.length, so the condition vote.length !== numChoices
will never be true; either remove that dead check or change the function
signature to accept an expected count (e.g., expectedNumChoices) and compare
that to vote.length. If you choose the latter, update the function
validateVote(vote: Vote, balance: bigint, expectedNumChoices: number) and
replace the current comparison with if (vote.length !== expectedNumChoices)
throw a descriptive Error; also ensure getMaxVoteValue(numChoices) uses the
intended count (use expectedNumChoices instead of numChoices) or adjust logic
accordingly.
🧹 Nitpick comments (5)
examples/CRISP/server/src/server/indexer.rs (1)

77-81: Consider validating num_options before use.

While the field is correctly extracted from the decoded tuple, there's no validation that num_options is a sensible value (e.g., > 0 and within the expected MAX_OPTIONS limit). If the contract enforces this, it may be acceptable, but defensive validation here would catch malformed data early.

examples/CRISP/server/src/cli/commands.rs (1)

102-105: Consider making num_options configurable.

The value is hardcoded to 2, which works for binary voting but limits the flexibility that this PR aims to introduce. Consider accepting num_options as a function parameter or reading it from the config, similar to token_address and balance_threshold.

 pub async fn initialize_crisp_round(
     token_address: &str,
     balance_threshold: &str,
+    num_options: u64,
 ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
     // ...
-    let num_options = U256::from(2);
+    let num_options = U256::from(num_options);
examples/CRISP/client/src/model/vote.model.ts (1)

59-59: Type definition aligns with SDK but creates duplication.

The Vote type is now defined identically in both this file and @crisp-e3/sdk (examples/CRISP/packages/crisp-sdk/src/types.ts line 69). Consider importing the type from the SDK instead of redefining it to maintain a single source of truth.

+import { Vote } from '@crisp-e3/sdk'
+
 // ... other type definitions ...
 
-export type Vote = bigint[]
+export { Vote }
examples/CRISP/packages/crisp-sdk/src/vote.ts (2)

52-54: Redundant conditional check.

Since choiceIdx iterates from 0 to n-1 and n = vote.length, the condition choiceIdx < vote.length is always true. The fallback 0n is unreachable.

🧹 Simplify to remove dead code
   for (let choiceIdx = 0; choiceIdx < n; choiceIdx += 1) {
-    const value = choiceIdx < vote.length ? vote[choiceIdx] : 0n
+    const value = vote[choiceIdx]
     const binary = toBinary(value).split('')

130-134: Update to align with decodeBytesToNumbers fix.

Once decodeBytesToNumbers returns bigint[] (per the fix above), the BigInt() conversion on line 133 becomes unnecessary.

🧹 After fixing decodeBytesToNumbers
     let value = 0n
     for (let i = 0; i < segment.length; i++) {
       const weight = 2n ** BigInt(segment.length - 1 - i)
-      value += BigInt(segment[i]) * weight
+      value += segment[i] * weight
     }

Comment thread examples/CRISP/client/src/hooks/voting/useVoteCasting.ts
Comment thread examples/CRISP/packages/crisp-sdk/src/vote.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/enclave-contracts/contracts/Enclave.sol (1)

262-284: ⚠️ Potential issue | 🔴 Critical

Persist encryptionSchemeId/decryptionVerifier to storage.
e3s[e3Id] is written before validate, but encryptionSchemeId and decryptionVerifier are only updated on the memory copy afterward. This leaves storage with zeroed fields and breaks later verification flows.

🛠️ Suggested fix
         e3.encryptionSchemeId = encryptionSchemeId;
         e3.decryptionVerifier = decryptionVerifier;
+
+        e3s[e3Id].encryptionSchemeId = encryptionSchemeId;
+        e3s[e3Id].decryptionVerifier = decryptionVerifier;
🤖 Fix all issues with AI agents
In `@examples/CRISP/packages/crisp-sdk/src/vote.ts`:
- Around line 111-140: The decodeTally function currently relies on
decodeBytesToNumbers which returns number[] and can lose precision for u64
coefficients; update decodeBytesToNumbers to decode into BigInt[] using
DataView.getBigUint64 (choose consistent endianness) so 64-bit values are
preserved, then update decodeTally to accept BigInt[] (adjust variables like
numbers, segment values, and the accumulation to use BigInt arithmetic) and
ensure MAX_VOTE_BITS and any comparisons use BigInt-safe logic; keep function
names decodeBytesToNumbers and decodeTally so callers remain valid.
- Around line 43-71: The encodeVote function must validate and reject empty vote
arrays to avoid division by zero: inside encodeVote (and since encryptVote calls
it), check if vote.length === 0 and throw a clear Error (e.g., "vote must
contain at least one choice") before computing segmentSize/degree; this mirrors
the guard in decodeTally and prevents Infinity/NaN propagation in segmentSize,
remainder, and the returned encoding.
🧹 Nitpick comments (2)
examples/CRISP/client/src/utils/constants.ts (1)

12-13: Centralize NUM_OPTIONS to avoid drift.
Consider sharing this constant with the worker/config (instead of duplicating) so a future option-count change doesn’t silently desync masking proofs.

examples/CRISP/client/libs/crispSDKWorker.js (1)

9-10: Avoid hardcoding numOptions in the worker.
If the app’s option count changes, masking proofs will be generated with the wrong value. Consider passing numOptions in the message payload (or importing a shared constant) to keep it in sync.

♻️ Suggested approach
-const NUM_OPTIONS = 2
+const DEFAULT_NUM_OPTIONS = 2

 self.onmessage = async function (event) {
   const { type, data } = event.data
   switch (type) {
     case 'generate_proof':
       try {
-        const { e3Id, vote, publicKey, balance, address: slotAddress, signature, messageHash, isMasking, crispServer, merkleLeaves } = data
+        const {
+          e3Id,
+          vote,
+          publicKey,
+          balance,
+          address: slotAddress,
+          signature,
+          messageHash,
+          isMasking,
+          crispServer,
+          merkleLeaves,
+          numOptions = DEFAULT_NUM_OPTIONS,
+        } = data
...
-            numOptions: NUM_OPTIONS,
+            numOptions,

Also applies to: 23-30

Comment thread examples/CRISP/packages/crisp-sdk/src/vote.ts
Comment thread examples/CRISP/packages/crisp-sdk/src/vote.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol (1)

52-70: ⚠️ Potential issue | 🟡 Minor

Return stored E3s to keep request/getE3 consistent.

request() persists to e3s, but getE3() ignores the mapping and always returns a static struct, which will be incorrect for non‑zero E3 IDs or multiple requests.

🛠️ Suggested fix
-  function getE3(uint256) external view returns (E3 memory) {
-    return
-      E3({
+  function getE3(uint256 e3Id) external view returns (E3 memory) {
+    E3 memory stored = e3s[e3Id];
+    if (address(stored.e3Program) != address(0)) {
+      return stored;
+    }
+    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: abi.encode(address(0), 0, 2),
+        customParams: abi.encode(address(0), e3Id, 2),
         decryptionVerifier: IDecryptionVerifier(address(0)),
         committeePublicKey: committeePublicKey,
         ciphertextOutput: bytes32(0),
         plaintextOutput: plaintextOutput,
         requester: address(0)
-      });
+      });
   }
🤖 Fix all issues with AI agents
In `@examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol`:
- Around line 181-193: The code can produce segmentSize == 0 (and thus silent
zero tallies) when numOptions (from e3Data[e3Id].numOptions) is greater than
tally.length; add a guard after decoding tally (from
_decodeBytesToUint64Array(e3.plaintextOutput)) to detect segmentSize == 0 (i.e.,
tally.length < numOptions) and return an empty uint256[] (or revert with a clear
message) instead of continuing; update logic around segmentSize, effectiveSize
and any downstream loops that assume non‑zero segments to early-exit on this
condition to avoid producing all-zero results.

In `@examples/CRISP/packages/crisp-sdk/src/vote.ts`:
- Around line 52-68: The encodeVote logic allows each choice to use up to
segmentSize bits which can exceed the total MAX_VOTE_BITS that decodeTally
reads; update encodeVote (the loop using segmentSize, voteArray, toBinary) to
enforce a per-choice bit cap derived from MAX_VOTE_BITS (e.g., maxSegment =
Math.floor(MAX_VOTE_BITS / n)) and validate that binary.length <= maxSegment (or
reduce segmentSize to min(segmentSize, maxSegment)) and throw a clear Error
referencing MAX_VOTE_BITS and the choice index when exceeded so
encryptVote/encodeVote cannot produce values that decodeTally will truncate.
🧹 Nitpick comments (1)
templates/default/contracts/MyProgram.sol (1)

54-65: Interface signature updated correctly for customParams support.

The validate function now includes the additional bytes calldata parameter to comply with the updated IE3Program interface. The parameter is intentionally unused in this template, which is appropriate.

Consider naming the parameter customParams and adding a brief NatSpec note for template users who may want to use it (as CRISPProgram.sol does for numOptions decoding):

📝 Suggested documentation improvement
   /// `@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, bytes calldata) external returns (bytes32) {
+  /// `@param` customParams Optional custom parameters for program-specific configuration
+  function validate(uint256 e3Id, uint256, bytes calldata e3ProgramParams, bytes calldata, bytes calldata customParams) external returns (bytes32) {
     require(authorizedContracts[msg.sender] || msg.sender == owner(), CallerNotAuthorized());
     require(paramsHashes[e3Id] == bytes32(0), E3AlreadyInitialized());
     paramsHashes[e3Id] = keccak256(e3ProgramParams);

Comment thread examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol
Comment thread examples/CRISP/packages/crisp-sdk/src/vote.ts
@vercel vercel Bot temporarily deployed to Preview – crisp February 5, 2026 09:00 Inactive
@vercel vercel Bot temporarily deployed to Preview – enclave-docs February 5, 2026 09:00 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@examples/CRISP/packages/crisp-sdk/src/vote.ts`:
- Around line 262-292: validateVote currently omits the minimum choices check
that encodeVote enforces, so add a guard at the start of validateVote (which
accepts vote: Vote and balance: bigint) to throw an Error if vote.length < 2;
keep this check before computing numChoices or maxValue (which uses
getMaxVoteValue) so the binary vs multi-choice logic only runs for 2+ options
and prevents incorrect "3+ options" handling for single-choice votes.

Comment thread examples/CRISP/packages/crisp-sdk/src/vote.ts
Comment thread examples/CRISP/client/package.json Outdated
Comment thread examples/CRISP/client/src/utils/constants.ts
- Updated @crisp-e3/sdk to 0.5.9
- Updated @crisp-e3/contracts to 0.5.9
- Updated @crisp-e3/zk-inputs to 0.5.9
- Published to npm
@vercel vercel Bot temporarily deployed to Preview – enclave-docs February 5, 2026 09:35 Inactive
@vercel vercel Bot temporarily deployed to Preview – crisp February 5, 2026 09:35 Inactive
@ctrlc03 ctrlc03 requested a review from cedoor February 5, 2026 09:43
@ctrlc03 ctrlc03 enabled auto-merge (squash) February 5, 2026 09:43
@ctrlc03 ctrlc03 merged commit 4f84a52 into main Feb 5, 2026
27 checks passed
@ctrlc03 ctrlc03 deleted the feat/generic-encoding-crisp branch February 5, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generalize CRISP encoding/decoding function to accept number of options

2 participants