Skip to content

refactor: new proof aggregation [skip-line-limit]#1516

Merged
cedoor merged 36 commits into
mainfrom
refactor/new-proof-aggregation
Apr 27, 2026
Merged

refactor: new proof aggregation [skip-line-limit]#1516
cedoor merged 36 commits into
mainfrom
refactor/new-proof-aggregation

Conversation

@cedoor

@cedoor cedoor commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Refactors proof aggregation around ad-hoc Noir bins under circuits/bin/recursive_aggregation/, replacing the old generic fold/ and wrapper/ trees. Aggregation is expressed as explicit fold + aggregator circuits with clear public layouts and key hashes.

Recursive aggregation layout & circuits

Circuit Role
c2ab_fold Combines C2a + C2b ZK proofs; surfaces C2 public inputs for downstream folds.
c3_fold Sequential C3 fold: inner ZK ShareEncryption + optional prior c3_fold non-ZK proof (is_first_step, slot_index).
c3ab_fold Combines final sk-chain and e_sm-chain c3_fold outputs.
c4ab_fold Combines C4a + C4b folds for the C4 path.
node_fold Per-node DKG fold: C0, C1, c2ab_fold, c3ab_fold, c4ab_fold with same-party assertions between stages.
nodes_fold Sequential non-ZK fold of H node_fold proofs (one honest slot per step), chained with prior nodes_fold.
dkg_aggregator Verifies one non-ZK nodes_fold proof + C5 (pk_aggregation) ZK; enforces cross-node grids and C5↔node public links.
c6_fold Sequential fold of T+1 C6 (ThresholdShareDecryption) rows for the phase-7 path.
decryption_aggregator Verifies one non-ZK c6_fold proof + C7 ZK; ties folded C6 columns to c7_public.

Removed: recursive_aggregation/fold/ and recursive_aggregation/wrapper/ (legacy two-proof fold and generic wrappers).

How reviewers can test

  1. Compile recursive-aggregation circuits (needed for zk-prover tests that read circuits/bin/...):

    pnpm install
    pnpm build:circuits --group recursive_aggregation
  2. Barretenberg — integration tests expect bb on PATH; they print a skip message if it’s missing.

  3. Optional zk-prover integration targets (not run by default CI for this crate yet):

    # Fold accumulators: sequential C3/C6 fold, ABI slot checks, pipeline JSON + artifact staging
    cargo test -p e3-zk-prover --test fold_accumulators_e2e_tests -- --nocapture
    
    # Full correlated node_fold chain (C0→…→node_fold); heavier, use single thread
    cargo test -p e3-zk-prover --test node_fold_correlated_e2e_tests -- --nocapture --test-threads=1

    CI still runs e3-zk-prover’s integration_tests and local_e2e_tests only; these two are for local / follow-up CI if you wire them in.

What those zk-prover tests cover

Test crate Purpose
fold_accumulators_e2e_tests generate_sequential_c3_fold / generate_sequential_c6_fold (prove + verify), slot counts from compiled c3_fold / c6_fold JSON, staging of recursive-aggregation artifacts, and loading the node_fold pipeline (C2ab → C3ab → C4ab → node_fold) without a full correlated DKG witness story.
node_fold_correlated_e2e_tests One end-to-end run with correlated witnesses (C1/C2/C3/C4 commitments aligned) through node_fold prove + verify.

Ciphernode flow & on-chain verifier refactor

The latest commit aligns the Solidity stack, the Rust aggregators and the indexer/EVM helpers with the new dkg_aggregator / decryption_aggregator layout. The on-chain flow is simpler: one EVM proof per phase, verified by a single aggregator verifier that internally checks the node/c6 folds and the C5/C7 sub-proofs.

Before vs. after

Before (main) After (this commit)
DKG verification IPkVerifier.verify(proof, foldProof) → pkCommitment — on-chain verified C5 proof + optional RecursiveAggregationFold proof; commitment extracted as last public input. IPkVerifier.verify(pkCommitment, proof) → bool — on-chain verifies one DkgAggregator proof and binds it to a caller-supplied pkCommitment.
Decryption verification IDecryptionVerifier.verify(plaintextHash, proof, foldProof) → bool — verified C7 + optional fold proof. IDecryptionVerifier.verify(plaintextHash, proof) → bool — verifies one DecryptionAggregator proof and binds its exposed plaintext to plaintextHash.
Committee publication publishCommittee(e3Id, nodes, publicKey, proof, foldProof) — registry extracted the commitment from proof public inputs. publishCommittee(e3Id, nodes, publicKey, pkCommitment, proof)pkCommitment is explicit; foldProof dropped; event CommitteePublished now includes pkCommitment.
Plaintext publication publishPlaintextOutput(e3Id, plaintextOutput, proof, foldProof) publishPlaintextOutput(e3Id, plaintextOutput, proof)
Proof aggregation disabled When e3.proofAggregationEnabled == false, proof may be empty and pkCommitment is trusted from the (signed) aggregator.

This PR requires working on two follow-up issues:

Summary by CodeRabbit

  • New Features

    • New DKG and Decryption aggregator circuits and proofs for on-chain verification; per-job decryption aggregation supported.
    • Ciphertext commitment (ct_commitment) exposed to bind share-encryption and threshold-decryption flows.
  • Bug Fixes

    • Public-key commitment handling corrected in committee publication and registry flows; event and verifier payloads updated.
  • Refactor

    • Proof aggregation redesigned into phase-specific aggregation requests (node/DKG/decryption); pairwise wrapper/fold flow removed.
    • Plaintext publication API simplified (fold-proof parameter removed).
  • Documentation

    • Verifier inventory, deployment guides, and circuit mapping updated.

cedoor added 3 commits April 9, 2026 20:26
- C3: private ct limbs, public ct_commitment output (Poseidon DS_CIPHERTEXT)
- C6: public ct_commitment input, witness-only ct0/ct1, verify limb hash
- c3_fold: inner public inputs [Field; 3]; Rust fold + layout metadata
- Flow-trace note on cross-phase ct_commitment vs user_data_encryption

Made-with: Cursor
@vercel

vercel Bot commented Apr 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
crisp Ready Ready Preview, Comment Apr 27, 2026 7:58am
enclave-docs Ready Ready Preview, Comment Apr 27, 2026 7:58am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the prior wrapper+fold recursive aggregation with phase-specific aggregation circuits and flows (NodeDkgFold, DkgAggregation, DecryptionAggregation). Adds many new Noir aggregation bins and zk‑prover aggregation APIs, removes wrapper/fold circuits and fold state, and updates multithread, aggregator actors, events, EVM contracts, tooling, and commitment/IO layouts (pk/ct commitments).

Changes

