Skip to content

chore: check if this is the first vote or mask#946

Merged
ctrlc03 merged 13 commits into
devfrom
feat/check-if-first-time-voting
Nov 4, 2025
Merged

chore: check if this is the first vote or mask#946
ctrlc03 merged 13 commits into
devfrom
feat/check-if-first-time-voting

Conversation

@ctrlc03

@ctrlc03 ctrlc03 commented Nov 3, 2025

Copy link
Copy Markdown
Collaborator

fix #944

merge #938 first

Summary by CodeRabbit

  • New Features

    • Added first-vote vs subsequent-vote handling and return-value-aware proof generation.
  • Improvements

    • Coefficient-zero check now returns a boolean status instead of asserting.
    • Vote encryption populates complete circuit inputs (signatures, Merkle data, balance, is_first_vote).
  • Tests

    • Added test utilities and updated tests to cover first-vote and return-value behavior.
  • Documentation

    • Added an explanatory comment about empty-slot checks in the validator.

@ctrlc03 ctrlc03 requested review from 0xjei and cedoor November 3, 2025 14:17
@ctrlc03 ctrlc03 self-assigned this Nov 3, 2025
@vercel

vercel Bot commented Nov 3, 2025

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
crisp Skipped Skipped Nov 4, 2025 3:15pm
enclave-docs Skipped Skipped Nov 4, 2025 3:15pm

@coderabbitai

coderabbitai Bot commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds an is_first_vote flag to the CRISP circuit and SDK, changes check_coefficient_zero to return a boolean, propagates the flag through masking/proof generation, extends SDK inputs/exports, updates tests and utilities, and adds a commented note in the contract validator.

Changes

Cohort / File(s) Summary
Noir circuit
examples/CRISP/circuits/src/main.nr, examples/CRISP/circuits/src/utils.nr
Added is_first_vote: pub bool to main. Control flow now returns (ct0is, ct1is) when is_first_vote is true; otherwise checks check_coefficient_zero (now returns bool) and validates/returns summed ciphertexts. Replaced direct assertion with a boolean result for coefficient-zero checks.
SDK types
examples/CRISP/packages/crisp-sdk/src/types.ts
Added is_first_vote: boolean to CRISPCircuitInputs and isFirstVote: boolean to EncryptVoteAndGenerateCRISPInputsParams.
SDK implementation & API
examples/CRISP/packages/crisp-sdk/src/vote.ts
Propagated isFirstVote through generateMaskVote and encryptVoteAndGenerateCRISPInputs; added generateProofWithReturnValue(crispInputs) to execute Noir and return { returnValue, proof }; populated CRISP inputs with signature/public-key/hashed message/merkle data/slot/balance and is_first_vote.
SDK test utilities
examples/CRISP/packages/crisp-sdk/tests/utils.ts
Added normalizeCoefficient() and compareCoefficientsArrays() helpers for coefficient normalization and comparison.
SDK tests
examples/CRISP/packages/crisp-sdk/tests/vote.test.ts
Updated tests to pass slotAddress and isFirstVote, use generateProofWithReturnValue, and assert returnValue matches expected ct0/ct1 or summed ct values via comparison utilities.
Contract notes
examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol
Added an inline commented note about checking for empty voter slot (documentation/comment only).

Sequence Diagram(s)

sequenceDiagram
    participant SDK as SDK (vote.ts)
    participant Noir as Noir runtime
    participant Circuit as Circuit (main.nr)

    SDK->>SDK: prepare crispInputs (includes is_first_vote)
    SDK->>Noir: execute circuit with crispInputs
    Noir->>Circuit: main(is_first_vote, inputs...)

    alt is_first_vote == true
        Note right of Circuit `#DFF2E1`: first-vote path (no sum validation)
        Circuit->>Circuit: return (ct0is, ct1is)
    else is_first_vote == false
        Note right of Circuit `#FFF3D6`: subsequent-vote path (validate sums)
        Circuit->>Circuit: is_vote_zero = check_coefficient_zero(k1) -> bool
        Circuit->>Circuit: assert(is_ct_add_valid) then return (sum_ct0is, sum_ct1is)
    end

    Circuit-->>Noir: returnValue
    Noir-->>SDK: { returnValue, proof }
    SDK->>SDK: return enhanced result to caller
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas needing extra attention:
    • Correct typing and propagation of is_first_vote across SDK ⇢ circuit inputs and tests.
    • All call sites updated for check_coefficient_zero now returning bool.
    • Integration and error handling around generateProofWithReturnValue and how returnValue is interpreted in tests.

Possibly related PRs

Suggested reviewers

  • 0xjei
  • ctrlc03

Poem

