Skip to content

feat: connect c4 and c6 [skip-line-limit]#1473

Closed
ctrlc03 wants to merge 14 commits into
mainfrom
feat/c4-c6
Closed

feat: connect c4 and c6 [skip-line-limit]#1473
ctrlc03 wants to merge 14 commits into
mainfrom
feat/c4-c6

Conversation

@ctrlc03

@ctrlc03 ctrlc03 commented Mar 23, 2026

Copy link
Copy Markdown
Collaborator

first merge #1460

Summary by CodeRabbit

  • New Features

    • Enhanced proof verification with consistency checking between DKG and threshold decryption proof stages.
    • Added proof payload tracking to verification events.
  • Refactor

    • Split DKG share decryption proof type into two distinct variants for improved cryptographic precision.
    • Reorganized ZK proof input/output field extraction for better verification accuracy.

@vercel

vercel Bot commented Mar 23, 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 Mar 26, 2026 5:33pm
enclave-docs Ready Ready Preview, Comment Mar 26, 2026 5:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Mar 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The changes extend ZK proof verification infrastructure by introducing public input field extraction from circuits, splitting the C4 DKG proof type into C4a and C4b variants, adding new C4-to-C6 commitment consistency links, enhancing verification with signal caching and pre-dispatch gates, and threading signed proof payloads through the verification pipeline.

Changes

Cohort / File(s) Summary
Public Input Extraction
crates/zk-helpers/src/circuits/output_layout.rs, crates/events/src/enclave_event/proof.rs
Introduces CircuitInputLayout enum and extract_field method for extracting named public input fields from circuit signals; adds SHARE_ENCRYPTION_INPUTS and THRESHOLD_SHARE_DECRYPTION_INPUTS constant slices; extends Proof with extract_input() method and CircuitName with input_layout() method.
ProofType Variant Refactoring
crates/events/src/enclave_event/signed_proof.rs, crates/zk-prover/src/actors/proof_request.rs
Replaces C4DkgShareDecryption variant with C4aSkShareDecryption (discriminant 6) and introduces C4bESmShareDecryption (discriminant 7); shifts subsequent variant discriminants; updates circuit mappings and slash reasons accordingly; updates proof-signing logic to use new variants.
New Commitment Links
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/actors/commitment_links/mod.rs
Implements two new commitment link types—C4aToC6SkCommitmentLink and C4bToC6ESmCommitmentLink—for verifying consistency between C4a/C4b outputs and C6 inputs; registers modules in public API.
Enhanced Verification & Caching
crates/zk-prover/src/actors/share_verification.rs
Adds caching of C4 public signals per party; introduces VerificationKind::ThresholdDecryptionProofs pre-dispatch branch performing C4→C6 consistency gate before main verification; updates ECDSA validation to skip already-dishonest parties; stores party_signed_proofs alongside hashes; suppresses ProofVerificationPassed emissions for dishonest parties.
Event & Proof Payload Updates
crates/events/src/enclave_event/proof_verification_passed.rs, crates/events/src/enclave_event/compute_request/zk.rs, crates/zk-prover/src/actors/proof_verification.rs
Adds signed_payload: SignedProofPayload field to ProofVerificationPassed; adds c5_pk_commitment: Option<ArcBytes> optional field to ThresholdShareDecryptionProofRequest; updates proof-verification handler to include signed payload in emitted event.
Keyshare & Consistency Checker Updates
crates/keyshare/src/threshold_keyshare.rs, crates/zk-prover/src/actors/commitment_consistency_checker.rs
Sets c5_pk_commitment: None when publishing decryption proof requests; extends cached verification data to include signed_payload; threads EventContext<Sequenced> through link evaluation; publishes SignedProofFailed event on commitment mismatch using stored payload metadata.
C1→C5 Link Import Refactoring
crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs
Replaces CircuitOutputLayout trait import with direct FIELD_BYTE_LEN constant import; updates extraction logic to use CircuitName::PkGeneration.output_layout() directly instead of trait method.
Import Cleanup
crates/aggregator/src/publickey_aggregator.rs
Removes unused ProofVerificationPassed import from e3_events prelude.

Sequence Diagram

