Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions crates/events/src/enclave_event/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,46 @@ mod tests {
assert_eq!(&*proof.extract_output("commitment").unwrap(), &[0xFF; 32]);
}

#[test]
fn extract_c6_d_commitment_after_pub_inputs() {
// C6: 2 public inputs + 1 output (`d_commitment` at tail).
let mut signals = vec![0u8; 96];
signals[0..32].copy_from_slice(&[0x11; 32]); // expected_sk_commitment
signals[32..64].copy_from_slice(&[0x22; 32]); // expected_e_sm_commitment
signals[64..96].copy_from_slice(&[0x77; 32]); // d_commitment

let proof = make_proof(CircuitName::ThresholdShareDecryption, &signals);
assert_eq!(&*proof.extract_output("d_commitment").unwrap(), &[0x77; 32]);
}

#[test]
fn extract_c6_public_inputs() {
let mut signals = vec![0u8; 96];
signals[0..32].copy_from_slice(&[0x11; 32]);
signals[32..64].copy_from_slice(&[0x22; 32]);
signals[64..96].copy_from_slice(&[0x77; 32]);

let proof = make_proof(CircuitName::ThresholdShareDecryption, &signals);
assert_eq!(
&*proof.extract_input("expected_sk_commitment").unwrap(),
&[0x11; 32]
);
assert_eq!(
&*proof.extract_input("expected_e_sm_commitment").unwrap(),
&[0x22; 32]
);
}

#[test]
fn extract_c7_has_no_named_public_outputs() {
// C7 (`DecryptedSharesAggregation`) has only public inputs in Noir; `output_layout` is
// `None`, so `extract_output` cannot resolve a return field.
let signals = vec![0xAB; 32 * 8];
let proof = make_proof(CircuitName::DecryptedSharesAggregation, &signals);
assert!(proof.extract_output("d_commitment").is_none());
assert!(proof.extract_output("commitment").is_none());
}

#[test]
fn extract_nonexistent_field() {
let proof = make_proof(CircuitName::PkBfv, &[0u8; 32]);
Expand Down
68 changes: 68 additions & 0 deletions crates/zk-helpers/src/circuits/output_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,72 @@ mod tests {
assert_eq!(CircuitOutputLayout::None.field_count(), Some(0));
assert_eq!(CircuitOutputLayout::Dynamic.field_count(), None);
}

#[test]
fn extract_c6_d_commitment_after_pub_inputs() {
let layout = CircuitOutputLayout::Fixed {
fields: THRESHOLD_SHARE_DECRYPTION_OUTPUTS,
};
// C6: 2 public inputs + 1 output = 96 bytes
let mut signals = vec![0u8; 96];
signals[0..32].copy_from_slice(&[0x11; 32]);
signals[32..64].copy_from_slice(&[0x22; 32]);
signals[64..96].copy_from_slice(&[0x77; 32]);

assert_eq!(
layout.extract_field(&signals, "d_commitment").unwrap(),
&[0x77; 32]
);
}

#[test]
fn extract_c6_public_inputs_via_input_layout() {
let layout = CircuitInputLayout::Fixed {
fields: THRESHOLD_SHARE_DECRYPTION_INPUTS,
};
let mut signals = vec![0u8; 96];
signals[0..32].copy_from_slice(&[0x11; 32]);
signals[32..64].copy_from_slice(&[0x22; 32]);
signals[64..96].copy_from_slice(&[0x77; 32]);

assert_eq!(
layout
.extract_field(&signals, "expected_sk_commitment")
.unwrap(),
&[0x11; 32]
);
assert_eq!(
layout
.extract_field(&signals, "expected_e_sm_commitment")
.unwrap(),
&[0x22; 32]
);
}

#[test]
fn extract_c6_input_signals_too_short_returns_none() {
let layout = CircuitInputLayout::Fixed {
fields: THRESHOLD_SHARE_DECRYPTION_INPUTS,
};
assert!(layout
.extract_field(&[0u8; 32], "expected_e_sm_commitment")
.is_none());
}

/// C7 (`DecryptedSharesAggregation`) has no `-> pub` return values; metadata uses `None`.
#[test]
fn c7_void_output_extract_field_returns_none() {
let layout = CircuitOutputLayout::None;
let signals = vec![0u8; 256];
assert!(layout.extract_field(&signals, "d_commitment").is_none());
}

/// C7: `extract_all` yields no named outputs when the layout is void.
#[test]
fn c7_void_output_extract_all_returns_empty() {
let layout = CircuitOutputLayout::None;
let signals = vec![0u8; 256];
let all = layout.extract_all(&signals).unwrap();
assert!(all.is_empty());
}
}
207 changes: 207 additions & 0 deletions crates/zk-prover/src/actors/commitment_links/c6_to_c7.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// 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.

