xmtp_mls/identity_updates/
dependencies.rs1#[cfg(test)]
4use super::load_identity_updates;
5use super::{get_association_state_for_client, load_identity_updates_for_client};
6use crate::{client::ClientError, context::XmtpSharedContext};
7use futures::{StreamExt, stream};
8use std::{
9 collections::{HashMap, HashSet},
10 sync::{Arc, OnceLock, Weak},
11};
12use thiserror::Error;
13use xmtp_common::{
14 RetryableError, retryable,
15 time::{Duration, Instant, sleep},
16};
17use xmtp_configuration::IDENTITY_REFERENCE_RETRY_INTERVAL;
18use xmtp_db::{DbQuery, StorageError, prelude::*};
19use xmtp_id::associations::{AssociationError, AssociationState};
20
21#[derive(Clone, Debug, Eq, Hash, PartialEq)]
23pub struct IdentityRequirement {
24 pub inbox_id: String,
26 pub sequence_id: u64,
28}
29
30#[derive(Debug, Error)]
32pub enum IdentityDependencyError {
33 #[error("A verified identity proof is required at sequence {}", .0.sequence_id)]
35 Need(IdentityRequirement),
36 #[error("Invalid identity sequence {0}")]
38 InvalidSequence(u64),
39 #[error("Identity reference is absent at sequence {}", .0.sequence_id)]
41 MissingReference(IdentityRequirement),
42 #[error(transparent)]
44 Client(#[from] Box<ClientError>),
45 #[error(transparent)]
47 Storage(#[from] StorageError),
48 #[error(transparent)]
50 Shared(Arc<IdentityDependencyError>),
51}
52
53impl From<ClientError> for IdentityDependencyError {
54 fn from(error: ClientError) -> Self {
55 Self::Client(Box::new(error))
56 }
57}
58
59impl RetryableError for IdentityDependencyError {
60 fn is_retryable(&self) -> bool {
61 match self {
62 Self::Need(_) => true,
63 Self::Client(error) => retryable!(error),
64 Self::Storage(error) => retryable!(error),
65 Self::Shared(error) => error.is_retryable(),
66 Self::InvalidSequence(_) | Self::MissingReference(_) => false,
67 }
68 }
69}
70
71type Resolution = tokio::sync::Mutex<Option<Result<(), Arc<IdentityDependencyError>>>>;
72
73impl crate::worker::NeedsDbReconnect for IdentityDependencyError {
74 fn needs_db_reconnect(&self) -> bool {
75 match self {
76 Self::Client(error) => error.db_needs_connection(),
77 Self::Storage(error) => error.db_needs_connection(),
78 Self::Shared(error) => error.needs_db_reconnect(),
79 Self::Need(_) | Self::InvalidSequence(_) | Self::MissingReference(_) => false,
80 }
81 }
82}
83
84#[derive(Default)]
86pub struct IdentityResolutionRegistry {
87 active: parking_lot::Mutex<HashMap<IdentityRequirement, Weak<Resolution>>>,
89 permits: OnceLock<Arc<tokio::sync::Semaphore>>,
91 own_inbox_refresh: tokio::sync::Mutex<()>,
93}
94
95impl IdentityResolutionRegistry {
96 pub(crate) fn own_inbox_refresh(&self) -> &tokio::sync::Mutex<()> {
97 &self.own_inbox_refresh
98 }
99
100 fn permits(&self, limit: usize) -> Arc<tokio::sync::Semaphore> {
101 self.permits
102 .get_or_init(|| Arc::new(tokio::sync::Semaphore::new(limit)))
103 .clone()
104 }
105
106 fn acquire(&self, requirement: &IdentityRequirement) -> Arc<Resolution> {
108 let mut active = self.active.lock();
109 active.retain(|_, gate| gate.strong_count() != 0);
110 if let Some(gate) = active.get(requirement).and_then(Weak::upgrade) {
111 return gate;
112 }
113 let gate = Arc::new(Resolution::default());
114 active.insert(requirement.clone(), Arc::downgrade(&gate));
115 gate
116 }
117}
118
119impl IdentityRequirement {
120 fn checked_sequence(&self) -> Result<i64, IdentityDependencyError> {
121 i64::try_from(self.sequence_id)
122 .ok()
123 .filter(|sequence| *sequence > 0)
124 .ok_or(IdentityDependencyError::InvalidSequence(self.sequence_id))
125 }
126}
127
128pub(crate) fn require_association_state(
132 conn: &impl DbQuery,
133 requirement: &IdentityRequirement,
134) -> Result<AssociationState, IdentityDependencyError> {
135 let sequence = requirement.checked_sequence()?;
136 let state = conn
137 .read_from_cache(&requirement.inbox_id, sequence)?
138 .ok_or_else(|| IdentityDependencyError::Need(requirement.clone()))?;
139 if state.inbox_id != requirement.inbox_id {
140 return Err(StorageError::DbDeserialize.into());
141 }
142 state
143 .try_into()
144 .map_err(StorageError::from)
145 .map_err(Into::into)
146}
147
148pub(crate) async fn resolve_identity_requirement(
153 context: &impl XmtpSharedContext,
154 requirement: &IdentityRequirement,
155) -> Result<(), IdentityDependencyError> {
156 match require_association_state(&context.db(), requirement) {
157 Ok(_) => return Ok(()),
158 Err(IdentityDependencyError::Need(_)) => {}
159 Err(error) => return Err(error),
160 }
161 let gate = context.identity_resolution_registry().acquire(requirement);
162 let mut resolution = gate.lock().await;
163 if resolution.is_none() {
164 let permits = context
165 .identity_resolution_registry()
166 .permits(context.incoming_runtime().policy().max_dependency_requests);
167 let _permit = permits
168 .acquire()
169 .await
170 .expect("identity request semaphore stays open");
171 *resolution = Some(
172 resolve_identity_requirement_with_wait(
173 context,
174 requirement,
175 context.incoming_runtime().policy().identity_reference_wait,
176 )
177 .await
178 .map_err(Arc::new),
179 );
180 }
181 match resolution.as_ref().expect("resolution is assigned") {
182 Ok(()) => Ok(()),
183 Err(error) => Err(match error.as_ref() {
184 IdentityDependencyError::MissingReference(requirement) => {
185 IdentityDependencyError::MissingReference(requirement.clone())
186 }
187 IdentityDependencyError::InvalidSequence(sequence) => {
188 IdentityDependencyError::InvalidSequence(*sequence)
189 }
190 _ => IdentityDependencyError::Shared(error.clone()),
191 }),
192 }
193}
194
195async fn resolve_identity_requirement_with_wait(
198 context: &impl XmtpSharedContext,
199 requirement: &IdentityRequirement,
200 absence_wait: Duration,
201) -> Result<(), IdentityDependencyError> {
202 let sequence = requirement.checked_sequence()?;
203 let mut healthy_since = None;
204 loop {
205 let conn = context.db();
206 match require_association_state(&conn, requirement) {
207 Ok(_) => return Ok(()),
208 Err(IdentityDependencyError::Need(_)) => {}
209 Err(error) => return Err(error),
210 }
211 load_identity_updates_for_client(&context, &conn, &[requirement.inbox_id.as_str()]).await?;
213 match get_association_state_for_client(
214 context,
215 &conn,
216 &requirement.inbox_id,
217 Some(sequence),
218 )
219 .await
220 {
221 Ok(_) => return Ok(()),
222 Err(ClientError::Association(AssociationError::MissingIdentityUpdate)) => {
223 let last = conn
226 .get_identity_updates(&requirement.inbox_id, None, Some(sequence))
227 .map_err(ClientError::from)?
228 .last()
229 .map(|update| update.sequence_id);
230 if let Some(last) = last {
231 get_association_state_for_client(
232 context,
233 &conn,
234 &requirement.inbox_id,
235 Some(last),
236 )
237 .await?;
238 }
239 }
240 Err(error) => return Err(error.into()),
241 }
242 let start = healthy_since.get_or_insert_with(Instant::now);
243 if start.elapsed() >= absence_wait {
244 return Err(IdentityDependencyError::MissingReference(
245 requirement.clone(),
246 ));
247 }
248 drop(conn);
249 sleep(IDENTITY_REFERENCE_RETRY_INTERVAL).await;
250 }
251}
252
253pub(crate) async fn resolve_identity_requirements(
257 context: &impl XmtpSharedContext,
258 requirements: impl IntoIterator<Item = IdentityRequirement>,
259) -> Vec<(IdentityRequirement, Result<(), IdentityDependencyError>)> {
260 let requirements: HashSet<_> = requirements.into_iter().collect();
261 stream::iter(requirements.into_iter().map(|requirement| async move {
262 let result = resolve_identity_requirement(context, &requirement).await;
263 (requirement, result)
264 }))
265 .buffer_unordered(context.incoming_runtime().policy().max_dependency_requests)
266 .collect()
267 .await
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::tester;
274
275 #[cfg(not(target_arch = "wasm32"))]
276 #[xmtp_common::test(unwrap_try = true)]
277 fn identity_and_query_futures_are_send() {
278 fn assert_send<T: Send>(_: T) {}
279 let context = crate::test::mock::context();
280 let requirement = IdentityRequirement {
281 inbox_id: "inbox".into(),
282 sequence_id: 1,
283 };
284 assert_send(resolve_identity_requirement(&context, &requirement));
285 assert_send(resolve_identity_requirements(
286 &context,
287 [requirement.clone()],
288 ));
289 assert_send(context.api().newest_topic_cursors(vec![]));
290 assert_send(context.api().query_ordered_page(
291 Default::default(),
292 1,
293 xmtp_proto::types::IncomingBatchLimits {
294 max_rows: 1,
295 max_bytes: 1024,
296 },
297 ));
298 let _: fn(&crate::Client<crate::MlsContext>) = |client| {
300 assert_send(async move { client.inbox_state(true).await });
301 assert_send(xmtp_common::bind_task_hub(async move {
302 client.inbox_state(true).await
303 }));
304 assert_send(async move {
305 client
306 .wait_for_registration_visible(Default::default())
307 .await
308 });
309 assert_send(async move { client.context.api().get_inbox_ids(vec![]).await });
310 assert_send(async move { client.context.api().newest_topic_cursors(vec![]).await });
311 };
312 }
313
314 #[xmtp_common::test(unwrap_try = true)]
315 async fn identity_requests_share_one_context_limit() {
316 let registry = IdentityResolutionRegistry::default();
317 let first = registry.permits(1);
318 let second = registry.permits(1);
319 assert!(Arc::ptr_eq(&first, &second));
320 let permit = first.acquire().await?;
321 assert!(second.try_acquire().is_err());
322 drop(permit);
323 assert!(second.try_acquire().is_ok());
324 }
325
326 #[xmtp_common::test(unwrap_try = true)]
327 async fn concurrent_requirements_share_one_attempt_but_later_calls_retry() {
328 let registry = IdentityResolutionRegistry::default();
329 let requirement = IdentityRequirement {
330 inbox_id: "inbox".into(),
331 sequence_id: 5,
332 };
333 let first = registry.acquire(&requirement);
334 let second = registry.acquire(&requirement);
335 assert!(Arc::ptr_eq(&first, &second));
336
337 let mut resolution = first.lock().await;
338 assert!(second.try_lock().is_err());
339 *resolution = Some(Err(Arc::new(IdentityDependencyError::InvalidSequence(5))));
340 drop(resolution);
341 assert!(matches!(second.lock().await.as_ref(), Some(Err(_))));
342
343 let completed = Arc::downgrade(&first);
344 drop(first);
345 drop(second);
346 let next = registry.acquire(&requirement);
347 assert!(completed.upgrade().is_none());
348 assert!(next.lock().await.is_none());
349 assert_eq!(registry.active.lock().len(), 1);
350 }
351
352 #[xmtp_common::test(unwrap_try = true)]
354 async fn historical_proof_does_not_use_a_newer_cached_snapshot() {
355 tester!(alix, disable_workers);
356 tester!(bo, disable_workers);
357 let first_sequence =
358 alix.context
359 .db()
360 .get_latest_sequence_id(&[alix.inbox_id()])?[alix.inbox_id()] as u64;
361 tester!(alix2, from: alix, disable_workers);
362 load_identity_updates(alix.context.api(), &alix.context.db(), &[alix.inbox_id()]).await?;
363 let last_sequence =
364 alix.context
365 .db()
366 .get_latest_sequence_id(&[alix.inbox_id()])?[alix.inbox_id()] as u64;
367 assert!(last_sequence > first_sequence);
368 let latest = IdentityRequirement {
369 inbox_id: alix.inbox_id().to_owned(),
370 sequence_id: last_sequence,
371 };
372 resolve_identity_requirement(&bo.context, &latest).await?;
373 let historical = IdentityRequirement {
374 sequence_id: first_sequence,
375 ..latest
376 };
377 assert!(matches!(
378 require_association_state(&bo.context.db(), &historical),
379 Err(IdentityDependencyError::Need(_))
380 ));
381 resolve_identity_requirement(&bo.context, &historical).await?;
382 let state = require_association_state(&bo.context.db(), &historical)?;
383 assert!(
384 state
385 .installation_ids()
386 .contains(&alix.installation_public_key().to_vec())
387 );
388 assert!(
389 !state
390 .installation_ids()
391 .contains(&alix2.installation_public_key().to_vec())
392 );
393 }
394
395 #[xmtp_common::test(unwrap_try = true)]
397 async fn dependency_batch_keeps_success_after_an_invalid_sibling() {
398 tester!(alix, disable_workers);
399 tester!(bo, disable_workers);
400 let sequence_id =
401 alix.context
402 .db()
403 .get_latest_sequence_id(&[alix.inbox_id()])?[alix.inbox_id()] as u64;
404 let valid = IdentityRequirement {
405 inbox_id: alix.inbox_id().to_owned(),
406 sequence_id,
407 };
408 let invalid = IdentityRequirement {
409 sequence_id: 0,
410 ..valid.clone()
411 };
412 let results =
413 resolve_identity_requirements(&bo.context, [valid.clone(), invalid, valid.clone()])
414 .await;
415 assert_eq!(results.len(), 2);
416 assert_eq!(
417 results.iter().filter(|(_, result)| result.is_ok()).count(),
418 1
419 );
420 assert!(require_association_state(&bo.context.db(), &valid).is_ok());
421 assert!(
422 results.iter().any(|(_, result)| matches!(
423 result,
424 Err(IdentityDependencyError::InvalidSequence(0))
425 ))
426 );
427 }
428
429 #[xmtp_common::test(unwrap_try = true)]
430 async fn primary_absence_is_terminal_only_after_successful_query() {
431 tester!(alix, disable_workers);
432 let requirement = IdentityRequirement {
433 inbox_id: hex::encode(xmtp_common::rand_vec::<32>()),
434 sequence_id: 1,
435 };
436 let result =
437 resolve_identity_requirement_with_wait(&alix.context, &requirement, Duration::ZERO)
438 .await;
439 assert!(
440 matches!(result, Err(IdentityDependencyError::MissingReference(found)) if found == requirement)
441 );
442 }
443}