sequenceDiagram
    participant Client as Proof Originator
    participant SV as ShareVerification<br/>Actor
    participant Cache as C4 Signal<br/>Cache
    participant CL as C4→C6<br/>Commitment Link
    participant PV as ProofVerification<br/>Actor
    participant CC as Commitment<br/>Consistency Checker
    participant EventBus as Event Bus

    Note over SV,PV: ThresholdDecryptionProofs Flow

    Client->>SV: ShareVerificationDispatched<br/>(with C4 & C6 proofs)
    
    SV->>SV: Identify VerificationKind::<br/>ThresholdDecryptionProofs
    
    SV->>PV: Request ZK verification<br/>for C4 proofs
    
    PV->>PV: Verify C4 proofs
    
    PV->>SV: ZkVerificationResponse<br/>(C4 success)
    
    SV->>Cache: Store C4 public_signals<br/>per party
    
    SV->>CL: Pre-dispatch gate:<br/>check C4→C6 consistency<br/>for each party
    
    alt Party Fails C4→C6 Check
        SV->>EventBus: SignedProofFailed<br/>(party added to pre_dishonest)
    else Party Passes
        SV->>PV: Request C6 ZK verification
        PV->>PV: Verify C6 proofs
        PV->>SV: ZkVerificationResponse<br/>(C6 success)
        SV->>EventBus: ProofVerificationPassed<br/>(with signed_payload)
    end
    
    SV->>Cache: Evict C4 signals<br/>after completion
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Suggested reviewers

  • hmzakhalid
  • 0xjei

Poem

🐰 Hops through circuits, new inputs to find,
C4 splits in two—a curious design!
C4a and C4b now dance with C6,
While payloads ride cached through verification's mix.
Commitments align, the links hold them tight, 🔗

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: connect c4 and c6' is a concise and specific reference to the main architectural change—establishing links/connections between C4 and C6 proof types. It is clearly and directly related to the primary objective of the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/c4-c6

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.

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

Caution

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

⚠️ Outside diff range comments (2)
crates/keyshare/src/threshold_keyshare.rs (1)

2135-2144: ⚠️ Potential issue | 🟠 Major

This still leaves the C5→C6 binding disabled.

ThresholdKeyshare is the main producer of ThresholdShareDecryptionProofRequest, but it always sends c5_pk_commitment: None. So the new field never carries the C5-certified commitment on the normal path, and C6 cannot actually be bound to what C5 attested. Please persist the C5 commitment when the aggregated key/proof arrives and thread it through here.

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

In `@crates/keyshare/src/threshold_keyshare.rs` around lines 2135 - 2144, The code
always sets c5_pk_commitment: None when building a
ThresholdShareDecryptionProofRequest in ThresholdKeyshare, so the C5→C6 binding
is never carried; persist the C5 commitment when the aggregated key/proof is
received (the place that processes the aggregated_pk_bytes / incoming aggregated
proof) and store it on the ThresholdKeyshare instance or state, then replace
c5_pk_commitment: None with that stored commitment when constructing
ThresholdShareDecryptionProofRequest (alongside aggregated_pk_bytes and
proof_aggregation_enabled) so the C5-certified commitment is threaded through to
C6.
crates/aggregator/src/publickey_aggregator.rs (1)

394-407: ⚠️ Potential issue | 🔴 Critical

Critical: keyshare_bytes and c1_signed_proofs are misaligned in state.

At line 397, keyshare_bytes: honest_keyshares stores the original insertion-order vector, but c1_signed_proofs (line 398) was re-aligned at lines 357-369 to match the OrderedSet-ordered keyshare_bytes (line 356).

This creates an index mismatch: when handle_c1_commitment_mismatch receives a mismatched index, it will look up the wrong keyshare or proof.

The PkAggregationProofPending event published at line 380 uses keyshare_bytes.clone() (OrderedSet order), so the prover operates on different indices than what's stored in state.

