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