Skip to content
6 changes: 6 additions & 0 deletions iris-mpc-common/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ pub struct Config {
#[serde(default)]
pub enable_sending_mirror_anonymized_stats_message: bool,

#[serde(default)]
pub enable_sending_anonymized_stats_2d_message: bool,

#[serde(default)]
pub enable_reauth: bool,

Expand Down Expand Up @@ -596,6 +599,7 @@ pub struct CommonConfig {
n_buckets: usize,
enable_sending_anonymized_stats_message: bool,
enable_sending_mirror_anonymized_stats_message: bool,
enable_sending_anonymized_stats_2d_message: bool,
enable_reauth: bool,
enable_reset: bool,
hawk_request_parallelism: usize,
Expand Down Expand Up @@ -674,6 +678,7 @@ impl From<Config> for CommonConfig {
n_buckets,
enable_sending_anonymized_stats_message,
enable_sending_mirror_anonymized_stats_message,
enable_sending_anonymized_stats_2d_message,
enable_reauth,
enable_reset,
hawk_request_parallelism,
Expand Down Expand Up @@ -728,6 +733,7 @@ impl From<Config> for CommonConfig {
n_buckets,
enable_sending_anonymized_stats_message,
enable_sending_mirror_anonymized_stats_message,
enable_sending_anonymized_stats_2d_message,
enable_reauth,
enable_reset,
hawk_request_parallelism,
Expand Down
1 change: 1 addition & 0 deletions iris-mpc-common/src/helpers/smpc_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ where

pub const IDENTITY_DELETION_MESSAGE_TYPE: &str = "identity_deletion";
pub const ANONYMIZED_STATISTICS_MESSAGE_TYPE: &str = "anonymized_statistics";
pub const ANONYMIZED_STATISTICS_2D_MESSAGE_TYPE: &str = "anonymized_statistics_2d";
pub const CIRCUIT_BREAKER_MESSAGE_TYPE: &str = "circuit_breaker";
pub const UNIQUENESS_MESSAGE_TYPE: &str = "uniqueness";
pub const REAUTH_MESSAGE_TYPE: &str = "reauth";
Expand Down
125 changes: 125 additions & 0 deletions iris-mpc-common/src/helpers/statistics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use chrono::{
use serde::{Deserialize, Serialize};
use std::fmt;

// 1D anonymized statistics types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BucketResult {
pub count: usize,
Expand Down Expand Up @@ -147,3 +148,127 @@ impl BucketStatistics {
self.next_start_time_utc_timestamp = Some(now_timestamp);
}
}

// 2D anonymized statistics types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bucket2DResult {
pub count: usize,
pub left_hamming_distance_bucket: [f64; 2],
pub right_hamming_distance_bucket: [f64; 2],
}

impl Eq for Bucket2DResult {}
impl PartialEq for Bucket2DResult {
fn eq(&self, other: &Self) -> bool {
self.count == other.count
&& (self.left_hamming_distance_bucket[0] - other.left_hamming_distance_bucket[0]).abs()
<= 1e-9
&& (self.left_hamming_distance_bucket[1] - other.left_hamming_distance_bucket[1]).abs()
<= 1e-9
&& (self.right_hamming_distance_bucket[0] - other.right_hamming_distance_bucket[0])
.abs()
<= 1e-9
&& (self.right_hamming_distance_bucket[1] - other.right_hamming_distance_bucket[1])
.abs()
<= 1e-9
}
}
Comment on lines +160 to +175
Copy link

Copilot AI Aug 28, 2025

Choose a reason for hiding this comment

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

Implementing Eq for a type with floating-point fields violates Rust's equivalence relation requirements. The PartialEq implementation uses approximate equality (1e-9 tolerance) which is not transitive, reflexive, or symmetric as required by Eq. Remove the Eq implementation and only keep PartialEq.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

That is actually true, the impl Eq should be removed.


#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BucketStatistics2D {
pub buckets: Vec<Bucket2DResult>,
pub n_buckets_per_side: usize,
// The number of two-sided matches gathered before sending the statistics
pub match_distances_buffer_size: usize,
pub party_id: usize,
#[serde(with = "ts_seconds")]
pub start_time_utc_timestamp: DateTime<Utc>,
#[serde(with = "ts_seconds_option")]
pub end_time_utc_timestamp: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
#[serde(skip_deserializing)]
#[serde(with = "ts_seconds_option")]
Comment on lines +188 to +190
Copy link

Copilot AI Aug 28, 2025

Choose a reason for hiding this comment

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

The next_start_time_utc_timestamp field has conflicting serde attributes. skip_serializing and skip_deserializing will prevent serialization/deserialization, making the with = \"ts_seconds_option\" attribute ineffective. Remove the with attribute since the field is being skipped.

Suggested change
#[serde(skip_serializing)]
#[serde(skip_deserializing)]
#[serde(with = "ts_seconds_option")]

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

also seems a reasonable suggestion.

pub next_start_time_utc_timestamp: Option<DateTime<Utc>>,
}

impl BucketStatistics2D {
pub fn is_empty(&self) -> bool {
self.buckets.is_empty()
}
}

impl fmt::Display for BucketStatistics2D {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, " party_id: {}", self.party_id)?;
writeln!(f, " start_time_utc: {}", self.start_time_utc_timestamp)?;
match &self.end_time_utc_timestamp {
Some(end) => writeln!(f, " end_time_utc: {}", end)?,
None => writeln!(f, " end_time_utc: <none>")?,
}
for bucket in &self.buckets {
writeln!(
f,
" L({:.3}-{:.3}), R({:.3}-{:.3}): {}",
bucket.left_hamming_distance_bucket[0],
bucket.left_hamming_distance_bucket[1],
bucket.right_hamming_distance_bucket[0],
bucket.right_hamming_distance_bucket[1],
bucket.count
)?;
}
Ok(())
}
}