Cohort / File(s) Summary
High-level docs & flow
agent/flow-trace/04_DKG_AND_COMPUTATION.md, docs/pages/ciphernode-operators/index.mdx, scripts/README.md
Documented migration from wrapper/fold to phase-specific aggregation and updated on-chain verifier inventory and verifier-generation docs.
Removed wrapper/fold circuits & artifacts
circuits/bin/recursive_aggregation/wrapper/..., circuits/bin/recursive_aggregation/fold/...
Deleted wrapper/fold READMEs, Nargo manifests and removed legacy wrapper/fold Noir entrypoints.
New aggregation/fold Noir bins
circuits/bin/recursive_aggregation/c2ab_fold/..., .../c3_fold/*, .../c3_fold_kernel/*, .../c3ab_fold/*, .../c4ab_fold/*, .../c6_fold/*, .../c6_fold_kernel/*, .../c3ab_fold/*, .../node_fold/*, .../nodes_fold*, .../dkg_aggregator/*, .../decryption_aggregator/*
Added dedicated Noir entrypoints for per‑phase folding/aggregation (C2ab, C3/C3-kernel, C3ab, C4ab, C6/C6-kernel, node/nodes folds, DKG/Decryption aggregators) with explicit public IO shapes.
Share encryption/decryption I/O & commitments
circuits/bin/dkg/share_encryption/src/main.nr, circuits/bin/threshold/share_decryption/src/main.nr, circuits/lib/src/core/dkg/share_encryption.nr, circuits/lib/src/core/threshold/share_decryption.nr
ShareEncryption now returns ct_commitment; ShareDecryption accepts/verifies ct_commitment, makes ct0/ct1 witnesses, and introduces d_native_trunc plus binding checks.
Commitment helpers
circuits/lib/src/math/commitments.nr
Removed obsolete share-encryption helper; commitment computation reorganized.
zk-prover: new aggregation modules & APIs
crates/zk-prover/src/circuits/aggregation/..., crates/zk-prover/src/circuits/mod.rs, crates/zk-prover/src/lib.rs, crates/zk-prover/src/prover.rs, crates/zk-prover/src/circuits/utils.rs, crates/zk-prover/src/circuits/vk.rs
Added sequential C3/C6/Nodes fold generators and node/DKG/decryption aggregator provers; removed old recursive-wrapper/fold APIs; added proof-byte helpers and unified VK loading to circuit dirs.
zk-prover tests & helpers
crates/zk-prover/tests/*, crates/zk-prover/tests/common/*
Added e2e/unit tests, witness builders, and artifact-staging helpers for new aggregation flows.
Events / ZK request-response types
crates/events/src/enclave_event/compute_request/zk.rs, crates/events/src/enclave_event/proof.rs, crates/events/src/enclave_event/*.rs
Replaced FoldProofs with NodeDkgFold, DkgAggregation, DecryptionAggregation variants; removed wrapper-proof fields; extended CircuitName with new aggregation circuit variants and output layouts.
Multithread & prover wiring
crates/multithread/src/multithread.rs, crates/zk-prover/src/actors/proof_request.rs, crates/zk-prover/src/actors/node_proof_aggregator.rs
Wired new prover APIs (prove_node_dkg_fold, prove_dkg_aggregation, prove_decryption_aggregation_jobs); removed wrapper-proof generation and fold-response flows; updated verification/correlation logic.
Aggregator actors & state
crates/aggregator/src/proof_fold.rs (removed), crates/aggregator/src/publickey_aggregator.rs, crates/aggregator/src/threshold_plaintext_aggregator.rs, crates/aggregator/src/ext.rs
Deleted ProofFoldState; publickey/plaintext aggregators now buffer inner proofs and dispatch single-shot aggregation requests; added proof_aggregation_enabled flag and new correlation/proof fields.
Emitted events / payload shapes
crates/events/src/enclave_event/plaintext_aggregated.rs, publickey_aggregated.rs, decryptionshare_created.rs, dkg_inner_proof_ready.rs, keyshare_created.rs
PlaintextAggregated now carries decryption_aggregator_proofs; PublicKeyAggregated includes pk_commitment and dkg_aggregator_proof; removed wrapped-proof fields and renamed wrapped_proofproof; KeyshareCreated adds party_id.
Threshold plaintext aggregation flow
crates/aggregator/src/threshold_plaintext_aggregator.rs
Replaced cross-node C6 fold with batched DecryptionAggregation job flow; aggregator collects inner C6 proofs, dispatches decryption aggregation jobs, and gates publish on aggregator proofs when enabled.
EVM & registry integration
crates/evm-helpers/src/contracts.rs, crates/evm-helpers/src/events.rs, crates/evm/src/enclave_sol_writer.rs, crates/evm/src/ciphernode_registry_sol.rs
Removed foldProof from publishPlaintextOutput; ABIs/bindings changed to accept pkCommitment (bytes32) and aggregator proofs and to use boolean verifier returns.
Solidity contracts, artifacts, tests & tooling
packages/enclave-contracts/..., packages/enclave-contracts/artifacts/..., packages/enclave-contracts/test/*, packages/enclave-contracts/ignition/modules/*, packages/enclave-contracts/scripts/*
Contracts/interfaces updated to accept pkCommitment and aggregator proofs; removed fold verifier chaining; updated tests, fixtures, Ignition modules and deploy scripts accordingly.
Build & verifier tooling
scripts/build-circuits.ts, scripts/generate-verifiers.ts, scripts/lint-circuits.sh, package.json, examples/..., templates/default/*
Adjusted artifact grouping, VK generation and verifier naming to treat aggregation circuits specially; EVM VK generation restricted to on-chain aggregator circuits; verifier generator updated to produce dkg_aggregator/decryption_aggregator.
zk-helpers & layouts
crates/zk-helpers/src/circuits/output_layout.rs, crates/zk-helpers/src/circuits/threshold/share_decryption/computation.rs
Added ct_commitment to C6 inputs and new SHARE_ENCRYPTION_OUTPUTS constant; Inputs computation now emits d_native_trunc and ct_commitment.
Tests/fixtures cleanup
packages/enclave-contracts/test/fixtures/pkProof.ts, packages/enclave-contracts/test/fixtures/index.ts
Removed encodePkProof helper and updated tests/fixtures to pass pkCommitment directly.

Sequence Diagram(s)

sequenceDiagram
    participant Node as Node/Producer
    participant Multithread as Multithread Worker
    participant ZkProver as ZkProver
    participant Registry as On-chain Registry/Verifier

    Node->>Multithread: publish inner proofs (C0..C5 / C6 / C7)
    Multithread->>ZkProver: Publish ZkRequest::NodeDkgFold / DkgAggregation / DecryptionAggregation
    ZkProver->>ZkProver: generate_recursive_aggregation_bin_proof(...) / sequential folds
    ZkProver-->>Multithread: ZkResponse::NodeDkgFold / DkgAggregation / DecryptionAggregation (proof(s))
    Multithread->>Node: emit events (PublicKeyAggregated / PlaintextAggregated) with pk_commitment + aggregator proof(s)
    Node->>Registry: publishCommittee / publishPlaintextOutput(pkCommitment, proof)
    Registry-->>Node: verification result (bool)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

ciphernode

Suggested reviewers

  • zahrajavar
  • 0xjei
  • cedoor

"🐰
I hopped through proofs both wide and deep,
Folded old paths so aggregators leap,
New circuits bloom in noir‑green light,
Commitments bound, the flows unite,
Hooray — aggregation springs to flight!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/new-proof-aggregation

- Redesign c3_fold: one inner ShareEncryption ZK proof + optional prior c3_fold non-ZK proof
- Pub outputs: per-party pk, msg, ct slots; skip acc verification when is_first_step
- Add generate_c3_fold_step and generate_sequential_c3_fold; inner C3 uses extract_output(ct_commitment)
- Remove C3FoldMerge circuit and CircuitName variant; update vk helper and lint-circuits

Made-with: Cursor
- Add c2ab_fold, c3ab_fold, c4ab_fold, c6_fold, nodes_fold, node_fold, dkg_aggregator, and decryption_aggregator Noir bins
- Remove legacy recursive_aggregation fold and wrapper trees; refresh c3_fold
- Document aggregation bins in flow-trace; extend lint-circuits.sh checks
@cedoor

cedoor commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

Could you do an early review of the new recursive-aggregation circuits? @zahrajavar @0xjei

@cedoor cedoor changed the title refactor: new proof aggregation refactor: new proof aggregation [skip-line-limit] Apr 13, 2026
@0xjei

0xjei commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Could you do an early review of the new recursive-aggregation circuits? @zahrajavar @0xjei

sure, will get a proper look tomorrow 🙏

@0xjei

0xjei commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

agreed in chat on fixing the C3 fold stuff

- Expand c3_fold and c3ab_fold public IO to N_PARTIES * L_THRESHOLD slots.

- Assert C2 share commitments against C3 for every party/limb in node_fold.

- Sync nodes_fold C3AB_FOLD_PUBLIC_LEN with c3ab_fold; use ASCII comments for nargo.

- Update zk-prover accumulator, VK wiring, events, multithread, and integration tests.
cedoor added 3 commits April 15, 2026 15:43
- Add c3_fold_kernel Noir package and wire recursive aggregation build
- Implement sequential C3 fold in c3_accumulator; export generate_sequential_c3_fold only
- Add recursive_aggregation e2e tests (two inner ShareEncryption proofs)
- Update prover, circuit helpers, Proof extract_input/output, multithread, flow-trace
- Add c6_fold_kernel Noir package for genesis accumulator (aligned with c3_fold_kernel).
- Make c6_fold always verify the non-ZK accumulator like c3_fold.
- Add CircuitName::C6FoldKernel and generate_sequential_c6_fold in aggregation/c6_accumulator.rs.
- Move c3 accumulator into circuits/aggregation/ with a small mod.rs.
- Copy c6_fold_kernel artifacts in integration test circuit staging.
- Extend recursive_aggregation e2e tests (C6 ABI, staged kernel, two-step fold + verify).
- Add NODE_FOLD_PIPELINE (c2ab_fold, c3ab_fold, c4ab_fold, node_fold) JSON load + staged VK tests.
- Stage the same bins in integration test default/recursive_aggregation fixture copy.

@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

Caution

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

⚠️ Outside diff range comments (3)
crates/aggregator/src/threshold_plaintext_aggregator.rs (1)

512-575: ⚠️ Potential issue | 🔴 Critical

Bind every C6 proof/share pair, not just index 0.

For multi-ciphertext decryptions, proofs.first() and shares.first() only validate the first share. Later shares can still feed plaintext aggregation without matching their verified C6 d_commitment, especially when recursive proof aggregation is disabled.

Suggested fix direction
-            let Some(first_proof) = proofs.first() else {
-                warn!(
-                    "Empty C6 proof list for party {} — marking as mismatched",
-                    party_id
-                );
-                mismatched.insert(*party_id);
-                continue;
-            };
-            let Some(c6_d_bytes) = c6_output_layout
-                .extract_field(&first_proof.payload.proof.public_signals, "d_commitment")
-            else {
-                warn!(
-                    "Could not extract d_commitment from C6 proof for party {} — marking as mismatched",
-                    party_id
-                );
-                mismatched.insert(*party_id);
-                continue;
-            };
-
-            let Some(share_bytes) = shares.first() else {
-                warn!(
-                    "No share bytes for party {} — marking as mismatched",
-                    party_id
-                );
-                mismatched.insert(*party_id);
-                continue;
-            };
-            let Ok(poly) = e3_trbfv::helpers::try_poly_from_bytes(share_bytes, &threshold_params)
-            else {
-                warn!(
-                    "Could not deserialize share for party {} — marking as mismatched",
-                    party_id
-                );
-                mismatched.insert(*party_id);
-                continue;
-            };
-            let crt = e3_polynomial::CrtPolynomial::from_fhe_polynomial(&poly);
-
-            // C6 public `d_commitment` hashes native truncated limbs (same layout as C7), not
-            // reversed+centered witness `d`.
-            let computed = compute_threshold_decryption_share_commitment(&crt, d_bit, max_k);
-
-            // Convert to big-endian 32-byte padded format matching
-            // Barretenberg's public_signals encoding.
-            let (_, be_bytes) = computed.to_bytes_be();
-            let mut computed_padded = [0u8; 32];
-            let start = 32usize.saturating_sub(be_bytes.len());
-            computed_padded[start..].copy_from_slice(&be_bytes[..be_bytes.len().min(32)]);
-
-            if computed_padded != c6_d_bytes {
-                warn!(
-                    "d_commitment mismatch for party {}: raw share commitment differs from C6 proof output",
-                    party_id
-                );
-                mismatched.insert(*party_id);
-            }
+            if proofs.len() != shares.len() {
+                warn!(
+                    "C6 proof/share count mismatch for party {}: {} proofs, {} shares",
+                    party_id,
+                    proofs.len(),
+                    shares.len()
+                );
+                mismatched.insert(*party_id);
+                continue;
+            }
+
+            for (ct_idx, share_bytes) in shares.iter().enumerate() {
+                let Some(proof) = proofs.get(ct_idx) else {
+                    mismatched.insert(*party_id);
+                    break;
+                };
+                let Some(c6_d_bytes) = c6_output_layout
+                    .extract_field(&proof.payload.proof.public_signals, "d_commitment")
+                else {
+                    warn!(
+                        "Could not extract d_commitment from C6 proof for party {} ct index {}",
+                        party_id, ct_idx
+                    );
+                    mismatched.insert(*party_id);
+                    break;
+                };
+                let Ok(poly) =
+                    e3_trbfv::helpers::try_poly_from_bytes(share_bytes, &threshold_params)
+                else {
+                    warn!(
+                        "Could not deserialize share for party {} ct index {}",
+                        party_id, ct_idx
+                    );
+                    mismatched.insert(*party_id);
+                    break;
+                };
+
+                let crt = e3_polynomial::CrtPolynomial::from_fhe_polynomial(&poly);
+                let computed = compute_threshold_decryption_share_commitment(&crt, d_bit, max_k);
+                let (_, be_bytes) = computed.to_bytes_be();
+                let mut computed_padded = [0u8; 32];
+                let start = 32usize.saturating_sub(be_bytes.len());
+                computed_padded[start..].copy_from_slice(&be_bytes[..be_bytes.len().min(32)]);
+
+                if computed_padded != c6_d_bytes {
+                    warn!(
+                        "d_commitment mismatch for party {} ct index {}",
+                        party_id, ct_idx
+                    );
+                    mismatched.insert(*party_id);
+                    break;
+                }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/aggregator/src/threshold_plaintext_aggregator.rs` around lines 512 -
575, The current loop in honest_shares only checks proofs.first() and
shares.first(), so only the first ciphertext/share is bound and validated;
update the logic to iterate over every proof/share pair for each party_id (use
c6_proofs.get(party_id) and the shares Vec in honest_shares) and compare each
proof's extracted "d_commitment" against the corresponding share's computed
commitment via compute_threshold_decryption_share_commitment (after
deserializing each share with e3_trbfv::helpers::try_poly_from_bytes and
converting to CrtPolynomial). If any pair mismatches, insert the party_id into
mismatched; otherwise accept that party only if all of its per-ciphertext pairs
match. Ensure you reference proofs[i].payload.proof.public_signals and shares[i]
(or equivalent index-aligned pairing) rather than .first().
crates/tests/tests/integration.rs (1)

661-670: ⚠️ Potential issue | 🔴 Critical

Fix tuple variant pattern syntax.

NodeDkgFold, DkgAggregation, and DecryptionAggregation are tuple variants defined in crates/events/src/enclave_event/compute_request/zk.rs. Using { .. } patterns will not compile; use tuple patterns (_) instead.

Compile fix
-                        | ComputeRequestKind::Zk(ZkRequest::NodeDkgFold { .. })
-                        | ComputeRequestKind::Zk(ZkRequest::DkgAggregation { .. })
-                        | ComputeRequestKind::Zk(ZkRequest::DecryptionAggregation { .. })
+                        | ComputeRequestKind::Zk(ZkRequest::NodeDkgFold(_))
+                        | ComputeRequestKind::Zk(ZkRequest::DkgAggregation(_))
+                        | ComputeRequestKind::Zk(ZkRequest::DecryptionAggregation(_))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tests/tests/integration.rs` around lines 661 - 670, The match arm for
EnclaveEventData::ComputeRequest uses struct-like patterns `{ .. }` for tuple
variants from ZkRequest which causes a compile error; inside the matches! for
ComputeRequestKind::Zk you should change the patterns ZkRequest::NodeDkgFold {
.. }, ZkRequest::DkgAggregation { .. }, and ZkRequest::DecryptionAggregation {
.. } to tuple patterns like ZkRequest::NodeDkgFold(_),
ZkRequest::DkgAggregation(_), and ZkRequest::DecryptionAggregation(_) so the
variant constructors match their actual definitions.
circuits/lib/src/core/threshold/share_decryption.nr (1)

186-195: ⚠️ Potential issue | 🔴 Critical

Assign the returned Vec from push to update the payload with commitments.

In lines 190–191, inputs.push(...) does not update inputs under Noir Vec semantics; the return value must be assigned. Without this fix, the two critical commitment fields are omitted from the Fiat-Shamir payload, breaking the transcript binding.

Proposed fix
         // Use commitments instead of full polynomials (saves constraints)
-        inputs.push(self.expected_sk_commitment);
-        inputs.push(self.expected_e_sm_commitment);
+        inputs = inputs.push(self.expected_sk_commitment);
+        inputs = inputs.push(self.expected_e_sm_commitment);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@circuits/lib/src/core/threshold/share_decryption.nr` around lines 186 - 195,
The payload() function is failing to include the commitments because Noir
Vec::push returns a new Vec which must be assigned; change the two lines that
call inputs.push(self.expected_sk_commitment) and
inputs.push(self.expected_e_sm_commitment) to assign their results back to
inputs (e.g., inputs = inputs.push(self.expected_sk_commitment); inputs =
inputs.push(self.expected_e_sm_commitment);), so the commitments
expected_sk_commitment and expected_e_sm_commitment are actually present before
calling flatten::<_, _, BIT_CT>(inputs, self.ct0) and flatten::<_, _,
BIT_CT>(inputs, self.ct1).
🧹 Nitpick comments (1)
examples/CRISP/server/src/cli/commands.rs (1)

189-211: Redundant timestamp reassignment at line 189.

With the new let current_timestamp = get_current_timestamp().await?; shadowing at line 205, the reassignment at line 189 is effectively dead — the value is only read by the info! at line 195 and then immediately overwritten (via shadowing) at line 205, along with window_start/input_window (lines 159-163 vs 207-211). Consider dropping the reassignment at line 189 (and the mut at line 153) to simplify, or conversely drop the re-read+shadow at lines 205-211 if the intent was only to log a fresh timestamp before the tx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/CRISP/server/src/cli/commands.rs` around lines 189 - 211, The first
call to get_current_timestamp() that assigns current_timestamp (used only for
logging) is redundant because current_timestamp is immediately shadowed by a
second get_current_timestamp() before window_start/input_window are computed;
remove the earlier assignment (and the mut on the original declaration if
present) and keep the later fresh get_current_timestamp() used to compute
window_start and input_window (which reference CONFIG.e3_duration), or
alternatively remove the later shadowing and use the initially fetched
current_timestamp consistently—update occurrences of current_timestamp,
window_start, and input_window accordingly to avoid dead code and shadowing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@circuits/lib/src/core/threshold/share_decryption.nr`:
- Around line 223-226: The commitment for d_native_trunc currently uses BIT_D
packing width causing rejection/ambiguity for values in the upper half; update
compute_threshold_decryption_share_commitment and any C6/C7 commitment
generation/checks to accept a native-width bit bound derived from the modulus
(i.e., pass a NATIVE_BIT_WIDTH parameter instead of BIT_D), ensure
verify_d_native_trunc_binding still enforces the native [0,q) constraint, and
propagate that same native bit-width through the Rust computation/codegen and
the C7 expected commitment comparisons so the hashing/packing uses the full
native modulus width consistently for d_native_trunc.

In `@crates/aggregator/src/threshold_plaintext_aggregator.rs`:
- Around line 660-668: The code in ThresholdPlaintextAggregator that sets
self.decryption_aggregator_proofs = Some(Vec::new()) when
honest_c6_proofs_for_agg is present but empty (or contains empty inner proofs)
must not silently publish an empty aggregation-proof; instead, detect this
invalid state and return an error to abort aggregation. Replace the branch that
currently logs and sets decryption_aggregator_proofs to Some(Vec::new()) with a
failure return (propagate or construct an appropriate Err) that names the
problem (e.g., "missing C6 inner proofs for aggregation") so callers of
PlaintextAggregated cannot receive an unverifiable decryption_aggregator_proofs
when aggregation is enabled. Ensure the error path uses the function's existing
error type and preserves the info! log for diagnostics.
- Around line 767-771: When handling
ComputeResponseKind::Zk(ZkResponse::DecryptionAggregation(resp)) you must
validate that resp.proofs contains exactly the expected number of
DecryptionAggregator proofs before setting self.decryption_aggregator_proofs and
calling self.try_publish_complete(); check resp.proofs.len() against the
aggregator's expected count (e.g. the number of pending ciphertext/C7 jobs
tracked by this aggregator) and if it does not match, do not overwrite
self.decryption_aggregator_proofs, log/return an error, and leave
self.decryption_aggregation_correlation intact so a partial response is not
published.

---

Outside diff comments:
In `@circuits/lib/src/core/threshold/share_decryption.nr`:
- Around line 186-195: The payload() function is failing to include the
commitments because Noir Vec::push returns a new Vec which must be assigned;
change the two lines that call inputs.push(self.expected_sk_commitment) and
inputs.push(self.expected_e_sm_commitment) to assign their results back to
inputs (e.g., inputs = inputs.push(self.expected_sk_commitment); inputs =
inputs.push(self.expected_e_sm_commitment);), so the commitments
expected_sk_commitment and expected_e_sm_commitment are actually present before
calling flatten::<_, _, BIT_CT>(inputs, self.ct0) and flatten::<_, _,
BIT_CT>(inputs, self.ct1).

In `@crates/aggregator/src/threshold_plaintext_aggregator.rs`:
- Around line 512-575: The current loop in honest_shares only checks
proofs.first() and shares.first(), so only the first ciphertext/share is bound
and validated; update the logic to iterate over every proof/share pair for each
party_id (use c6_proofs.get(party_id) and the shares Vec in honest_shares) and
compare each proof's extracted "d_commitment" against the corresponding share's
computed commitment via compute_threshold_decryption_share_commitment (after
deserializing each share with e3_trbfv::helpers::try_poly_from_bytes and
converting to CrtPolynomial). If any pair mismatches, insert the party_id into
mismatched; otherwise accept that party only if all of its per-ciphertext pairs
match. Ensure you reference proofs[i].payload.proof.public_signals and shares[i]
(or equivalent index-aligned pairing) rather than .first().

In `@crates/tests/tests/integration.rs`:
- Around line 661-670: The match arm for EnclaveEventData::ComputeRequest uses
struct-like patterns `{ .. }` for tuple variants from ZkRequest which causes a
compile error; inside the matches! for ComputeRequestKind::Zk you should change
the patterns ZkRequest::NodeDkgFold { .. }, ZkRequest::DkgAggregation { .. },
and ZkRequest::DecryptionAggregation { .. } to tuple patterns like
ZkRequest::NodeDkgFold(_), ZkRequest::DkgAggregation(_), and
ZkRequest::DecryptionAggregation(_) so the variant constructors match their
actual definitions.

---

Nitpick comments:
In `@examples/CRISP/server/src/cli/commands.rs`:
- Around line 189-211: The first call to get_current_timestamp() that assigns
current_timestamp (used only for logging) is redundant because current_timestamp
is immediately shadowed by a second get_current_timestamp() before
window_start/input_window are computed; remove the earlier assignment (and the
mut on the original declaration if present) and keep the later fresh
get_current_timestamp() used to compute window_start and input_window (which
reference CONFIG.e3_duration), or alternatively remove the later shadowing and
use the initially fetched current_timestamp consistently—update occurrences of
current_timestamp, window_start, and input_window accordingly to avoid dead code
and shadowing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3cd58633-c01c-4367-a118-8a2e2e9d5328

📥 Commits

Reviewing files that changed from the base of the PR and between 7958dbd and ea1c5ec.

📒 Files selected for processing (10)
  • circuits/bin/threshold/share_decryption/src/main.nr
  • circuits/lib/src/core/threshold/share_decryption.nr
  • crates/aggregator/src/threshold_plaintext_aggregator.rs
  • crates/tests/tests/integration.rs
  • crates/zk-helpers/src/circuits/threshold/share_decryption/computation.rs
  • crates/zk-prover/tests/local_e2e_tests.rs
  • examples/CRISP/enclave.config.yaml
  • examples/CRISP/packages/crisp-contracts/deployed_contracts.json
  • examples/CRISP/server/.env.example
  • examples/CRISP/server/src/cli/commands.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • circuits/bin/threshold/share_decryption/src/main.nr
  • examples/CRISP/packages/crisp-contracts/deployed_contracts.json

Comment thread circuits/lib/src/core/threshold/share_decryption.nr
Comment thread crates/aggregator/src/threshold_plaintext_aggregator.rs Outdated
Comment thread crates/aggregator/src/threshold_plaintext_aggregator.rs

@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 (3)
scripts/generate-verifiers.ts (1)

16-21: ⚠️ Potential issue | 🟡 Minor

Stale example in header docstring.

The header still references the old pk,fold circuit names, which were removed in this PR (per the package.json change to dkg_aggregator,decryption_aggregator). The CLI showHelp() examples at lines 415-416 were updated; please update this top-of-file usage block too for consistency.

📝 Suggested doc fix
  * Usage:
  *   pnpm generate:verifiers                  # All circuits (or --circuits from package.json)
- *   pnpm generate:verifiers --circuits pk,fold  # Specific circuits
+ *   pnpm generate:verifiers --circuits dkg_aggregator,decryption_aggregator  # Specific circuits
  *   pnpm generate:verifiers --clean          # Remove existing verifiers first
  *   pnpm generate:verifiers --dry-run        # Show what would be generated
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate-verifiers.ts` around lines 16 - 21, Update the top-of-file
usage docstring in scripts/generate-verifiers.ts to match the updated circuit
names from package.json: replace the stale "pk,fold" example with
"dkg_aggregator,decryption_aggregator" (so the line "pnpm generate:verifiers
--circuits pk,fold" becomes "pnpm generate:verifiers --circuits
dkg_aggregator,decryption_aggregator"), and ensure this header now matches the
CLI showHelp() examples and any other occurrences of the old circuit names in
the file.
docs/pages/cryptography.mdx (2)

443-446: ⚠️ Potential issue | 🟡 Minor

Stale reference to "wrapper circuits" under recursive_aggregation/.

recursive_aggregation/wrapper/ was removed in this PR; aggregation is now expressed as the ad-hoc bins (c2ab_fold, c3_fold, c3ab_fold, c4ab_fold, node_fold, nodes_fold, dkg_aggregator, decryption_aggregator, c6_fold). Consider rewording to reference the inner circuits and the new aggregation bins instead of "inner UltraHonk proofs, wrapper circuits, and the folding machinery", to avoid pointing readers at a directory that no longer exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/pages/cryptography.mdx` around lines 443 - 446, Update the sentence that
mentions "inner UltraHonk proofs, wrapper circuits, and the folding machinery"
to remove the stale "wrapper circuits" reference and instead point readers to
the inner UltraHonk circuits and the new ad-hoc aggregation bins; specifically
mention the aggregation bins like c2ab_fold, c3_fold, c3ab_fold, c4ab_fold,
node_fold, nodes_fold, dkg_aggregator, decryption_aggregator, and c6_fold (under
recursive_aggregation/) and keep the Noir Circuits link for how to build and
verify them.

199-200: ⚠️ Potential issue | 🟡 Minor

Stale "proposed design" note — this PR implements proof aggregation.

This PR adds the dkg_aggregator/decryption_aggregator circuits and updates Solidity, Rust aggregators, and EVM helpers so the on-chain flow verifies a single aggregator proof per phase (DKG/decryption). The note describing the section as a "proposed design that has not been implemented yet" should be removed or rephrased to reflect the now-implemented architecture (with any open follow-ups, e.g. #1524/#1525, called out narrowly instead of blanket-disclaiming the whole section).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/pages/cryptography.mdx` around lines 199 - 200, Remove or update the
stale "proposed design" note in docs/pages/cryptography.mdx that states the
design "has not been implemented yet" — replace it with a concise statement that
the dkg_aggregator and decryption_aggregator circuits plus the Solidity and Rust
aggregators and EVM helpers are now implemented and that the on-chain flow
verifies a single aggregator proof per phase; if necessary, briefly note
outstanding follow-ups (e.g. `#1524`, `#1525`) rather than blanket-disclaiming the
section.
♻️ Duplicate comments (2)
packages/enclave-contracts/contracts/Enclave.sol (1)

414-419: ⚠️ Potential issue | 🔴 Critical

Decryption proof is still bound only to keccak256(plaintextOutput).

The verifier call does not include any E3-scoped public input (e.g. e3.ciphertextOutput, e3.committeePublicKey, or an E3-bound domain separator), so a valid DecryptionAggregator proof for a different ciphertext/E3 that yields the same plaintext hash could be replayed here to complete this E3 and trigger reward distribution. The IDecryptionVerifier.verify(bytes32, bytes) interface in IDecryptionVerifier.sol would need to be extended (or its publicInputs framing tightened) to accept and check the E3-scoped values against the aggregator's public-input slots.

This was previously flagged on an outdated commit; the current code path still has the same shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/enclave-contracts/contracts/Enclave.sol` around lines 414 - 419, The
decryption proof is currently verified only against keccak256(plaintextOutput)
via e3.decryptionVerifier.verify, allowing replay across different E3 instances;
extend the IDecryptionVerifier.verify interface (and its implementations) to
accept E3-scoped public inputs (e.g. e3.ciphertextOutput, e3.committeePublicKey
and/or an E3 domain separator) and update the call site in Enclave.sol (where
e3.proofAggregationEnabled is checked and e3.decryptionVerifier.verify is
invoked) to pass a tightly framed publicInputs bundle that includes those
E3-specific values so the verifier can bind the aggregated proof to this exact
ciphertext/E3 context before setting success.
crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs (1)

187-213: ⚠️ Potential issue | 🟠 Major

C0 still not correlated with the C1/C2/C3 chain — test name overpromises.

pk_bfv_data (line 197) is generated as an independent random sample via PkCircuitData::generate_sample(preset), while the C1/C2 chain comes from pk_generation_sample_with_esi at lines 187‑188. The C0 proof at lines 204‑213 is therefore unrelated to the BFV public key used everywhere else in this "correlated" e2e, so the test cannot fail on a real C0↔chain mismatch (e.g. a C0 commitment for a different pk leaking through).

Either:

  • derive pk_bfv_data from the BFV public key embedded in pk_gen, or
  • before constructing the nf JSON, assert c0_proof's pk_commitment public output equals the corresponding C2ab/C3ab/C4ab pk_commitment used downstream.

This was raised previously and is still outstanding in the current code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs` around lines 187 -
213, The test currently generates pk_bfv_data via PkCircuitData::generate_sample
which is independent from the BFV public key produced by
pk_generation_sample_with_esi (pk_gen), so the C0 proof (c0_proof) is not
correlated with the C1/C2 chain; fix by either deriving pk_bfv_data from pk_gen
(replace PkCircuitData::generate_sample with a constructor that builds
PkCircuitData from pk_gen’s BFV public key) or, if keeping the separate sample,
add an assertion after proving C0 that c0_proof's pk_commitment public output
equals the pk_commitment used downstream (e.g., the one in share_esm/share_sk or
the C2ab/C3ab/C4ab data) before constructing the nf JSON to ensure the proof is
genuinely correlated.
🧹 Nitpick comments (10)
packages/enclave-contracts/contracts/interfaces/IEnclave.sol (1)

428-433: Stale NatSpec for proofAggregationEnabled references removed wrapper/fold flow.

The doc still says ciphernodes "generate and fold wrapper proofs" and that "wrapper/fold proofs are skipped" when disabled, but this PR removes the wrapper/fold trees in favor of the new dkg_aggregator / decryption_aggregator circuits. Consider updating the wording to reflect the new aggregator-based flow (and the fact that C5/C7 verification on-chain is gated by this flag, since Enclave.sol::publishPlaintextOutput only invokes the decryption verifier when the flag is true).

📝 Suggested wording update
-        /// `@notice` When true, ciphernodes generate and fold wrapper proofs
-        ///         for DKG proof aggregation (public verifiability). When
-        ///         false, wrapper/fold proofs are skipped to reduce latency.
-        ///         C5 and C7 proofs are always generated and verified on-chain
-        ///         regardless of this flag.
+        /// `@notice` When true, ciphernodes generate the DkgAggregator and
+        ///         DecryptionAggregator proofs for public on-chain
+        ///         verifiability of DKG and threshold decryption. When
+        ///         false, aggregator proof generation/verification is
+        ///         skipped to reduce latency.
         bool proofAggregationEnabled;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/enclave-contracts/contracts/interfaces/IEnclave.sol` around lines
428 - 433, The NatSpec for the boolean field proofAggregationEnabled is stale:
replace references to "wrapper/fold proofs" with the current aggregator-based
flow and clarify gating of on-chain verification; update the comment on
proofAggregationEnabled to state that when true ciphernodes generate and fold
proofs via the dkg_aggregator and decryption_aggregator circuits (and that
publishPlaintextOutput only invokes the decryption verifier when this flag is
true), and that C5/C7 verification remains performed on-chain regardless of the
flag; modify the docstring near proofAggregationEnabled in IEnclave.sol to
mention the new aggregator names (dkg_aggregator / decryption_aggregator) and
the gating behavior tied to Enclave.sol::publishPlaintextOutput.
crates/aggregator/src/publickey_aggregator.rs (1)

72-86: Remove unused carry-through state fields from GeneratingC5Proof.

keyshare_bytes and dishonest_parties are assigned when entering GeneratingC5Proof but never read afterward. They are threaded through every state mutation (try_receive_c5_proof, try_receive_dkg_node_proof, try_dispatch_dkg_aggregation, try_publish_complete, try_handle_dkg_aggregation_response) via destructure-and-reconstruct patterns without being used in the logic. These appear to be remnants from the removed pairwise-fold flow and can be dropped to reduce the persisted state size.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/aggregator/src/publickey_aggregator.rs` around lines 72 - 86, Remove
the unused carry-through fields keyshare_bytes and dishonest_parties from the
GeneratingC5Proof enum variant and eliminate them from every place that
destructures/reconstructs that state: update the constructor/transition that
creates GeneratingC5Proof (where those fields were initially assigned) and
adjust all state-match and rebuild sites inside try_receive_c5_proof,
try_receive_dkg_node_proof, try_dispatch_dkg_aggregation, try_publish_complete,
and try_handle_dkg_aggregation_response to stop extracting or reattaching
keyshare_bytes and dishonest_parties; ensure no remaining references to those
symbols exist and that the remaining fields (public_key, nodes, dkg_node_proofs,
honest_party_ids, dkg_aggregation_correlation, dkg_aggregated_proof,
c5_proof_pending, last_ec) are preserved in the reconstructed state.
scripts/build-circuits.ts (1)

587-587: Stale "wrapper" terminology in comment.

The PR removes wrapper circuits, but the comment for the recursive/ variant still describes the artifacts as "inner/base proofs fed into wrapper". Consider updating to reflect the new aggregation pipeline (e.g., "inner/base proofs consumed by the aggregation fold circuits").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build-circuits.ts` at line 587, Update the stale comment that
references "wrapper" in the recursive/ variant: replace "inner/base proofs fed
into wrapper" with wording that reflects the new aggregation pipeline such as
"inner/base proofs consumed by the aggregation fold circuits" (or similar
wording) in the comment near the recursive/ variant in scripts/build-circuits.ts
so it accurately describes the new aggregation flow.
crates/zk-prover/src/circuits/aggregation/helpers.rs (2)

63-83: Guard slot_width == 0 to avoid a % panic.

If a future caller passes slot_width = 0, the (v.len() - prefix_len) % slot_width check panics on integer division. All current callers pass non-zero widths (3 or 4), but a tiny upfront guard makes this helper robust against future misuse.

🛡️ Proposed change
 ) -> Result<Vec<String>, ZkError> {
     if proof.circuit != expected_circuit {
         return Err(ZkError::InvalidInput(format!(
             "expected {expected_circuit} proof, got {}",
             proof.circuit
         )));
     }
+    if slot_width == 0 {
+        return Err(ZkError::InvalidInput(
+            "parse_acc_public_field_strings: slot_width must be > 0".into(),
+        ));
+    }
     let v = bytes_to_field_strings(proof.public_signals.as_ref())?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/src/circuits/aggregation/helpers.rs` around lines 63 - 83,
The function parse_acc_public_field_strings should guard against slot_width == 0
to avoid a divide-by-zero panic: add an early validation in
parse_acc_public_field_strings that returns Err(ZkError::InvalidInput(...)) when
slot_width is zero (include a clear message like "slot_width must be non-zero"),
then keep the existing checks
(bytes_to_field_strings(proof.public_signals.as_ref())?, length vs prefix_len,
and the modulo check) unchanged; reference parse_acc_public_field_strings and
slot_width to locate the change.

32-59: Replace stringly-typed kind with an enum.

kind: &str here accepts only the magic strings "input" / "output" and falls through to a runtime error otherwise. Since this helper is only used internally (e.g. from share_encryption_inner_public_inputs in c3_accumulator.rs), promoting kind to a small enum (e.g. PublicSignalKind::{Input, Output}) eliminates the string-comparison branch, the unreachable error path, and the risk of typos at call sites — fully resolved at compile time.

♻️ Proposed change
+pub enum PublicSignalKind {
+    Input,
+    Output,
+}
+
 /// Extract a single 32-byte public input/output by name. `kind` must be `"input"` or `"output"`.
 pub fn extract_single_field(
     proof: &Proof,
-    kind: &str,
+    kind: PublicSignalKind,
     name: &str,
     context: &str,
 ) -> Result<String, ZkError> {
     let bytes = match kind {
-        "input" => proof
+        PublicSignalKind::Input => proof
             .extract_input(name)
             .ok_or_else(|| ZkError::InvalidInput(format!("{context}: missing input {name}")))?,
-        "output" => proof
+        PublicSignalKind::Output => proof
             .extract_output(name)
             .ok_or_else(|| ZkError::InvalidInput(format!("{context}: missing output {name}")))?,
-        _ => {
-            return Err(ZkError::InvalidInput(format!(
-                "extract_single_field: kind must be input or output, got {kind}"
-            )));
-        }
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/src/circuits/aggregation/helpers.rs` around lines 32 - 59,
extract_single_field currently takes a stringly-typed kind: &str and matches on
"input"/"output", allowing runtime typos and unreachable branches; change the
API to accept a small enum (e.g. PublicSignalKind with variants Input and
Output), update extract_single_field signature to (proof: &Proof, kind:
PublicSignalKind, name: &str, context: &str) and replace the string match with
match PublicSignalKind::{Input,Output} to call proof.extract_input or
proof.extract_output accordingly, and then update all call sites (for example
share_encryption_inner_public_inputs in c3_accumulator.rs) to pass the enum
variant instead of the string literal so the choice is enforced at compile time.
crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs (2)

296-298: Hardcoded total_slots == 6 assertion couples the test to a single preset.

The constant assert_eq!(total_slots, 6, ...) makes this test silently break (with a confusing assertion message) the moment BfvPreset::InsecureThreshold512 or CiphernodesCommitteeSize::Micro parameters move. Since total_slots is already derived dynamically from the compiled c3_fold.json ABI just above, consider dropping the hard-coded 6 (the rest of the test already adapts via slot_indices = (0..total_slots as u32).collect()), or computing the expected value from N_PARTIES * L_THRESHOLD exposed by the preset rather than a literal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs` around lines 296 -
298, The test asserts total_slots == 6 via a hard-coded literal which couples it
to a specific preset; instead either remove that rigid assert or compute the
expected value from the preset constants and compare to total_slots. Locate the
call to c3_fold_total_slots_from_compiled_json() and the assertion using
total_slots, then replace assert_eq!(total_slots, 6, ...) with one of: (A) drop
the assertion entirely since slot_indices already uses total_slots, or (B)
derive expected_slots from the preset (e.g. use BfvPreset::InsecureThreshold512
and CiphernodesCommitteeSize::Micro semantics or compute expected_slots =
N_PARTIES * L_THRESHOLD from the preset values) and assert_eq!(total_slots,
expected_slots, "...") so the test adapts when those constants change.

132-148: Test silently passes when artifacts are missing.

Three return paths (lines 132‑135, 137‑144, 145‑148) cause the test to be skipped without failing whenever bb, node_fold.json, or c3_fold.json aren't present, only printing a println!. That makes a CI environment that forgets to run pnpm build:circuits --group recursive_aggregation look green even though the entire correlated path is untested. If this is intentional for local dev, consider gating with #[ignore = "..."] or a cfg(feature = "circuit_artifacts") so the skip is visible in CI summaries instead of being indistinguishable from a real pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs` around lines 132 -
148, The test currently silently returns when artifacts are missing (in the
find_bb() branch and when
recursive_aggregation_compiled_json_path(CircuitName::NodeFold) or
c3_fold_json_path() don't exist), so change those early-return branches to
either (A) assert or panic with a clear message (e.g., replace the println! +
return with panic!("missing artifact: ...") or assert!(path.exists(), "...")) so
CI fails when artifacts are absent, or (B) gate the whole test with an explicit
annotation like #[ignore = "requires circuit artifacts"] or cfg(feature =
"circuit_artifacts") so the skip is visible and deliberate; update the checks
around find_bb(),
recursive_aggregation_compiled_json_path(CircuitName::NodeFold), and
c3_fold_json_path() accordingly.
crates/zk-prover/src/circuits/aggregation/c3_accumulator.rs (2)

39-92: Document why the kernel proof uses slot_index = 0.

Line 69 hardcodes slot_index: 0 in the kernel witness, but the kernel runs with the full first inner proof's c3_public_inputs and writes them at slot 0 of its public output. This is correct only because the immediate next c3_fold step runs with is_first_step = true, in which the sibling c3_fold circuit ignores acc_public_inputs entirely and re-derives the slot row from the inner proof. A short comment near line 69 stating "kernel slot index is irrelevant — first c3_fold step has is_first_step=true and ignores acc_public_inputs" would prevent a future contributor from "fixing" this to the real slot_index and breaking the contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/src/circuits/aggregation/c3_accumulator.rs` around lines 39
- 92, In generate_c3_fold_kernel_genesis_proof, add a brief comment next to the
slot_index: 0 assignment explaining that the kernel's slot_index value is
irrelevant here because the kernel is fed the full first inner proof's
c3_public_inputs and the immediate next C3 fold step runs with is_first_step =
true and therefore ignores acc_public_inputs and re-derives the slot row from
the inner proof; reference the slot_index field and is_first_step boolean so
future contributors won't change slot_index to the "real" value and break the
contract.

84-92: Don't silently swallow kernel-cleanup errors.

let _ = prover.cleanup(job_id); discards any cleanup failure for the kernel work directory. If cleanup fails (disk pressure, permissions, etc.) the temp artifacts accumulate silently across E3 runs. At minimum, log the error at warn level so operators see it; bubbling it up to the caller is also reasonable since the proof itself has already been produced.

♻️ Proposed change
-    let _ = prover.cleanup(job_id);
+    if let Err(e) = prover.cleanup(job_id) {
+        tracing::warn!(error = %e, job_id, "c3_fold kernel cleanup failed");
+    }
     Ok(proof)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/src/circuits/aggregation/c3_accumulator.rs` around lines 84
- 92, The code currently swallows errors from prover.cleanup(job_id) after
generating the proof (in the generate_recursive_aggregation_bin_proof ->
prover.cleanup sequence), which lets temp kernel artifacts accumulate; change
the cleanup call to handle errors instead of ignoring them: call
prover.cleanup(job_id) and if it returns Err, either log the error at warn level
via the existing logger or propagate the error to the caller (return Err) so
failures are visible; ensure this change is made in the function that calls
prover.generate_recursive_aggregation_bin_proof (referencing prover.cleanup,
job_id, and CircuitName::C3FoldKernel) and keep the successful Ok(proof) return
only after handling or forwarding the cleanup error.
circuits/bin/recursive_aggregation/nodes_fold_kernel/src/main.nr (1)

15-27: Unused intermediate constants kept for ABI parity?

C2_PUBLIC_LEN, C2AB_FOLD_PUBLIC_LEN, C3_SLOTS, C3AB_FOLD_PUBLIC_LEN, C4_PUBLIC_LEN, C4AB_FOLD_PUBLIC_LEN are declared pub global here but not referenced in this kernel's main. If these are kept solely for parity with nodes_fold (or for external Noir consumers), a brief comment would help future readers; otherwise consider trimming the unused ones to reduce drift risk if the sibling layout changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@circuits/bin/recursive_aggregation/nodes_fold_kernel/src/main.nr` around
lines 15 - 27, Several public global constants (C2_PUBLIC_LEN,
C2AB_FOLD_PUBLIC_LEN, C3_SLOTS, C3AB_FOLD_PUBLIC_LEN, C4_PUBLIC_LEN,
C4AB_FOLD_PUBLIC_LEN) are declared but not used in this kernel's main; either
remove them or document why they remain for ABI parity. Fix by either (a)
deleting the unused pub global declarations, or (b) adding a concise comment
above those declarations stating they are intentionally kept for ABI/layout
parity with sibling kernels or external consumers, and ensure
NODE_FOLD_PUBLIC_LEN and NODES_FOLD_PUBLIC_LEN (the actually used symbols)
remain unchanged; choose one approach and apply it consistently to avoid future
drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/aggregator/src/publickey_aggregator.rs`:
- Around line 627-662: The code currently only checks key presence via
all_honest_proofs_present but then drops None values when building pairs,
allowing silent truncation; change the guard in publickey_aggregator.rs (around
all_honest_proofs_present, dkg_node_proofs, pairs, node_fold_proofs) to
explicitly verify that for every honest_party_id dkg_node_proofs.get(id) is
Some(Some(_)) (i.e., the map contains the key and the value is Some), and if any
honest id maps to None either return early (same as the all-None branch) or
return an error/emit a clear log and skip dispatch — do not proceed to
filter_map and dispatch when there is a mixed Some/None state. Ensure the new
check replaces or augments the existing all_honest_proofs_present logic and add
a clear info/error log indicating mixed proofs when you early-return.

In `@packages/enclave-contracts/tasks/enclave.ts`:
- Around line 318-327: The task must validate that publicKey and pkCommitment
are consistent before calling publishCommittee: when proof aggregation is
disabled (proof equals 0x or equivalent), ensure publicKey is provided and
pkCommitment is a non-zero bytes32 and equals the hash of publicKey (e.g.,
keccak256(publicKey)); reject or normalize malformed/zero pkCommitment values
instead of just checking falsiness. Update the caller that constructs the
arguments for publishCommittee to perform this check (reference variables
publicKey, pkCommitment, proof and the publishCommittee invocation) and throw a
clear error or exit when the consistency check fails.

---

Outside diff comments:
In `@docs/pages/cryptography.mdx`:
- Around line 443-446: Update the sentence that mentions "inner UltraHonk
proofs, wrapper circuits, and the folding machinery" to remove the stale
"wrapper circuits" reference and instead point readers to the inner UltraHonk
circuits and the new ad-hoc aggregation bins; specifically mention the
aggregation bins like c2ab_fold, c3_fold, c3ab_fold, c4ab_fold, node_fold,
nodes_fold, dkg_aggregator, decryption_aggregator, and c6_fold (under
recursive_aggregation/) and keep the Noir Circuits link for how to build and
verify them.
- Around line 199-200: Remove or update the stale "proposed design" note in
docs/pages/cryptography.mdx that states the design "has not been implemented
yet" — replace it with a concise statement that the dkg_aggregator and
decryption_aggregator circuits plus the Solidity and Rust aggregators and EVM
helpers are now implemented and that the on-chain flow verifies a single
aggregator proof per phase; if necessary, briefly note outstanding follow-ups
(e.g. `#1524`, `#1525`) rather than blanket-disclaiming the section.

In `@scripts/generate-verifiers.ts`:
- Around line 16-21: Update the top-of-file usage docstring in
scripts/generate-verifiers.ts to match the updated circuit names from
package.json: replace the stale "pk,fold" example with
"dkg_aggregator,decryption_aggregator" (so the line "pnpm generate:verifiers
--circuits pk,fold" becomes "pnpm generate:verifiers --circuits
dkg_aggregator,decryption_aggregator"), and ensure this header now matches the
CLI showHelp() examples and any other occurrences of the old circuit names in
the file.

---

Duplicate comments:
In `@crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs`:
- Around line 187-213: The test currently generates pk_bfv_data via
PkCircuitData::generate_sample which is independent from the BFV public key
produced by pk_generation_sample_with_esi (pk_gen), so the C0 proof (c0_proof)
is not correlated with the C1/C2 chain; fix by either deriving pk_bfv_data from
pk_gen (replace PkCircuitData::generate_sample with a constructor that builds
PkCircuitData from pk_gen’s BFV public key) or, if keeping the separate sample,
add an assertion after proving C0 that c0_proof's pk_commitment public output
equals the pk_commitment used downstream (e.g., the one in share_esm/share_sk or
the C2ab/C3ab/C4ab data) before constructing the nf JSON to ensure the proof is
genuinely correlated.

In `@packages/enclave-contracts/contracts/Enclave.sol`:
- Around line 414-419: The decryption proof is currently verified only against
keccak256(plaintextOutput) via e3.decryptionVerifier.verify, allowing replay
across different E3 instances; extend the IDecryptionVerifier.verify interface
(and its implementations) to accept E3-scoped public inputs (e.g.
e3.ciphertextOutput, e3.committeePublicKey and/or an E3 domain separator) and
update the call site in Enclave.sol (where e3.proofAggregationEnabled is checked
and e3.decryptionVerifier.verify is invoked) to pass a tightly framed
publicInputs bundle that includes those E3-specific values so the verifier can
bind the aggregated proof to this exact ciphertext/E3 context before setting
success.

---

Nitpick comments:
In `@circuits/bin/recursive_aggregation/nodes_fold_kernel/src/main.nr`:
- Around line 15-27: Several public global constants (C2_PUBLIC_LEN,
C2AB_FOLD_PUBLIC_LEN, C3_SLOTS, C3AB_FOLD_PUBLIC_LEN, C4_PUBLIC_LEN,
C4AB_FOLD_PUBLIC_LEN) are declared but not used in this kernel's main; either
remove them or document why they remain for ABI parity. Fix by either (a)
deleting the unused pub global declarations, or (b) adding a concise comment
above those declarations stating they are intentionally kept for ABI/layout
parity with sibling kernels or external consumers, and ensure
NODE_FOLD_PUBLIC_LEN and NODES_FOLD_PUBLIC_LEN (the actually used symbols)
remain unchanged; choose one approach and apply it consistently to avoid future
drift.

In `@crates/aggregator/src/publickey_aggregator.rs`:
- Around line 72-86: Remove the unused carry-through fields keyshare_bytes and
dishonest_parties from the GeneratingC5Proof enum variant and eliminate them
from every place that destructures/reconstructs that state: update the
constructor/transition that creates GeneratingC5Proof (where those fields were
initially assigned) and adjust all state-match and rebuild sites inside
try_receive_c5_proof, try_receive_dkg_node_proof, try_dispatch_dkg_aggregation,
try_publish_complete, and try_handle_dkg_aggregation_response to stop extracting
or reattaching keyshare_bytes and dishonest_parties; ensure no remaining
references to those symbols exist and that the remaining fields (public_key,
nodes, dkg_node_proofs, honest_party_ids, dkg_aggregation_correlation,
dkg_aggregated_proof, c5_proof_pending, last_ec) are preserved in the
reconstructed state.

In `@crates/zk-prover/src/circuits/aggregation/c3_accumulator.rs`:
- Around line 39-92: In generate_c3_fold_kernel_genesis_proof, add a brief
comment next to the slot_index: 0 assignment explaining that the kernel's
slot_index value is irrelevant here because the kernel is fed the full first
inner proof's c3_public_inputs and the immediate next C3 fold step runs with
is_first_step = true and therefore ignores acc_public_inputs and re-derives the
slot row from the inner proof; reference the slot_index field and is_first_step
boolean so future contributors won't change slot_index to the "real" value and
break the contract.
- Around line 84-92: The code currently swallows errors from
prover.cleanup(job_id) after generating the proof (in the
generate_recursive_aggregation_bin_proof -> prover.cleanup sequence), which lets
temp kernel artifacts accumulate; change the cleanup call to handle errors
instead of ignoring them: call prover.cleanup(job_id) and if it returns Err,
either log the error at warn level via the existing logger or propagate the
error to the caller (return Err) so failures are visible; ensure this change is
made in the function that calls prover.generate_recursive_aggregation_bin_proof
(referencing prover.cleanup, job_id, and CircuitName::C3FoldKernel) and keep the
successful Ok(proof) return only after handling or forwarding the cleanup error.

In `@crates/zk-prover/src/circuits/aggregation/helpers.rs`:
- Around line 63-83: The function parse_acc_public_field_strings should guard
against slot_width == 0 to avoid a divide-by-zero panic: add an early validation
in parse_acc_public_field_strings that returns Err(ZkError::InvalidInput(...))
when slot_width is zero (include a clear message like "slot_width must be
non-zero"), then keep the existing checks
(bytes_to_field_strings(proof.public_signals.as_ref())?, length vs prefix_len,
and the modulo check) unchanged; reference parse_acc_public_field_strings and
slot_width to locate the change.
- Around line 32-59: extract_single_field currently takes a stringly-typed kind:
&str and matches on "input"/"output", allowing runtime typos and unreachable
branches; change the API to accept a small enum (e.g. PublicSignalKind with
variants Input and Output), update extract_single_field signature to (proof:
&Proof, kind: PublicSignalKind, name: &str, context: &str) and replace the
string match with match PublicSignalKind::{Input,Output} to call
proof.extract_input or proof.extract_output accordingly, and then update all
call sites (for example share_encryption_inner_public_inputs in
c3_accumulator.rs) to pass the enum variant instead of the string literal so the
choice is enforced at compile time.

In `@crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs`:
- Around line 296-298: The test asserts total_slots == 6 via a hard-coded
literal which couples it to a specific preset; instead either remove that rigid
assert or compute the expected value from the preset constants and compare to
total_slots. Locate the call to c3_fold_total_slots_from_compiled_json() and the
assertion using total_slots, then replace assert_eq!(total_slots, 6, ...) with
one of: (A) drop the assertion entirely since slot_indices already uses
total_slots, or (B) derive expected_slots from the preset (e.g. use
BfvPreset::InsecureThreshold512 and CiphernodesCommitteeSize::Micro semantics or
compute expected_slots = N_PARTIES * L_THRESHOLD from the preset values) and
assert_eq!(total_slots, expected_slots, "...") so the test adapts when those
constants change.
- Around line 132-148: The test currently silently returns when artifacts are
missing (in the find_bb() branch and when
recursive_aggregation_compiled_json_path(CircuitName::NodeFold) or
c3_fold_json_path() don't exist), so change those early-return branches to
either (A) assert or panic with a clear message (e.g., replace the println! +
return with panic!("missing artifact: ...") or assert!(path.exists(), "...")) so
CI fails when artifacts are absent, or (B) gate the whole test with an explicit
annotation like #[ignore = "requires circuit artifacts"] or cfg(feature =
"circuit_artifacts") so the skip is visible and deliberate; update the checks
around find_bb(),
recursive_aggregation_compiled_json_path(CircuitName::NodeFold), and
c3_fold_json_path() accordingly.

In `@packages/enclave-contracts/contracts/interfaces/IEnclave.sol`:
- Around line 428-433: The NatSpec for the boolean field proofAggregationEnabled
is stale: replace references to "wrapper/fold proofs" with the current
aggregator-based flow and clarify gating of on-chain verification; update the
comment on proofAggregationEnabled to state that when true ciphernodes generate
and fold proofs via the dkg_aggregator and decryption_aggregator circuits (and
that publishPlaintextOutput only invokes the decryption verifier when this flag
is true), and that C5/C7 verification remains performed on-chain regardless of
the flag; modify the docstring near proofAggregationEnabled in IEnclave.sol to
mention the new aggregator names (dkg_aggregator / decryption_aggregator) and
the gating behavior tied to Enclave.sol::publishPlaintextOutput.

In `@scripts/build-circuits.ts`:
- Line 587: Update the stale comment that references "wrapper" in the recursive/
variant: replace "inner/base proofs fed into wrapper" with wording that reflects
the new aggregation pipeline such as "inner/base proofs consumed by the
aggregation fold circuits" (or similar wording) in the comment near the
recursive/ variant in scripts/build-circuits.ts so it accurately describes the
new aggregation flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02d6e2d7-219f-4216-9855-f493e059bf61

📥 Commits

Reviewing files that changed from the base of the PR and between ea1c5ec and c4a2b1e.

📒 Files selected for processing (39)
  • agent/flow-trace/04_DKG_AND_COMPUTATION.md
  • circuits/bin/recursive_aggregation/c3_fold_kernel/src/main.nr
  • circuits/bin/recursive_aggregation/c6_fold_kernel/src/main.nr
  • circuits/bin/recursive_aggregation/decryption_aggregator/src/main.nr
  • circuits/bin/recursive_aggregation/node_fold/src/main.nr
  • circuits/bin/recursive_aggregation/nodes_fold_kernel/src/main.nr
  • crates/aggregator/src/publickey_aggregator.rs
  • crates/dashboard/src/dashboard.html
  • crates/events/src/enclave_event/keyshare_created.rs
  • crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs
  • crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs
  • crates/zk-prover/src/circuits/aggregation/c3_accumulator.rs
  • crates/zk-prover/src/circuits/aggregation/c6_accumulator.rs
  • crates/zk-prover/src/circuits/aggregation/helpers.rs
  • crates/zk-prover/src/circuits/aggregation/mod.rs
  • crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs
  • crates/zk-prover/src/circuits/aggregation/nodes_fold_accumulator.rs
  • crates/zk-prover/tests/node_fold_correlated_e2e_tests.rs
  • docs/pages/cryptography.mdx
  • docs/pages/noir-circuits.mdx
  • docs/pages/tutorials/deploy-to-testnet.mdx
  • examples/CRISP/enclave.config.yaml
  • examples/CRISP/packages/crisp-contracts/deployed_contracts.json
  • packages/enclave-contracts/artifacts/contracts/Enclave.sol/Enclave.json
  • packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json
  • packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json
  • packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json
  • packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json
  • packages/enclave-contracts/artifacts/contracts/registry/CiphernodeRegistryOwnable.sol/CiphernodeRegistryOwnable.json
  • packages/enclave-contracts/contracts/Enclave.sol
  • packages/enclave-contracts/contracts/interfaces/IEnclave.sol
  • packages/enclave-contracts/contracts/verifiers/bfv/honk/DecryptionAggregatorVerifier.sol
  • packages/enclave-contracts/contracts/verifiers/bfv/honk/DkgAggregatorVerifier.sol
  • packages/enclave-contracts/tasks/enclave.ts
  • packages/enclave-contracts/test/BfvPkVerifier.spec.ts
  • packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts
  • scripts/build-circuits.ts
  • scripts/generate-verifiers.ts
  • scripts/lint-circuits.sh
✅ Files skipped from review due to trivial changes (6)
  • docs/pages/tutorials/deploy-to-testnet.mdx
  • packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json
  • crates/dashboard/src/dashboard.html
  • docs/pages/noir-circuits.mdx
  • packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json
  • examples/CRISP/enclave.config.yaml
🚧 Files skipped from review as they are similar to previous changes (7)
  • crates/zk-prover/src/circuits/aggregation/mod.rs
  • scripts/lint-circuits.sh
  • crates/zk-prover/src/circuits/aggregation/nodes_fold_accumulator.rs
  • crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs
  • crates/zk-prover/src/circuits/aggregation/c6_accumulator.rs
  • circuits/bin/recursive_aggregation/node_fold/src/main.nr
  • circuits/bin/recursive_aggregation/decryption_aggregator/src/main.nr

Comment thread crates/aggregator/src/publickey_aggregator.rs
Comment thread packages/enclave-contracts/tasks/enclave.ts
@hmzakhalid

hmzakhalid commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

Did a Quick review with Claude. Here are the items to verify and confirm


On-chain BFV wrappers do not pin intermediate VK hashes

circuits/bin/recursive_aggregation/dkg_aggregator/src/main.nr declares nodes_fold_key_hash and c5_key_hash as pub parameters (witnesses elevated to public IO). dkg_aggregator returns key_hash = compute_vk_hash([nodes_fold_kh, c5_kh]) as the first field of the return tuple.

BfvPkVerifier.verify only validates two things:

  1. publicInputs[publicInputs.length - 1] == pkCommitment
  2. circuitVerifier.verify(rawProof, publicInputs) (the codegen Honk verifier for the dkg_aggregator VK).

Nowhere does it compare publicInputs[0] (nodes_fold_key_hash), publicInputs[1] (c5_key_hash), or the recursive key_hash output to expected constants. BfvDecryptionVerifier has the same pattern: it pins only the last MESSAGE_COEFFS_COUNT plaintext coefficients, not c6_fold_key_hash / c7_key_hash.

Every verify_honk_proof_non_zk(...) inside dkg_aggregator / decryption_aggregator / nodes_fold / node_fold / c?_fold takes vk and key_hash as witnesses. A malicious prover can substitute any inner circuit at any intermediate fold layer — including a no-op identity circuit. The chain still verifies, the proof still passes the on-chain Honk check, and the public IO faithfully reports the malicious VK hash. The only externally pinned hashes are:

  • the dkg_aggregator / decryption_aggregator VK itself (baked into the codegen verifier), and
  • whatever the wrapper pins (currently: pkCommitment / plaintextOutputHash only).

So a prover can generate a "valid" dkg_aggregator proof for an arbitrary aggregated_pk_commit without ever running the real nodes_fold, c5, node_fold, etc. The claim "on-chain recursive proof — trustless cryptographic guarantee" is not actually achieved by what's on-chain today.


Critical layout constants are duplicated across circuits with "Must match" comments

The same pub global constants are redeclared in many places, each tagged // Must match X:

Every one of these is derived from the same handful of inputs (N_PARTIES, H, T, L_THRESHOLD, MAX_MSG_NON_ZERO_COEFFS). They belong in a single module under circuits/lib/src/configs/ (e.g. aggregation_layout.nr) and imported by each circuit. The current "Must match X" approach is a maintenance hazard — any IO change requires editing 4–6 files, and a missed update produces a witness-shape mismatch at proof-generation time, not at compile time.


c?_fold "previous slot must be zero" check leans on a witness-only invariant

c3_fold/src/main.nr:L70-L74 and c6_fold/src/main.nr:L68-L72 both assert the prior accumulator's column at slot_index is zero before writing — good. But is_first_step and slot_index are both pub, so the prover chooses them. If a prover sets is_first_step = true mid-chain, the prior accumulator's columns are silently discarded rather than checked.

The chain has a guard at the next step but does not check that all OTHER slots from the prior step are non-zero. After a prover lies about is_first_step once, the produced fold has only one slot populated; c3ab_fold will consume it. This collapses at node_fold's same-party C2↔C3 binding (because zero slots will not equal real C2 values), so it is not exploitable today. But it's brittle.

cc @0xjei @cedoor

@cedoor

cedoor commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

@hmzakhalid

On-chain BFV wrappers do not pin intermediate VK hashes

That are two separate follow-up tasks for this PR defined in #1524 and #1525

Critical layout constants are duplicated across circuits with "Must match" comments

True, let's keep as it is tho. Needs more refactoring as there are some circular dependencies on default imports if we use a unified file.

c?_fold "previous slot must be zero" check leans on a witness-only invariant

True, tho it wouldn't make any sense for a node to do that and then be unable to fold those proofs. All those proofs are generated at the same step.

- Add BIT_D_NATIVE / NATIVE_BIT_WIDTH for C6 d_commitment and C7 share commitment checks
- Introduce compute_native_crt_coeff_bit and C6 Bits.d_native_bit; C7 uses d_native_bit only
- Keep THRESHOLD_SHARE_DECRYPTION_BIT_D for centered d in Fiat–Shamir payload
- Regenerate threshold.nr globals, codegen, aggregator, and local e2e expectation

@0xjei 0xjei 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.

utACK

@cedoor cedoor enabled auto-merge (squash) April 27, 2026 08:01
@cedoor cedoor merged commit e651f30 into main Apr 27, 2026
33 checks passed
@github-actions github-actions Bot deleted the refactor/new-proof-aggregation branch May 5, 2026 03:22
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.

5 participants