🐇
A new-first hop into a slot so bright,
Flags set, the mask returns its light,
Sums wait until the rounds proceed,
Proofs whisper what the logic need,
Little rabbit cheers: vote right!

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: check if this is the first vote or mask' directly addresses the main objective: handling the first vote/masking operation on a slot, which aligns with the changes throughout the codebase.
Linked Issues check ✅ Passed The PR implements the core requirement of #944 by adding is_first_vote parameter to circuits and SDK, enabling first-vote vs. subsequent-vote path differentiation in the validation logic.
Out of Scope Changes check ✅ Passed All changes are scoped to supporting first-vote detection: circuit logic, type definitions, and utility functions for vote handling. No unrelated modifications are present.
✨ 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/check-if-first-time-voting

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8e2d75 and cf977a2.

📒 Files selected for processing (7)
  • examples/CRISP/circuits/src/main.nr (3 hunks)
  • examples/CRISP/circuits/src/utils.nr (5 hunks)
  • examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/types.ts (2 hunks)
  • examples/CRISP/packages/crisp-sdk/src/vote.ts (7 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/utils.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/CRISP/packages/crisp-sdk/tests/vote.test.ts
  • examples/CRISP/packages/crisp-sdk/src/types.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T13:49:48.617Z
Learning: In CRISP circuits using Greco library, the binary check `assert(0 == b * (qmt - b))` for polynomial k1 should not be applied to all coefficients - only specific coefficients require this constraint, such as `k1.coefficients[2048 - 1]`.
📚 Learning: 2025-08-27T13:49:48.617Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T13:49:48.617Z
Learning: In CRISP circuits using Greco library, the binary check `assert(0 == b * (qmt - b))` for polynomial k1 should not be applied to all coefficients - only specific coefficients require this constraint, such as `k1.coefficients[2048 - 1]`.

Applied to files:

  • examples/CRISP/circuits/src/main.nr
  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-09-22T15:08:29.814Z
Learnt from: ozgurarmanc
Repo: gnosisguild/enclave PR: 734
File: packages/circuits/crates/libs/polynomial/src/lib.nr:140-155
Timestamp: 2025-09-22T15:08:29.814Z
Learning: Greco (packages/circuits/crates/libs/greco/src/lib.nr) performs range_check_1bound/2bounds on all polynomials (u, e0/e1, k1, pk*, r*, p*) before serialization; packer/flatten rely on these bounds, so per-limb asserts inside packer are unnecessary in this crate’s flow.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-08-27T14:02:25.412Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T14:02:25.412Z
Learning: In CRISP circuits using Greco library, k1 represents a binary value encoded as a polynomial where only one specific coefficient (k1.coefficients[2048 - 1]) carries the actual binary information and needs the constraint `assert(0 == b * (qmt - b))`. Other coefficients in the polynomial are not significant and don't require the binary constraint.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-10-10T12:56:40.538Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 830
File: templates/default/README.md:123-128
Timestamp: 2025-10-10T12:56:40.538Z
Learning: In the Enclave repository, the hard-coded Hardhat development private key `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` is acceptable in template README files and documentation for local testing/interaction purposes.

Applied to files:

  • examples/CRISP/packages/crisp-sdk/src/vote.ts
🧬 Code graph analysis (1)
examples/CRISP/packages/crisp-sdk/src/vote.ts (1)
examples/CRISP/packages/crisp-sdk/src/types.ts (1)
  • CRISPCircuitInputs (135-173)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: build_enclave_cli
  • GitHub Check: integration_prebuild
  • GitHub Check: build_sdk
  • GitHub Check: rust_integration
  • GitHub Check: test_contracts
  • GitHub Check: test_net
  • GitHub Check: build_e3_support_dev
  • GitHub Check: rust_unit

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 linked an issue Nov 3, 2025 that may be closed by this pull request
@vercel vercel Bot temporarily deployed to Preview – enclave-docs November 3, 2025 14:25 Inactive
@vercel vercel Bot temporarily deployed to Preview – crisp November 3, 2025 14:25 Inactive
@ctrlc03 ctrlc03 changed the title chore: check vote is zero for masking chore: check if this is the first vote or mask Nov 3, 2025

@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: 3

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c478909 and d4a0b19.

📒 Files selected for processing (10)
  • examples/CRISP/circuits/src/main.nr (4 hunks)
  • examples/CRISP/circuits/src/utils.nr (2 hunks)
  • examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/constants.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/types.ts (2 hunks)
  • examples/CRISP/packages/crisp-sdk/src/utils.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/vote.ts (7 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/constants.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/utils.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-27T13:49:48.617Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T13:49:48.617Z
Learning: In CRISP circuits using Greco library, the binary check `assert(0 == b * (qmt - b))` for polynomial k1 should not be applied to all coefficients - only specific coefficients require this constraint, such as `k1.coefficients[2048 - 1]`.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-09-22T15:08:29.814Z
Learnt from: ozgurarmanc
Repo: gnosisguild/enclave PR: 734
File: packages/circuits/crates/libs/polynomial/src/lib.nr:140-155
Timestamp: 2025-09-22T15:08:29.814Z
Learning: Greco (packages/circuits/crates/libs/greco/src/lib.nr) performs range_check_1bound/2bounds on all polynomials (u, e0/e1, k1, pk*, r*, p*) before serialization; packer/flatten rely on these bounds, so per-limb asserts inside packer are unnecessary in this crate’s flow.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2024-10-22T02:10:34.864Z
Learnt from: ryardley
Repo: gnosisguild/enclave PR: 145
File: packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs:82-83
Timestamp: 2024-10-22T02:10:34.864Z
Learning: In the file `packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs`, when reviewing test functions like `generate_pk_share`, minor performance optimizations (e.g., minimizing mutex locks) are not a priority.

Applied to files:

  • examples/CRISP/circuits/src/main.nr
🧬 Code graph analysis (4)
examples/CRISP/packages/crisp-sdk/tests/constants.ts (1)
examples/CRISP/packages/crisp-sdk/src/utils.ts (1)
  • generateMerkleProof (39-79)
examples/CRISP/packages/crisp-sdk/src/constants.ts (1)
examples/CRISP/packages/crisp-sdk/tests/constants.ts (1)
  • MESSAGE (11-11)
examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (4)
examples/CRISP/packages/crisp-sdk/tests/constants.ts (6)
  • testAddress (32-32)
  • merkleProof (33-33)
  • MESSAGE (11-11)
  • votingPowerLeaf (31-31)
  • LEAVES (16-27)
  • VOTE (14-14)
examples/CRISP/packages/crisp-sdk/src/vote.ts (5)
  • generateMaskVote (215-256)
  • encodeVote (64-90)
  • encryptVoteAndGenerateCRISPInputs (164-202)
  • generateProof (258-266)
  • generateProofWithReturnValue (268-276)
examples/CRISP/packages/crisp-sdk/src/utils.ts (2)
  • hashLeaf (18-20)
  • generateMerkleProof (39-79)
examples/CRISP/packages/crisp-sdk/tests/utils.ts (1)
  • compareCoefficientsArrays (14-35)
examples/CRISP/packages/crisp-sdk/src/vote.ts (3)
examples/CRISP/packages/crisp-sdk/src/constants.ts (2)
  • DEFAULT_BFV_PARAMS (24-24)
  • MESSAGE (29-29)
examples/CRISP/packages/crisp-sdk/src/signature.ts (1)
  • extractSignature (17-45)
examples/CRISP/packages/crisp-sdk/src/types.ts (1)
  • CRISPCircuitInputs (135-173)
🪛 Gitleaks (8.28.0)
examples/CRISP/packages/crisp-sdk/tests/vote.test.ts

[high] 208-208: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: integration_prebuild
  • GitHub Check: build_sdk
  • GitHub Check: test_net
  • GitHub Check: build_enclave_cli
  • GitHub Check: test_contracts
  • GitHub Check: rust_integration
  • GitHub Check: build_e3_support_risc0
  • GitHub Check: rust_unit
🔇 Additional comments (6)
examples/CRISP/packages/crisp-sdk/src/utils.ts (1)

65-66: Consistent index source for proof bits

Pulling the bitmask from proof.index keeps the derived indices aligned with the proof data—nice cleanup.

examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol (1)

92-96: Documenting the upcoming first-vote guard

Appreciate the inline note capturing the intent to surface the first-vote flag to the verifier—it clarifies the roadmap while keeping logic untouched.

examples/CRISP/packages/crisp-sdk/src/types.ts (2)

171-173: Propagation hook for first-vote flag

Adding is_first_vote to the circuit inputs ensures the new Noir constraint has a clear entry point from the SDK surface.


218-220: SDK params stay in sync with circuit inputs

Extending the encrypt/generate params with slotAddress and isFirstVote keeps the higher-level API aligned with the circuit’s expectations.

examples/CRISP/packages/crisp-sdk/src/constants.ts (1)

26-29: Centralizing the default masking message

Elevating the vote message into shared constants avoids hard-coded duplicates elsewhere—good call.

examples/CRISP/packages/crisp-sdk/tests/constants.ts (1)

31-33: Reusable slot address for test proof generation

Breaking the slot address into its own export keeps the test fixtures tidy and easier to reuse.

Comment thread examples/CRISP/circuits/src/utils.nr Outdated
Comment thread examples/CRISP/circuits/src/utils.nr
Comment thread examples/CRISP/packages/crisp-sdk/src/vote.ts
@vercel vercel Bot temporarily deployed to Preview – crisp November 3, 2025 14:41 Inactive
@vercel vercel Bot temporarily deployed to Preview – enclave-docs November 3, 2025 14:41 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: 0

🧹 Nitpick comments (2)
examples/CRISP/circuits/src/utils.nr (2)

34-63: Critical security issue from past review has been resolved.

The new implementation correctly checks each coefficient individually instead of using sum-based validation, which eliminates the Field arithmetic cancellation vulnerability. The tests now properly assert the return value as well.

Optional: Consider early return for efficiency.

While functionally correct, the current implementation continues checking all coefficients even after finding a non-zero value. For better performance, consider returning false immediately upon finding any non-zero coefficient:

 pub fn check_coefficient_zero<let D: u32>(k1: Polynomial<D>) -> bool {
     let HALF_LARGEST_MINIMUM_DEGREE = 28;
     let HALF_D = D / 2;
     let START_INDEX_Y = HALF_D - HALF_LARGEST_MINIMUM_DEGREE;
     let START_INDEX_N = D - HALF_LARGEST_MINIMUM_DEGREE;
-    let mut res = true;
 
     // Loop through all coefficients in the space where we could have a vote
     // yes part
     for i in START_INDEX_Y..HALF_D {
         if k1.coefficients[i] != 0 {
-            res = false;
+            return false;
         }
     }
 
     // no part
     for i in START_INDEX_N..D {
         if k1.coefficients[i] != 0 {
-            res = false;
+            return false;
         }
     }
 
-    res 
+    true
 }

39-43: Optional: Extract shared constants to reduce duplication.

The constants HALF_LARGEST_MINIMUM_DEGREE, HALF_D, START_INDEX_Y, and START_INDEX_N are duplicated between check_coefficient_values and check_coefficient_zero. Consider extracting them to module-level constants for maintainability.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b0729b and c95a19c.

📒 Files selected for processing (1)
  • examples/CRISP/circuits/src/utils.nr (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-27T13:49:48.617Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T13:49:48.617Z
Learning: In CRISP circuits using Greco library, the binary check `assert(0 == b * (qmt - b))` for polynomial k1 should not be applied to all coefficients - only specific coefficients require this constraint, such as `k1.coefficients[2048 - 1]`.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-09-22T15:08:29.814Z
Learnt from: ozgurarmanc
Repo: gnosisguild/enclave PR: 734
File: packages/circuits/crates/libs/polynomial/src/lib.nr:140-155
Timestamp: 2025-09-22T15:08:29.814Z
Learning: Greco (packages/circuits/crates/libs/greco/src/lib.nr) performs range_check_1bound/2bounds on all polynomials (u, e0/e1, k1, pk*, r*, p*) before serialization; packer/flatten rely on these bounds, so per-limb asserts inside packer are unnecessary in this crate’s flow.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-08-27T14:02:25.412Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T14:02:25.412Z
Learning: In CRISP circuits using Greco library, k1 represents a binary value encoded as a polynomial where only one specific coefficient (k1.coefficients[2048 - 1]) carries the actual binary information and needs the constraint `assert(0 == b * (qmt - b))`. Other coefficients in the polynomial are not significant and don't require the binary constraint.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test_net
  • GitHub Check: build_e3_support_dev
  • GitHub Check: test_contracts
  • GitHub Check: rust_integration
  • GitHub Check: build_enclave_cli
  • GitHub Check: integration_prebuild
  • GitHub Check: rust_unit
  • GitHub Check: build_sdk
🔇 Additional comments (2)
examples/CRISP/circuits/src/utils.nr (2)

93-105: LGTM! Test properly validates failure case.

The test correctly asserts the return value and places a non-zero coefficient (1 at index 99) within the checked range, which should cause the function to return false. This addresses the concern from the past review.


107-119: LGTM! Test properly validates success case.

The test correctly asserts the return value for a polynomial with all zero coefficients, verifying the expected true return. This addresses the concern from the past review.

@cedoor cedoor force-pushed the feat/check-if-first-time-voting branch from c95a19c to 7ba9d83 Compare November 4, 2025 10:51
@vercel vercel Bot temporarily deployed to Preview – crisp November 4, 2025 10:52 Inactive
@vercel vercel Bot temporarily deployed to Preview – enclave-docs November 4, 2025 10:52 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: 3

♻️ Duplicate comments (1)
examples/CRISP/packages/crisp-sdk/src/vote.ts (1)

236-255: Strip the hardcoded private key from production SDK code.

This security issue was previously flagged. The hardcoded Hardhat key should not be embedded in SDK functions; instead, accept signature inputs as parameters so callers control the signing key.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c95a19c and 7ba9d83.

📒 Files selected for processing (10)
  • examples/CRISP/circuits/src/main.nr (4 hunks)
  • examples/CRISP/circuits/src/utils.nr (2 hunks)
  • examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/constants.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/types.ts (2 hunks)
  • examples/CRISP/packages/crisp-sdk/src/utils.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/vote.ts (7 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/constants.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/utils.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/CRISP/packages/crisp-sdk/tests/utils.ts
  • examples/CRISP/packages/crisp-sdk/tests/constants.ts
  • examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-08-27T13:49:48.617Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T13:49:48.617Z
Learning: In CRISP circuits using Greco library, the binary check `assert(0 == b * (qmt - b))` for polynomial k1 should not be applied to all coefficients - only specific coefficients require this constraint, such as `k1.coefficients[2048 - 1]`.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-09-22T15:08:29.814Z
Learnt from: ozgurarmanc
Repo: gnosisguild/enclave PR: 734
File: packages/circuits/crates/libs/polynomial/src/lib.nr:140-155
Timestamp: 2025-09-22T15:08:29.814Z
Learning: Greco (packages/circuits/crates/libs/greco/src/lib.nr) performs range_check_1bound/2bounds on all polynomials (u, e0/e1, k1, pk*, r*, p*) before serialization; packer/flatten rely on these bounds, so per-limb asserts inside packer are unnecessary in this crate’s flow.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-08-27T14:02:25.412Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T14:02:25.412Z
Learning: In CRISP circuits using Greco library, k1 represents a binary value encoded as a polynomial where only one specific coefficient (k1.coefficients[2048 - 1]) carries the actual binary information and needs the constraint `assert(0 == b * (qmt - b))`. Other coefficients in the polynomial are not significant and don't require the binary constraint.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2024-10-22T02:10:34.864Z
Learnt from: ryardley
Repo: gnosisguild/enclave PR: 145
File: packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs:82-83
Timestamp: 2024-10-22T02:10:34.864Z
Learning: In the file `packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs`, when reviewing test functions like `generate_pk_share`, minor performance optimizations (e.g., minimizing mutex locks) are not a priority.

Applied to files:

  • examples/CRISP/circuits/src/main.nr
📚 Learning: 2025-10-10T12:56:40.538Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 830
File: templates/default/README.md:123-128
Timestamp: 2025-10-10T12:56:40.538Z
Learning: In the Enclave repository, the hard-coded Hardhat development private key `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` is acceptable in template README files and documentation for local testing/interaction purposes.

Applied to files:

  • examples/CRISP/packages/crisp-sdk/src/vote.ts
🧬 Code graph analysis (3)
examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (5)
examples/CRISP/packages/crisp-sdk/tests/constants.ts (5)
  • testAddress (32-32)
  • MESSAGE (11-11)
  • votingPowerLeaf (31-31)
  • LEAVES (16-27)
  • VOTE (14-14)
examples/CRISP/packages/crisp-sdk/src/vote.ts (6)
  • generateMaskVote (215-256)
  • encodeVote (64-90)
  • encryptVoteAndGenerateCRISPInputs (164-202)
  • generateProof (258-266)
  • verifyProof (278-282)
  • generateProofWithReturnValue (268-276)
examples/CRISP/packages/crisp-sdk/src/constants.ts (2)
  • DEFAULT_BFV_PARAMS (24-24)
  • MESSAGE (29-29)
examples/CRISP/packages/crisp-sdk/src/utils.ts (2)
  • hashLeaf (18-20)
  • generateMerkleProof (39-79)
examples/CRISP/packages/crisp-sdk/tests/utils.ts (1)
  • compareCoefficientsArrays (14-35)
examples/CRISP/packages/crisp-sdk/src/constants.ts (1)
examples/CRISP/packages/crisp-sdk/tests/constants.ts (1)
  • MESSAGE (11-11)
examples/CRISP/packages/crisp-sdk/src/vote.ts (3)
examples/CRISP/packages/crisp-sdk/src/constants.ts (2)
  • DEFAULT_BFV_PARAMS (24-24)
  • MESSAGE (29-29)
examples/CRISP/packages/crisp-sdk/src/signature.ts (1)
  • extractSignature (17-45)
examples/CRISP/packages/crisp-sdk/src/types.ts (1)
  • CRISPCircuitInputs (135-173)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: build_e3_support_risc0
  • GitHub Check: rust_integration
  • GitHub Check: integration_prebuild
  • GitHub Check: test_net
  • GitHub Check: rust_unit
  • GitHub Check: build_sdk
  • GitHub Check: build_enclave_cli
  • GitHub Check: test_contracts
🔇 Additional comments (6)
examples/CRISP/packages/crisp-sdk/src/constants.ts (1)

26-29: Message literal centralization looks good

Bundling the masking/signature message behind MESSAGE keeps the signing flow and tests in sync. 👍

examples/CRISP/packages/crisp-sdk/src/utils.ts (1)

65-67: Correct bit derivation

Using proof.index to compute the padded index bits keeps the witness consistent even when the caller no longer tracks the raw index separately. Looks good.

examples/CRISP/packages/crisp-sdk/src/types.ts (1)

171-173: Type surface matches the circuit changes

The extra slotAddress/isFirstVote fields and the is_first_vote circuit input keep the TS types aligned with the Noir entrypoint. 👍

Also applies to: 218-220

examples/CRISP/circuits/src/utils.nr (1)

34-63: Zero-vote guard helper looks good

Iterating the yes/no windows and flipping the flag on the first non-zero coefficient gives the precise “is it really zero?” predicate we needed, and the new tests cover both pass/fail paths. Nice addition.

Also applies to: 94-119

examples/CRISP/packages/crisp-sdk/src/vote.ts (2)

164-201: LGTM! Function properly extended for first-vote support.

The additions of slotAddress, isFirstVote, and merkle/signature parameters correctly populate all required circuit inputs.


248-249: Zero merkle proofs for mask votes are correctly handled by the circuit design.

After examining the circuit source (examples/CRISP/circuits/src/main.nr), I can confirm the merkle proof validation is not skipped—it's always performed via get_merkle_root() at line 79-85. However, the zero proofs (combined with balance='0' and merkle_proof_length='1') intentionally compute a merkle root that won't match the provided merkle_root, causing the circuit to enter the mask vote code path (else block, lines 111-122).

This is the intended design: normal votes must pass merkle validation; mask votes deliberately fail it to trigger the alternative branch. The circuit then verifies the vote is zero and validates the ciphertext addition. The is_first_vote flag controls what gets returned. Tests confirm this works as expected with both isFirstVote states passing verification.

Comment thread examples/CRISP/circuits/src/main.nr
Comment thread examples/CRISP/packages/crisp-sdk/src/vote.ts
Comment thread examples/CRISP/packages/crisp-sdk/tests/vote.test.ts
@ctrlc03 ctrlc03 force-pushed the feat/check-if-first-time-voting branch from 7ba9d83 to fb8d8e2 Compare November 4, 2025 12:07
@vercel vercel Bot temporarily deployed to Preview – enclave-docs November 4, 2025 12:07 Inactive
@vercel vercel Bot temporarily deployed to Preview – crisp November 4, 2025 12:07 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: 0

♻️ Duplicate comments (3)
examples/CRISP/packages/crisp-sdk/src/vote.ts (1)

236-240: Hardcoded private key remains a security risk.

The function still embeds the Hardhat default private key and derives signatures internally. This is a security footgun — any production code consuming this helper ships a known secret. Please refactor to accept signature inputs (hashed_message, pub_key_x, pub_key_y, signature) from the caller, allowing tests to pass the Hardhat key externally while production code provides real credentials.

Extend the function signature:

 export const generateMaskVote = async (
   publicKey: Uint8Array,
   previousCiphertext: Uint8Array,
   bfvParams = DEFAULT_BFV_PARAMS,
   merkleRoot: bigint,
   slotAddress: string,
   isFirstVote: boolean,
+  signatureInputs: NoirSignatureInputs,
 ): Promise<CRISPCircuitInputs> => {

Then replace lines 236-240 with:

-  // hardhat default private key
-  const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
-  const account = privateKeyToAccount(privateKey)
-  const signature = await account.signMessage({ message: MESSAGE })
-  const { hashed_message, pub_key_x, pub_key_y, signature: extractedSignature } = await extractSignature(MESSAGE, signature)
-
   return {
     ...crispInputs,
-    hashed_message: Array.from(hashed_message).map((b) => b.toString()),
-    public_key_x: Array.from(pub_key_x).map((b) => b.toString()),
-    public_key_y: Array.from(pub_key_y).map((b) => b.toString()),
-    signature: Array.from(extractedSignature).map((b) => b.toString()),
+    hashed_message: Array.from(signatureInputs.hashed_message).map((b) => b.toString()),
+    public_key_x: Array.from(signatureInputs.pub_key_x).map((b) => b.toString()),
+    public_key_y: Array.from(signatureInputs.pub_key_y).map((b) => b.toString()),
+    signature: Array.from(signatureInputs.signature).map((b) => b.toString()),
examples/CRISP/circuits/src/main.nr (1)

132-139: CRITICAL: is_first_vote still not validated against ciphertext state.

The circuit trusts the is_first_vote flag without verifying it matches the actual ciphertext state. An attacker can set is_first_vote = true on a populated slot (where prev_ct*/sum_ct* are non-zero), pass a zero mask, and the circuit will return (ct0is, ct1is) — effectively wiping existing votes.

The circuit must enforce that is_first_vote can only be true when sum_ct* == ct* (indicating no prior votes). Please add coefficient-wise equality checks between sum and ct polynomials, assert the equality matches the flag, and only then branch.

Apply this pattern:

+        let mut sums_match_ct = true;
+        for i in 0..1 {
+            for j in 0..2048 {
+                if sum_ct0is[i].coefficients[j] != ct0is[i].coefficients[j] {
+                    sums_match_ct = false;
+                }
+                if sum_ct1is[i].coefficients[j] != ct1is[i].coefficients[j] {
+                    sums_match_ct = false;
+                }
+            }
+        }
+
+        assert(is_first_vote == sums_match_ct);
+
         if is_first_vote {
             (ct0is, ct1is)
         } else {
             (sum_ct0is, sum_ct1is)
         }
examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (1)

251-267: CRITICAL: Test exercises the exploit path.

This test passes a non-empty encryptedVote as previousCiphertext while setting isFirstVote: true, which is precisely the attack vector the circuit should prevent. The test expects the circuit to return ct0is/ct1is, validating the vote-wipeout exploit.

Once the circuit enforces is_first_vote == (sum_ct* == ct*), this test will fail. Please update it to use a genuine empty/zero previousCiphertext (e.g., zkInputsGenerator.encryptVote(publicKey, new BigInt64Array([0n])) as used on line 32) when isFirstVote: true.

🧹 Nitpick comments (1)
examples/CRISP/circuits/src/utils.nr (1)

35-63: LGTM! Sum-based vulnerability fixed.

The coefficient-by-coefficient check correctly addresses the past review concern about Field arithmetic allowing non-zero coefficients to cancel out in a sum. Each coefficient is now validated individually, preventing the exploit.

As an optional micro-optimization, you could short-circuit by returning false immediately when a non-zero coefficient is found, rather than continuing to check all coefficients. However, this is not critical.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba9d83 and fb8d8e2.

📒 Files selected for processing (7)
  • examples/CRISP/circuits/src/main.nr (3 hunks)
  • examples/CRISP/circuits/src/utils.nr (5 hunks)
  • examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol (1 hunks)
  • examples/CRISP/packages/crisp-sdk/src/types.ts (2 hunks)
  • examples/CRISP/packages/crisp-sdk/src/vote.ts (7 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/utils.ts (1 hunks)
  • examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/CRISP/packages/crisp-contracts/contracts/CRISPInputValidator.sol
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-10-10T12:56:40.538Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 830
File: templates/default/README.md:123-128
Timestamp: 2025-10-10T12:56:40.538Z
Learning: In the Enclave repository, the hard-coded Hardhat development private key `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` is acceptable in template README files and documentation for local testing/interaction purposes.

Applied to files:

  • examples/CRISP/packages/crisp-sdk/src/vote.ts
📚 Learning: 2024-10-22T02:10:34.864Z
Learnt from: ryardley
Repo: gnosisguild/enclave PR: 145
File: packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs:82-83
Timestamp: 2024-10-22T02:10:34.864Z
Learning: In the file `packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs`, when reviewing test functions like `generate_pk_share`, minor performance optimizations (e.g., minimizing mutex locks) are not a priority.

Applied to files:

  • examples/CRISP/packages/crisp-sdk/tests/vote.test.ts
📚 Learning: 2024-10-03T23:02:41.732Z
Learnt from: ryardley
Repo: gnosisguild/enclave PR: 133
File: packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs:137-139
Timestamp: 2024-10-03T23:02:41.732Z
Learning: In `packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs`, using the same RNG instance `rng_test` for generating multiple key shares without advancing its state is acceptable.

Applied to files:

  • examples/CRISP/packages/crisp-sdk/tests/vote.test.ts
📚 Learning: 2025-08-27T13:49:48.617Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T13:49:48.617Z
Learning: In CRISP circuits using Greco library, the binary check `assert(0 == b * (qmt - b))` for polynomial k1 should not be applied to all coefficients - only specific coefficients require this constraint, such as `k1.coefficients[2048 - 1]`.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
  • examples/CRISP/circuits/src/main.nr
📚 Learning: 2025-09-22T15:08:29.814Z
Learnt from: ozgurarmanc
Repo: gnosisguild/enclave PR: 734
File: packages/circuits/crates/libs/polynomial/src/lib.nr:140-155
Timestamp: 2025-09-22T15:08:29.814Z
Learning: Greco (packages/circuits/crates/libs/greco/src/lib.nr) performs range_check_1bound/2bounds on all polynomials (u, e0/e1, k1, pk*, r*, p*) before serialization; packer/flatten rely on these bounds, so per-limb asserts inside packer are unnecessary in this crate’s flow.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
📚 Learning: 2025-08-27T14:02:25.412Z
Learnt from: 0xjei
Repo: gnosisguild/enclave PR: 648
File: examples/CRISP/circuits/src/main.nr:40-44
Timestamp: 2025-08-27T14:02:25.412Z
Learning: In CRISP circuits using Greco library, k1 represents a binary value encoded as a polynomial where only one specific coefficient (k1.coefficients[2048 - 1]) carries the actual binary information and needs the constraint `assert(0 == b * (qmt - b))`. Other coefficients in the polynomial are not significant and don't require the binary constraint.

Applied to files:

  • examples/CRISP/circuits/src/utils.nr
🧬 Code graph analysis (2)
examples/CRISP/packages/crisp-sdk/src/vote.ts (1)
examples/CRISP/packages/crisp-sdk/src/types.ts (1)
  • CRISPCircuitInputs (135-173)
examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (4)
examples/CRISP/packages/crisp-sdk/src/vote.ts (4)
  • generateMaskVote (215-256)
  • generateProof (258-266)
  • encodeVote (64-90)
  • generateProofWithReturnValue (268-276)
examples/CRISP/packages/crisp-sdk/src/constants.ts (1)
  • DEFAULT_BFV_PARAMS (24-24)
examples/CRISP/packages/crisp-sdk/tests/constants.ts (3)
  • merkleProof (33-33)
  • testAddress (32-32)
  • VOTE (14-14)
examples/CRISP/packages/crisp-sdk/tests/utils.ts (1)
  • compareCoefficientsArrays (14-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: rust_unit
  • GitHub Check: build_enclave_cli
  • GitHub Check: test_net
  • GitHub Check: build_sdk
  • GitHub Check: rust_integration
  • GitHub Check: integration_prebuild
  • GitHub Check: test_contracts
🔇 Additional comments (14)
examples/CRISP/circuits/src/utils.nr (1)

93-119: Tests now properly exercise the helper.

The assertions on the boolean return values correctly address the past review concern. Both the success and failure cases are now validated.

examples/CRISP/packages/crisp-sdk/tests/utils.ts (2)

7-12: LGTM! Normalization handles hex/decimal differences.

The function correctly normalizes coefficient representations for comparison. Since the caller (compareCoefficientsArrays) validates non-null values before invoking this, the implementation is safe for test utilities.


14-35: Well-structured coefficient comparison utility.

The function performs thorough validation (length checks, null guards) and correctly normalizes coefficients before comparison. This is appropriate for test validation of circuit return values.

examples/CRISP/packages/crisp-sdk/src/types.ts (2)

171-172: Type definition correctly added for first-vote flag.

The new field follows the existing naming convention (snake_case for circuit inputs) and uses the appropriate boolean type.


219-219: Parameter type correctly extended.

The new isFirstVote field follows the camelCase convention for SDK parameters and properly propagates to circuit inputs.

examples/CRISP/circuits/src/main.nr (1)

55-56: Public parameter added for first-vote detection.

The parameter is correctly declared as public, allowing verification of the flag on-chain.

examples/CRISP/packages/crisp-sdk/tests/vote.test.ts (4)

144-144: Correct usage of isFirstVote parameter.

The test properly sets isFirstVote: false for a regular vote following previous ciphertext.


204-231: Test correctly exercises voter proof generation.

The test properly generates and verifies a proof for a legitimate voter with isFirstVote: false.


233-249: Test correctly exercises masking proof generation.

The test properly generates and verifies a masking proof with isFirstVote: false.


269-285: Test correctly validates non-first masking operation.

This test properly exercises the non-first-vote path, expecting sum_ct0is/sum_ct1is in the return value when isFirstVote: false.

examples/CRISP/packages/crisp-sdk/src/vote.ts (4)

161-162: Documentation correctly describes new parameter.

The JSDoc properly documents the isFirstVote parameter.


164-201: Function correctly propagates isFirstVote.

The implementation properly accepts isFirstVote from the params and includes it in the returned CRISP inputs as is_first_vote.


212-213: Documentation correctly describes new parameter.

The JSDoc properly documents the isFirstVote parameter for the mask vote generation.


268-276: LGTM! Proof generation now implemented.

The function now properly generates the proof via backend.generateProof(witness) and returns both returnValue and proof. This addresses the past review concern about returning placeholder proofs.

@ctrlc03 ctrlc03 enabled auto-merge (squash) November 4, 2025 12:14
Comment thread examples/CRISP/circuits/src/main.nr Outdated
@vercel vercel Bot temporarily deployed to Preview – crisp November 4, 2025 12:27 Inactive
@ctrlc03 ctrlc03 requested a review from cedoor November 4, 2025 12:27
@vercel vercel Bot temporarily deployed to Preview – enclave-docs November 4, 2025 12:27 Inactive
@cedoor cedoor force-pushed the feat/check-if-first-time-voting branch from f8e2d75 to cf977a2 Compare November 4, 2025 15:15
@vercel vercel Bot temporarily deployed to Preview – enclave-docs November 4, 2025 15:15 Inactive
@vercel vercel Bot temporarily deployed to Preview – crisp November 4, 2025 15:15 Inactive

@cedoor cedoor 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.

LGTM 👍🏽

@ctrlc03 ctrlc03 merged commit 63ee5c3 into dev Nov 4, 2025
23 checks passed
@ctrlc03 ctrlc03 deleted the feat/check-if-first-time-voting branch November 6, 2025 08:30
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.

Support case where the vote/masking is the first on a slot

2 participants