//! C6 (ShareDecryption) → C7 (DecryptedSharesAggregation) `d_commitment` consistency link.
//!
//! ## Circuit layouts
//!
//! **C6 (ThresholdShareDecryption)** outputs `d_commitment`.
//!
//! **C7 (DecryptedSharesAggregation)** `main` has (in order) `expected_d_commitments`,
//! `party_ids`, and `message` as public inputs; there are no public return values. So
//! `public_signals` are:
//! `[d_commitments (T+1)] [party_ids (T+1)] [message coefficients (MAX_MSG_NON_ZERO_COEFFS)]`.
//!
//! We recover `T+1` as `(total_fields - MAX_MSG_NON_ZERO_COEFFS) / 2`; see **Caveat** below and
//! `circuits/bin/threshold/decrypted_shares_aggregation/src/main.nr`.
//!
//! ## Check
//!
//! The C6 `d_commitment` must appear in the `expected_d_commitments` prefix only (not in party IDs
//! or message).
//!
//! ## Caveat
//!
//! The Rust constant `MAX_MSG_NON_ZERO_COEFFS` (imported from
//! `e3_zk_helpers::circuits::threshold::decrypted_shares_aggregation`) must match the **compiled**
//! C7 Noir circuit (same value as `Polynomial<MAX_MSG_NON_ZERO_COEFFS>` /
//! `lib::configs::default::MAX_MSG_NON_ZERO_COEFFS` for that artifact). If you ever ship multiple C7
//! builds with different message widths, verifying or linking against the wrong artifact will make
//! `(total_fields - MAX_MSG_NON_ZERO_COEFFS) / 2` wrong and this check will mis-parse
//! `public_signals` (false negatives/positives relative to the intended circuit).

use super::{CommitmentLink, FieldValue, LinkScope};
use e3_events::{CircuitName, ProofType};
use e3_zk_helpers::circuits::threshold::decrypted_shares_aggregation::MAX_MSG_NON_ZERO_COEFFS;
use e3_zk_helpers::FIELD_BYTE_LEN;

/// C6 → C7 `d_commitment` consistency link.
pub struct C6ToC7DCommitmentLink;

impl CommitmentLink for C6ToC7DCommitmentLink {
fn name(&self) -> &'static str {
"C6->C7 d_commitment"
}

fn source_proof_type(&self) -> ProofType {
ProofType::C6ThresholdShareDecryption
}

fn target_proof_type(&self) -> ProofType {
ProofType::C7DecryptedSharesAggregation
}

fn scope(&self) -> LinkScope {
LinkScope::CrossParty
}