🐛 Proposed fix
         self.state.try_mutate(&ec, |_| {
             Ok(PublicKeyAggregatorState::GeneratingC5Proof {
                 public_key: pubkey.clone(),
-                keyshare_bytes: honest_keyshares,
+                keyshare_bytes: keyshare_bytes.clone(),
                 c1_signed_proofs,
                 nodes: honest_nodes_set,
🤖 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 394 - 407, The
state stores keyshare_bytes and c1_signed_proofs out of sync: when you re-order
keyshares to match OrderedSet earlier (the vector used for the
PkAggregationProofPending event and c1_signed_proofs), you must save that same
ordered vector into PublicKeyAggregatorState::GeneratingC5Proof so indices
align; update the construction so keyshare_bytes is the re-ordered/aligned
vector (the same variable used to build c1_signed_proofs and the
PkAggregationProofPending payload) and ensure c1_signed_proofs is created from
that same aligned vector (so handle_c1_commitment_mismatch looks up matching
entries).
🧹 Nitpick comments (3)
crates/aggregator/src/publickey_aggregator.rs (3)

872-879: Consider adding threshold check before re-aggregation.

The code only verifies remaining_count > 0 before re-aggregating. If remaining_count <= threshold_m, the subsequent C5 proof will fail. While the comment notes the circuit enforces this, adding an early check would avoid wasted computation and provide a clearer error message.

Note: threshold_m is not currently available in GeneratingC5Proof state, so this would require carrying it forward from VerifyingC1.

🤖 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 872 - 879, The
code only checks remaining_count == 0 before re-aggregation; add an explicit
threshold check so re-aggregation is skipped if too few honest shares remain:
propagate threshold_m from VerifyingC1 into the GeneratingC5Proof state (carry
it through the state transition that creates GeneratingC5Proof), then before
using remaining_keyshares compare remaining_count against threshold_m (require
remaining_count > threshold_m) and return an Err with a clear message like
"insufficient honest parties after C1 filtering: remaining_count X <=
threshold_m Y" to avoid wasted C5 computation and give a precise failure reason.

28-59: Verbose debug logging should use debug! or trace! level.

The tracing::info! calls on lines 36-45 and 48-52 emit potentially large hex dumps of public signals for every C1 proof processed. This is useful during development but may flood production logs.

Consider using debug! or trace! level, or gating behind a feature flag.

🔧 Suggested change
-            tracing::info!(
+            tracing::debug!(
                 "C1 proof[{}]: circuit={:?}, public_signals_len={}, signals_hex={}",
                 i,
                 proof.circuit,
                 proof.public_signals.len(),
                 proof.public_signals[..std::cmp::min(128, proof.public_signals.len())]
                     .iter()
                     .map(|b| format!("{:02x}", b))
                     .collect::<String>()
             );
             let commitment = proof.extract_output("pk_commitment");
             if let Some(ref c) = commitment {
-                tracing::info!(
+                tracing::debug!(
                     "C1 proof[{}]: extracted pk_commitment={}",
                     i,
                     c.iter().map(|b| format!("{:02x}", b)).collect::<String>()
                 );
🤖 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 28 - 59, The
current verbose tracing inside derive_c1_commitments uses tracing::info! for
large hex dumps and should be demoted to tracing::debug! or tracing::trace! (or
wrapped behind a feature flag) to avoid flooding production logs; update the two
tracing::info! calls and the tracing::warn!/info! that print extracted
pk_commitment to use debug!/trace! (or conditionally log when a debug feature or
env var is enabled) so the same diagnostic data is preserved for development but
not emitted at info level in production, locating the changes in the
derive_c1_commitments function around proof.public_signals logging and the
pk_commitment extraction log.

357-369: Keyshare-to-proof mapping assumes unique keyshares.

The HashMap keyed by ks.to_vec() will overwrite entries if two honest parties submitted identical keyshare bytes. While this would be a protocol violation (and OrderedSet already deduplicates keyshares), consider adding a debug assertion or log if duplicates are detected during the map build.

This is a defensive measure; the current logic is correct under normal operation.

🤖 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 357 - 369, The
code builds ks_to_proof from honest_keyshares and c1_signed_proofs using
ks.to_vec() as key which will silently overwrite if duplicate keyshare bytes
exist; add a defensive check while constructing ks_to_proof (e.g., iterate over
honest_keyshares.zip(c1_signed_proofs) and on each insert check the previous
value) and emit a debug_assert or a log/error if an insert would replace an
existing entry to catch duplicate keyshares; reference the honest_keyshares,
c1_signed_proofs, keyshare_bytes, and ks_to_proof identifiers when implementing
this check so the later map lookup logic remains unchanged.
🤖 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 859-867: The current use of OrderedSet (backed by BTreeSet)
reorders elements so filtering `nodes` and `keyshare_bytes` by the same
`mismatched_indices` misaligns entries; fix by preserving (node, keyshare)
pairings and filtering those pairs before creating any OrderedSet. Concretely,
change the logic around `nodes`, `keyshare_bytes`, `mismatched_indices` and
`honest_entries` to iterate `honest_entries` as (node, keyshare) tuples, filter
out entries whose index is in `mismatched_indices`, then collect the remaining
nodes and keyshares from the filtered pairs (or construct OrderedSet from the
paired sequence) so indices remain aligned; update code that references `nodes`
or `keyshare_bytes` to use the filtered paired results instead of independently
sorted OrderedSets.

In `@crates/events/src/enclave_event/compute_request/zk.rs`:
- Around line 64-65: The new required field c1_commitments in
PkAggregationProofRequest (which is nested in PkAggregationProofPending) changes
the on‑wire shape and will break mixed-version deserialization; make this a
versioned addition instead: add a version tag/enum for PkAggregationProofRequest
or wrap PkAggregationProofPending in a versioned envelope, keep c1_commitments
optional or behind a new version variant, and update
serialization/deserialization logic to accept old versions (treat missing
c1_commitments as empty/None) while emitting the new version when serializing;
ensure references to PkAggregationProofRequest, PkAggregationProofPending, and
c1_commitments are updated so older nodes can still decode requests.

In `@crates/multithread/src/multithread.rs`:
- Around line 348-398: Check and enforce that all three arrays have the expected
length before zipping: validate req.c1_commitments.len(),
req.keyshare_bytes.len() (or the actual keyshare field name), and expected.len()
(from PkAggInputs::compute) are equal to req.committee_h (or all equal to each
other) and return a Zk error if not; then replace the zip over
expected.iter().zip(req.c1_commitments.iter()) in the C1 commitment loop with an
index-based loop (e.g., for i in 0..expected.len()) that indexes into
expected[i], req.c1_commitments[i], and req.keyshare_bytes[i] so no element is
silently skipped and mismatched_indices (and the ComputeRequestError::new path)
correctly capture all discrepancies.

In `@crates/zk-prover/src/actors/share_verification.rs`:
- Around line 166-198: The cross-check loop marks some parties as failed
(pre_dishonest via check_c6_party_against_c4 and emit_signed_proof_failed) but
verify_proofs is still called with the original msg.share_proofs, allowing
already-failed parties to be dispatched to ZK; before calling verify_proofs for
VerificationKind::ThresholdDecryptionProofs, filter msg.share_proofs to exclude
any party whose sender_party_id is present in pre_dishonest (e.g. build a new
Vec filtered_share_proofs from msg.share_proofs where party.sender_party_id ∉
pre_dishonest) and pass that filtered_share_proofs (and the merged
pre_dishonest) into verify_proofs so only non-failed parties are validated and
sent to ZK (refer to check_c6_party_against_c4, emit_signed_proof_failed,
pre_dishonest, verify_proofs, and VerifyShareProofs).

---

Outside diff comments:
In `@crates/aggregator/src/publickey_aggregator.rs`:
- Around line 394-407: The state stores keyshare_bytes and c1_signed_proofs out
of sync: when you re-order keyshares to match OrderedSet earlier (the vector
used for the PkAggregationProofPending event and c1_signed_proofs), you must
save that same ordered vector into PublicKeyAggregatorState::GeneratingC5Proof
so indices align; update the construction so keyshare_bytes is the
re-ordered/aligned vector (the same variable used to build c1_signed_proofs and
the PkAggregationProofPending payload) and ensure c1_signed_proofs is created
from that same aligned vector (so handle_c1_commitment_mismatch looks up
matching entries).

In `@crates/keyshare/src/threshold_keyshare.rs`:
- Around line 2135-2144: The code always sets c5_pk_commitment: None when
building a ThresholdShareDecryptionProofRequest in ThresholdKeyshare, so the
C5→C6 binding is never carried; persist the C5 commitment when the aggregated
key/proof is received (the place that processes the aggregated_pk_bytes /
incoming aggregated proof) and store it on the ThresholdKeyshare instance or
state, then replace c5_pk_commitment: None with that stored commitment when
constructing ThresholdShareDecryptionProofRequest (alongside aggregated_pk_bytes
and proof_aggregation_enabled) so the C5-certified commitment is threaded
through to C6.

---

Nitpick comments:
In `@crates/aggregator/src/publickey_aggregator.rs`:
- Around line 872-879: The code only checks remaining_count == 0 before
re-aggregation; add an explicit threshold check so re-aggregation is skipped if
too few honest shares remain: propagate threshold_m from VerifyingC1 into the
GeneratingC5Proof state (carry it through the state transition that creates
GeneratingC5Proof), then before using remaining_keyshares compare
remaining_count against threshold_m (require remaining_count > threshold_m) and
return an Err with a clear message like "insufficient honest parties after C1
filtering: remaining_count X <= threshold_m Y" to avoid wasted C5 computation
and give a precise failure reason.
- Around line 28-59: The current verbose tracing inside derive_c1_commitments
uses tracing::info! for large hex dumps and should be demoted to tracing::debug!
or tracing::trace! (or wrapped behind a feature flag) to avoid flooding
production logs; update the two tracing::info! calls and the
tracing::warn!/info! that print extracted pk_commitment to use debug!/trace! (or
conditionally log when a debug feature or env var is enabled) so the same
diagnostic data is preserved for development but not emitted at info level in
production, locating the changes in the derive_c1_commitments function around
proof.public_signals logging and the pk_commitment extraction log.
- Around line 357-369: The code builds ks_to_proof from honest_keyshares and
c1_signed_proofs using ks.to_vec() as key which will silently overwrite if
duplicate keyshare bytes exist; add a defensive check while constructing
ks_to_proof (e.g., iterate over honest_keyshares.zip(c1_signed_proofs) and on
each insert check the previous value) and emit a debug_assert or a log/error if
an insert would replace an existing entry to catch duplicate keyshares;
reference the honest_keyshares, c1_signed_proofs, keyshare_bytes, and
ks_to_proof identifiers when implementing this check so the later map lookup
logic remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5c4c1c5d-3afc-411d-bd29-fb23b92f99ae

📥 Commits

Reviewing files that changed from the base of the PR and between 12e2752 and bb073ae.

📒 Files selected for processing (9)
  • crates/aggregator/src/publickey_aggregator.rs
  • crates/events/src/enclave_event/compute_request/zk.rs
  • crates/events/src/enclave_event/proof.rs
  • crates/keyshare/src/threshold_keyshare.rs
  • crates/multithread/src/multithread.rs
  • crates/zk-helpers/src/circuits/mod.rs
  • crates/zk-helpers/src/circuits/output_layout.rs
  • crates/zk-helpers/src/circuits/threshold/pk_generation/computation.rs
  • crates/zk-prover/src/actors/share_verification.rs

Comment thread crates/aggregator/src/publickey_aggregator.rs Outdated
Comment thread crates/events/src/enclave_event/compute_request/zk.rs Outdated
Comment thread crates/multithread/src/multithread.rs Outdated
Comment thread crates/zk-prover/src/actors/share_verification.rs
@ctrlc03 ctrlc03 changed the title feat: connect c4 and c6 feat: connect c4 and c6 [skip-line-limit] Mar 23, 2026
@vercel vercel Bot temporarily deployed to Preview – enclave-docs March 23, 2026 21:28 Inactive
@vercel vercel Bot temporarily deployed to Preview – crisp March 23, 2026 21:28 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: 4

♻️ Duplicate comments (2)
crates/multithread/src/multithread.rs (1)

348-361: ⚠️ Potential issue | 🔴 Critical

Validate keyshare_bytes before PkAggInputs::compute.

Only c1_commitments and expected are length-checked here. PkAggInputs::compute walks 0..committee_h and indexes pk0_shares, so a short or extra req.keyshare_bytes list still turns this path into a panic instead of a ZK InvalidParams error.

♻️ Proposed fix
     if req.c1_commitments.len() != req.committee_h {
         return Err(make_zk_error(
             &request,
             format!(
                 "c1_commitments length {} != committee_h {}",
                 req.c1_commitments.len(),
                 req.committee_h
             ),
         ));
     }
+    if req.keyshare_bytes.len() != req.committee_h {
+        return Err(make_zk_error(
+            &request,
+            format!(
+                "keyshare_bytes length {} != committee_h {}",
+                req.keyshare_bytes.len(),
+                req.committee_h
+            ),
+        ));
+    }

     let pk_agg_inputs = PkAggInputs::compute(req.params_preset.clone(), &circuit_data)
         .map_err(|e| make_zk_error(&request, format!("PkAggInputs::compute: {}", e)))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/multithread/src/multithread.rs` around lines 348 - 361, Validate
req.keyshare_bytes length against req.committee_h before calling
PkAggInputs::compute: check that req.keyshare_bytes.len() == req.committee_h and
if not return Err(make_zk_error(&request, format!("InvalidParams: keyshare_bytes
length {} != committee_h {}", req.keyshare_bytes.len(), req.committee_h))). This
prevents PkAggInputs::compute from indexing pk0_shares (and similar) and
panicking; ensure the validation occurs prior to invoking PkAggInputs::compute.
crates/aggregator/src/publickey_aggregator.rs (1)

353-380: ⚠️ Potential issue | 🔴 Critical

Persist C5 retry state in the same order you send to multithread.

PkAggregationProofPending.keyshare_bytes is built in OrderedSet order, but GeneratingC5Proof.keyshare_bytes stores honest_keyshares (submission order) and handle_c1_commitment_mismatch resolves returned indices against honest_party_ids (party-id order). If those orders differ, a mismatched_indices response removes/blames the wrong party and re-aggregates the wrong share set.

Also applies to: 405-410, 813-823

🤖 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 353 - 380, The
retry state is being persisted using submission/party-id order (honest_keyshares
/ honest_nodes) while the multithreaded C5 work and
PkAggregationProofPending.keyshare_bytes use OrderedSet ordering, causing
mismatched blame/aggregation when orders differ; fix by normalizing everything
to the OrderedSet order: build and persist GeneratingC5Proof.keyshare_bytes and
any retry state from the OrderedSet-derived keyshare_bytes (use the same
keyshare_bytes, ks_to_proof and ks_to_node alignment logic shown) and modify
handle_c1_commitment_mismatch to interpret incoming indices against the
OrderedSet order (honest_keyshares_set / honest_nodes_set) so that
c1_signed_proofs, nodes_aligned, and persisted keyshare_bytes all share the same
ordering.
🤖 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 891-898: After filtering out C1-mismatched parties
(remaining_keyshares), re-check the threshold using threshold_m: current code
only checks remaining_count == 0, which allows continuing with <= threshold_m
parties; change the check to ensure remaining_count > threshold_m (i.e., return
an error if remaining_count <= threshold_m) so we abort the retry path when
there are not enough honest parties to meet the decryption threshold; update the
error message in the branch that inspects remaining_keyshares to reference this
threshold and include context (e.g., mention
honest_keyshares/remaining_keyshares and threshold_m) so the failure is clear.
- Around line 28-59: derive_c1_commitments currently uses filter_map and drops
None results from proof.extract_output("pk_commitment"), shortening
c1_commitments and breaking the 1:1 contract with keyshare_bytes; change
derive_c1_commitments to preserve indices by returning Vec<Option<ArcBytes>>
(update its signature) and replace the filter_map with a plain map that returns
the extracted Option (still logging success/failure as before), and then update
any callers (e.g., handle_pk_aggregation_proof) to accept and handle the
Vec<Option<ArcBytes>> so missing pk_commitment values are detected and handled
rather than silently removed.

In `@crates/zk-prover/src/actors/share_verification.rs`:
- Around line 112-115: The c4_cache (HashMap<E3id, HashMap<u64, C4Commitments>>)
is never evicted, leaking per-E3 data for the lifetime of the process; add an
explicit eviction path that removes the c4_cache entry when an E3 completes or
fails. Specifically, in the actor handlers that finalize an E3 (e.g., the
ThresholdDecryptionProofs completion/error handler—look for
handle_threshold_decryption_proofs or similar) add code to call
c4_cache.remove(&e3id) (or clear the inner map) and also implement/handle an
E3RequestComplete message/hook (e.g., handle_e3_request_complete) to remove the
same entry; ensure the removal uses the E3id used to populate the cache and is
done in the same actor/context to avoid concurrency issues.

---

Duplicate comments:
In `@crates/aggregator/src/publickey_aggregator.rs`:
- Around line 353-380: The retry state is being persisted using
submission/party-id order (honest_keyshares / honest_nodes) while the
multithreaded C5 work and PkAggregationProofPending.keyshare_bytes use
OrderedSet ordering, causing mismatched blame/aggregation when orders differ;
fix by normalizing everything to the OrderedSet order: build and persist
GeneratingC5Proof.keyshare_bytes and any retry state from the OrderedSet-derived
keyshare_bytes (use the same keyshare_bytes, ks_to_proof and ks_to_node
alignment logic shown) and modify handle_c1_commitment_mismatch to interpret
incoming indices against the OrderedSet order (honest_keyshares_set /
honest_nodes_set) so that c1_signed_proofs, nodes_aligned, and persisted
keyshare_bytes all share the same ordering.

In `@crates/multithread/src/multithread.rs`:
- Around line 348-361: Validate req.keyshare_bytes length against
req.committee_h before calling PkAggInputs::compute: check that
req.keyshare_bytes.len() == req.committee_h and if not return
Err(make_zk_error(&request, format!("InvalidParams: keyshare_bytes length {} !=
committee_h {}", req.keyshare_bytes.len(), req.committee_h))). This prevents
PkAggInputs::compute from indexing pk0_shares (and similar) and panicking;
ensure the validation occurs prior to invoking PkAggInputs::compute.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d3748817-14fe-464f-8b79-31e629fae1cf

📥 Commits

Reviewing files that changed from the base of the PR and between bb073ae and 3671c3f.

📒 Files selected for processing (3)
  • crates/aggregator/src/publickey_aggregator.rs
  • crates/multithread/src/multithread.rs
  • crates/zk-prover/src/actors/share_verification.rs

Comment thread crates/aggregator/src/publickey_aggregator.rs Outdated
Comment thread crates/aggregator/src/publickey_aggregator.rs Outdated
Comment thread crates/zk-prover/src/actors/share_verification.rs Outdated
Comment thread crates/zk-prover/src/actors/share_verification.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/keyshare/src/threshold_keyshare.rs (1)

2146-2154: This still leaves the C5→C6 binding unset.

c5_pk_commitment is hard-coded to None here, so the new request shape never carries a C5 commitment at its only producer. Since the current threshold-share-decryption prover path also doesn't consume that field yet, this change does not actually bind C6 to the C5-certified aggregated key. Either thread the real commitment through here or mark the field as reserved/future work so the API does not imply that check exists today.

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

In `@crates/keyshare/src/threshold_keyshare.rs` around lines 2146 - 2154, The
request is setting ThresholdShareDecryptionProofRequest.c5_pk_commitment to None
which leaves the C5→C6 binding unimplemented; update the code to pass the actual
C5 commitment into the request (e.g., use the commitment available on the
decryption context such as decrypting.c5_pk_commitment or the corresponding
value on state) so the proof can assert the C5-certified aggregated key, or if
that commitment is intentionally unavailable yet, explicitly mark the field as
reserved/future work (add a clear TODO comment and rename/annotate the field
usage) so the API does not misleadingly imply the check exists. Ensure you
modify the construction of ThresholdShareDecryptionProofRequest to use the real
commitment symbol (c5_pk_commitment) or add the reserve annotation where the
request is created.
crates/zk-prover/src/actors/commitment_consistency_checker.rs (1)

182-204: Potential duplicate SignedProofFailed emissions for cross-party links.

When a single source is inconsistent with multiple targets, emit_fault(src, ec) is called once per mismatching target. This could flood downstream consumers with duplicate fault events for the same party/proof.

Consider tracking already-faulted sources and emitting only once per source:

♻️ Suggested deduplication
     fn check_cross_party_link(
         &self,
         link: &dyn CommitmentLink,
         new_proof_type: ProofType,
         ec: &e3_events::EventContext<e3_events::Sequenced>,
     ) {
         // ... existing code ...
 
+        let mut faulted_sources: HashSet<Address> = HashSet::new();
         // For each (source, target) pair, check consistency.
         for src in &sources {
             let source_values = link.extract_source_values(&src.public_signals);
             if source_values.is_empty() {
                 continue;
             }
             for tgt in &targets {
                 if !link.check_consistency(&source_values, &tgt.public_signals) {
                     warn!(/* ... */);
-                    self.emit_fault(src, ec);
+                    if faulted_sources.insert(src.address) {
+                        self.emit_fault(src, ec);
+                    }
                 }
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/src/actors/commitment_consistency_checker.rs` around lines
182 - 204, The loop in commitment_consistency_checker currently calls
self.emit_fault(src, ec) for every target mismatch, risking duplicate
SignedProofFailed events; modify the logic in the for src in &sources loop to
track which source party/proof has already had a fault emitted (e.g., a HashSet
keyed by src.party_id plus a proof identifier or src reference) and only call
self.emit_fault(src, ec) the first time a mismatch is detected for that source;
use the existing link.extract_source_values, link.check_consistency, and
self.emit_fault symbols to locate the code and ensure the dedup set is
checked/updated before emitting to prevent repeated emissions for the same
source across multiple target mismatches.
crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs (1)

40-42: Potential panic if extracted field has unexpected length.

copy_from_slice panics if bytes.len() != FIELD_BYTE_LEN. While the circuit layout should guarantee correct sizing, adding a length check would be more defensive:

🛡️ Proposed defensive check
     fn extract_source_values(&self, public_signals: &[u8]) -> Vec<FieldValue> {
         let layout = CircuitName::DkgShareDecryption.output_layout();
         let Some(bytes) = layout.extract_field(public_signals, "commitment") else {
             return vec![];
         };
+        if bytes.len() != FIELD_BYTE_LEN {
+            return vec![];
+        }
         let mut value = [0u8; FIELD_BYTE_LEN];
         value.copy_from_slice(bytes);
         vec![value]
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs` around lines 40 -
42, The current use of value.copy_from_slice(bytes) in the c4a_to_c6 conversion
can panic if bytes.len() != FIELD_BYTE_LEN; add a defensive length check before
copying (e.g., verify bytes.len() == FIELD_BYTE_LEN) and handle the mismatch
gracefully by returning an Err or a meaningful error variant from the
surrounding function (or mapping to a safe default), rather than letting
copy_from_slice panic; update the function that constructs vec![value] (the
block that allocates value and calls copy_from_slice) to perform the check and
propagate or log the error with context.
🤖 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/events/src/enclave_event/signed_proof.rs`:
- Around line 42-51: The enum ProofType was renumbered, which breaks consumers
that encode proof_type as u8 (e.g., ProofPayload::digest() and the slashing
writer in crates/evm/src/slashing_manager_sol_writer.rs); restore stable numeric
values by not shifting existing variants and instead append a new explicit value
for the C4b variant (keep C6ThresholdShareDecryption,
C7DecryptedSharesAggregation, C5PkAggregation pinned to their original numeric
values) and set explicit = N for each enum variant to prevent future drift;
update the enum in signed_proof.rs (e.g., add a new C4bESmShareDecryption with a
new unused numeric ID and ensure C5/C6/C7 retain their original numbers) and add
a regression test that asserts numeric values of ProofType variants and that
ProofPayload::digest() encodes proof_type as the expected u8 to prevent
accidental renumbering.

---

Nitpick comments:
In `@crates/keyshare/src/threshold_keyshare.rs`:
- Around line 2146-2154: The request is setting
ThresholdShareDecryptionProofRequest.c5_pk_commitment to None which leaves the
C5→C6 binding unimplemented; update the code to pass the actual C5 commitment
into the request (e.g., use the commitment available on the decryption context
such as decrypting.c5_pk_commitment or the corresponding value on state) so the
proof can assert the C5-certified aggregated key, or if that commitment is
intentionally unavailable yet, explicitly mark the field as reserved/future work
(add a clear TODO comment and rename/annotate the field usage) so the API does
not misleadingly imply the check exists. Ensure you modify the construction of
ThresholdShareDecryptionProofRequest to use the real commitment symbol
(c5_pk_commitment) or add the reserve annotation where the request is created.

In `@crates/zk-prover/src/actors/commitment_consistency_checker.rs`:
- Around line 182-204: The loop in commitment_consistency_checker currently
calls self.emit_fault(src, ec) for every target mismatch, risking duplicate
SignedProofFailed events; modify the logic in the for src in &sources loop to
track which source party/proof has already had a fault emitted (e.g., a HashSet
keyed by src.party_id plus a proof identifier or src reference) and only call
self.emit_fault(src, ec) the first time a mismatch is detected for that source;
use the existing link.extract_source_values, link.check_consistency, and
self.emit_fault symbols to locate the code and ensure the dedup set is
checked/updated before emitting to prevent repeated emissions for the same
source across multiple target mismatches.

In `@crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs`:
- Around line 40-42: The current use of value.copy_from_slice(bytes) in the
c4a_to_c6 conversion can panic if bytes.len() != FIELD_BYTE_LEN; add a defensive
length check before copying (e.g., verify bytes.len() == FIELD_BYTE_LEN) and
handle the mismatch gracefully by returning an Err or a meaningful error variant
from the surrounding function (or mapping to a safe default), rather than
letting copy_from_slice panic; update the function that constructs vec![value]
(the block that allocates value and calls copy_from_slice) to perform the check
and propagate or log the error with context.
🪄 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: 3ab30f1a-b78c-49c2-b752-e175f87a0a91

📥 Commits

Reviewing files that changed from the base of the PR and between 3671c3f and 9fa42fb.

📒 Files selected for processing (15)
  • crates/aggregator/src/publickey_aggregator.rs
  • crates/events/src/enclave_event/compute_request/zk.rs
  • crates/events/src/enclave_event/proof.rs
  • crates/events/src/enclave_event/proof_verification_passed.rs
  • crates/events/src/enclave_event/signed_proof.rs
  • crates/keyshare/src/threshold_keyshare.rs
  • crates/zk-helpers/src/circuits/output_layout.rs
  • crates/zk-prover/src/actors/commitment_consistency_checker.rs
  • crates/zk-prover/src/actors/commitment_links/c1_to_c5.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/actors/commitment_links/mod.rs
  • crates/zk-prover/src/actors/proof_request.rs
  • crates/zk-prover/src/actors/proof_verification.rs
  • crates/zk-prover/src/actors/share_verification.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/zk-prover/src/actors/commitment_links/mod.rs
  • crates/aggregator/src/publickey_aggregator.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/events/src/enclave_event/proof.rs

Comment thread crates/events/src/enclave_event/signed_proof.rs
@ctrlc03 ctrlc03 requested a review from hmzakhalid March 27, 2026 10:14
@ctrlc03 ctrlc03 closed this Mar 28, 2026
@ctrlc03 ctrlc03 deleted the feat/c4-c6 branch June 16, 2026 09:44
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.

Cross-circuit check: C4→C6 — verify C6 expected_sk/e_sm_commitment match C4 return values

1 participant