From 9ad844e9890e906857ec3c8c6ef06dbb5182753a Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:06:08 +0100 Subject: [PATCH 1/3] feat: link c0-c3 --- crates/events/src/enclave_event/proof.rs | 44 +++- .../zk-helpers/src/circuits/output_layout.rs | 155 ++++++++++--- .../src/actors/commitment_links/c0_to_c3.rs | 213 +++++++++++++++++ .../src/actors/commitment_links/c2_to_c3.rs | 215 ++++++++++++++++++ .../src/actors/commitment_links/mod.rs | 6 + 5 files changed, 596 insertions(+), 37 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/c2_to_c3.rs diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index bee8ce63e8..5bf0999eea 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -53,7 +53,8 @@ impl Proof { /// 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. + /// return values. The field name must match one declared in the circuit's + /// [`CircuitInputLayout`]. pub fn extract_input(&self, field_name: &str) -> Option { let layout = self.circuit.input_layout(); layout @@ -180,6 +181,8 @@ impl CircuitName { format!("recursive_aggregation/wrapper/{}", self.dir_path()) } + /// Public input layout for this circuit. + /// /// Public output (return value) layout for this circuit. pub fn output_layout(&self) -> CircuitOutputLayout { match self { @@ -344,4 +347,43 @@ mod tests { let proof = make_proof(CircuitName::PkGeneration, &[]); assert!(proof.extract_output("pk_commitment").is_none()); } + + #[test] + fn input_layout_share_encryption() { + let layout = CircuitName::ShareEncryption.input_layout(); + assert_eq!(layout.field_count(), Some(2)); + } + + #[test] + fn input_layout_other_circuits_none() { + assert_eq!(CircuitName::PkBfv.input_layout().field_count(), Some(0)); + assert_eq!( + CircuitName::PkGeneration.input_layout().field_count(), + Some(0) + ); + } + + #[test] + fn extract_input_from_share_encryption() { + // C3: 2 pub inputs at HEAD + rest of signals + let mut signals = vec![0u8; 96]; + signals[0..32].copy_from_slice(&[0xAA; 32]); // expected_pk_commitment + signals[32..64].copy_from_slice(&[0xBB; 32]); // expected_message_commitment + + let proof = make_proof(CircuitName::ShareEncryption, &signals); + assert_eq!( + &*proof.extract_input("expected_pk_commitment").unwrap(), + &[0xAA; 32] + ); + assert_eq!( + &*proof.extract_input("expected_message_commitment").unwrap(), + &[0xBB; 32] + ); + } + + #[test] + fn extract_input_from_non_input_circuit() { + let proof = make_proof(CircuitName::PkBfv, &[0u8; 32]); + assert!(proof.extract_input("anything").is_none()); + } } diff --git a/crates/zk-helpers/src/circuits/output_layout.rs b/crates/zk-helpers/src/circuits/output_layout.rs index a9dd797a16..5e4c31d843 100644 --- a/crates/zk-helpers/src/circuits/output_layout.rs +++ b/crates/zk-helpers/src/circuits/output_layout.rs @@ -110,42 +110,6 @@ 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")]; @@ -178,6 +142,61 @@ pub const PK_AGGREGATION_OUTPUTS: &[OutputField] = &[f("commitment")]; /// C6 — Threshold share decryption (prefix commitment to `d`, per CRT limb). pub const THRESHOLD_SHARE_DECRYPTION_OUTPUTS: &[OutputField] = &[f("d_commitment")]; +// ── Per-circuit input field constants ─────────────────────────────────────── + +/// C3 — Share encryption public inputs (at HEAD of `public_signals`). +pub const SHARE_ENCRYPTION_INPUTS: &[OutputField] = &[ + f("expected_pk_commitment"), + f("expected_message_commitment"), +]; + +/// Describes the public input layout of a circuit. +/// +/// Unlike [`CircuitOutputLayout`] which indexes from the TAIL of +/// `public_signals`, input fields sit at the HEAD. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CircuitInputLayout { + /// Fixed number of `Field`-sized inputs, names known at compile time. + Fixed { fields: &'static [OutputField] }, + /// The circuit has no named public inputs (or they are not tracked). + None, +} + +impl CircuitInputLayout { + /// Number of fixed input fields, or `None` for void layouts. + pub fn field_count(&self) -> Option { + match self { + CircuitInputLayout::Fixed { fields } => Some(fields.len()), + CircuitInputLayout::None => Some(0), + } + } + + /// Look up a field index by name. + pub fn field_index(&self, name: &str) -> Option { + match self { + CircuitInputLayout::Fixed { fields } => fields.iter().position(|f| f.name == name), + _ => None, + } + } + + /// Extract a named input field from raw `public_signals` bytes. + /// + /// Input fields sit at the **beginning** of `public_signals`. + /// This method indexes from the head (offset = idx * FIELD_BYTE_LEN). + 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; + if public_signals.len() < offset + FIELD_BYTE_LEN { + return None; + } + Some(&public_signals[offset..offset + FIELD_BYTE_LEN]) + } +} + #[cfg(test)] mod tests { use super::*; @@ -332,6 +351,31 @@ mod tests { ); } + // ── CircuitInputLayout tests ──────────────────────────────────────── + + #[test] + fn extract_input_field_from_head() { + let layout = CircuitInputLayout::Fixed { + fields: SHARE_ENCRYPTION_INPUTS, + }; + let mut signals = vec![0u8; 128]; + signals[0..32].copy_from_slice(&[0xAA; 32]); + signals[32..64].copy_from_slice(&[0xBB; 32]); + + assert_eq!( + layout + .extract_field(&signals, "expected_pk_commitment") + .unwrap(), + &[0xAA; 32] + ); + assert_eq!( + layout + .extract_field(&signals, "expected_message_commitment") + .unwrap(), + &[0xBB; 32] + ); + } + #[test] fn extract_c6_public_inputs_via_input_layout() { let layout = CircuitInputLayout::Fixed { @@ -366,6 +410,45 @@ mod tests { .is_none()); } + #[test] + fn input_layout_nonexistent_field_returns_none() { + let layout = CircuitInputLayout::Fixed { + fields: SHARE_ENCRYPTION_INPUTS, + }; + let signals = vec![0u8; 64]; + assert!(layout.extract_field(&signals, "nonexistent").is_none()); + } + + #[test] + fn input_layout_none_returns_none() { + let layout = CircuitInputLayout::None; + let signals = vec![0u8; 64]; + assert!(layout.extract_field(&signals, "anything").is_none()); + } + + #[test] + fn input_signals_too_short_returns_none() { + let layout = CircuitInputLayout::Fixed { + fields: SHARE_ENCRYPTION_INPUTS, + }; + let signals = vec![0u8; 32]; + assert!(layout + .extract_field(&signals, "expected_message_commitment") + .is_none()); + } + + #[test] + fn input_field_count() { + assert_eq!( + CircuitInputLayout::Fixed { + fields: SHARE_ENCRYPTION_INPUTS + } + .field_count(), + Some(2) + ); + assert_eq!(CircuitInputLayout::None.field_count(), Some(0)); + } + /// C7 (`DecryptedSharesAggregation`) has no `-> pub` return values; metadata uses `None`. #[test] fn c7_void_output_extract_field_returns_none() { 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..fcafbede2b --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs @@ -0,0 +1,213 @@ +// 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) -> C3a/C3b (ShareEncryption) pk_commitment consistency links. +//! +//! ## Circuit layouts +//! +//! **C0 (PkBfv)** outputs `(pk_commitment)`. Public signals contain the +//! output at the TAIL. +//! +//! **C3 (ShareEncryption)** takes `expected_pk_commitment` and +//! `expected_message_commitment` as public inputs at the HEAD of +//! `public_signals`. +//! +//! ## Check +//! +//! C0's `pk_commitment` output must equal C3's `expected_pk_commitment` input. + +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; + +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 + } + + /// Cross-party: node i's C3 encrypts a share under node j's individual + /// public key, so C0 (source) comes from the recipient while C3 (target) + /// comes from the sender. + fn scope(&self) -> LinkScope { + LinkScope::CrossParty + } + + fn extract_source_values(&self, 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] + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + check_pk_commitment_consistency(source_values, target_public_signals) + } +} + +/// C0 -> C3b pk_commitment consistency link. +pub struct C0ToC3bPkCommitmentLink; + +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 + } + + /// Cross-party: same reasoning as C0→C3a — recipient's C0 pk vs sender's C3. + fn scope(&self) -> LinkScope { + LinkScope::CrossParty + } + + fn extract_source_values(&self, 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] + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + check_pk_commitment_consistency(source_values, target_public_signals) + } +} + +/// Shared check: source `pk_commitment` must equal C3's `expected_pk_commitment` +/// input at the HEAD of target public signals. +fn check_pk_commitment_consistency( + source_values: &[FieldValue], + target_public_signals: &[u8], +) -> bool { + if source_values.is_empty() { + return false; + } + let layout = CircuitName::ShareEncryption.input_layout(); + let Some(target_bytes) = layout.extract_field(target_public_signals, "expected_pk_commitment") + else { + return false; + }; + target_bytes == source_values[0] +} + +#[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: just 1 output (pk_commitment). + fn c0_signals(pk_commitment: [u8; 32]) -> Vec { + pk_commitment.to_vec() + } + + /// Build C3 public signals: 2 inputs at HEAD + optional extra bytes. + fn c3_signals(expected_pk: [u8; 32], expected_msg: [u8; 32]) -> Vec { + let mut signals = Vec::new(); + signals.extend_from_slice(&expected_pk); + signals.extend_from_slice(&expected_msg); + signals + } + + #[test] + fn extract_pk_commitment_from_c0() { + let link = C0ToC3aPkCommitmentLink; + let pk = make_field(42); + let signals = 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_c3a() { + let link = C0ToC3aPkCommitmentLink; + let pk = make_field(42); + let source_values = vec![pk]; + let target = c3_signals(pk, make_field(99)); + assert!(link.check_consistency(&source_values, &target)); + } + + #[test] + fn consistency_fails_when_pk_mismatches_c3a() { + let link = C0ToC3aPkCommitmentLink; + let pk = make_field(42); + let source_values = vec![pk]; + let target = c3_signals(make_field(99), make_field(99)); + assert!(!link.check_consistency(&source_values, &target)); + } + + #[test] + fn consistency_passes_when_pk_matches_c3b() { + let link = C0ToC3bPkCommitmentLink; + let pk = make_field(7); + let source_values = vec![pk]; + let target = c3_signals(pk, make_field(55)); + assert!(link.check_consistency(&source_values, &target)); + } + + #[test] + fn consistency_fails_when_pk_mismatches_c3b() { + let link = C0ToC3bPkCommitmentLink; + let pk = make_field(7); + let source_values = vec![pk]; + let target = c3_signals(make_field(8), make_field(55)); + assert!(!link.check_consistency(&source_values, &target)); + } + + #[test] + fn empty_source_is_inconsistent() { + let link = C0ToC3aPkCommitmentLink; + // Empty source values means malformed proof — should be inconsistent + assert!(!link.check_consistency(&[], &[0u8; 64])); + } + + #[test] + fn short_target_signals_is_inconsistent() { + let link = C0ToC3aPkCommitmentLink; + // Target too short for 2 input fields + assert!(!link.check_consistency(&[make_field(1)], &[0u8; 31])); + } + + #[test] + fn short_source_signals_returns_empty() { + let link = C0ToC3aPkCommitmentLink; + assert!(link.extract_source_values(&[0u8; 16]).is_empty()); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs b/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs new file mode 100644 index 0000000000..c637196f12 --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs @@ -0,0 +1,215 @@ +// 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. + +//! C2a/C2b (ShareComputation) -> C3a/C3b (ShareEncryption) message commitment +//! consistency links. +//! +//! ## Circuit layouts +//! +//! **C2 (ShareComputation)** outputs `(key_hash, commitment)`. The +//! `commitment` field sits at the TAIL of `public_signals`. +//! +//! **C3 (ShareEncryption)** takes `expected_pk_commitment` and +//! `expected_message_commitment` as public inputs at the HEAD of +//! `public_signals`. +//! +//! ## Check +//! +//! C2's `commitment` output must equal C3's `expected_message_commitment` +//! input. + +use super::{CommitmentLink, FieldValue, LinkScope}; +use e3_events::{CircuitName, ProofType}; +use e3_zk_helpers::FIELD_BYTE_LEN; + +/// C2a -> C3a message commitment consistency link. +pub struct C2aToC3aMessageCommitmentLink; + +impl CommitmentLink for C2aToC3aMessageCommitmentLink { + fn name(&self) -> &'static str { + "C2a->C3a message_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C2aSkShareComputation + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C3aSkShareEncryption + } + + fn scope(&self) -> LinkScope { + LinkScope::SameParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_commitment(public_signals) + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + check_message_commitment_consistency(source_values, target_public_signals) + } +} + +/// C2b -> C3b message commitment consistency link. +pub struct C2bToC3bMessageCommitmentLink; + +impl CommitmentLink for C2bToC3bMessageCommitmentLink { + fn name(&self) -> &'static str { + "C2b->C3b message_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C2bESmShareComputation + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C3bESmShareEncryption + } + + fn scope(&self) -> LinkScope { + LinkScope::SameParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_commitment(public_signals) + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + ) -> bool { + check_message_commitment_consistency(source_values, target_public_signals) + } +} + +/// Extract the `commitment` output from C2 (ShareComputation) public signals. +fn extract_commitment(public_signals: &[u8]) -> Vec { + let layout = CircuitName::ShareComputation.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] +} + +/// Shared check: source `commitment` must equal C3's +/// `expected_message_commitment` input at the HEAD of target public signals. +fn check_message_commitment_consistency( + source_values: &[FieldValue], + target_public_signals: &[u8], +) -> bool { + if source_values.is_empty() { + return false; + } + let layout = CircuitName::ShareEncryption.input_layout(); + let Some(target_bytes) = + layout.extract_field(target_public_signals, "expected_message_commitment") + else { + return false; + }; + target_bytes == source_values[0] +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_field(val: u8) -> [u8; 32] { + let mut f = [0u8; 32]; + f[31] = val; + f + } + + /// Build C2 public signals: 1 pub input + 2 outputs (key_hash, commitment). + /// Simulate 1 pub input field → total 96 bytes. + fn c2_signals(key_hash: [u8; 32], commitment: [u8; 32]) -> Vec { + let mut signals = vec![0xAA; 32]; // 1 pub input + signals.extend_from_slice(&key_hash); + signals.extend_from_slice(&commitment); + signals + } + + /// Build C3 public signals: 2 inputs at HEAD. + fn c3_signals(expected_pk: [u8; 32], expected_msg: [u8; 32]) -> Vec { + let mut signals = Vec::new(); + signals.extend_from_slice(&expected_pk); + signals.extend_from_slice(&expected_msg); + signals + } + + #[test] + fn extract_commitment_from_c2() { + let link = C2aToC3aMessageCommitmentLink; + let commitment = make_field(42); + let signals = c2_signals(make_field(10), commitment); + let values = link.extract_source_values(&signals); + assert_eq!(values.len(), 1); + assert_eq!(values[0], commitment); + } + + #[test] + fn consistency_passes_when_commitment_matches_c3a() { + let link = C2aToC3aMessageCommitmentLink; + let commitment = make_field(42); + let source_values = vec![commitment]; + let target = c3_signals(make_field(99), commitment); + assert!(link.check_consistency(&source_values, &target)); + } + + #[test] + fn consistency_fails_when_commitment_mismatches_c3a() { + let link = C2aToC3aMessageCommitmentLink; + let commitment = make_field(42); + let source_values = vec![commitment]; + let target = c3_signals(make_field(99), make_field(55)); + assert!(!link.check_consistency(&source_values, &target)); + } + + #[test] + fn consistency_passes_when_commitment_matches_c3b() { + let link = C2bToC3bMessageCommitmentLink; + let commitment = make_field(7); + let source_values = vec![commitment]; + let target = c3_signals(make_field(99), commitment); + assert!(link.check_consistency(&source_values, &target)); + } + + #[test] + fn consistency_fails_when_commitment_mismatches_c3b() { + let link = C2bToC3bMessageCommitmentLink; + let commitment = make_field(7); + let source_values = vec![commitment]; + let target = c3_signals(make_field(99), make_field(8)); + assert!(!link.check_consistency(&source_values, &target)); + } + + #[test] + fn empty_source_is_inconsistent() { + let link = C2aToC3aMessageCommitmentLink; + // Empty source values means malformed proof — should be inconsistent + assert!(!link.check_consistency(&[], &[0u8; 64])); + } + + #[test] + fn short_target_signals_is_inconsistent() { + let link = C2aToC3aMessageCommitmentLink; + assert!(!link.check_consistency(&[make_field(1)], &[0u8; 31])); + } + + #[test] + fn short_source_signals_returns_empty() { + let link = C2aToC3aMessageCommitmentLink; + // C2 needs at least 64 bytes for 2 outputs; 32 is too short + assert!(link.extract_source_values(&[0u8; 32]).is_empty()); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/mod.rs b/crates/zk-prover/src/actors/commitment_links/mod.rs index 11487bc20e..16f007bf87 100644 --- a/crates/zk-prover/src/actors/commitment_links/mod.rs +++ b/crates/zk-prover/src/actors/commitment_links/mod.rs @@ -12,7 +12,9 @@ //! [`CommitmentConsistencyChecker`](super::commitment_consistency_checker::CommitmentConsistencyChecker) //! evaluates these links as verified proofs arrive. +pub mod c0_to_c3; pub mod c1_to_c5; +pub mod c2_to_c3; pub mod c4a_to_c6; pub mod c4b_to_c6; pub mod c6_to_c7; @@ -62,7 +64,11 @@ pub trait CommitmentLink: Send + Sync { /// Returns the default set of commitment links to register. pub fn default_links() -> Vec> { vec![ + Box::new(c0_to_c3::C0ToC3aPkCommitmentLink), + Box::new(c0_to_c3::C0ToC3bPkCommitmentLink), Box::new(c1_to_c5::C1ToC5PkCommitmentLink), + Box::new(c2_to_c3::C2aToC3aMessageCommitmentLink), + Box::new(c2_to_c3::C2bToC3bMessageCommitmentLink), Box::new(c4a_to_c6::C4aToC6SkCommitmentLink), Box::new(c4b_to_c6::C4bToC6ESmCommitmentLink), Box::new(c6_to_c7::C6ToC7DCommitmentLink), From 1e2040e907c790b9191a5ae924ce215c5100d8f2 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:18:56 +0100 Subject: [PATCH 2/3] refactor: change link direction --- .../actors/commitment_consistency_checker.rs | 153 +++++++++---- .../src/actors/commitment_links/c0_to_c3.rs | 157 ++++++------- .../src/actors/commitment_links/c2_to_c3.rs | 215 ------------------ .../src/actors/commitment_links/mod.rs | 20 +- crates/zk-prover/src/actors/proof_request.rs | 37 ++- 5 files changed, 232 insertions(+), 350 deletions(-) delete mode 100644 crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs diff --git a/crates/zk-prover/src/actors/commitment_consistency_checker.rs b/crates/zk-prover/src/actors/commitment_consistency_checker.rs index e86f601e37..bd6a079a5d 100644 --- a/crates/zk-prover/src/actors/commitment_consistency_checker.rs +++ b/crates/zk-prover/src/actors/commitment_consistency_checker.rs @@ -66,7 +66,8 @@ pub struct CommitmentConsistencyChecker { e3_id: E3id, links: Vec>, /// Verified proof outputs: `(address, proof_type) → data`. - verified: HashMap<(Address, ProofType), VerifiedProofData>, + /// Multiple proofs per key are supported (e.g. N-1 C3a proofs per sender). + verified: HashMap<(Address, ProofType), Vec>, } impl CommitmentConsistencyChecker { @@ -97,50 +98,118 @@ impl CommitmentConsistencyChecker { let tgt_type = link.target_proof_type(); match link.scope() { - LinkScope::SameParty => self - .verified - .keys() - .filter(|(_, pt)| *pt == src_type) - .filter_map(|(addr, _)| { - let src = self.verified.get(&(*addr, src_type))?; - let tgt = self.verified.get(&(*addr, tgt_type))?; - let vals = link.extract_source_values(&src.public_signals); - (!link.check_consistency(&vals, &tgt.public_signals)).then(|| Mismatch { - party_id: src.party_id, - address: *addr, - proof_type: src_type, - data_hash: src.data_hash, - }) - }) - .collect(), + // Same address: each source entry must be consistent with each + // target entry from the same address. + LinkScope::SameParty => { + let mut mismatches = Vec::new(); + for ((addr, pt), srcs) in &self.verified { + if *pt != src_type { + continue; + } + let Some(tgts) = self.verified.get(&(*addr, tgt_type)) else { + continue; + }; + for src in srcs { + let vals = link.extract_source_values(&src.public_signals); + for tgt in tgts { + if !link.check_consistency(&vals, &tgt.public_signals) { + mismatches.push(Mismatch { + party_id: src.party_id, + address: *addr, + proof_type: src_type, + data_hash: src.data_hash, + }); + break; // one mismatch per source entry is enough + } + } + } + } + mismatches + } + // Cross-party: each source's extracted value must appear in at + // least one target's public signals. Fault the source if no match. + // If no targets are cached yet, skip — the check will run again + // when a target arrives. LinkScope::CrossParty => { - let targets: Vec<_> = self + let all_targets: Vec<&VerifiedProofData> = self .verified .iter() .filter(|((_, pt), _)| *pt == tgt_type) - .map(|(_, v)| v) + .flat_map(|(_, entries)| entries) .collect(); - self.verified + if all_targets.is_empty() { + return Vec::new(); + } + + let mut mismatches = Vec::new(); + for ((_, pt), srcs) in &self.verified { + if *pt != src_type { + continue; + } + for src in srcs { + let vals = link.extract_source_values(&src.public_signals); + if vals.is_empty() { + continue; + } + // Source must match AT LEAST ONE target. + let found = all_targets + .iter() + .any(|tgt| link.check_consistency(&vals, &tgt.public_signals)); + if !found { + mismatches.push(Mismatch { + party_id: src.party_id, + address: src.address, + proof_type: src_type, + data_hash: src.data_hash, + }); + } + } + } + mismatches + } + + // Each source claims a value that must exist among any target's + // outputs. Fault the source (e.g. C3) when no target (e.g. C0) + // matches. If no targets are cached yet, skip — the check will + // run when a target arrives via post-ZK ProofVerificationPassed. + LinkScope::SourceMustExistInTargets => { + let all_targets: Vec<&VerifiedProofData> = self + .verified .iter() - .filter(|((_, pt), _)| *pt == src_type) - .filter_map(|(_, src)| { + .filter(|((_, pt), _)| *pt == tgt_type) + .flat_map(|(_, entries)| entries) + .collect(); + + if all_targets.is_empty() { + return Vec::new(); + } + + let mut mismatches = Vec::new(); + for ((_, pt), srcs) in &self.verified { + if *pt != src_type { + continue; + } + for src in srcs { let vals = link.extract_source_values(&src.public_signals); if vals.is_empty() { - return None; + continue; } - targets + let found = all_targets .iter() - .any(|tgt| !link.check_consistency(&vals, &tgt.public_signals)) - .then(|| Mismatch { + .any(|tgt| link.check_consistency(&vals, &tgt.public_signals)); + if !found { + mismatches.push(Mismatch { party_id: src.party_id, address: src.address, proof_type: src_type, data_hash: src.data_hash, - }) - }) - .collect() + }); + } + } + } + mismatches } } } @@ -245,15 +314,15 @@ impl Handler> for CommitmentConsistencyCheck let proof_type = data.proof_type; let address = data.address; - self.verified.insert( - (address, proof_type), - VerifiedProofData { + self.verified + .entry((address, proof_type)) + .or_default() + .push(VerifiedProofData { party_id: data.party_id, address, public_signals: data.public_signals, data_hash: data.data_hash, - }, - ); + }); self.check_links(proof_type, &ec); } @@ -274,15 +343,15 @@ impl Handler> for CommitmentCons // Cache each party's proof data for link evaluation. for party in &data.party_proofs { for (proof_type, public_signals, data_hash) in &party.proofs { - self.verified.insert( - (party.address, *proof_type), - VerifiedProofData { + self.verified + .entry((party.address, *proof_type)) + .or_default() + .push(VerifiedProofData { party_id: party.party_id, address: party.address, public_signals: public_signals.clone(), data_hash: *data_hash, - }, - ); + }); } } @@ -307,8 +376,10 @@ impl Handler> for CommitmentCons // Remove cached entries for inconsistent parties so they don't // participate in future post-ZK `find_mismatches` evaluations. if !inconsistent_parties.is_empty() { - self.verified - .retain(|_, v| !inconsistent_parties.contains(&v.party_id)); + self.verified.retain(|_, entries| { + entries.retain(|v| !inconsistent_parties.contains(&v.party_id)); + !entries.is_empty() + }); } // Respond to ShareVerificationActor. 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 index fcafbede2b..ef47552c14 100644 --- a/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs +++ b/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs @@ -4,56 +4,49 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! C0 (PkBfv) -> C3a/C3b (ShareEncryption) pk_commitment consistency links. +//! C3a/C3b (ShareEncryption) → C0 (PkBfv) pk_commitment consistency links. //! -//! ## Circuit layouts +//! Each C3 proof declares an `expected_pk_commitment` (the recipient's +//! individual DKG public key commitment). This link verifies that the +//! declared value matches some C0 proof's `pk_commitment` output. //! -//! **C0 (PkBfv)** outputs `(pk_commitment)`. Public signals contain the -//! output at the TAIL. +//! ## Direction //! -//! **C3 (ShareEncryption)** takes `expected_pk_commitment` and -//! `expected_message_commitment` as public inputs at the HEAD of -//! `public_signals`. +//! Source is C3 (the proof making the claim), target is C0 (the proof that +//! established the commitment). Fault is attributed to the C3 sender if its +//! claimed pk_commitment doesn't match any C0 output. //! -//! ## Check +//! ## Scope //! -//! C0's `pk_commitment` output must equal C3's `expected_pk_commitment` input. +//! `SourceMustExistInTargets` — each C3's `expected_pk_commitment` must +//! appear among the set of C0 `pk_commitment` outputs from any party. 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; +/// C3a → C0 pk_commitment consistency link. +pub struct C3aToC0PkCommitmentLink; -impl CommitmentLink for C0ToC3aPkCommitmentLink { +impl CommitmentLink for C3aToC0PkCommitmentLink { fn name(&self) -> &'static str { - "C0->C3a pk_commitment" + "C3a->C0 pk_commitment" } fn source_proof_type(&self) -> ProofType { - ProofType::C0PkBfv + ProofType::C3aSkShareEncryption } fn target_proof_type(&self) -> ProofType { - ProofType::C3aSkShareEncryption + ProofType::C0PkBfv } - /// Cross-party: node i's C3 encrypts a share under node j's individual - /// public key, so C0 (source) comes from the recipient while C3 (target) - /// comes from the sender. fn scope(&self) -> LinkScope { - LinkScope::CrossParty + LinkScope::SourceMustExistInTargets } fn extract_source_values(&self, 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] + extract_expected_pk_commitment(public_signals) } fn check_consistency( @@ -61,39 +54,32 @@ impl CommitmentLink for C0ToC3aPkCommitmentLink { source_values: &[FieldValue], target_public_signals: &[u8], ) -> bool { - check_pk_commitment_consistency(source_values, target_public_signals) + check_pk_exists_in_c0(source_values, target_public_signals) } } -/// C0 -> C3b pk_commitment consistency link. -pub struct C0ToC3bPkCommitmentLink; +/// C3b → C0 pk_commitment consistency link. +pub struct C3bToC0PkCommitmentLink; -impl CommitmentLink for C0ToC3bPkCommitmentLink { +impl CommitmentLink for C3bToC0PkCommitmentLink { fn name(&self) -> &'static str { - "C0->C3b pk_commitment" + "C3b->C0 pk_commitment" } fn source_proof_type(&self) -> ProofType { - ProofType::C0PkBfv + ProofType::C3bESmShareEncryption } fn target_proof_type(&self) -> ProofType { - ProofType::C3bESmShareEncryption + ProofType::C0PkBfv } - /// Cross-party: same reasoning as C0→C3a — recipient's C0 pk vs sender's C3. fn scope(&self) -> LinkScope { - LinkScope::CrossParty + LinkScope::SourceMustExistInTargets } fn extract_source_values(&self, 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] + extract_expected_pk_commitment(public_signals) } fn check_consistency( @@ -101,22 +87,29 @@ impl CommitmentLink for C0ToC3bPkCommitmentLink { source_values: &[FieldValue], target_public_signals: &[u8], ) -> bool { - check_pk_commitment_consistency(source_values, target_public_signals) + check_pk_exists_in_c0(source_values, target_public_signals) } } -/// Shared check: source `pk_commitment` must equal C3's `expected_pk_commitment` -/// input at the HEAD of target public signals. -fn check_pk_commitment_consistency( - source_values: &[FieldValue], - target_public_signals: &[u8], -) -> bool { +/// Extract `expected_pk_commitment` from C3's public inputs (HEAD of signals). +fn extract_expected_pk_commitment(public_signals: &[u8]) -> Vec { + let layout = CircuitName::ShareEncryption.input_layout(); + let Some(bytes) = layout.extract_field(public_signals, "expected_pk_commitment") else { + return vec![]; + }; + let mut value = [0u8; FIELD_BYTE_LEN]; + value.copy_from_slice(bytes); + vec![value] +} + +/// Check whether the source's `expected_pk_commitment` matches C0's +/// `pk_commitment` output. +fn check_pk_exists_in_c0(source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { if source_values.is_empty() { return false; } - let layout = CircuitName::ShareEncryption.input_layout(); - let Some(target_bytes) = layout.extract_field(target_public_signals, "expected_pk_commitment") - else { + let layout = CircuitName::PkBfv.output_layout(); + let Some(target_bytes) = layout.extract_field(target_public_signals, "pk_commitment") else { return false; }; target_bytes == source_values[0] @@ -132,12 +125,7 @@ mod tests { f } - /// Build C0 public signals: just 1 output (pk_commitment). - fn c0_signals(pk_commitment: [u8; 32]) -> Vec { - pk_commitment.to_vec() - } - - /// Build C3 public signals: 2 inputs at HEAD + optional extra bytes. + /// Build C3 public signals: 2 inputs at HEAD + ciphertext bytes after. fn c3_signals(expected_pk: [u8; 32], expected_msg: [u8; 32]) -> Vec { let mut signals = Vec::new(); signals.extend_from_slice(&expected_pk); @@ -145,69 +133,70 @@ mod tests { signals } + /// Build C0 public signals: just 1 output (pk_commitment). + fn c0_signals(pk_commitment: [u8; 32]) -> Vec { + pk_commitment.to_vec() + } + #[test] - fn extract_pk_commitment_from_c0() { - let link = C0ToC3aPkCommitmentLink; + fn extract_expected_pk_from_c3() { + let link = C3aToC0PkCommitmentLink; let pk = make_field(42); - let signals = c0_signals(pk); + let signals = c3_signals(pk, make_field(99)); let values = link.extract_source_values(&signals); assert_eq!(values.len(), 1); assert_eq!(values[0], pk); } #[test] - fn consistency_passes_when_pk_matches_c3a() { - let link = C0ToC3aPkCommitmentLink; + fn consistency_passes_when_c3_pk_matches_c0() { + let link = C3aToC0PkCommitmentLink; let pk = make_field(42); let source_values = vec![pk]; - let target = c3_signals(pk, make_field(99)); + let target = c0_signals(pk); assert!(link.check_consistency(&source_values, &target)); } #[test] - fn consistency_fails_when_pk_mismatches_c3a() { - let link = C0ToC3aPkCommitmentLink; - let pk = make_field(42); - let source_values = vec![pk]; - let target = c3_signals(make_field(99), make_field(99)); + fn consistency_fails_when_c3_pk_doesnt_match_c0() { + let link = C3aToC0PkCommitmentLink; + let source_values = vec![make_field(42)]; + let target = c0_signals(make_field(99)); assert!(!link.check_consistency(&source_values, &target)); } #[test] - fn consistency_passes_when_pk_matches_c3b() { - let link = C0ToC3bPkCommitmentLink; + fn consistency_passes_c3b_variant() { + let link = C3bToC0PkCommitmentLink; let pk = make_field(7); let source_values = vec![pk]; - let target = c3_signals(pk, make_field(55)); + let target = c0_signals(pk); assert!(link.check_consistency(&source_values, &target)); } #[test] - fn consistency_fails_when_pk_mismatches_c3b() { - let link = C0ToC3bPkCommitmentLink; - let pk = make_field(7); - let source_values = vec![pk]; - let target = c3_signals(make_field(8), make_field(55)); + fn consistency_fails_c3b_variant() { + let link = C3bToC0PkCommitmentLink; + let source_values = vec![make_field(7)]; + let target = c0_signals(make_field(8)); assert!(!link.check_consistency(&source_values, &target)); } #[test] fn empty_source_is_inconsistent() { - let link = C0ToC3aPkCommitmentLink; - // Empty source values means malformed proof — should be inconsistent - assert!(!link.check_consistency(&[], &[0u8; 64])); + let link = C3aToC0PkCommitmentLink; + assert!(!link.check_consistency(&[], &c0_signals(make_field(1)))); } #[test] fn short_target_signals_is_inconsistent() { - let link = C0ToC3aPkCommitmentLink; - // Target too short for 2 input fields - assert!(!link.check_consistency(&[make_field(1)], &[0u8; 31])); + let link = C3aToC0PkCommitmentLink; + assert!(!link.check_consistency(&[make_field(1)], &[0u8; 16])); } #[test] fn short_source_signals_returns_empty() { - let link = C0ToC3aPkCommitmentLink; + let link = C3aToC0PkCommitmentLink; assert!(link.extract_source_values(&[0u8; 16]).is_empty()); } } diff --git a/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs b/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs deleted file mode 100644 index c637196f12..0000000000 --- a/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs +++ /dev/null @@ -1,215 +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. - -//! C2a/C2b (ShareComputation) -> C3a/C3b (ShareEncryption) message commitment -//! consistency links. -//! -//! ## Circuit layouts -//! -//! **C2 (ShareComputation)** outputs `(key_hash, commitment)`. The -//! `commitment` field sits at the TAIL of `public_signals`. -//! -//! **C3 (ShareEncryption)** takes `expected_pk_commitment` and -//! `expected_message_commitment` as public inputs at the HEAD of -//! `public_signals`. -//! -//! ## Check -//! -//! C2's `commitment` output must equal C3's `expected_message_commitment` -//! input. - -use super::{CommitmentLink, FieldValue, LinkScope}; -use e3_events::{CircuitName, ProofType}; -use e3_zk_helpers::FIELD_BYTE_LEN; - -/// C2a -> C3a message commitment consistency link. -pub struct C2aToC3aMessageCommitmentLink; - -impl CommitmentLink for C2aToC3aMessageCommitmentLink { - fn name(&self) -> &'static str { - "C2a->C3a message_commitment" - } - - fn source_proof_type(&self) -> ProofType { - ProofType::C2aSkShareComputation - } - - fn target_proof_type(&self) -> ProofType { - ProofType::C3aSkShareEncryption - } - - fn scope(&self) -> LinkScope { - LinkScope::SameParty - } - - fn extract_source_values(&self, public_signals: &[u8]) -> Vec { - extract_commitment(public_signals) - } - - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { - check_message_commitment_consistency(source_values, target_public_signals) - } -} - -/// C2b -> C3b message commitment consistency link. -pub struct C2bToC3bMessageCommitmentLink; - -impl CommitmentLink for C2bToC3bMessageCommitmentLink { - fn name(&self) -> &'static str { - "C2b->C3b message_commitment" - } - - fn source_proof_type(&self) -> ProofType { - ProofType::C2bESmShareComputation - } - - fn target_proof_type(&self) -> ProofType { - ProofType::C3bESmShareEncryption - } - - fn scope(&self) -> LinkScope { - LinkScope::SameParty - } - - fn extract_source_values(&self, public_signals: &[u8]) -> Vec { - extract_commitment(public_signals) - } - - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { - check_message_commitment_consistency(source_values, target_public_signals) - } -} - -/// Extract the `commitment` output from C2 (ShareComputation) public signals. -fn extract_commitment(public_signals: &[u8]) -> Vec { - let layout = CircuitName::ShareComputation.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] -} - -/// Shared check: source `commitment` must equal C3's -/// `expected_message_commitment` input at the HEAD of target public signals. -fn check_message_commitment_consistency( - source_values: &[FieldValue], - target_public_signals: &[u8], -) -> bool { - if source_values.is_empty() { - return false; - } - let layout = CircuitName::ShareEncryption.input_layout(); - let Some(target_bytes) = - layout.extract_field(target_public_signals, "expected_message_commitment") - else { - return false; - }; - target_bytes == source_values[0] -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_field(val: u8) -> [u8; 32] { - let mut f = [0u8; 32]; - f[31] = val; - f - } - - /// Build C2 public signals: 1 pub input + 2 outputs (key_hash, commitment). - /// Simulate 1 pub input field → total 96 bytes. - fn c2_signals(key_hash: [u8; 32], commitment: [u8; 32]) -> Vec { - let mut signals = vec![0xAA; 32]; // 1 pub input - signals.extend_from_slice(&key_hash); - signals.extend_from_slice(&commitment); - signals - } - - /// Build C3 public signals: 2 inputs at HEAD. - fn c3_signals(expected_pk: [u8; 32], expected_msg: [u8; 32]) -> Vec { - let mut signals = Vec::new(); - signals.extend_from_slice(&expected_pk); - signals.extend_from_slice(&expected_msg); - signals - } - - #[test] - fn extract_commitment_from_c2() { - let link = C2aToC3aMessageCommitmentLink; - let commitment = make_field(42); - let signals = c2_signals(make_field(10), commitment); - let values = link.extract_source_values(&signals); - assert_eq!(values.len(), 1); - assert_eq!(values[0], commitment); - } - - #[test] - fn consistency_passes_when_commitment_matches_c3a() { - let link = C2aToC3aMessageCommitmentLink; - let commitment = make_field(42); - let source_values = vec![commitment]; - let target = c3_signals(make_field(99), commitment); - assert!(link.check_consistency(&source_values, &target)); - } - - #[test] - fn consistency_fails_when_commitment_mismatches_c3a() { - let link = C2aToC3aMessageCommitmentLink; - let commitment = make_field(42); - let source_values = vec![commitment]; - let target = c3_signals(make_field(99), make_field(55)); - assert!(!link.check_consistency(&source_values, &target)); - } - - #[test] - fn consistency_passes_when_commitment_matches_c3b() { - let link = C2bToC3bMessageCommitmentLink; - let commitment = make_field(7); - let source_values = vec![commitment]; - let target = c3_signals(make_field(99), commitment); - assert!(link.check_consistency(&source_values, &target)); - } - - #[test] - fn consistency_fails_when_commitment_mismatches_c3b() { - let link = C2bToC3bMessageCommitmentLink; - let commitment = make_field(7); - let source_values = vec![commitment]; - let target = c3_signals(make_field(99), make_field(8)); - assert!(!link.check_consistency(&source_values, &target)); - } - - #[test] - fn empty_source_is_inconsistent() { - let link = C2aToC3aMessageCommitmentLink; - // Empty source values means malformed proof — should be inconsistent - assert!(!link.check_consistency(&[], &[0u8; 64])); - } - - #[test] - fn short_target_signals_is_inconsistent() { - let link = C2aToC3aMessageCommitmentLink; - assert!(!link.check_consistency(&[make_field(1)], &[0u8; 31])); - } - - #[test] - fn short_source_signals_returns_empty() { - let link = C2aToC3aMessageCommitmentLink; - // C2 needs at least 64 bytes for 2 outputs; 32 is too short - assert!(link.extract_source_values(&[0u8; 32]).is_empty()); - } -} diff --git a/crates/zk-prover/src/actors/commitment_links/mod.rs b/crates/zk-prover/src/actors/commitment_links/mod.rs index 16f007bf87..1741b70a66 100644 --- a/crates/zk-prover/src/actors/commitment_links/mod.rs +++ b/crates/zk-prover/src/actors/commitment_links/mod.rs @@ -14,7 +14,6 @@ pub mod c0_to_c3; pub mod c1_to_c5; -pub mod c2_to_c3; pub mod c4a_to_c6; pub mod c4b_to_c6; pub mod c6_to_c7; @@ -24,14 +23,23 @@ use e3_events::ProofType; /// A 32-byte BN254 field element extracted from public signals. pub type FieldValue = [u8; 32]; -/// Whether the linked proofs come from the same node or different nodes. +/// How source and target proofs relate and where faults are attributed. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinkScope { /// Both proofs are generated by the same party (same Ethereum address). + /// One source entry is compared to one target entry per address. SameParty, /// The source proof is per-party while the target is from a different - /// party (e.g. per-node C1 vs aggregator C5). + /// party (e.g. per-node C1 vs aggregator C5). Each source's extracted + /// value must appear in at least one target's public signals. + /// Fault is attributed to the source if no target matches. CrossParty, + /// Each source proof claims a value that must exist among the set of + /// target proof outputs (from any party). Fault is attributed to the + /// **source** when no target matches. Used when there are many source + /// proofs (e.g. N-1 C3 proofs per sender) each referencing a different + /// target (e.g. the recipient's C0 pk_commitment). + SourceMustExistInTargets, } /// Defines a cross-circuit commitment consistency check. @@ -64,11 +72,9 @@ pub trait CommitmentLink: Send + Sync { /// Returns the default set of commitment links to register. pub fn default_links() -> Vec> { vec![ - Box::new(c0_to_c3::C0ToC3aPkCommitmentLink), - Box::new(c0_to_c3::C0ToC3bPkCommitmentLink), + Box::new(c0_to_c3::C3aToC0PkCommitmentLink), + Box::new(c0_to_c3::C3bToC0PkCommitmentLink), Box::new(c1_to_c5::C1ToC5PkCommitmentLink), - Box::new(c2_to_c3::C2aToC3aMessageCommitmentLink), - Box::new(c2_to_c3::C2bToC3bMessageCommitmentLink), Box::new(c4a_to_c6::C4aToC6SkCommitmentLink), Box::new(c4b_to_c6::C4bToC6ESmCommitmentLink), Box::new(c6_to_c7::C6ToC7DCommitmentLink), diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index c2f4356e98..a18b9b4feb 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -8,7 +8,9 @@ use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use actix::{Actor, Addr, Context, Handler}; +use alloy::primitives::{keccak256, Bytes}; use alloy::signers::local::PrivateKeySigner; +use alloy::sol_types::SolValue; use e3_events::{ AggregationProofPending, AggregationProofSigned, BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, ComputeResponseKind, @@ -17,9 +19,10 @@ use e3_events::{ EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, EncryptionKeyPending, EventContext, EventPublisher, EventSubscriber, EventType, FailureReason, PkAggregationProofPending, PkAggregationProofRequest, PkAggregationProofSigned, - PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, ProofType, Sequenced, - ShareDecryptionProofPending, SignedProofFailed, SignedProofPayload, ThresholdShare, - ThresholdShareCreated, ThresholdSharePending, TypedEvent, ZkRequest, ZkResponse, + PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, ProofType, + ProofVerificationPassed, Sequenced, ShareDecryptionProofPending, SignedProofFailed, + SignedProofPayload, ThresholdShare, ThresholdShareCreated, ThresholdSharePending, TypedEvent, + ZkRequest, ZkResponse, }; use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; @@ -1368,6 +1371,7 @@ impl ProofRequestActor { } } + let local_party_id = key.party_id; if let Err(err) = self.bus.publish( EncryptionKeyCreated { e3_id: e3_id.clone(), @@ -1379,6 +1383,33 @@ impl ProofRequestActor { error!("Failed to publish EncryptionKeyCreated: {err}"); } + // Publish the local node's own C0 as ProofVerificationPassed so the + // CommitmentConsistencyChecker caches it. Without this, C3 proofs from + // other parties that encrypt under this node's pk would fail the C3→C0 + // consistency check (the local C0 target wouldn't exist in the cache). + { + let msg = ( + Bytes::copy_from_slice(&proof.data), + Bytes::copy_from_slice(&proof.public_signals), + ) + .abi_encode(); + let data_hash: [u8; 32] = keccak256(&msg).into(); + + if let Err(err) = self.bus.publish( + ProofVerificationPassed { + e3_id: e3_id.clone(), + party_id: local_party_id, + address: self.signer.address(), + proof_type: ProofType::C0PkBfv, + data_hash, + public_signals: proof.public_signals.clone(), + }, + ec.clone(), + ) { + error!("Failed to publish local C0 ProofVerificationPassed: {err}"); + } + } + // Emit DKGInnerProofReady for C0, or buffer if meta not yet available if let Some(meta) = self.node_agg_meta.get(&e3_id) { if meta.proof_aggregation_enabled { From 9b0818f7cb4338fb117f17134d879eabf927a14e Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:49:23 +0100 Subject: [PATCH 3/3] chore: pr comments --- .../actors/commitment_consistency_checker.rs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/zk-prover/src/actors/commitment_consistency_checker.rs b/crates/zk-prover/src/actors/commitment_consistency_checker.rs index bd6a079a5d..5719d48680 100644 --- a/crates/zk-prover/src/actors/commitment_consistency_checker.rs +++ b/crates/zk-prover/src/actors/commitment_consistency_checker.rs @@ -80,6 +80,21 @@ impl CommitmentConsistencyChecker { } } + /// Insert a proof into the cache, deduplicating by `data_hash` to avoid + /// double-counting when the same proof arrives via both the pre-ZK batch + /// and the post-ZK `ProofVerificationPassed` path. + fn insert_verified( + &mut self, + address: Address, + proof_type: ProofType, + data: VerifiedProofData, + ) { + let entries = self.verified.entry((address, proof_type)).or_default(); + if !entries.iter().any(|e| e.data_hash == data.data_hash) { + entries.push(data); + } + } + pub fn setup(bus: &BusHandle, e3_id: E3id, links: Vec>) -> Addr { let actor = Self::new(bus, e3_id, links); let addr = actor.start(); @@ -314,15 +329,16 @@ impl Handler> for CommitmentConsistencyCheck let proof_type = data.proof_type; let address = data.address; - self.verified - .entry((address, proof_type)) - .or_default() - .push(VerifiedProofData { + self.insert_verified( + address, + proof_type, + VerifiedProofData { party_id: data.party_id, address, public_signals: data.public_signals, data_hash: data.data_hash, - }); + }, + ); self.check_links(proof_type, &ec); } @@ -343,15 +359,16 @@ impl Handler> for CommitmentCons // Cache each party's proof data for link evaluation. for party in &data.party_proofs { for (proof_type, public_signals, data_hash) in &party.proofs { - self.verified - .entry((party.address, *proof_type)) - .or_default() - .push(VerifiedProofData { + self.insert_verified( + party.address, + *proof_type, + VerifiedProofData { party_id: party.party_id, address: party.address, public_signals: public_signals.clone(), data_hash: *data_hash, - }); + }, + ); } }