fn extract_source_values(&self, public_signals: &[u8]) -> Vec<FieldValue> {
let layout = CircuitName::ThresholdShareDecryption.output_layout();
let Some(bytes) = layout.extract_field(public_signals, "d_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;
}

if target_public_signals.len() % FIELD_BYTE_LEN != 0 {
return false;
}

// C7: `circuits/.../decrypted_shares_aggregation/src/main.nr` — public inputs in order:
// (T+1) d commitments, (T+1) party IDs, `MAX_MSG_NON_ZERO_COEFFS` message coefficients.
let total_fields = target_public_signals.len() / FIELD_BYTE_LEN;
let rem = total_fields.checked_sub(MAX_MSG_NON_ZERO_COEFFS);
let Some(rem) = rem else {
return false;
};
if rem % 2 != 0 {
return false;
}
let d_commitment_fields = rem / 2;
if d_commitment_fields == 0 {
return false;
}

let source_d_commitment = &source_values[0];

for i in 0..d_commitment_fields {
let offset = i * FIELD_BYTE_LEN;
if target_public_signals[offset..offset + FIELD_BYTE_LEN] == *source_d_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
}

/// Builds C7 `public_signals` bytes: `(T+1)` d commitments, `(T+1)` party IDs, then message
/// coeffs (`MAX_MSG_NON_ZERO_COEFFS` fields), matching `decrypted_shares_aggregation/src/main.nr`.
fn c7_public_signals(
d_commitments: &[[u8; FIELD_BYTE_LEN]],
party_ids: &[[u8; FIELD_BYTE_LEN]],
) -> Vec<u8> {
assert_eq!(d_commitments.len(), party_ids.len());
let mut v = Vec::new();
for c in d_commitments {
v.extend_from_slice(c);
}
for p in party_ids {
v.extend_from_slice(p);
}
for _ in 0..MAX_MSG_NON_ZERO_COEFFS {
v.extend_from_slice(&make_field(0));
}
v
}

#[test]
fn extract_d_commitment_from_c6() {
let link = C6ToC7DCommitmentLink;
let d = make_field(7);
let signals = d.to_vec();

let values = link.extract_source_values(&signals);
assert_eq!(values.len(), 1);
assert_eq!(values[0], d);
}

#[test]
fn consistency_passes_when_d_present_in_c7_inputs() {
let link = C6ToC7DCommitmentLink;
let d = make_field(42);
let source_values = vec![d];

let d_comm = [make_field(10), d, make_field(99)];
let party = [make_field(1), make_field(2), make_field(3)];
let c7_signals = c7_public_signals(&d_comm, &party);

assert!(link.check_consistency(&source_values, &c7_signals));
}

#[test]
fn consistency_fails_when_d_missing_from_commitments() {
let link = C6ToC7DCommitmentLink;
let d = make_field(42);
let source_values = vec![d];

let d_comm = [make_field(10), make_field(20), make_field(99)];
let party = [make_field(1), make_field(2), make_field(3)];
let c7_signals = c7_public_signals(&d_comm, &party);

assert!(!link.check_consistency(&source_values, &c7_signals));
}

#[test]
fn consistency_fails_when_d_only_appears_in_message_tail() {
let link = C6ToC7DCommitmentLink;
let d = make_field(42);
let source_values = vec![d];

let d_comm = [make_field(10), make_field(20), make_field(99)];
let party = [make_field(1), make_field(2), make_field(3)];
let mut c7_signals = c7_public_signals(&d_comm, &party);
// Overwrite first message coefficient with `d` — must not count as a match.
let msg_off = 6 * FIELD_BYTE_LEN;
c7_signals[msg_off..msg_off + FIELD_BYTE_LEN].copy_from_slice(&d);

assert!(!link.check_consistency(&source_values, &c7_signals));
}

#[test]
fn short_source_signals_treated_as_inconsistent() {
let link = C6ToC7DCommitmentLink;
assert!(link.extract_source_values(&[0u8; 16]).is_empty());
assert!(!link.check_consistency(&[], &[0u8; 32]));
}

#[test]
fn short_target_signals_treated_as_inconsistent() {
let link = C6ToC7DCommitmentLink;
assert!(!link.check_consistency(&[make_field(1)], &[0u8; 16]));
}
}
2 changes: 2 additions & 0 deletions crates/zk-prover/src/actors/commitment_links/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
pub mod c1_to_c5;
pub mod c4a_to_c6;
pub mod c4b_to_c6;
pub mod c6_to_c7;

use e3_events::ProofType;

Expand Down Expand Up @@ -64,5 +65,6 @@ pub fn default_links() -> Vec<Box<dyn CommitmentLink>> {
Box::new(c1_to_c5::C1ToC5PkCommitmentLink),
Box::new(c4a_to_c6::C4aToC6SkCommitmentLink),
Box::new(c4b_to_c6::C4bToC6ESmCommitmentLink),
Box::new(c6_to_c7::C6ToC7DCommitmentLink),
]
}
Loading