diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 22300ebc62..817e49ab93 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 C3 and C6 circuits (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..f5c2ce1090 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, + /// C5 — Public key aggregation proof (Proof 5). + C5PkAggregation = 8, /// C6 — Threshold share decryption proof (Proof 6). - C6ThresholdShareDecryption = 7, + C6ThresholdShareDecryption = 9, /// C7 — Decrypted shares aggregation proof (Proof 7). - C7DecryptedSharesAggregation = 8, - /// C5 — Public key aggregation proof (Proof 5). - C5PkAggregation = 9, + C7DecryptedSharesAggregation = 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/c1_to_c5.rs b/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs index db5519cdb0..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 @@ -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 false; + } + + // 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_inconsistent() { + let link = C1ToC5PkCommitmentLink; + // 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])); + } + + #[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..19c79667a0 --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs @@ -0,0 +1,114 @@ +// 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 false; + } + 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()); + // 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 new file mode 100644 index 0000000000..10a02c0540 --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c4b_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. + +//! 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 false; + } + 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()); + // 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/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..dd0ca42287 100644 --- a/crates/zk-prover/src/actors/node_proof_aggregator.rs +++ b/crates/zk-prover/src/actors/node_proof_aggregator.rs @@ -142,9 +142,13 @@ impl NodeProofAggregator { state.buffer.insert(msg.seq, msg.wrapped_proof); state.last_ec = ec; + let buffered = state.buffer.len(); + let folded = state + .total_expected + .saturating_sub(state.remaining.saturating_add(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",