core_crypto/mls/conversation/
commit_delay.rs1use itertools::Itertools;
2use log::{debug, trace};
3use openmls::prelude::LeafNodeIndex;
4
5use super::MlsConversation;
6
7const DELAY_RAMP_UP_MULTIPLIER: f32 = 120.0;
9const DELAY_RAMP_UP_SUB: u64 = 106;
10const DELAY_POS_LINEAR_INCR: u64 = 15;
11const DELAY_POS_LINEAR_RANGE: std::ops::RangeInclusive<u64> = 1..=3;
12
13impl MlsConversation {
14 pub fn compute_next_commit_delay(&self) -> Option<u64> {
20 use openmls::messages::proposals::Proposal;
21
22 if self.group.pending_proposals().next().is_none() {
23 trace!("No pending proposals, no delay needed");
24 return None;
25 }
26
27 let removed_index = self
28 .group
29 .pending_proposals()
30 .filter_map(|proposal| {
31 if let Proposal::Remove(remove_proposal) = proposal.proposal() {
32 Some(remove_proposal.removed())
33 } else {
34 None
35 }
36 })
37 .collect::<Vec<LeafNodeIndex>>();
38
39 let self_index = self.group.own_leaf_index();
40 debug!(removed_index:? = removed_index, self_index:? = self_index; "Indexes");
41 let is_self_removed = removed_index.contains(&self_index);
43
44 if is_self_removed {
46 debug!("Self removed from group, no delay needed");
47 return None;
48 }
49
50 let epoch = self.group.epoch().as_u64();
51
52 let self_position = self
54 .group
55 .members()
56 .find_position(|member| member.index == self_index)
57 .map(|pos| pos.0 as u64)
58 .expect("self_index should be in members since we already checked for self-removal");
59 let removed_indices_to_the_left = removed_index
60 .iter()
61 .filter(|index| index.u32() < self_index.u32())
62 .count() as u64;
63
64 let own_index = self_position - removed_indices_to_the_left;
67
68 let nb_members = (self.group.members().count() as u64).saturating_sub(removed_index.len() as u64);
70
71 Some(Self::calculate_delay(own_index, epoch, nb_members))
72 }
73
74 fn calculate_delay(self_index: u64, epoch: u64, nb_members: u64) -> u64 {
75 let position = if nb_members > 0 {
76 ((epoch % nb_members) + (self_index % nb_members)) % nb_members + 1
77 } else {
78 1
79 };
80
81 if DELAY_POS_LINEAR_RANGE.contains(&position) {
82 position.saturating_sub(1) * DELAY_POS_LINEAR_INCR
83 } else {
84 (((position as f32).ln() * DELAY_RAMP_UP_MULTIPLIER) as u64).saturating_sub(DELAY_RAMP_UP_SUB)
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::test_utils::*;
93
94 fn assert_delays_are_unique(delays: &[(&str, Option<u64>)]) {
95 for (i, (name_a, delay_a)) in delays.iter().enumerate() {
96 assert!(delay_a.is_some(), "{name_a} should have a delay");
97 for (name_b, delay_b) in &delays[i + 1..] {
98 assert_ne!(
99 delay_a, delay_b,
100 "{name_a} and {name_b} have the same commit delay; all delays: {delays:?}"
101 );
102 }
103 }
104 }
105
106 #[test]
107 fn calculate_delay_single() {
108 let (self_index, epoch, nb_members) = (0, 0, 1);
109 let delay = MlsConversation::calculate_delay(self_index, epoch, nb_members);
110 assert_eq!(delay, 0);
111 }
112
113 #[test]
114 fn calculate_delay_max() {
115 let (self_index, epoch, nb_members) = (u64::MAX, u64::MAX, u64::MAX);
116 let delay = MlsConversation::calculate_delay(self_index, epoch, nb_members);
117 assert_eq!(delay, 0);
118 }
119
120 #[test]
121 fn calculate_delay_min() {
122 let (self_index, epoch, nb_members) = (u64::MIN, u64::MIN, u64::MAX);
123 let delay = MlsConversation::calculate_delay(self_index, epoch, nb_members);
124 assert_eq!(delay, 0);
125 }
126
127 #[test]
128 fn calculate_delay_zero_members() {
129 let (self_index, epoch, nb_members) = (0, 0, u64::MIN);
130 let delay = MlsConversation::calculate_delay(self_index, epoch, nb_members);
131 assert_eq!(delay, 0);
132 }
133
134 #[test]
135 fn calculate_delay_min_max() {
136 let (self_index, epoch, nb_members) = (u64::MIN, u64::MAX, u64::MAX);
137 let delay = MlsConversation::calculate_delay(self_index, epoch, nb_members);
138 assert_eq!(delay, 0);
139 }
140
141 #[test]
142 fn calculate_delay_n() {
143 let epoch = 1;
144 let nb_members = 10;
145
146 let indexes_delays = [
147 (0, 15),
148 (1, 30),
149 (2, 60),
150 (3, 87),
151 (4, 109),
152 (5, 127),
153 (6, 143),
154 (7, 157),
155 (8, 170),
156 (9, 0),
157 (10, 15),
159 ];
160
161 for (self_index, expected_delay) in indexes_delays {
162 let delay = MlsConversation::calculate_delay(self_index, epoch, nb_members);
163 assert_eq!(delay, expected_delay);
164 }
165 }
166
167 #[apply(all_cred_cipher)]
168 async fn calculate_delay_creator_removed(case: TestContext) {
169 let [alice, bob, charlie] = case.sessions().await;
170 Box::pin(async move {
171 let conversation = case
172 .create_conversation([&alice, &bob])
173 .await
174 .invite_notify([&charlie])
175 .await;
176 assert_eq!(conversation.member_count().await, 3);
177
178 let proposal_guard = conversation.remove_proposal(&alice).await;
179 let (proposal_guard, result) = proposal_guard.notify_member_fallible(&bob).await;
180 let bob_decrypted_message = result.unwrap();
181 let (_, result) = proposal_guard.notify_member_fallible(&charlie).await;
182 let charlie_decrypted_message = result.unwrap();
183
184 let bob_hypothetical_position = 0;
185 let charlie_hypothetical_position = 1;
186
187 assert_eq!(
188 bob_decrypted_message.delay,
189 Some(DELAY_POS_LINEAR_INCR * bob_hypothetical_position)
190 );
191
192 assert_eq!(
193 charlie_decrypted_message.delay,
194 Some(DELAY_POS_LINEAR_INCR * charlie_hypothetical_position)
195 );
196 })
197 .await;
198 }
199
200 #[apply(all_cred_cipher)]
201 async fn calculate_delay_is_unique_with_blank_leaves(case: TestContext) {
202 let [alice, bob, charlie, dave, eve, frank] = case.sessions().await;
203 let conversation = case
205 .create_conversation([&alice, &bob, &charlie, &dave, &eve])
206 .await
207 .remove_notify(&bob)
209 .await;
210 assert_eq!(conversation.member_count().await, 4);
211
212 let mut proposal_guard = conversation.invite_proposal(&frank).await;
214 let mut delays = vec![(
215 "alice",
216 proposal_guard
217 .conversation()
218 .guard_of(&alice)
219 .await
220 .conversation_mut()
221 .await
222 .compute_next_commit_delay(),
223 )];
224 for (name, member) in [("charlie", &charlie), ("dave", &dave), ("eve", &eve)] {
225 let (guard, result) = proposal_guard.notify_member_fallible(member).await;
226 proposal_guard = guard;
227 delays.push((name, result.unwrap().delay));
228 }
229
230 assert_delays_are_unique(&delays);
231 }
232
233 #[apply(all_cred_cipher)]
234 async fn calculate_delay_with_blank_leaves_ignores_removed_members_to_the_right(case: TestContext) {
235 let [alice, bob, charlie, dave, eve] = case.sessions().await;
236 let conversation = case
238 .create_conversation([&alice, &bob, &charlie, &dave, &eve])
239 .await
240 .remove_notify(&bob)
242 .await
243 .remove_notify(&charlie)
244 .await;
245 assert_eq!(conversation.member_count().await, 3);
246
247 let proposal_guard = conversation.remove_proposal(&eve).await;
249 let alice_delay = proposal_guard
250 .conversation()
251 .guard_of(&alice)
252 .await
253 .conversation_mut()
254 .await
255 .compute_next_commit_delay();
256 let (_, result) = proposal_guard.notify_member_fallible(&dave).await;
257 let dave_delay = result.unwrap().delay;
258 let delays = vec![("alice", alice_delay), ("dave", dave_delay)];
259
260 assert_delays_are_unique(&delays);
262 }
263}