From eb96f9d3a20b62996e60f698e0b82edd9a18cd55 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:26:40 +0000 Subject: [PATCH 1/5] feat: link c4 and c6 --- crates/events/src/enclave_event/proof.rs | 29 +- .../events/src/enclave_event/signed_proof.rs | 25 +- .../zk-helpers/src/circuits/output_layout.rs | 40 +++ .../src/actors/commitment_links/c0_to_c3.rs | 199 +++++++++++ .../src/actors/commitment_links/c1_to_c5.rs | 332 +++++++++--------- .../src/actors/commitment_links/c4a_to_c6.rs | 113 ++++++ .../src/actors/commitment_links/c4b_to_c6.rs | 112 ++++++ .../src/actors/commitment_links/mod.rs | 8 +- .../src/actors/node_proof_aggregator.rs | 6 +- crates/zk-prover/src/actors/proof_request.rs | 4 +- 10 files changed, 687 insertions(+), 181 deletions(-) create mode 100644 crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs create mode 100644 crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs create mode 100644 crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 22300ebc62..77135ca2b6 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -3,8 +3,9 @@ use derivative::Derivative; use e3_utils::utility_types::ArcBytes; use e3_zk_helpers::{ - CircuitOutputLayout, DKG_SHARE_DECRYPTION_OUTPUTS, PK_AGGREGATION_OUTPUTS, PK_BFV_OUTPUTS, - PK_GENERATION_OUTPUTS, SHARE_COMPUTATION_CHUNK_BATCH_OUTPUTS, SHARE_COMPUTATION_OUTPUTS, + CircuitInputLayout, CircuitOutputLayout, DKG_SHARE_DECRYPTION_OUTPUTS, PK_AGGREGATION_OUTPUTS, + PK_BFV_OUTPUTS, PK_GENERATION_OUTPUTS, SHARE_COMPUTATION_CHUNK_BATCH_OUTPUTS, + SHARE_COMPUTATION_OUTPUTS, SHARE_ENCRYPTION_INPUTS, THRESHOLD_SHARE_DECRYPTION_INPUTS, THRESHOLD_SHARE_DECRYPTION_OUTPUTS, }; use serde::{Deserialize, Serialize}; @@ -48,6 +49,17 @@ impl Proof { .extract_field(&self.public_signals, field_name) .map(ArcBytes::from_bytes) } + + /// Extract a named public input field from this proof's public signals. + /// + /// Public inputs sit at the **start** of `public_signals`, before any + /// return values. + pub fn extract_input(&self, field_name: &str) -> Option { + let layout = self.circuit.input_layout(); + layout + .extract_field(&self.public_signals, field_name) + .map(ArcBytes::from_bytes) + } } /// Circuit variants determine the hash oracle used for VK generation and proving. @@ -202,6 +214,19 @@ impl CircuitName { CircuitName::Fold => CircuitOutputLayout::None, } } + + /// Public input layout for this circuit (fields at the start of public_signals). + pub fn input_layout(&self) -> CircuitInputLayout { + match self { + CircuitName::ShareEncryption => CircuitInputLayout::Fixed { + fields: SHARE_ENCRYPTION_INPUTS, + }, + CircuitName::ThresholdShareDecryption => CircuitInputLayout::Fixed { + fields: THRESHOLD_SHARE_DECRYPTION_INPUTS, + }, + _ => CircuitInputLayout::None, + } + } } impl fmt::Display for CircuitName { diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index c7c95bac94..eb050d15dd 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -39,14 +39,16 @@ pub enum ProofType { C3aSkShareEncryption = 4, /// C3b — Smudging noise share encryption proof (Proof 3b). C3bESmShareEncryption = 5, - /// C4 — DKG share decryption proof (Proof 4). - C4DkgShareDecryption = 6, + /// C4a — SK share decryption proof (Proof 4a). + C4aSkShareDecryption = 6, + /// C4b — Smudging noise share decryption proof (Proof 4b). + C4bESmShareDecryption = 7, /// C6 — Threshold share decryption proof (Proof 6). - C6ThresholdShareDecryption = 7, + C6ThresholdShareDecryption = 8, /// C7 — Decrypted shares aggregation proof (Proof 7). - C7DecryptedSharesAggregation = 8, + C7DecryptedSharesAggregation = 9, /// C5 — Public key aggregation proof (Proof 5). - C5PkAggregation = 9, + C5PkAggregation = 10, } impl ProofType { @@ -59,7 +61,9 @@ impl ProofType { ProofType::C2bESmShareComputation => vec![CircuitName::ShareComputation], ProofType::C3aSkShareEncryption => vec![CircuitName::ShareEncryption], ProofType::C3bESmShareEncryption => vec![CircuitName::ShareEncryption], - ProofType::C4DkgShareDecryption => vec![CircuitName::DkgShareDecryption], + ProofType::C4aSkShareDecryption | ProofType::C4bESmShareDecryption => { + vec![CircuitName::DkgShareDecryption] + } ProofType::C6ThresholdShareDecryption => vec![CircuitName::ThresholdShareDecryption], ProofType::C7DecryptedSharesAggregation => { vec![CircuitName::DecryptedSharesAggregation] @@ -77,7 +81,8 @@ impl ProofType { | ProofType::C2bESmShareComputation | ProofType::C3aSkShareEncryption | ProofType::C3bESmShareEncryption - | ProofType::C4DkgShareDecryption => "E3_BAD_DKG_PROOF", + | ProofType::C4aSkShareDecryption + | ProofType::C4bESmShareDecryption => "E3_BAD_DKG_PROOF", ProofType::C6ThresholdShareDecryption => "E3_BAD_DECRYPTION_PROOF", ProofType::C7DecryptedSharesAggregation => "E3_BAD_AGGREGATION_PROOF", ProofType::C5PkAggregation => "E3_BAD_PK_AGGREGATION_PROOF", @@ -391,7 +396,11 @@ mod tests { vec![CircuitName::ShareEncryption] ); assert_eq!( - ProofType::C4DkgShareDecryption.circuit_names(), + ProofType::C4aSkShareDecryption.circuit_names(), + vec![CircuitName::DkgShareDecryption] + ); + assert_eq!( + ProofType::C4bESmShareDecryption.circuit_names(), vec![CircuitName::DkgShareDecryption] ); assert_eq!( diff --git a/crates/zk-helpers/src/circuits/output_layout.rs b/crates/zk-helpers/src/circuits/output_layout.rs index 58118dd4ee..c5822f6488 100644 --- a/crates/zk-helpers/src/circuits/output_layout.rs +++ b/crates/zk-helpers/src/circuits/output_layout.rs @@ -110,6 +110,46 @@ impl CircuitOutputLayout { } } +// ── Public input layout (fields at the HEAD of public_signals) ────────────── + +/// Describes the public input fields of a circuit. +/// Inputs sit at the **start** of `public_signals`, before any return values. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CircuitInputLayout { + /// Fixed number of named `Field`-sized inputs, known at compile time. + Fixed { fields: &'static [OutputField] }, + /// No known public input layout. + None, +} + +impl CircuitInputLayout { + /// Extract a named public input field from raw `public_signals` bytes. + /// Inputs sit at the **start** of `public_signals`. + pub fn extract_field<'a>(&self, public_signals: &'a [u8], name: &str) -> Option<&'a [u8]> { + let fields = match self { + CircuitInputLayout::Fixed { fields } => fields, + _ => return None, + }; + let idx = fields.iter().position(|f| f.name == name)?; + let offset = idx * FIELD_BYTE_LEN; + let end = offset + FIELD_BYTE_LEN; + if public_signals.len() < end { + return None; + } + Some(&public_signals[offset..end]) + } +} + +/// C3 — Share encryption public inputs. +pub const SHARE_ENCRYPTION_INPUTS: &[OutputField] = &[ + f("expected_pk_commitment"), + f("expected_message_commitment"), +]; + +/// C6 — Threshold share decryption public inputs. +pub const THRESHOLD_SHARE_DECRYPTION_INPUTS: &[OutputField] = + &[f("expected_sk_commitment"), f("expected_e_sm_commitment")]; + // ── Per-circuit output field constants ────────────────────────────────────── const fn f(name: &'static str) -> OutputField { diff --git a/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs b/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs new file mode 100644 index 0000000000..6c7120b8c4 --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! C0 (PkBfv) → C3 (ShareEncryption) pk_commitment consistency link. +//! +//! ## Circuit layouts +//! +//! **C0 (PkBfv)** outputs `(pk_commitment)`. +//! `pk_commitment` is extracted from the tail of `public_signals` via the +//! output layout. +//! +//! **C3 (ShareEncryption)** takes `(expected_pk_commitment, expected_message_commitment)` +//! as public inputs and produces no outputs (`CircuitOutputLayout::None`). +//! `expected_pk_commitment` is the first 32 bytes of `public_signals`. +//! +//! ## Scope +//! +//! Cross-party: C0 is from the verifying node (proving its own BFV public key), +//! while C3a/C3b are from other committee members (proving they encrypted +//! shares using that key). +//! +//! ## Check +//! +//! The `pk_commitment` output from C0 must equal the `expected_pk_commitment` +//! public input in C3. + +use super::{CommitmentLink, FieldValue, LinkScope}; +use e3_events::{CircuitName, ProofType}; +use e3_zk_helpers::FIELD_BYTE_LEN; + +/// C0 → C3a pk_commitment consistency link. +pub struct C0ToC3aPkCommitmentLink; + +/// C0 → C3b pk_commitment consistency link. +pub struct C0ToC3bPkCommitmentLink; + +fn extract_c0_pk_commitment(public_signals: &[u8]) -> Vec { + let layout = CircuitName::PkBfv.output_layout(); + let Some(bytes) = layout.extract_field(public_signals, "pk_commitment") else { + return vec![]; + }; + let mut value = [0u8; FIELD_BYTE_LEN]; + value.copy_from_slice(bytes); + vec![value] +} + +/// C3 has no outputs (`CircuitOutputLayout::None`), so all public_signals are +/// inputs. The layout is `[expected_pk_commitment, expected_message_commitment]`, +/// making `expected_pk_commitment` the first 32 bytes. +fn check_c3_pk_commitment(source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { + if source_values.is_empty() { + return true; + } + if target_public_signals.len() < FIELD_BYTE_LEN { + return false; + } + source_values[0] == target_public_signals[..FIELD_BYTE_LEN] +} + +impl CommitmentLink for C0ToC3aPkCommitmentLink { + fn name(&self) -> &'static str { + "C0→C3a pk_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C0PkBfv + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C3aSkShareEncryption + } + + fn scope(&self) -> LinkScope { + LinkScope::CrossParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_c0_pk_commitment(public_signals) + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + check_c3_pk_commitment(source_values, target_public_signals) + } +} + +impl CommitmentLink for C0ToC3bPkCommitmentLink { + fn name(&self) -> &'static str { + "C0→C3b pk_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C0PkBfv + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C3bESmShareEncryption + } + + fn scope(&self) -> LinkScope { + LinkScope::CrossParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_c0_pk_commitment(public_signals) + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + check_c3_pk_commitment(source_values, target_public_signals) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_field(val: u8) -> [u8; 32] { + let mut f = [0u8; 32]; + f[31] = val; + f + } + + /// Build C0 public_signals: [pub_input(s)...] [pk_commitment output]. + /// C0 has 1 output field; prepend some pub input bytes to simulate real signals. + fn make_c0_signals(pk_commitment: [u8; 32]) -> Vec { + // Simulate 1 pub input field + 1 output field = 64 bytes + let mut signals = vec![0xAAu8; 32]; + signals.extend_from_slice(&pk_commitment); + signals + } + + /// Build C3 public_signals: [expected_pk_commitment, expected_message_commitment]. + /// C3 has no outputs, only 2 input fields = 64 bytes. + fn make_c3_signals(expected_pk_commitment: [u8; 32]) -> Vec { + let mut signals = Vec::new(); + signals.extend_from_slice(&expected_pk_commitment); + signals.extend_from_slice(&make_field(0xFF)); // expected_message_commitment + signals + } + + #[test] + fn extract_pk_commitment_from_c0() { + let link = C0ToC3aPkCommitmentLink; + let pk = make_field(42); + let signals = make_c0_signals(pk); + let values = link.extract_source_values(&signals); + assert_eq!(values.len(), 1); + assert_eq!(values[0], pk); + } + + #[test] + fn consistency_passes_when_pk_matches() { + let pk = make_field(42); + let source_values = vec![pk]; + let c3_signals = make_c3_signals(pk); + + assert!(C0ToC3aPkCommitmentLink.check_consistency(&source_values, &c3_signals)); + assert!(C0ToC3bPkCommitmentLink.check_consistency(&source_values, &c3_signals)); + } + + #[test] + fn consistency_fails_when_pk_differs() { + let source_values = vec![make_field(42)]; + let c3_signals = make_c3_signals(make_field(99)); + + assert!(!C0ToC3aPkCommitmentLink.check_consistency(&source_values, &c3_signals)); + assert!(!C0ToC3bPkCommitmentLink.check_consistency(&source_values, &c3_signals)); + } + + #[test] + fn empty_source_is_vacuously_consistent() { + let c3_signals = make_c3_signals(make_field(1)); + assert!(C0ToC3aPkCommitmentLink.check_consistency(&[], &c3_signals)); + } + + #[test] + fn short_c0_signals_extract_empty() { + let link = C0ToC3aPkCommitmentLink; + // Too short for C0 output extraction + assert!(link.extract_source_values(&[0u8; 16]).is_empty()); + } + + #[test] + fn short_c3_signals_treated_as_inconsistent() { + let source_values = vec![make_field(1)]; + // Only 16 bytes — too short for expected_pk_commitment + assert!(!C0ToC3aPkCommitmentLink.check_consistency(&source_values, &[0u8; 16])); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs b/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs index db5519cdb0..79ba413a21 100644 --- a/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs +++ b/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs @@ -1,166 +1,166 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -//! C1 (PkGeneration) → C5 (PkAggregation) pk_commitment consistency link. -//! -//! ## Circuit layouts -//! -//! **C1 (PkGeneration)** outputs `(sk_commitment, pk_commitment, e_sm_commitment)`. -//! Public signals contain 3 fields (no public inputs); `pk_commitment` is at -//! field index 1 (byte offset 32..64). -//! -//! **C5 (PkAggregation)** takes `expected_threshold_pk_commitments: pub [Field; H]` -//! as public inputs and returns `pk_agg_commitment` as a single public output. -//! Public signals contain H+1 fields; the first H fields are per-party -//! `pk_commitment` values and the last field is the aggregated commitment. -//! -//! ## Check -//! -//! Each cipher node's C1 `pk_commitment` must appear somewhere in C5's -//! `expected_threshold_pk_commitments` array. - -use super::{CommitmentLink, FieldValue, LinkScope}; -use e3_events::{CircuitName, ProofType}; -use e3_zk_helpers::{CircuitOutputLayout, FIELD_BYTE_LEN}; - -/// C1 → C5 pk_commitment consistency link. -pub struct C1ToC5PkCommitmentLink; - -impl CommitmentLink for C1ToC5PkCommitmentLink { - fn name(&self) -> &'static str { - "C1→C5 pk_commitment" - } - - fn source_proof_type(&self) -> ProofType { - ProofType::C1PkGeneration - } - - fn target_proof_type(&self) -> ProofType { - ProofType::C5PkAggregation - } - - fn scope(&self) -> LinkScope { - LinkScope::CrossParty - } - - fn extract_source_values(&self, public_signals: &[u8]) -> Vec { - let layout = CircuitName::PkGeneration.output_layout(); - let Some(bytes) = layout.extract_field(public_signals, "pk_commitment") else { - return vec![]; - }; - let mut value = [0u8; FIELD_BYTE_LEN]; - value.copy_from_slice(bytes); - vec![value] - } - - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { - if source_values.is_empty() { - return true; - } - - // C5 public_signals layout: [pub inputs: pk_commitments[0..H]] [output: commitment] - // The output count comes from the circuit layout; everything before it is public inputs. - let output_count = CircuitName::PkAggregation - .output_layout() - .field_count() - .unwrap_or(1); - let total_fields = target_public_signals.len() / FIELD_BYTE_LEN; - if total_fields <= output_count { - return false; - } - let h = total_fields - output_count; - - let source_pk_commitment = &source_values[0]; - - // Check if the source pk_commitment appears in any of the H input fields - for i in 0..h { - let offset = i * FIELD_BYTE_LEN; - if target_public_signals[offset..offset + FIELD_BYTE_LEN] == *source_pk_commitment { - return true; - } - } - - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_field(val: u8) -> [u8; 32] { - let mut f = [0u8; 32]; - f[31] = val; - f - } - - #[test] - fn extract_pk_commitment_from_c1() { - let link = C1ToC5PkCommitmentLink; - let sk = make_field(1); - let pk = make_field(2); - let esm = make_field(3); - let mut signals = Vec::new(); - signals.extend_from_slice(&sk); - signals.extend_from_slice(&pk); - signals.extend_from_slice(&esm); - - let values = link.extract_source_values(&signals); - assert_eq!(values.len(), 1); - assert_eq!(values[0], pk); - } - - #[test] - fn consistency_passes_when_pk_present_in_c5() { - let link = C1ToC5PkCommitmentLink; - let pk = make_field(42); - let source_values = vec![pk]; - - // C5: [pk_comm_0, pk_comm_1(=42), pk_agg_commitment] - let mut c5_signals = Vec::new(); - c5_signals.extend_from_slice(&make_field(10)); - c5_signals.extend_from_slice(&pk); - c5_signals.extend_from_slice(&make_field(99)); - - assert!(link.check_consistency(&source_values, &c5_signals)); - } - - #[test] - fn consistency_fails_when_pk_missing_from_c5() { - let link = C1ToC5PkCommitmentLink; - let pk = make_field(42); - let source_values = vec![pk]; - - // C5: [pk_comm_0, pk_comm_1, pk_agg_commitment] — neither matches 42 - let mut c5_signals = Vec::new(); - c5_signals.extend_from_slice(&make_field(10)); - c5_signals.extend_from_slice(&make_field(20)); - c5_signals.extend_from_slice(&make_field(99)); - - assert!(!link.check_consistency(&source_values, &c5_signals)); - } - - #[test] - fn short_source_signals_treated_as_consistent() { - let link = C1ToC5PkCommitmentLink; - // Too short for C1 — extract returns empty, so vacuously consistent - assert!(link.extract_source_values(&[0u8; 60]).is_empty()); - assert!(link.check_consistency(&[], &[0u8; 31])); - } - - #[test] - fn short_target_signals_treated_as_inconsistent() { - let link = C1ToC5PkCommitmentLink; - // Source has valid data but target C5 is truncated — non-consistent - assert!(!link.check_consistency(&[make_field(1)], &[0u8; 31])); - // Only one field (< 2 required) — non-consistent - assert!(!link.check_consistency(&[make_field(1)], &make_field(1))); - } -} +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! C1 (PkGeneration) → C5 (PkAggregation) pk_commitment consistency link. +//! +//! ## Circuit layouts +//! +//! **C1 (PkGeneration)** outputs `(sk_commitment, pk_commitment, e_sm_commitment)`. +//! Public signals contain 3 fields (no public inputs); `pk_commitment` is at +//! field index 1 (byte offset 32..64). +//! +//! **C5 (PkAggregation)** takes `expected_threshold_pk_commitments: pub [Field; H]` +//! as public inputs and returns `pk_agg_commitment` as a single public output. +//! Public signals contain H+1 fields; the first H fields are per-party +//! `pk_commitment` values and the last field is the aggregated commitment. +//! +//! ## Check +//! +//! Each cipher node's C1 `pk_commitment` must appear somewhere in C5's +//! `expected_threshold_pk_commitments` array. + +use super::{CommitmentLink, FieldValue, LinkScope}; +use e3_events::{CircuitName, ProofType}; +use e3_zk_helpers::FIELD_BYTE_LEN; + +/// C1 → C5 pk_commitment consistency link. +pub struct C1ToC5PkCommitmentLink; + +impl CommitmentLink for C1ToC5PkCommitmentLink { + fn name(&self) -> &'static str { + "C1->C5 pk_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C1PkGeneration + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C5PkAggregation + } + + fn scope(&self) -> LinkScope { + LinkScope::CrossParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + let layout = CircuitName::PkGeneration.output_layout(); + let Some(bytes) = layout.extract_field(public_signals, "pk_commitment") else { + return vec![]; + }; + let mut value = [0u8; FIELD_BYTE_LEN]; + value.copy_from_slice(bytes); + vec![value] + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + if source_values.is_empty() { + return true; + } + + // C5 public_signals layout: [pub inputs: pk_commitments[0..H]] [output: commitment] + // The output count comes from the circuit layout; everything before it is public inputs. + let output_count = CircuitName::PkAggregation + .output_layout() + .field_count() + .unwrap_or(1); + let total_fields = target_public_signals.len() / FIELD_BYTE_LEN; + if total_fields <= output_count { + return false; + } + let h = total_fields - output_count; + + let source_pk_commitment = &source_values[0]; + + // Check if the source pk_commitment appears in any of the H input fields + for i in 0..h { + let offset = i * FIELD_BYTE_LEN; + if target_public_signals[offset..offset + FIELD_BYTE_LEN] == *source_pk_commitment { + return true; + } + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_field(val: u8) -> [u8; 32] { + let mut f = [0u8; 32]; + f[31] = val; + f + } + + #[test] + fn extract_pk_commitment_from_c1() { + let link = C1ToC5PkCommitmentLink; + let sk = make_field(1); + let pk = make_field(2); + let esm = make_field(3); + let mut signals = Vec::new(); + signals.extend_from_slice(&sk); + signals.extend_from_slice(&pk); + signals.extend_from_slice(&esm); + + let values = link.extract_source_values(&signals); + assert_eq!(values.len(), 1); + assert_eq!(values[0], pk); + } + + #[test] + fn consistency_passes_when_pk_present_in_c5() { + let link = C1ToC5PkCommitmentLink; + let pk = make_field(42); + let source_values = vec![pk]; + + // C5: [pk_comm_0, pk_comm_1(=42), pk_agg_commitment] + let mut c5_signals = Vec::new(); + c5_signals.extend_from_slice(&make_field(10)); + c5_signals.extend_from_slice(&pk); + c5_signals.extend_from_slice(&make_field(99)); + + assert!(link.check_consistency(&source_values, &c5_signals)); + } + + #[test] + fn consistency_fails_when_pk_missing_from_c5() { + let link = C1ToC5PkCommitmentLink; + let pk = make_field(42); + let source_values = vec![pk]; + + // C5: [pk_comm_0, pk_comm_1, pk_agg_commitment] — neither matches 42 + let mut c5_signals = Vec::new(); + c5_signals.extend_from_slice(&make_field(10)); + c5_signals.extend_from_slice(&make_field(20)); + c5_signals.extend_from_slice(&make_field(99)); + + assert!(!link.check_consistency(&source_values, &c5_signals)); + } + + #[test] + fn short_source_signals_treated_as_consistent() { + let link = C1ToC5PkCommitmentLink; + // Too short for C1 — extract returns empty, so vacuously consistent + assert!(link.extract_source_values(&[0u8; 60]).is_empty()); + assert!(link.check_consistency(&[], &[0u8; 31])); + } + + #[test] + fn short_target_signals_treated_as_inconsistent() { + let link = C1ToC5PkCommitmentLink; + // Source has valid data but target C5 is truncated — non-consistent + assert!(!link.check_consistency(&[make_field(1)], &[0u8; 31])); + // Only one field (< 2 required) — non-consistent + assert!(!link.check_consistency(&[make_field(1)], &make_field(1))); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs b/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs new file mode 100644 index 0000000000..09b4b00ddf --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! C4a (SK share decryption) → C6 (ThresholdShareDecryption) sk_commitment link. +//! +//! C4a outputs a single `commitment` field (the sk_commitment). +//! C6 takes `expected_sk_commitment` as a public input. + +use super::{CommitmentLink, FieldValue, LinkScope}; +use e3_events::{CircuitName, ProofType}; +use e3_zk_helpers::FIELD_BYTE_LEN; + +pub struct C4aToC6SkCommitmentLink; + +impl CommitmentLink for C4aToC6SkCommitmentLink { + fn name(&self) -> &'static str { + "C4a->C6 sk_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C4aSkShareDecryption + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C6ThresholdShareDecryption + } + + fn scope(&self) -> LinkScope { + LinkScope::SameParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + let layout = CircuitName::DkgShareDecryption.output_layout(); + let Some(bytes) = layout.extract_field(public_signals, "commitment") else { + return vec![]; + }; + let mut value = [0u8; FIELD_BYTE_LEN]; + value.copy_from_slice(bytes); + vec![value] + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + if source_values.is_empty() { + return true; + } + let layout = CircuitName::ThresholdShareDecryption.input_layout(); + layout + .extract_field(target_public_signals, "expected_sk_commitment") + .map_or(false, |extracted| extracted == source_values[0].as_slice()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_field(val: u8) -> [u8; 32] { + let mut f = [0u8; 32]; + f[31] = val; + f + } + + #[test] + fn extract_commitment_from_c4a() { + let link = C4aToC6SkCommitmentLink; + // C4 has no public inputs, just one output: commitment + let signals = make_field(42); + let values = link.extract_source_values(&signals); + assert_eq!(values.len(), 1); + assert_eq!(values[0], make_field(42)); + } + + #[test] + fn consistency_passes_when_sk_matches() { + let link = C4aToC6SkCommitmentLink; + let sk_commitment = make_field(42); + let source_values = vec![sk_commitment]; + + // C6 inputs: [expected_sk_commitment=42, expected_e_sm_commitment=99] + let mut c6_signals = Vec::new(); + c6_signals.extend_from_slice(&sk_commitment); + c6_signals.extend_from_slice(&make_field(99)); + + assert!(link.check_consistency(&source_values, &c6_signals)); + } + + #[test] + fn consistency_fails_when_sk_differs() { + let link = C4aToC6SkCommitmentLink; + let source_values = vec![make_field(42)]; + + // C6 inputs: [expected_sk_commitment=99, expected_e_sm_commitment=99] + let mut c6_signals = Vec::new(); + c6_signals.extend_from_slice(&make_field(99)); + c6_signals.extend_from_slice(&make_field(99)); + + assert!(!link.check_consistency(&source_values, &c6_signals)); + } + + #[test] + fn short_signals() { + let link = C4aToC6SkCommitmentLink; + assert!(link.extract_source_values(&[0u8; 10]).is_empty()); + assert!(link.check_consistency(&[], &[0u8; 64])); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs b/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs new file mode 100644 index 0000000000..b68e9d71b9 --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! C4b (ESM share decryption) → C6 (ThresholdShareDecryption) e_sm_commitment link. +//! +//! C4b outputs a single `commitment` field (the e_sm_commitment). +//! C6 takes `expected_e_sm_commitment` as a public input. + +use super::{CommitmentLink, FieldValue, LinkScope}; +use e3_events::{CircuitName, ProofType}; +use e3_zk_helpers::FIELD_BYTE_LEN; + +pub struct C4bToC6ESmCommitmentLink; + +impl CommitmentLink for C4bToC6ESmCommitmentLink { + fn name(&self) -> &'static str { + "C4b->C6 e_sm_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C4bESmShareDecryption + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C6ThresholdShareDecryption + } + + fn scope(&self) -> LinkScope { + LinkScope::SameParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + let layout = CircuitName::DkgShareDecryption.output_layout(); + let Some(bytes) = layout.extract_field(public_signals, "commitment") else { + return vec![]; + }; + let mut value = [0u8; FIELD_BYTE_LEN]; + value.copy_from_slice(bytes); + vec![value] + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + if source_values.is_empty() { + return true; + } + let layout = CircuitName::ThresholdShareDecryption.input_layout(); + layout + .extract_field(target_public_signals, "expected_e_sm_commitment") + .map_or(false, |extracted| extracted == source_values[0].as_slice()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_field(val: u8) -> [u8; 32] { + let mut f = [0u8; 32]; + f[31] = val; + f + } + + #[test] + fn extract_commitment_from_c4b() { + let link = C4bToC6ESmCommitmentLink; + let signals = make_field(77); + let values = link.extract_source_values(&signals); + assert_eq!(values.len(), 1); + assert_eq!(values[0], make_field(77)); + } + + #[test] + fn consistency_passes_when_esm_matches() { + let link = C4bToC6ESmCommitmentLink; + let esm_commitment = make_field(99); + let source_values = vec![esm_commitment]; + + // C6 inputs: [expected_sk_commitment=42, expected_e_sm_commitment=99] + let mut c6_signals = Vec::new(); + c6_signals.extend_from_slice(&make_field(42)); + c6_signals.extend_from_slice(&esm_commitment); + + assert!(link.check_consistency(&source_values, &c6_signals)); + } + + #[test] + fn consistency_fails_when_esm_differs() { + let link = C4bToC6ESmCommitmentLink; + let source_values = vec![make_field(99)]; + + // C6 inputs: [expected_sk_commitment=42, expected_e_sm_commitment=42] + let mut c6_signals = Vec::new(); + c6_signals.extend_from_slice(&make_field(42)); + c6_signals.extend_from_slice(&make_field(42)); + + assert!(!link.check_consistency(&source_values, &c6_signals)); + } + + #[test] + fn short_signals() { + let link = C4bToC6ESmCommitmentLink; + assert!(link.extract_source_values(&[0u8; 10]).is_empty()); + assert!(link.check_consistency(&[], &[0u8; 64])); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/mod.rs b/crates/zk-prover/src/actors/commitment_links/mod.rs index c368209c8f..5005dd6818 100644 --- a/crates/zk-prover/src/actors/commitment_links/mod.rs +++ b/crates/zk-prover/src/actors/commitment_links/mod.rs @@ -13,6 +13,8 @@ //! evaluates these links as verified proofs arrive. pub mod c1_to_c5; +pub mod c4a_to_c6; +pub mod c4b_to_c6; use e3_events::ProofType; @@ -58,5 +60,9 @@ pub trait CommitmentLink: Send + Sync { /// Returns the default set of commitment links to register. pub fn default_links() -> Vec> { - vec![Box::new(c1_to_c5::C1ToC5PkCommitmentLink)] + vec![ + Box::new(c1_to_c5::C1ToC5PkCommitmentLink), + Box::new(c4a_to_c6::C4aToC6SkCommitmentLink), + Box::new(c4b_to_c6::C4bToC6ESmCommitmentLink), + ] } diff --git a/crates/zk-prover/src/actors/node_proof_aggregator.rs b/crates/zk-prover/src/actors/node_proof_aggregator.rs index d77f575578..672b72be43 100644 --- a/crates/zk-prover/src/actors/node_proof_aggregator.rs +++ b/crates/zk-prover/src/actors/node_proof_aggregator.rs @@ -142,9 +142,11 @@ impl NodeProofAggregator { state.buffer.insert(msg.seq, msg.wrapped_proof); state.last_ec = ec; + let buffered = state.buffer.len(); + let folded = state.total_expected - state.remaining - buffered; info!( - "NodeProofAggregator: buffered seq={} for E3 {} (remaining={})", - msg.seq, e3_id, state.remaining + "NodeProofAggregator: buffered seq={} for E3 {} (buffered={}, folded={}, remaining={})", + msg.seq, e3_id, buffered, folded, state.remaining ); self.try_advance(&e3_id); diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 61fc4ac134..c2f4356e98 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -712,7 +712,7 @@ impl ProofRequestActor { // Sign C4a (SK decryption proof) let Some(signed_sk) = self.sign_proof( e3_id, - ProofType::C4DkgShareDecryption, + ProofType::C4aSkShareDecryption, pending.sk_proof.expect("checked in is_complete"), ) else { error!("Failed to sign C4a SK proof — DecryptionKeyShared will not be published"); @@ -727,7 +727,7 @@ impl ProofRequestActor { .get(&idx) .expect("checked in is_complete") .clone(); - let Some(signed) = self.sign_proof(e3_id, ProofType::C4DkgShareDecryption, proof) + let Some(signed) = self.sign_proof(e3_id, ProofType::C4bESmShareDecryption, proof) else { error!( "Failed to sign C4b ESM proof [{}] — DecryptionKeyShared will not be published", From 63a5b195259478c027d3f9fc3722c5fad1ed8354 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:34:57 +0000 Subject: [PATCH 2/5] chore: remove redundant file --- .../src/actors/commitment_links/c0_to_c3.rs | 199 ------------------ 1 file changed, 199 deletions(-) delete mode 100644 crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs diff --git a/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs b/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs deleted file mode 100644 index 6c7120b8c4..0000000000 --- a/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -//! C0 (PkBfv) → C3 (ShareEncryption) pk_commitment consistency link. -//! -//! ## Circuit layouts -//! -//! **C0 (PkBfv)** outputs `(pk_commitment)`. -//! `pk_commitment` is extracted from the tail of `public_signals` via the -//! output layout. -//! -//! **C3 (ShareEncryption)** takes `(expected_pk_commitment, expected_message_commitment)` -//! as public inputs and produces no outputs (`CircuitOutputLayout::None`). -//! `expected_pk_commitment` is the first 32 bytes of `public_signals`. -//! -//! ## Scope -//! -//! Cross-party: C0 is from the verifying node (proving its own BFV public key), -//! while C3a/C3b are from other committee members (proving they encrypted -//! shares using that key). -//! -//! ## Check -//! -//! The `pk_commitment` output from C0 must equal the `expected_pk_commitment` -//! public input in C3. - -use super::{CommitmentLink, FieldValue, LinkScope}; -use e3_events::{CircuitName, ProofType}; -use e3_zk_helpers::FIELD_BYTE_LEN; - -/// C0 → C3a pk_commitment consistency link. -pub struct C0ToC3aPkCommitmentLink; - -/// C0 → C3b pk_commitment consistency link. -pub struct C0ToC3bPkCommitmentLink; - -fn extract_c0_pk_commitment(public_signals: &[u8]) -> Vec { - let layout = CircuitName::PkBfv.output_layout(); - let Some(bytes) = layout.extract_field(public_signals, "pk_commitment") else { - return vec![]; - }; - let mut value = [0u8; FIELD_BYTE_LEN]; - value.copy_from_slice(bytes); - vec![value] -} - -/// C3 has no outputs (`CircuitOutputLayout::None`), so all public_signals are -/// inputs. The layout is `[expected_pk_commitment, expected_message_commitment]`, -/// making `expected_pk_commitment` the first 32 bytes. -fn check_c3_pk_commitment(source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { - if source_values.is_empty() { - return true; - } - if target_public_signals.len() < FIELD_BYTE_LEN { - return false; - } - source_values[0] == target_public_signals[..FIELD_BYTE_LEN] -} - -impl CommitmentLink for C0ToC3aPkCommitmentLink { - fn name(&self) -> &'static str { - "C0→C3a pk_commitment" - } - - fn source_proof_type(&self) -> ProofType { - ProofType::C0PkBfv - } - - fn target_proof_type(&self) -> ProofType { - ProofType::C3aSkShareEncryption - } - - fn scope(&self) -> LinkScope { - LinkScope::CrossParty - } - - fn extract_source_values(&self, public_signals: &[u8]) -> Vec { - extract_c0_pk_commitment(public_signals) - } - - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { - check_c3_pk_commitment(source_values, target_public_signals) - } -} - -impl CommitmentLink for C0ToC3bPkCommitmentLink { - fn name(&self) -> &'static str { - "C0→C3b pk_commitment" - } - - fn source_proof_type(&self) -> ProofType { - ProofType::C0PkBfv - } - - fn target_proof_type(&self) -> ProofType { - ProofType::C3bESmShareEncryption - } - - fn scope(&self) -> LinkScope { - LinkScope::CrossParty - } - - fn extract_source_values(&self, public_signals: &[u8]) -> Vec { - extract_c0_pk_commitment(public_signals) - } - - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { - check_c3_pk_commitment(source_values, target_public_signals) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_field(val: u8) -> [u8; 32] { - let mut f = [0u8; 32]; - f[31] = val; - f - } - - /// Build C0 public_signals: [pub_input(s)...] [pk_commitment output]. - /// C0 has 1 output field; prepend some pub input bytes to simulate real signals. - fn make_c0_signals(pk_commitment: [u8; 32]) -> Vec { - // Simulate 1 pub input field + 1 output field = 64 bytes - let mut signals = vec![0xAAu8; 32]; - signals.extend_from_slice(&pk_commitment); - signals - } - - /// Build C3 public_signals: [expected_pk_commitment, expected_message_commitment]. - /// C3 has no outputs, only 2 input fields = 64 bytes. - fn make_c3_signals(expected_pk_commitment: [u8; 32]) -> Vec { - let mut signals = Vec::new(); - signals.extend_from_slice(&expected_pk_commitment); - signals.extend_from_slice(&make_field(0xFF)); // expected_message_commitment - signals - } - - #[test] - fn extract_pk_commitment_from_c0() { - let link = C0ToC3aPkCommitmentLink; - let pk = make_field(42); - let signals = make_c0_signals(pk); - let values = link.extract_source_values(&signals); - assert_eq!(values.len(), 1); - assert_eq!(values[0], pk); - } - - #[test] - fn consistency_passes_when_pk_matches() { - let pk = make_field(42); - let source_values = vec![pk]; - let c3_signals = make_c3_signals(pk); - - assert!(C0ToC3aPkCommitmentLink.check_consistency(&source_values, &c3_signals)); - assert!(C0ToC3bPkCommitmentLink.check_consistency(&source_values, &c3_signals)); - } - - #[test] - fn consistency_fails_when_pk_differs() { - let source_values = vec![make_field(42)]; - let c3_signals = make_c3_signals(make_field(99)); - - assert!(!C0ToC3aPkCommitmentLink.check_consistency(&source_values, &c3_signals)); - assert!(!C0ToC3bPkCommitmentLink.check_consistency(&source_values, &c3_signals)); - } - - #[test] - fn empty_source_is_vacuously_consistent() { - let c3_signals = make_c3_signals(make_field(1)); - assert!(C0ToC3aPkCommitmentLink.check_consistency(&[], &c3_signals)); - } - - #[test] - fn short_c0_signals_extract_empty() { - let link = C0ToC3aPkCommitmentLink; - // Too short for C0 output extraction - assert!(link.extract_source_values(&[0u8; 16]).is_empty()); - } - - #[test] - fn short_c3_signals_treated_as_inconsistent() { - let source_values = vec![make_field(1)]; - // Only 16 bytes — too short for expected_pk_commitment - assert!(!C0ToC3aPkCommitmentLink.check_consistency(&source_values, &[0u8; 16])); - } -} From e57516d8a7e89884c3a1bb57eb271ded36660842 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:02:52 +0000 Subject: [PATCH 3/5] chore: pr comments --- crates/zk-prover/src/actors/node_proof_aggregator.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/zk-prover/src/actors/node_proof_aggregator.rs b/crates/zk-prover/src/actors/node_proof_aggregator.rs index 672b72be43..dd0ca42287 100644 --- a/crates/zk-prover/src/actors/node_proof_aggregator.rs +++ b/crates/zk-prover/src/actors/node_proof_aggregator.rs @@ -143,7 +143,9 @@ impl NodeProofAggregator { state.last_ec = ec; let buffered = state.buffer.len(); - let folded = state.total_expected - state.remaining - buffered; + let folded = state + .total_expected + .saturating_sub(state.remaining.saturating_add(buffered)); info!( "NodeProofAggregator: buffered seq={} for E3 {} (buffered={}, folded={}, remaining={})", msg.seq, e3_id, buffered, folded, state.remaining From 3069f196d7de056657d61506020c31309019e6ef Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:55:29 +0100 Subject: [PATCH 4/5] chore: pr comments --- crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs | 8 ++++---- crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs | 5 +++-- crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs b/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs index 79ba413a21..3039e9108a 100644 --- a/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs +++ b/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs @@ -62,7 +62,7 @@ impl CommitmentLink for C1ToC5PkCommitmentLink { target_public_signals: &[u8], ) -> bool { if source_values.is_empty() { - return true; + return false; } // C5 public_signals layout: [pub inputs: pk_commitments[0..H]] [output: commitment] @@ -148,11 +148,11 @@ mod tests { } #[test] - fn short_source_signals_treated_as_consistent() { + fn short_source_signals_treated_as_inconsistent() { let link = C1ToC5PkCommitmentLink; - // Too short for C1 — extract returns empty, so vacuously consistent + // Too short for C1 — extract returns empty, malformed source is a fault assert!(link.extract_source_values(&[0u8; 60]).is_empty()); - assert!(link.check_consistency(&[], &[0u8; 31])); + assert!(!link.check_consistency(&[], &[0u8; 31])); } #[test] diff --git a/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs b/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs index 09b4b00ddf..19c79667a0 100644 --- a/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs +++ b/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs @@ -48,7 +48,7 @@ impl CommitmentLink for C4aToC6SkCommitmentLink { target_public_signals: &[u8], ) -> bool { if source_values.is_empty() { - return true; + return false; } let layout = CircuitName::ThresholdShareDecryption.input_layout(); layout @@ -108,6 +108,7 @@ mod tests { fn short_signals() { let link = C4aToC6SkCommitmentLink; assert!(link.extract_source_values(&[0u8; 10]).is_empty()); - assert!(link.check_consistency(&[], &[0u8; 64])); + // Empty source values means malformed proof — should be inconsistent + assert!(!link.check_consistency(&[], &[0u8; 64])); } } diff --git a/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs b/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs index b68e9d71b9..10a02c0540 100644 --- a/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs +++ b/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs @@ -48,7 +48,7 @@ impl CommitmentLink for C4bToC6ESmCommitmentLink { target_public_signals: &[u8], ) -> bool { if source_values.is_empty() { - return true; + return false; } let layout = CircuitName::ThresholdShareDecryption.input_layout(); layout @@ -107,6 +107,7 @@ mod tests { fn short_signals() { let link = C4bToC6ESmCommitmentLink; assert!(link.extract_source_values(&[0u8; 10]).is_empty()); - assert!(link.check_consistency(&[], &[0u8; 64])); + // Empty source values means malformed proof — should be inconsistent + assert!(!link.check_consistency(&[], &[0u8; 64])); } } From 69670682db368735eae39167a1d37072a51d31a9 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:57:56 +0100 Subject: [PATCH 5/5] chore: pr comments --- crates/events/src/enclave_event/proof.rs | 2 +- crates/events/src/enclave_event/signed_proof.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 77135ca2b6..817e49ab93 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -215,7 +215,7 @@ impl CircuitName { } } - /// Public input layout for this circuit (fields at the start of public_signals). + /// Public input layout for C3 and C6 circuits (fields at the start of public_signals). pub fn input_layout(&self) -> CircuitInputLayout { match self { CircuitName::ShareEncryption => CircuitInputLayout::Fixed { diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index eb050d15dd..f5c2ce1090 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -43,12 +43,12 @@ pub enum ProofType { C4aSkShareDecryption = 6, /// C4b — Smudging noise share decryption proof (Proof 4b). C4bESmShareDecryption = 7, + /// C5 — Public key aggregation proof (Proof 5). + C5PkAggregation = 8, /// C6 — Threshold share decryption proof (Proof 6). - C6ThresholdShareDecryption = 8, + C6ThresholdShareDecryption = 9, /// C7 — Decrypted shares aggregation proof (Proof 7). - C7DecryptedSharesAggregation = 9, - /// C5 — Public key aggregation proof (Proof 5). - C5PkAggregation = 10, + C7DecryptedSharesAggregation = 10, } impl ProofType {