confium_privacy/
secure_aggregation.rs1use getrandom::SysRng;
9use p256::elliptic_curve::rand_core::Rng;
10use p256::elliptic_curve::rand_core::UnwrapErr;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
15pub struct AggregationParty {
16 pub id: u32,
17 pub value: i64,
18 pub masks_to_apply: HashMap<u32, i64>,
21}
22
23#[derive(Debug)]
25pub struct SecureAggregation {
26 pub parties: Vec<AggregationParty>,
27}
28
29impl SecureAggregation {
30 pub fn new(party_count: u32) -> Self {
31 let parties = (0..party_count)
32 .map(|id| AggregationParty {
33 id,
34 value: 0,
35 masks_to_apply: HashMap::new(),
36 })
37 .collect();
38 Self { parties }
39 }
40
41 pub fn set_value(&mut self, party_id: u32, value: i64) {
43 if let Some(party) = self.parties.iter_mut().find(|p| p.id == party_id) {
44 party.value = value;
45 }
46 }
47
48 pub fn establish_masks(&mut self) {
52 let n = self.parties.len();
53 let mut rng = UnwrapErr(SysRng);
54 for i in 0..n {
55 for j in (i + 1)..n {
56 let mask = (rng.next_u32() as i64) % 1000;
58 let id_i = self.parties[i].id;
59 let id_j = self.parties[j].id;
60 self.parties[i].masks_to_apply.insert(id_j, mask);
61 self.parties[j].masks_to_apply.insert(id_i, -mask);
62 }
63 }
64 }
65
66 pub fn masked_values(&self) -> Vec<i64> {
68 self.parties
69 .iter()
70 .map(|p| {
71 let mask_sum: i64 = p.masks_to_apply.values().sum();
72 p.value + mask_sum
73 })
74 .collect()
75 }
76
77 pub fn aggregate(&self) -> i64 {
80 self.masked_values().iter().sum()
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn sum_preserved_with_masks() {
90 let mut agg = SecureAggregation::new(3);
91 agg.set_value(0, 10);
92 agg.set_value(1, 20);
93 agg.set_value(2, 30);
94 agg.establish_masks();
95 assert_eq!(agg.aggregate(), 60);
96 }
97
98 #[test]
99 fn single_party() {
100 let mut agg = SecureAggregation::new(1);
101 agg.set_value(0, 42);
102 agg.establish_masks();
103 assert_eq!(agg.aggregate(), 42);
104 }
105
106 #[test]
107 fn negative_values() {
108 let mut agg = SecureAggregation::new(3);
109 agg.set_value(0, -10);
110 agg.set_value(1, 20);
111 agg.set_value(2, -5);
112 agg.establish_masks();
113 assert_eq!(agg.aggregate(), 5);
114 }
115
116 #[test]
117 fn many_parties() {
118 let n = 10;
119 let mut agg = SecureAggregation::new(n);
120 let total: i64 = (1..=n as i64).sum();
121 for i in 0..n {
122 agg.set_value(i, (i + 1) as i64);
123 }
124 agg.establish_masks();
125 assert_eq!(agg.aggregate(), total);
126 }
127
128 #[test]
129 fn masked_values_hide_individuals() {
130 let mut agg = SecureAggregation::new(2);
131 agg.set_value(0, 100);
132 agg.set_value(1, 200);
133 agg.establish_masks();
134 let masked = agg.masked_values();
135 assert_ne!(masked[0], 100);
137 assert_ne!(masked[1], 200);
138 }
139
140 #[test]
141 fn zero_values() {
142 let mut agg = SecureAggregation::new(3);
143 agg.establish_masks();
144 assert_eq!(agg.aggregate(), 0);
145 }
146
147 #[test]
148 fn masks_symmetric() {
149 let mut agg = SecureAggregation::new(2);
150 agg.establish_masks();
151 let m01 = agg.parties[0].masks_to_apply.get(&1).copied().unwrap_or(0);
153 let m10 = agg.parties[1].masks_to_apply.get(&0).copied().unwrap_or(0);
154 assert_eq!(m01, -m10);
155 }
156}