impl BucketStatistics2D {
pub fn new(
match_distances_buffer_size: usize,
n_buckets_per_side: usize,
party_id: usize,
) -> Self {
Self {
buckets: Vec::with_capacity(n_buckets_per_side * n_buckets_per_side),
n_buckets_per_side,
match_distances_buffer_size,
party_id,
start_time_utc_timestamp: Utc::now(),
end_time_utc_timestamp: None,
next_start_time_utc_timestamp: None,
}
}

/// Fill bucket counts for the 2D histogram.
/// buckets_2d is expected in row-major order (left index major):
/// buckets_2d[left_idx * n_buckets_per_side + right_idx]
pub fn fill_buckets(
&mut self,
buckets_2d: &[u32],
match_threshold_ratio: f64,
start_timestamp: Option<DateTime<Utc>>,
) {
tracing::info!("Filling 2D buckets: {} entries", buckets_2d.len());

let now_timestamp = Utc::now();

self.buckets.clear();
self.end_time_utc_timestamp = Some(now_timestamp);

let step = match_threshold_ratio / (self.n_buckets_per_side as f64);
for (i, &count) in buckets_2d.iter().enumerate() {
let left_idx = i / self.n_buckets_per_side;
let right_idx = i % self.n_buckets_per_side;
let left_range = [step * (left_idx as f64), step * ((left_idx + 1) as f64)];
let right_range = [step * (right_idx as f64), step * ((right_idx + 1) as f64)];
self.buckets.push(Bucket2DResult {
count: count as usize,
left_hamming_distance_bucket: left_range,
right_hamming_distance_bucket: right_range,
});
}

if let Some(start_timestamp) = start_timestamp {
self.start_time_utc_timestamp = start_timestamp;
}
self.next_start_time_utc_timestamp = Some(now_timestamp);
}
}
5 changes: 4 additions & 1 deletion iris-mpc-common/src/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::helpers::batch_sync::{
use crate::{
galois_engine::degree4::{GaloisRingIrisCodeShare, GaloisRingTrimmedMaskCodeShare},
helpers::{
statistics::BucketStatistics,
statistics::{BucketStatistics, BucketStatistics2D},
sync::{Modification, ModificationKey},
},
ROTATIONS,
Expand Down Expand Up @@ -407,6 +407,9 @@ pub struct ServerJobResult<A = ()> {
// See struct definition for more details
pub anonymized_bucket_statistics_left: BucketStatistics,
pub anonymized_bucket_statistics_right: BucketStatistics,
// 2D anonymized statistics across both eyes (only for matches on both sides)
// Only for Normal orientation
pub anonymized_bucket_statistics_2d: BucketStatistics2D,
// Mirror orientation bucket statistics
pub anonymized_bucket_statistics_left_mirror: BucketStatistics,
pub anonymized_bucket_statistics_right_mirror: BucketStatistics,
Expand Down
3 changes: 2 additions & 1 deletion iris-mpc-cpu/src/execution/hawk_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use iris_mpc_common::{
config::TlsConfig,
helpers::{
smpc_request::{REAUTH_MESSAGE_TYPE, RESET_CHECK_MESSAGE_TYPE, UNIQUENESS_MESSAGE_TYPE},
statistics::BucketStatistics,
statistics::{BucketStatistics, BucketStatistics2D},
},
vector_id::VectorId,
};
Expand Down Expand Up @@ -1154,6 +1154,7 @@ impl HawkResult {
anonymized_bucket_statistics_right,
anonymized_bucket_statistics_left_mirror: BucketStatistics::default(), // TODO.
anonymized_bucket_statistics_right_mirror: BucketStatistics::default(), // TODO.
anonymized_bucket_statistics_2d: BucketStatistics2D::default(), // TODO.

successful_reauths,
reauth_target_indices: batch.reauth_target_indices,
Expand Down
18 changes: 17 additions & 1 deletion iris-mpc-gpu/src/server/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use iris_mpc_common::{
inmemory_store::InMemoryStore,
sha256::sha256_bytes,
smpc_request::{REAUTH_MESSAGE_TYPE, RESET_CHECK_MESSAGE_TYPE, UNIQUENESS_MESSAGE_TYPE},
statistics::BucketStatistics,
statistics::{BucketStatistics, BucketStatistics2D},
},
iris_db::{get_dummy_shares_for_deletion, iris::MATCH_THRESHOLD_RATIO},
job::{Eye, JobSubmissionHandle, ServerJobResult},
Expand Down Expand Up @@ -178,6 +178,7 @@ pub struct ServerActor {
anonymized_bucket_statistics_right_mirror: BucketStatistics,
// 2D anon stats buffer
both_side_match_distances_buffer: Vec<TwoSidedDistanceCache>,
anonymized_bucket_statistics_2d: BucketStatistics2D,
full_scan_side: Eye,
full_scan_side_switching_enabled: bool,
}
Expand Down Expand Up @@ -523,6 +524,9 @@ impl ServerActor {
let both_side_match_distances_buffer =
vec![TwoSidedDistanceCache::default(); device_manager.device_count()];

let anonymized_bucket_statistics_2d =
BucketStatistics2D::new(match_distances_2d_buffer_size, n_buckets, party_id);

Ok(Self {
party_id,
job_queue,
Expand Down Expand Up @@ -576,6 +580,7 @@ impl ServerActor {
full_scan_side,
full_scan_side_switching_enabled,
both_side_match_distances_buffer,
anonymized_bucket_statistics_2d,
})
}

Expand Down Expand Up @@ -633,6 +638,7 @@ impl ServerActor {
self.anonymized_bucket_statistics_right_mirror
.buckets
.clear();
self.anonymized_bucket_statistics_2d.buckets.clear();

tracing::info!(
"Full batch duration took: {:?}",
Expand Down Expand Up @@ -1598,6 +1604,7 @@ impl ServerActor {
);
}

// Attempt for 2D anonymized bucket statistics calculation
let (one_sided_distance_cache_left, one_sided_distance_cache_right) =
if self.full_scan_side == Eye::Left {
(
Expand Down Expand Up @@ -1676,6 +1683,14 @@ impl ServerActor {
);
}
tracing::info!("Bucket statistics calculated:\n{}", buckets_2d_string);

// Fill the 2D anonymized statistics structure for propagation
self.anonymized_bucket_statistics_2d.fill_buckets(
&buckets_2d,
MATCH_THRESHOLD_RATIO,
self.anonymized_bucket_statistics_left
.next_start_time_utc_timestamp,
);
}

// Instead of sending to return_channel, we'll return this at the end
Expand Down Expand Up @@ -1704,6 +1719,7 @@ impl ServerActor {
matched_batch_request_ids,
anonymized_bucket_statistics_left: self.anonymized_bucket_statistics_left.clone(),
anonymized_bucket_statistics_right: self.anonymized_bucket_statistics_right.clone(),
anonymized_bucket_statistics_2d: self.anonymized_bucket_statistics_2d.clone(),
anonymized_bucket_statistics_left_mirror: self
.anonymized_bucket_statistics_left_mirror
.clone(),
Expand Down
27 changes: 24 additions & 3 deletions iris-mpc/bin/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ use iris_mpc_common::{
decrypt_iris_share, get_iris_data_by_party_id, validate_iris_share,
CircuitBreakerRequest, IdentityDeletionRequest, ReAuthRequest, ReceiveRequestError,
ResetCheckRequest, ResetUpdateRequest, SQSMessage, UniquenessRequest,
ANONYMIZED_STATISTICS_MESSAGE_TYPE, CIRCUIT_BREAKER_MESSAGE_TYPE,
IDENTITY_DELETION_MESSAGE_TYPE, REAUTH_MESSAGE_TYPE, RESET_CHECK_MESSAGE_TYPE,
RESET_UPDATE_MESSAGE_TYPE, UNIQUENESS_MESSAGE_TYPE,
ANONYMIZED_STATISTICS_2D_MESSAGE_TYPE, ANONYMIZED_STATISTICS_MESSAGE_TYPE,
CIRCUIT_BREAKER_MESSAGE_TYPE, IDENTITY_DELETION_MESSAGE_TYPE, REAUTH_MESSAGE_TYPE,
RESET_CHECK_MESSAGE_TYPE, RESET_UPDATE_MESSAGE_TYPE, UNIQUENESS_MESSAGE_TYPE,
},
smpc_response::{
create_message_type_attribute_map, IdentityDeletionResult, ReAuthResult,
Expand Down Expand Up @@ -892,6 +892,8 @@ async fn server_main(config: Config) -> Result<()> {
create_message_type_attribute_map(RESET_UPDATE_MESSAGE_TYPE);
let anonymized_statistics_attributes =
create_message_type_attribute_map(ANONYMIZED_STATISTICS_MESSAGE_TYPE);
let anonymized_statistics_2d_attributes =
create_message_type_attribute_map(ANONYMIZED_STATISTICS_2D_MESSAGE_TYPE);
let identity_deletion_result_attributes =
create_message_type_attribute_map(IDENTITY_DELETION_MESSAGE_TYPE);

Expand Down Expand Up @@ -1403,6 +1405,7 @@ async fn server_main(config: Config) -> Result<()> {
mut modifications,
actor_data: _,
full_face_mirror_attack_detected,
anonymized_bucket_statistics_2d,
}) = rx.recv().await
{
let dummy_deletion_shares = get_dummy_shares_for_deletion(party_id);
Expand Down Expand Up @@ -1872,6 +1875,24 @@ async fn server_main(config: Config) -> Result<()> {
.await?;
}

// Send 2D anonymized statistics if present with their own flag
if config_bg.enable_sending_anonymized_stats_2d_message
&& !anonymized_bucket_statistics_2d.buckets.is_empty()
{
tracing::info!("Sending 2D anonymized stats results");
let serialized = serde_json::to_string(&anonymized_bucket_statistics_2d)
.wrap_err("failed to serialize 2D anonymized statistics result")?;
send_results_to_sns(
vec![serialized],
&metadata,
&sns_client_bg,
&config_bg,
&anonymized_statistics_2d_attributes,
ANONYMIZED_STATISTICS_2D_MESSAGE_TYPE,
)
.await?;
}

shutdown_handler_bg.decrement_batches_pending_completion();
}

Expand Down
Loading