1use crate::{
2 XmtpApi,
3 client::ClientError,
4 context::XmtpSharedContext,
5 groups::group_membership::{GroupMembership, MembershipDiff},
6 identity::{IdentityError, IdentityExt},
7};
8use futures::future::try_join_all;
9use std::collections::{HashMap, HashSet};
10use thiserror::Error;
11use xmtp_common::{Event, Retry, RetryableError, retry_async, retryable};
12use xmtp_cryptography::CredentialSign;
13use xmtp_db::StorageError;
14use xmtp_db::XmtpDb;
15use xmtp_db::prelude::*;
16use xmtp_db::{db_connection::DbConnection, identity_update::StoredIdentityUpdate};
17use xmtp_id::{
18 AsIdRef, InboxIdRef,
19 associations::{
20 AssociationError, AssociationState, Handle, Identifier, IdentityAction,
21 InstallationKeyContext, MemberIdentifier,
22 builder::{SignatureRequest, SignatureRequestBuilder, SignatureRequestError},
23 unverified::{
24 UnverifiedIdentityUpdate, UnverifiedInstallationKeySignature, UnverifiedSignature,
25 },
26 },
27 scw_verifier::SmartContractSignatureVerifier,
28};
29use xmtp_macro::log_event;
30use xmtp_proto::{
31 ShortHex,
32 api_client::XmtpBackendClient,
33 types::{Cursor, GroupId},
34};
35
36use xmtp_api::{ApiClientWrapper, GetIdentityUpdatesV2Filter};
37use xmtp_id::InboxUpdate;
38
39mod dependencies;
40mod state_store;
41pub use dependencies::{IdentityDependencyError, IdentityRequirement, IdentityResolutionRegistry};
42pub(crate) use dependencies::{
43 require_association_state, resolve_identity_requirement, resolve_identity_requirements,
44};
45pub(crate) use state_store::get_association_state_for_client;
46#[cfg(test)]
47pub(crate) use state_store::store_association_state;
48use state_store::{ResolvedState, resolve_association_state};
49
50#[derive(Debug, Error)]
51pub enum IdentityUpdateError {
52 #[error(transparent)]
53 InvalidSignatureRequest(#[from] SignatureRequestError),
54 #[error(transparent)]
55 Api(#[from] xmtp_api::ApiError),
56 #[error(transparent)]
57 Validation(#[from] xmtp_mls_validation::ValidationError),
58 #[error(transparent)]
59 Load(Box<ClientError>),
60}
61
62const IDENTITY_UPDATE_CONFLICT_RETRIES: usize = 3;
63
64pub(crate) async fn publish_with_conflict_retry<ApiClient: XmtpApi>(
67 api_client: &ApiClientWrapper<ApiClient>,
68 conn: &impl DbQuery,
69 update: UnverifiedIdentityUpdate,
70 verifier: &impl SmartContractSignatureVerifier,
71) -> Result<Cursor, IdentityUpdateError> {
72 for attempt in 0..=IDENTITY_UPDATE_CONFLICT_RETRIES {
73 match api_client.publish_identity_update(update.clone()).await {
74 Ok(cursor) => return Ok(cursor),
75 Err(xmtp_api::ApiError::IdentityUpdateConflict)
76 if attempt < IDENTITY_UPDATE_CONFLICT_RETRIES =>
77 {
78 load_identity_updates(api_client, conn, &[update.inbox_id.as_str()])
79 .await
80 .map_err(|error| IdentityUpdateError::Load(Box::new(error)))?;
81 let history = conn
82 .get_identity_updates(&update.inbox_id, None, None)
83 .map_err(|error| IdentityUpdateError::Load(Box::new(error.into())))?;
84 let history = history
85 .into_iter()
86 .map(|stored| stored.to_unverified().map(Into::into))
87 .collect::<Result<Vec<_>, _>>()
88 .map_err(|error| IdentityUpdateError::Load(Box::new(error.into())))?;
89 xmtp_mls_validation::validate_identity_updates(
90 history,
91 vec![update.clone().into()],
92 verifier,
93 )
94 .await?;
95 }
96 Err(error) => return Err(error.into()),
97 }
98 }
99 unreachable!("the final publish attempt returns its result")
100}
101
102#[derive(Debug)]
103pub struct InstallationDiff {
104 pub added_installations: HashSet<Vec<u8>>,
105 pub removed_installations: HashSet<Vec<u8>>,
106}
107
108#[derive(Debug, Error)]
109pub enum InstallationDiffError {
110 #[error(transparent)]
111 IdentityDependency(#[from] IdentityDependencyError),
112 #[error(transparent)]
113 Client(#[from] ClientError),
114 #[error(transparent)]
115 Db(#[from] xmtp_db::ConnectionError),
116 #[error(transparent)]
117 Storage(#[from] StorageError),
118}
119
120impl RetryableError for InstallationDiffError {
121 fn is_retryable(&self) -> bool {
122 match self {
123 InstallationDiffError::IdentityDependency(error) => retryable!(error),
124 InstallationDiffError::Client(client_error) => retryable!(client_error),
125 InstallationDiffError::Storage(e) => retryable!(e),
126 InstallationDiffError::Db(e) => retryable!(e),
127 }
128 }
129}
130
131pub struct IdentityUpdates<Context> {
132 context: Context,
133}
134
135impl<Context> IdentityUpdates<Context> {
136 pub fn new(context: Context) -> Self {
137 Self { context }
138 }
139}
140
141pub async fn get_association_state_with_verifier(
147 conn: &impl DbQuery,
148 inbox_id: &str,
149 to_sequence_id: Option<i64>,
150 scw_verifier: &impl SmartContractSignatureVerifier,
151) -> Result<AssociationState, ClientError> {
152 match resolve_association_state(conn, inbox_id, to_sequence_id, scw_verifier).await? {
153 ResolvedState::Cached(state) => Ok(state),
154 ResolvedState::Computed { sequence_id, state } => {
155 conn.write_to_cache(inbox_id.to_owned(), sequence_id, state.clone().into())?;
156 Ok(state)
157 }
158 }
159}
160
161pub async fn identifier_membership(
165 conn: &impl DbQuery,
166 inbox_id: &str,
167 identifier: &Identifier,
168 scw_verifier: &impl SmartContractSignatureVerifier,
169) -> Result<Option<bool>, ClientError> {
170 match get_association_state_with_verifier(conn, inbox_id, None, scw_verifier).await {
171 Ok(state) => Ok(Some(state.get(&identifier.clone().into()).is_some())),
172 Err(ClientError::Association(AssociationError::MissingIdentityUpdate)) => Ok(None),
173 Err(error) => Err(error),
174 }
175}
176
177pub fn identifier_opens_inbox(
181 membership: Option<bool>,
182 identifier: &Identifier,
183 nonce: u64,
184 inbox_id: &str,
185) -> bool {
186 membership.unwrap_or_else(|| identifier.inbox_id(nonce).is_ok_and(|own| own == inbox_id))
187}
188
189pub fn revoke_installations_with_verifier(
191 identifier: &Identifier,
192 inbox_id: &str,
193 installation_ids: Vec<Vec<u8>>,
194) -> Result<SignatureRequest, ClientError> {
195 let mut builder = SignatureRequestBuilder::new(inbox_id);
196
197 for installation_id in installation_ids {
198 builder = builder.revoke_association(
199 identifier.clone().into(),
200 MemberIdentifier::installation(installation_id),
201 )
202 }
203
204 Ok(builder.build())
205}
206
207pub async fn apply_signature_request_with_verifier<ApiClient: XmtpApi>(
214 api_client: &ApiClientWrapper<ApiClient>,
215 conn: &impl DbQuery,
216 signature_request: SignatureRequest,
217 scw_verifier: &impl SmartContractSignatureVerifier,
218) -> Result<(), ClientError> {
219 let identity_update = signature_request
221 .build_identity_update()
222 .map_err(IdentityUpdateError::from)?;
223
224 identity_update.to_verified(scw_verifier).await?;
225
226 publish_with_conflict_retry(api_client, conn, identity_update, scw_verifier).await?;
227
228 Ok(())
229}
230
231pub async fn batch_get_association_state_with_verifier(
235 conn: &impl DbQuery,
236 identifiers: &[(impl AsIdRef, Option<i64>)],
237 scw_verifier: &impl SmartContractSignatureVerifier,
238) -> Result<Vec<AssociationState>, ClientError> {
239 let association_states = try_join_all(
240 identifiers
241 .iter()
242 .map(|(inbox_id, to_sequence_id)| {
243 get_association_state_with_verifier(
244 conn,
245 inbox_id.as_ref(),
246 *to_sequence_id,
247 scw_verifier,
248 )
249 })
250 .collect::<Vec<_>>(),
251 )
252 .await?;
253
254 Ok(association_states)
255}
256
257impl<'a, Context> IdentityUpdates<Context>
258where
259 Context: XmtpSharedContext,
260{
261 pub async fn batch_get_association_state(
265 &self,
266 conn: &impl DbQuery,
267 identifiers: &[(impl AsIdRef, Option<i64>)],
268 ) -> Result<Vec<AssociationState>, ClientError> {
269 try_join_all(identifiers.iter().map(|(inbox_id, to_sequence_id)| {
270 get_association_state_for_client(
271 &self.context,
272 conn,
273 inbox_id.as_ref(),
274 *to_sequence_id,
275 )
276 }))
277 .await
278 }
279
280 #[tracing::instrument(level = "trace", skip_all)]
282 pub async fn get_latest_association_state(
283 &self,
284 conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
285 inbox_id: InboxIdRef<'a>,
286 ) -> Result<AssociationState, ClientError> {
287 load_identity_updates_for_client(&self.context, conn, &[inbox_id]).await?;
288 self.get_association_state(conn, inbox_id, None).await
289 }
290
291 fn emit_own_installation_changes(
292 &self,
293 inbox_id: InboxIdRef<'_>,
294 before: Option<&AssociationState>,
295 after: &AssociationState,
296 ) {
297 use xmtp_events::{ClientEvent, EventWriter, InstallationRef, InstallationRevoked};
298 if inbox_id != self.context.inbox_id() {
299 return;
300 }
301 let Some(before) = before else {
302 return;
303 };
304 let old: HashSet<_> = before.installation_ids().into_iter().collect();
305 let new: HashSet<_> = after.installation_ids().into_iter().collect();
306 for installation_key in new.difference(&old) {
307 self.context.events().emit(
308 Some(ClientEvent::IdentityOwnInstallationAdded(InstallationRef {
309 installation_key: installation_key.clone(),
310 })),
311 None,
312 );
313 }
314 for installation_key in old.difference(&new) {
315 self.context.events().emit(
316 Some(ClientEvent::IdentityOwnInstallationRevoked(
317 InstallationRevoked {
318 installation_key: installation_key.clone(),
319 is_this_installation: installation_key.as_slice()
320 == self.context.installation_id().to_vec().as_slice(),
321 },
322 )),
323 None,
324 );
325 }
326 }
327
328 pub(crate) async fn emit_own_installation_updates(
329 &self,
330 conn: &impl DbQuery,
331 inbox_id: InboxIdRef<'_>,
332 before: Option<AssociationState>,
333 previous_sequence: Option<i64>,
334 ) -> Result<(), ClientError> {
335 let (Some(mut state), Some(previous_sequence)) = (before, previous_sequence) else {
336 return Ok(());
337 };
338 if inbox_id != self.context.inbox_id() {
339 return Ok(());
340 }
341 let updates = conn.get_identity_updates(inbox_id, None, None)?;
342 for update in updates
343 .into_iter()
344 .filter(|update| update.sequence_id > previous_sequence)
345 {
346 let next = self
347 .get_association_state(conn, inbox_id, Some(update.sequence_id))
348 .await?;
349 self.emit_own_installation_changes(inbox_id, Some(&state), &next);
350 state = next;
351 }
352 Ok(())
353 }
354
355 pub async fn get_association_state(
358 &self,
359 conn: &impl xmtp_db::DbQuery,
360 inbox_id: InboxIdRef<'_>,
361 to_sequence_id: Option<i64>,
362 ) -> Result<AssociationState, ClientError> {
363 get_association_state_for_client(&self.context, conn, inbox_id, to_sequence_id).await
364 }
365
366 #[tracing::instrument(level = "trace", skip_all)]
369 pub async fn create_inbox(
370 &self,
371 identifier: Identifier,
372 maybe_nonce: Option<u64>,
373 ) -> Result<SignatureRequest, ClientError> {
374 let nonce = maybe_nonce.unwrap_or(0);
375 let inbox_id = identifier.inbox_id(nonce)?;
376 let installation_public_key = self.context.identity().installation_keys.verifying_key();
377
378 let builder = SignatureRequestBuilder::new(inbox_id);
379 let mut signature_request = builder
380 .create_inbox(identifier.clone(), nonce)
381 .add_association(
382 MemberIdentifier::installation(installation_public_key.as_bytes().to_vec()),
383 identifier.into(),
384 )
385 .build();
386
387 let sig_bytes = self
388 .context
389 .identity()
390 .sign_identity_update(signature_request.signature_text())?
391 .to_vec();
392 signature_request
394 .add_signature(
395 UnverifiedSignature::InstallationKey(UnverifiedInstallationKeySignature::new(
396 sig_bytes,
397 installation_public_key,
398 )),
399 &self.context.scw_verifier(),
400 )
401 .await?;
402
403 self.context
406 .server_configuration()
407 .restrict(&mut signature_request);
408
409 Ok(signature_request)
410 }
411
412 #[tracing::instrument(level = "trace", skip_all)]
414 pub async fn associate_identity(
415 &self,
416 new_identifier: Identifier,
417 ) -> Result<SignatureRequest, ClientError> {
418 tracing::info!(
419 "Associating new wallet with inbox_id {}",
420 self.context.inbox_id()
421 );
422 let inbox_id = self.context.inbox_id();
423 let builder = SignatureRequestBuilder::new(inbox_id);
424 let installation_public_key = self.context.identity().installation_keys.verifying_key();
425
426 let mut signature_request = builder
427 .add_association(new_identifier.into(), installation_public_key.into())
428 .build();
429
430 let signature = self
431 .context
432 .identity()
433 .installation_keys
434 .credential_sign::<InstallationKeyContext>(signature_request.signature_text())?;
435
436 signature_request
437 .add_signature(
438 UnverifiedSignature::new_installation_key(signature, installation_public_key),
439 &self.context.scw_verifier(),
440 )
441 .await?;
442
443 self.context
446 .server_configuration()
447 .restrict(&mut signature_request);
448
449 Ok(signature_request)
450 }
451
452 pub async fn claim_handle_request(
455 &self,
456 handle: Handle,
457 ) -> Result<SignatureRequest, ClientError> {
458 let installation_keys = &self.context.identity().installation_keys;
459 let installation_public_key = installation_keys.verifying_key();
460 let mut signature_request = SignatureRequestBuilder::new(self.context.inbox_id())
461 .claim_handle(handle, installation_public_key.into())
462 .build();
463 let signature = installation_keys
464 .credential_sign::<InstallationKeyContext>(signature_request.signature_text())?;
465 signature_request
466 .add_signature(
467 UnverifiedSignature::new_installation_key(signature, installation_public_key),
468 &self.context.scw_verifier(),
469 )
470 .await?;
471 Ok(signature_request)
472 }
473
474 pub async fn revoke_identities(
476 &self,
477 identities_to_revoke: Vec<Identifier>,
478 ) -> Result<SignatureRequest, ClientError> {
479 let inbox_id = self.context.inbox_id();
480 let current_state = retry_async!(
481 Retry::default(),
482 (async {
483 self.get_association_state(&self.context.db(), inbox_id, None)
484 .await
485 })
486 )?;
487 let mut builder = SignatureRequestBuilder::new(inbox_id);
488
489 for ident in identities_to_revoke {
490 builder = builder.revoke_association(
491 current_state.recovery_identifier().clone().into(),
492 ident.into(),
493 )
494 }
495
496 let mut signature_request = builder.build();
497 self.context
498 .server_configuration()
499 .restrict(&mut signature_request);
500 Ok(signature_request)
501 }
502
503 pub async fn revoke_installations(
505 &self,
506 installation_ids: Vec<Vec<u8>>,
507 ) -> Result<SignatureRequest, ClientError> {
508 let inbox_id = self.context.inbox_id();
509 let current_state = retry_async!(
510 Retry::default(),
511 (async {
512 self.get_association_state(&self.context.db(), inbox_id, None)
513 .await
514 })
515 )?;
516
517 let mut result = revoke_installations_with_verifier(
518 ¤t_state.recovery_identifier().clone(),
519 inbox_id,
520 installation_ids,
521 )?;
522 self.context.server_configuration().restrict(&mut result);
526
527 Ok(result)
528 }
529
530 pub async fn change_recovery_identifier(
532 &self,
533 new_recovery_identifier: Identifier,
534 ) -> Result<SignatureRequest, ClientError> {
535 let inbox_id = self.context.inbox_id();
536 let current_state = retry_async!(
537 Retry::default(),
538 (async {
539 self.get_association_state(&self.context.db(), inbox_id, None)
540 .await
541 })
542 )?;
543 let mut builder = SignatureRequestBuilder::new(inbox_id);
544 let member_identifier: MemberIdentifier =
545 current_state.recovery_identifier().clone().into();
546 builder = builder.change_recovery_address(member_identifier, new_recovery_identifier);
547 let mut signature_request = builder.build();
548 self.context
549 .server_configuration()
550 .restrict(&mut signature_request);
551 Ok(signature_request)
552 }
553
554 pub async fn apply_signature_request(
561 &self,
562 signature_request: SignatureRequest,
563 ) -> Result<(), ClientError> {
564 self.context.server_configuration().check()?;
566 let inbox_id = signature_request.inbox_id().to_string();
567 let _own_refresh = if inbox_id == self.context.inbox_id() {
568 Some(
569 self.context
570 .identity_resolution_registry()
571 .own_inbox_refresh()
572 .lock()
573 .await,
574 )
575 } else {
576 None
577 };
578 let previous_sequence = self
579 .context
580 .db()
581 .get_latest_sequence_id(&[inbox_id.as_str()])?
582 .get(&inbox_id)
583 .copied();
584 let before = if previous_sequence.is_some() {
585 Some(
586 self.get_association_state(&self.context.db(), &inbox_id, None)
587 .await?,
588 )
589 } else {
590 None
591 };
592
593 apply_signature_request_with_verifier(
594 self.context.api(),
595 &self.context.db(),
596 signature_request,
597 &self.context.scw_verifier(),
598 )
599 .await?;
600
601 retry_async!(
603 Retry::default(),
604 (async {
605 load_identity_updates(self.context.api(), &self.context.db(), &[inbox_id.as_str()])
606 .await
607 })
608 )?;
609
610 self.emit_own_installation_updates(
611 &self.context.db(),
612 &inbox_id,
613 before,
614 previous_sequence,
615 )
616 .await?;
617
618 Ok(())
619 }
620
621 #[tracing::instrument(level = "trace", skip_all)]
624 pub async fn get_installation_diff(
625 &self,
626 conn: &impl DbQuery,
627 group_id: &GroupId, old_group_membership: &GroupMembership,
629 new_group_membership: &GroupMembership,
630 membership_diff: &MembershipDiff<'_>,
631 ) -> Result<InstallationDiff, InstallationDiffError> {
632 let diff = loop {
633 match get_installation_diff_local(
634 conn,
635 old_group_membership,
636 new_group_membership,
637 membership_diff,
638 ) {
639 Ok(diff) => break diff,
640 Err(IdentityDependencyError::Need(requirement)) => {
641 resolve_identity_requirement(&self.context, &requirement).await?;
642 }
643 Err(error) => return Err(error.into()),
644 }
645 };
646 let InstallationDiff {
647 added_installations,
648 removed_installations,
649 } = diff;
650
651 if !added_installations.is_empty() || !removed_installations.is_empty() {
652 let added_installations: Vec<_> =
653 added_installations.iter().map(|i| i.short_hex()).collect();
654 let removed_installations: Vec<_> = removed_installations
655 .iter()
656 .map(|i| i.short_hex())
657 .collect();
658 log_event!(
659 Event::UpdatedGroupMembership,
660 self.context.installation_id(),
661 #group_id,
662 ?added_installations,
663 ?removed_installations
664 );
665 }
666
667 Ok(InstallationDiff {
668 added_installations,
669 removed_installations,
670 })
671 }
672}
673
674pub(crate) fn get_installation_diff_local(
678 conn: &impl DbQuery,
679 old_membership: &GroupMembership,
680 new_membership: &GroupMembership,
681 membership_diff: &MembershipDiff<'_>,
682) -> Result<InstallationDiff, IdentityDependencyError> {
683 let mut added_installations = HashSet::new();
684 let mut removed_installations = HashSet::new();
685 for inbox_id in membership_diff
686 .added_inboxes
687 .iter()
688 .chain(&membership_diff.updated_inboxes)
689 {
690 let final_state = require_association_state(
691 conn,
692 &IdentityRequirement {
693 inbox_id: (*inbox_id).clone(),
694 sequence_id: new_membership.get(inbox_id).copied().unwrap_or(0),
695 },
696 )?;
697 let diff = match old_membership.get(inbox_id) {
698 None | Some(0) => final_state.as_diff(),
699 Some(sequence_id) => require_association_state(
700 conn,
701 &IdentityRequirement {
702 inbox_id: (*inbox_id).clone(),
703 sequence_id: *sequence_id,
704 },
705 )?
706 .diff(&final_state),
707 };
708 added_installations.extend(diff.new_installations());
709 removed_installations.extend(diff.removed_installations());
710 }
711 for inbox_id in &membership_diff.removed_inboxes {
712 let state = require_association_state(
713 conn,
714 &IdentityRequirement {
715 inbox_id: (*inbox_id).clone(),
716 sequence_id: old_membership.get(inbox_id).copied().unwrap_or(0),
717 },
718 )?;
719 removed_installations.extend(state.installation_ids());
720 }
721 Ok(InstallationDiff {
722 added_installations,
723 removed_installations,
724 })
725}
726
727pub(crate) async fn load_identity_updates_for_client<Context: XmtpSharedContext>(
729 context: &Context,
730 conn: &impl DbQuery,
731 inbox_ids: &[&str],
732) -> Result<HashMap<String, Vec<InboxUpdate>>, ClientError> {
733 let own_inbox = context.inbox_id();
734 let includes_own = inbox_ids.contains(&own_inbox);
735 let _own_refresh = if includes_own {
736 Some(
737 context
738 .identity_resolution_registry()
739 .own_inbox_refresh()
740 .lock()
741 .await,
742 )
743 } else {
744 None
745 };
746 let previous_sequence = if includes_own {
747 conn.get_latest_sequence_id(&[own_inbox])?
748 .get(own_inbox)
749 .copied()
750 } else {
751 None
752 };
753 let before = if previous_sequence.is_some() {
754 Some(get_association_state_for_client(context, conn, own_inbox, None).await?)
755 } else {
756 None
757 };
758 let updates = load_identity_updates(context.api(), conn, inbox_ids).await?;
759 IdentityUpdates::new(context)
760 .emit_own_installation_updates(conn, own_inbox, before, previous_sequence)
761 .await?;
762 Ok(updates)
763}
764
765#[tracing::instrument(level = "trace", skip_all)]
768pub async fn load_identity_updates<ApiClient: XmtpApi>(
769 api_client: &ApiClientWrapper<ApiClient>,
770 conn: &impl xmtp_db::DbQuery,
771 inbox_ids: &[&str],
772) -> Result<HashMap<String, Vec<InboxUpdate>>, ClientError> {
773 if inbox_ids.is_empty() {
774 return Ok(HashMap::new());
775 }
776 tracing::debug!("Fetching identity updates for: {:?}", inbox_ids);
777
778 let existing_sequence_ids = conn.get_latest_sequence_id(inbox_ids)?;
779 let filters: Vec<GetIdentityUpdatesV2Filter> = inbox_ids
780 .iter()
781 .map(|inbox_id| GetIdentityUpdatesV2Filter {
782 sequence_id: existing_sequence_ids.get(*inbox_id).map(|i| *i as u64),
783 inbox_id: inbox_id.to_string(),
784 })
785 .collect();
786
787 let updates = api_client.get_identity_updates_v2(filters).await?;
788 let updates = updates
789 .into_iter()
790 .map(|(inbox_id, entries)| {
791 let entries = entries
792 .into_iter()
793 .map(|entry| {
794 Ok(InboxUpdate {
795 sequence_id: entry
796 .meta
797 .cursor
798 .ok_or(xmtp_api::ApiError::InvalidResponse("identity cursor"))?
799 .sequence_id,
800 server_timestamp_ns: entry.meta.server_ns,
801 update: entry.update.try_into()?,
802 })
803 })
804 .collect::<Result<Vec<_>, ClientError>>()?;
805 Ok((inbox_id, entries))
806 })
807 .collect::<Result<HashMap<_, _>, ClientError>>()?;
808 let to_store = updates
809 .iter()
810 .flat_map(move |(inbox_id, updates)| {
811 updates.iter().map(move |update| StoredIdentityUpdate {
812 inbox_id: inbox_id.clone(),
813 sequence_id: update.sequence_id as i64,
814 server_timestamp_ns: update.server_timestamp_ns as i64,
815 payload: update.update.clone().into(),
816 })
817 })
818 .collect::<Vec<StoredIdentityUpdate>>();
819
820 conn.insert_or_ignore_identity_updates(&to_store)?;
821 Ok(updates)
822}
823
824pub async fn is_member_of_association_state<Client>(
826 api_client: &ApiClientWrapper<Client>,
827 inbox_id: &str,
828 identifier: &MemberIdentifier,
829 scw_verifier: Option<Box<dyn SmartContractSignatureVerifier>>,
830) -> Result<bool, ClientError>
831where
832 Client: XmtpBackendClient + Clone,
833{
834 let filters = vec![GetIdentityUpdatesV2Filter {
835 inbox_id: inbox_id.to_string(),
836 sequence_id: None,
837 }];
838 let mut updates = api_client.get_identity_updates_v2(filters).await?;
839
840 let Some(updates) = updates.remove(inbox_id) else {
841 return Err(ClientError::Generic(
842 "Unable to find provided inbox_id".to_string(),
843 ));
844 };
845 let updates: Vec<UnverifiedIdentityUpdate> = updates
846 .into_iter()
847 .map(|u| u.update.try_into())
848 .collect::<Result<_, _>>()?;
849
850 let mut association_state = None;
851
852 let scw_verifier = scw_verifier
853 .unwrap_or_else(|| Box::new(api_client.clone()) as Box<dyn SmartContractSignatureVerifier>);
854
855 let updates: Vec<_> = updates
856 .iter()
857 .map(|u| u.to_verified(&scw_verifier))
858 .collect();
859 let updates = try_join_all(updates).await?;
860
861 for update in updates {
862 association_state =
863 Some(update.update_state(association_state, update.client_timestamp_ns)?);
864 }
865 let association_state = association_state.ok_or(ClientError::Generic(
866 "Unable to create association state".to_string(),
867 ))?;
868
869 Ok(association_state.get(identifier).is_some())
870}
871
872#[tracing::instrument(level = "trace", skip_all)]
873pub async fn get_creation_signature_kind(
874 conn: &impl xmtp_db::DbQuery,
875 scw_verifier: impl SmartContractSignatureVerifier,
876 inbox_id: InboxIdRef<'_>,
877) -> Result<Option<xmtp_id::associations::SignatureKind>, ClientError> {
878 let updates = conn.get_identity_updates(inbox_id, None, None)?;
879
880 let first_update = updates
881 .first()
882 .ok_or_else(|| ClientError::Identity(IdentityError::RequiredIdentityNotFound))?;
883
884 let unverified_update: UnverifiedIdentityUpdate = first_update.clone().to_unverified()?;
885
886 let verified = unverified_update.to_verified(scw_verifier).await?;
887
888 Ok(verified.creation_signature_kind())
889}
890
891#[cfg(test)]
892pub(crate) mod tests {
893 #![allow(unused)] use crate::{
895 Client, XmtpApi,
896 builder::ClientBuilder,
897 context::XmtpSharedContext,
898 groups::group_membership::GroupMembership,
899 identity_updates::IdentityUpdates,
900 tester,
901 utils::{FullXmtpClient, Tester},
902 };
903 use alloy::signers::Signer;
904 use xmtp_cryptography::utils::generate_local_wallet;
905 use xmtp_id::{
906 InboxOwner,
907 associations::{
908 AssociationState, MemberIdentifier,
909 builder::{SignatureRequest, SignatureRequestError},
910 test_utils::{MockSmartContractSignatureVerifier, WalletTestExt, add_wallet_signature},
911 unverified::UnverifiedSignature,
912 },
913 };
914
915 use xmtp_db::{
916 ConnectionExt, db_connection::DbConnection, identity_update::StoredIdentityUpdate,
917 prelude::*,
918 };
919 use xmtp_proto::types::GroupId;
920
921 use xmtp_common::rand_vec;
922
923 use super::{is_member_of_association_state, load_identity_updates};
924
925 async fn get_association_state<Context>(
926 client: &Client<Context>,
927 inbox_id: &str,
928 ) -> AssociationState
929 where
930 Context: XmtpSharedContext,
931 {
932 let conn = client.context.db();
933 load_identity_updates(client.context.api(), &conn, &[inbox_id])
934 .await
935 .unwrap();
936
937 IdentityUpdates::new(&client.context)
938 .get_association_state(&conn, inbox_id, None)
939 .await
940 .unwrap()
941 }
942
943 fn insert_identity_update<C>(conn: &DbConnection<C>, inbox_id: &str, sequence_id: i64)
944 where
945 C: ConnectionExt,
946 {
947 let identity_update =
948 StoredIdentityUpdate::new(inbox_id.to_string(), sequence_id, 0, rand_vec::<24>());
949
950 conn.insert_or_ignore_identity_updates(&[identity_update])
951 .expect("insert should succeed");
952 }
953
954 #[rstest::rstest]
955 #[xmtp_common::test]
956 async fn test_is_member_of_association_state() {
957 let wallet = generate_local_wallet();
958 let client = ClientBuilder::new_test_client(&wallet).await;
959
960 let wallet2 = generate_local_wallet();
961 let client_identity_updates = IdentityUpdates::new(&client.context);
962
963 let mut request = client_identity_updates
964 .associate_identity(wallet2.identifier())
965 .await
966 .unwrap();
967 add_wallet_signature(&mut request, &wallet2).await;
968 client_identity_updates
969 .apply_signature_request(request)
970 .await
971 .unwrap();
972
973 let conn = client.context.db();
974
975 xmtp_common::wait_for_eq(
978 || async {
979 client_identity_updates
980 .get_latest_association_state(&conn, client.inbox_id())
981 .await
982 .unwrap()
983 .members()
984 .len()
985 },
986 3,
987 )
988 .await
989 .unwrap();
990
991 let api_client = client.context.api();
992
993 let is_member = is_member_of_association_state(
995 api_client,
996 client.inbox_id(),
997 &wallet2.member_identifier(),
998 None,
999 )
1000 .await
1001 .unwrap();
1002
1003 assert!(is_member);
1004 }
1005
1006 #[rstest::rstest]
1007 #[xmtp_common::test]
1008 async fn create_inbox_round_trip() {
1009 let wallet = generate_local_wallet();
1010 let wallet_ident = wallet.identifier();
1011 let client = ClientBuilder::new_test_client(&wallet).await;
1012
1013 let mut signature_request: SignatureRequest = client
1014 .identity_updates()
1015 .create_inbox(wallet_ident.clone(), None)
1016 .await
1017 .unwrap();
1018 let inbox_id = signature_request.inbox_id().to_string();
1019
1020 add_wallet_signature(&mut signature_request, &wallet).await;
1021
1022 client
1023 .identity_updates()
1024 .apply_signature_request(signature_request)
1025 .await
1026 .unwrap();
1027
1028 let association_state = get_association_state(&client, &inbox_id).await;
1029
1030 assert_eq!(association_state.members().len(), 2);
1031 assert_eq!(association_state.recovery_identifier(), &wallet_ident);
1032 assert!(association_state.get(&wallet_ident.into()).is_some())
1033 }
1034
1035 #[rstest::rstest]
1036 #[xmtp_common::test]
1037 async fn add_association() {
1038 let wallet_2 = generate_local_wallet();
1039 let wallet2_ident = wallet_2.identifier();
1040
1041 tester!(client);
1042
1043 let mut add_association_request = client
1044 .identity_updates()
1045 .associate_identity(wallet2_ident.clone())
1046 .await
1047 .unwrap();
1048
1049 add_wallet_signature(&mut add_association_request, &wallet_2).await;
1050
1051 client
1052 .identity_updates()
1053 .apply_signature_request(add_association_request)
1054 .await
1055 .unwrap();
1056 let association_state = get_association_state(&client, client.inbox_id()).await;
1057
1058 let members = association_state
1059 .members_by_parent(&client.builder.owner.get_identifier().unwrap().into());
1060 for member in members {
1062 assert!(member.client_timestamp_ns.is_some());
1063 }
1064
1065 assert_eq!(association_state.members().len(), 3);
1066 assert_eq!(
1067 *association_state.recovery_identifier(),
1068 client.builder.owner.get_identifier().unwrap()
1069 );
1070 assert!(association_state.get(&wallet2_ident.into()).is_some());
1071 }
1072
1073 #[cfg_attr(not(target_arch = "wasm32"), test)]
1074 #[cfg(not(target_arch = "wasm32"))]
1075 fn cache_association_state() {
1076 use std::sync::Arc;
1077
1078 use xmtp_common::{assert_logged, traced_test::LOG_BUFFER};
1079
1080 use crate::{
1081 utils::LocalTester, worker::device_sync::DeviceSyncClient,
1082 worker::metrics::WorkerMetrics,
1083 };
1084
1085 xmtp_common::traced_test!(async {
1086 let client = Tester::new().await;
1087 let inbox_id = client.inbox_id();
1088 let metrics = WorkerMetrics::new(client.context.installation_id());
1089 let device_sync = DeviceSyncClient::new(&client.context, Arc::new(metrics));
1090 device_sync.wait_for_sync_worker_init().await;
1091
1092 let wallet_2 = generate_local_wallet();
1093
1094 get_association_state(&client, inbox_id).await;
1095
1096 assert_logged!("Loaded association", 0);
1097 assert_logged!("Wrote association", 1);
1099
1100 let association_state = get_association_state(&client, inbox_id).await;
1101
1102 assert_eq!(association_state.members().len(), 2);
1103 assert_eq!(
1104 association_state.recovery_identifier(),
1105 &client.builder.owner.identifier()
1106 );
1107 assert!(
1108 association_state
1109 .get(&client.builder.owner.identifier().into())
1110 .is_some()
1111 );
1112
1113 assert_logged!("Loaded association", 1);
1114 assert_logged!("Wrote association", 1);
1115
1116 let mut add_association_request = client
1117 .identity_updates()
1118 .associate_identity(wallet_2.identifier())
1119 .await
1120 .unwrap();
1121
1122 add_wallet_signature(&mut add_association_request, &wallet_2).await;
1123
1124 client
1125 .identity_updates()
1126 .apply_signature_request(add_association_request)
1127 .await
1128 .unwrap();
1129
1130 assert_logged!("Wrote association", 2);
1131 LOG_BUFFER.with(|buf| buf.clear());
1133 get_association_state(&client, inbox_id).await;
1134
1135 assert_logged!("Loaded association", 1);
1136 assert_logged!("Wrote association", 0);
1137
1138 let association_state = get_association_state(&client, inbox_id).await;
1139
1140 assert_logged!("Loaded association", 2);
1141 assert_logged!("Wrote association", 0);
1142
1143 assert_eq!(association_state.members().len(), 3);
1144 assert_eq!(
1145 association_state.recovery_identifier(),
1146 &client.builder.owner.identifier()
1147 );
1148 assert!(
1149 association_state
1150 .get(&wallet_2.member_identifier())
1151 .is_some()
1152 );
1153 });
1154 }
1155
1156 #[rstest::rstest]
1157 #[xmtp_common::test]
1158 async fn load_identity_updates_if_needed() {
1159 let wallet = generate_local_wallet();
1160 let client = ClientBuilder::new_test_client(&wallet).await;
1161 let conn = client.context.db();
1162
1163 insert_identity_update(&conn, "inbox_1", 1);
1164 insert_identity_update(&conn, "inbox_2", 2);
1165 insert_identity_update(&conn, "inbox_3", 3);
1166
1167 let filtered =
1168 crate::groups::filter_inbox_ids_needing_updates(&conn, &[("inbox_1", 3), ("inbox_2", 2), ("inbox_3", 2)]);
1171 assert_eq!(filtered.unwrap(), vec!["inbox_1"]);
1172 }
1173
1174 #[rstest::rstest]
1175 #[xmtp_common::test]
1176 async fn get_installation_diff() {
1177 let wallet_1 = generate_local_wallet();
1178 let wallet_2 = generate_local_wallet();
1179 let wallet_3 = generate_local_wallet();
1180
1181 let client_1 = ClientBuilder::new_test_client(&wallet_1).await;
1182 let client_2 = ClientBuilder::new_test_client(&wallet_2).await;
1183 let client_3 = ClientBuilder::new_test_client(&wallet_3).await;
1184
1185 let client_2_installation_key = client_2.installation_public_key().to_vec();
1186 let client_3_installation_key = client_3.installation_public_key().to_vec();
1187
1188 let mut inbox_ids: Vec<String> = vec![];
1189
1190 for (client, wallet) in [
1192 (client_1, wallet_1),
1193 (client_2, wallet_2),
1194 (client_3, wallet_3),
1195 ] {
1196 let mut signature_request: SignatureRequest = client
1197 .identity_updates()
1198 .create_inbox(wallet.identifier(), None)
1199 .await
1200 .unwrap();
1201 let inbox_id = signature_request.inbox_id().to_string();
1202 inbox_ids.push(inbox_id);
1203
1204 add_wallet_signature(&mut signature_request, &wallet).await;
1205 client
1206 .identity_updates()
1207 .apply_signature_request(signature_request)
1208 .await
1209 .unwrap();
1210 let new_wallet = generate_local_wallet();
1211 let mut add_association_request = client
1212 .identity_updates()
1213 .associate_identity(new_wallet.identifier())
1214 .await
1215 .unwrap();
1216
1217 add_wallet_signature(&mut add_association_request, &new_wallet).await;
1218
1219 client
1220 .identity_updates()
1221 .apply_signature_request(add_association_request)
1222 .await
1223 .unwrap();
1224 }
1225
1226 let other_client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
1228 let other_conn = other_client.context.db();
1229 let ids = inbox_ids.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
1230 load_identity_updates(other_client.context.api(), &other_conn, ids.as_slice())
1232 .await
1233 .expect("load should succeed");
1234
1235 let latest_sequence_ids = other_conn.get_latest_sequence_id(ids.as_slice()).unwrap();
1237
1238 let inbox_1_first_sequence_id = other_conn
1239 .get_identity_updates(inbox_ids[0].clone(), None, None)
1240 .unwrap()
1241 .first()
1242 .unwrap()
1243 .sequence_id;
1244
1245 let mut original_group_membership = GroupMembership::new();
1246 original_group_membership.add(inbox_ids[0].to_string(), inbox_1_first_sequence_id as u64);
1247 original_group_membership.add(
1248 inbox_ids[1].to_string(),
1249 *latest_sequence_ids.get(&inbox_ids[1]).unwrap() as u64,
1250 );
1251
1252 let mut new_group_membership = original_group_membership.clone();
1253 new_group_membership.add(
1255 inbox_ids[0].to_string(),
1256 *latest_sequence_ids.get(&inbox_ids[0]).unwrap() as u64,
1257 );
1258 new_group_membership.add(
1259 inbox_ids[2].to_string(),
1260 *latest_sequence_ids.get(&inbox_ids[2]).unwrap() as u64,
1261 );
1262 new_group_membership.remove(&inbox_ids[1]);
1263
1264 let membership_diff = original_group_membership.diff(&new_group_membership);
1265
1266 let installation_diff = other_client
1267 .identity_updates()
1268 .get_installation_diff(
1269 &other_conn,
1270 &GroupId::default(),
1271 &original_group_membership,
1272 &new_group_membership,
1273 &membership_diff,
1274 )
1275 .await
1276 .unwrap();
1277
1278 assert_eq!(installation_diff.added_installations.len(), 1);
1279 assert!(
1280 installation_diff
1281 .added_installations
1282 .contains(&client_3_installation_key.to_vec()),
1283 );
1284 assert_eq!(installation_diff.removed_installations.len(), 1);
1285 assert!(
1286 installation_diff
1287 .removed_installations
1288 .contains(&client_2_installation_key.to_vec())
1289 );
1290 }
1291
1292 #[rstest::rstest]
1296 #[xmtp_common::test]
1297 async fn get_installation_diff_rejects_added_inbox_at_sequence_zero() {
1298 let wallet = generate_local_wallet();
1299 let client = ClientBuilder::new_test_client(&wallet).await;
1300
1301 let mut signature_request: SignatureRequest = client
1302 .identity_updates()
1303 .create_inbox(wallet.identifier(), None)
1304 .await
1305 .unwrap();
1306 let inbox_id = signature_request.inbox_id().to_string();
1307 add_wallet_signature(&mut signature_request, &wallet).await;
1308 client
1309 .identity_updates()
1310 .apply_signature_request(signature_request)
1311 .await
1312 .unwrap();
1313
1314 let other_client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
1317 let other_conn = other_client.context.db();
1318 load_identity_updates(
1319 other_client.context.api(),
1320 &other_conn,
1321 &[inbox_id.as_str()],
1322 )
1323 .await
1324 .expect("load should succeed");
1325 let latest_sequence_id = *other_conn
1326 .get_latest_sequence_id(&[inbox_id.as_str()])
1327 .unwrap()
1328 .get(&inbox_id)
1329 .unwrap();
1330
1331 let old_membership = GroupMembership::new();
1332
1333 let mut honest_membership = GroupMembership::new();
1335 honest_membership.add(inbox_id.clone(), latest_sequence_id as u64);
1336 other_client
1337 .identity_updates()
1338 .get_installation_diff(
1339 &other_conn,
1340 &GroupId::default(),
1341 &old_membership,
1342 &honest_membership,
1343 &old_membership.diff(&honest_membership),
1344 )
1345 .await
1346 .expect("an add at a real sequence id is valid");
1347
1348 let mut crafted_membership = GroupMembership::new();
1350 crafted_membership.add(inbox_id.clone(), 0);
1351 let crafted_diff = old_membership.diff(&crafted_membership);
1352 assert_eq!(crafted_diff.added_inboxes, vec![&inbox_id]);
1353
1354 let result = other_client
1355 .identity_updates()
1356 .get_installation_diff(
1357 &other_conn,
1358 &GroupId::default(),
1359 &old_membership,
1360 &crafted_membership,
1361 &crafted_diff,
1362 )
1363 .await;
1364
1365 assert!(
1366 result.is_err(),
1367 "an inbox added at sequence_id 0 must be rejected, not resolved at latest"
1368 );
1369 }
1370
1371 #[rstest::rstest]
1372 #[xmtp_common::test]
1373 pub async fn revoke_wallet() {
1374 let recovery_wallet = generate_local_wallet();
1375 let second_wallet = generate_local_wallet();
1376 let client = ClientBuilder::new_test_client(&recovery_wallet).await;
1377
1378 let mut add_wallet_signature_request = client
1379 .identity_updates()
1380 .associate_identity(second_wallet.identifier())
1381 .await
1382 .unwrap();
1383
1384 add_wallet_signature(&mut add_wallet_signature_request, &second_wallet).await;
1385
1386 client
1387 .identity_updates()
1388 .apply_signature_request(add_wallet_signature_request)
1389 .await
1390 .unwrap();
1391
1392 let association_state_after_add = get_association_state(&client, client.inbox_id()).await;
1393 assert_eq!(association_state_after_add.identifiers().len(), 2);
1394
1395 let inbox_ids = client
1397 .context
1398 .api()
1399 .get_inbox_ids(vec![second_wallet.identifier().into()])
1400 .await
1401 .unwrap();
1402 assert_eq!(inbox_ids.len(), 1);
1403
1404 let mut revoke_signature_request = client
1407 .identity_updates()
1408 .revoke_identities(vec![second_wallet.identifier()])
1409 .await
1410 .unwrap();
1411 add_wallet_signature(&mut revoke_signature_request, &recovery_wallet).await;
1412
1413 client
1414 .identity_updates()
1415 .apply_signature_request(revoke_signature_request)
1416 .await
1417 .unwrap();
1418
1419 let association_state_after_revoke =
1421 get_association_state(&client, client.inbox_id()).await;
1422 assert_eq!(association_state_after_revoke.identifiers().len(), 1);
1423
1424 let inbox_ids = client
1426 .context
1427 .api()
1428 .get_inbox_ids(vec![second_wallet.identifier().into()])
1429 .await
1430 .unwrap();
1431 assert_eq!(inbox_ids, vec![None]);
1432 }
1433
1434 #[rstest::rstest]
1435 #[xmtp_common::test]
1436 pub async fn revoke_installation() {
1437 use xmtp_events::{ClientEvent, EventFilter, EventKind};
1438 let wallet = generate_local_wallet();
1439 let client1: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1440 let client2: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1441
1442 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1443 assert_eq!(association_state.installation_ids().len(), 2);
1445 let events = client1.context.events().subscribe(
1446 EventFilter::new([EventKind::IdentityOwnInstallationRevoked]),
1447 Some(10),
1448 );
1449
1450 let mut revoke_installation_request = client1
1452 .identity_updates()
1453 .revoke_installations(vec![client2.installation_public_key().to_vec()])
1454 .await
1455 .unwrap();
1456 add_wallet_signature(&mut revoke_installation_request, &wallet).await;
1457 assert!(events.drain().is_empty());
1458 client1
1459 .identity_updates()
1460 .apply_signature_request(revoke_installation_request)
1461 .await
1462 .unwrap();
1463
1464 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1466 assert_eq!(association_state.installation_ids().len(), 1);
1467 let revoked = events.drain();
1468 assert_eq!(revoked.len(), 1);
1469 assert!(matches!(&revoked[0].client,
1470 Some(ClientEvent::IdentityOwnInstallationRevoked(event))
1471 if event.installation_key == client2.installation_public_key()
1472 && !event.is_this_installation));
1473 }
1474
1475 #[rstest::rstest]
1477 #[xmtp_common::test(unwrap_try = true)]
1478 async fn direct_own_inbox_refresh_reports_each_installation_transition() {
1479 use xmtp_events::{ClientEvent, EventFilter, EventKind};
1480 let wallet = generate_local_wallet();
1481 let observer: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1482 observer.inbox_state(false).await?;
1483 let events = observer.context.events().subscribe(
1484 EventFilter::new([
1485 EventKind::IdentityOwnInstallationAdded,
1486 EventKind::IdentityOwnInstallationRevoked,
1487 ]),
1488 Some(10),
1489 );
1490
1491 let transient: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1492 let actor: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1493 let mut request = actor
1494 .identity_updates()
1495 .revoke_installations(vec![transient.installation_public_key().to_vec()])
1496 .await?;
1497 add_wallet_signature(&mut request, &wallet).await;
1498 actor
1499 .identity_updates()
1500 .apply_signature_request(request)
1501 .await?;
1502
1503 observer.inbox_state(true).await?;
1504 let changes = events.drain();
1505 assert_eq!(changes.len(), 3);
1506 assert!(changes.iter().any(|event| matches!(
1507 &event.client,
1508 Some(ClientEvent::IdentityOwnInstallationAdded(added))
1509 if added.installation_key == transient.installation_public_key().to_vec()
1510 )));
1511 assert!(changes.iter().any(|event| matches!(
1512 &event.client,
1513 Some(ClientEvent::IdentityOwnInstallationRevoked(revoked))
1514 if revoked.installation_key == transient.installation_public_key().to_vec()
1515 )));
1516 assert!(changes.iter().any(|event| matches!(
1517 &event.client,
1518 Some(ClientEvent::IdentityOwnInstallationAdded(added))
1519 if added.installation_key == actor.installation_public_key().to_vec()
1520 )));
1521 }
1522
1523 #[rstest::rstest]
1525 #[xmtp_common::test(unwrap_try = true)]
1526 async fn concurrent_own_inbox_refresh_emits_one_addition() {
1527 use xmtp_events::{ClientEvent, EventFilter, EventKind};
1528
1529 let wallet = generate_local_wallet();
1530 let observer: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1531 observer.inbox_state(false).await?;
1532 let events = observer.context.events().subscribe(
1533 EventFilter::new([EventKind::IdentityOwnInstallationAdded]),
1534 Some(10),
1535 );
1536 let added: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1537
1538 let (first, second) = tokio::join!(observer.inbox_state(true), observer.inbox_state(true));
1539 first?;
1540 second?;
1541 let additions = events.drain();
1542 assert!(matches!(additions.as_slice(), [event]
1543 if matches!(&event.client,
1544 Some(ClientEvent::IdentityOwnInstallationAdded(value))
1545 if value.installation_key == added.installation_public_key().to_vec()
1546 )
1547 ));
1548 }
1549
1550 #[cfg(not(target_arch = "wasm32"))]
1551 #[tokio::test(flavor = "multi_thread")]
1552 pub async fn revoke_installation_with_malformed_keypackage() {
1553 use crate::utils::test_mocks_helpers::set_test_mode_upload_malformed_keypackage;
1554
1555 let wallet = generate_local_wallet();
1556 let client1: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1557 let client2: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1558
1559 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1560 assert_eq!(association_state.installation_ids().len(), 2);
1562
1563 set_test_mode_upload_malformed_keypackage(
1564 true,
1565 Some(vec![client2.installation_public_key().to_vec()]),
1566 );
1567
1568 let mut revoke_installation_request = client1
1570 .identity_updates()
1571 .revoke_installations(vec![client2.installation_public_key().to_vec()])
1572 .await
1573 .unwrap();
1574 add_wallet_signature(&mut revoke_installation_request, &wallet).await;
1575 client1
1576 .identity_updates()
1577 .apply_signature_request(revoke_installation_request)
1578 .await
1579 .unwrap();
1580
1581 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1583 assert_eq!(association_state.installation_ids().len(), 1);
1584 }
1585
1586 #[cfg(not(target_arch = "wasm32"))]
1587 #[tokio::test(flavor = "multi_thread")]
1588 pub async fn revoke_good_installation_with_other_malformed_keypackage() {
1589 use crate::utils::test_mocks_helpers::set_test_mode_upload_malformed_keypackage;
1590
1591 let wallet = generate_local_wallet();
1592 let client1: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1593 let client2: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1594 let client3: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1595
1596 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1597 assert_eq!(association_state.installation_ids().len(), 3);
1599
1600 set_test_mode_upload_malformed_keypackage(
1601 true,
1602 Some(vec![client2.installation_public_key().to_vec()]),
1603 );
1604
1605 let mut revoke_installation_request = client1
1607 .identity_updates()
1608 .revoke_installations(vec![client3.installation_public_key().to_vec()])
1609 .await
1610 .unwrap();
1611 add_wallet_signature(&mut revoke_installation_request, &wallet).await;
1612 client1
1613 .identity_updates()
1614 .apply_signature_request(revoke_installation_request)
1615 .await
1616 .unwrap();
1617
1618 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1620 assert_eq!(association_state.installation_ids().len(), 2);
1621 }
1622
1623 #[rstest::rstest]
1624 #[xmtp_common::test]
1625 pub async fn change_recovery_address() {
1626 let original_wallet = generate_local_wallet();
1627 let new_recovery_wallet = generate_local_wallet();
1628 let client = ClientBuilder::new_test_client(&original_wallet).await;
1629
1630 let association_state_before = get_association_state(&client, client.inbox_id()).await;
1632 assert_eq!(
1633 association_state_before.recovery_identifier(),
1634 &original_wallet.identifier()
1635 );
1636
1637 assert!(association_state_before.members().len() == 2);
1639 let binding = association_state_before.members();
1641 let recovery_member = binding
1642 .iter()
1643 .find(|m| m.identifier == original_wallet.identifier());
1644 assert!(recovery_member.is_some());
1645 let recovery_member_timestamp = recovery_member.unwrap().client_timestamp_ns;
1646 assert!(recovery_member_timestamp.is_none());
1648 let installation_member = binding
1650 .iter()
1651 .find(|m| matches!(m.identifier, MemberIdentifier::Installation(_)));
1652 assert!(installation_member.is_some());
1653 assert!(
1654 installation_member
1655 .unwrap()
1656 .identifier
1657 .installation_key()
1658 .unwrap()
1659 == client.installation_public_key().to_vec()
1660 );
1661 let installation_member_timestamp = installation_member.unwrap().client_timestamp_ns;
1662 assert!(installation_member_timestamp.is_some());
1663
1664 let mut change_recovery_request = client
1666 .identity_updates()
1667 .change_recovery_identifier(new_recovery_wallet.identifier())
1668 .await
1669 .unwrap();
1670
1671 add_wallet_signature(&mut change_recovery_request, &original_wallet).await;
1673
1674 client
1676 .identity_updates()
1677 .apply_signature_request(change_recovery_request)
1678 .await
1679 .unwrap();
1680
1681 let association_state_after = get_association_state(&client, client.inbox_id()).await;
1683 assert_eq!(
1684 association_state_after.recovery_identifier(),
1685 &new_recovery_wallet.identifier()
1686 );
1687
1688 assert!(association_state_after.members().len() == 2);
1690 let binding = association_state_after.members();
1692 let recovery_member = binding
1693 .iter()
1694 .find(|m| m.identifier == original_wallet.identifier());
1695 assert!(recovery_member.is_some());
1696 let recovery_member_timestamp = recovery_member.unwrap().client_timestamp_ns;
1697 assert!(recovery_member_timestamp.is_none());
1699 let installation_member = binding
1701 .iter()
1702 .find(|m| matches!(m.identifier, MemberIdentifier::Installation(_)));
1703 assert!(installation_member.is_some());
1704 assert!(
1705 installation_member
1706 .unwrap()
1707 .identifier
1708 .installation_key()
1709 .unwrap()
1710 == client.installation_public_key().to_vec()
1711 );
1712 let installation_member_timestamp = installation_member.unwrap().client_timestamp_ns;
1713 assert!(installation_member_timestamp.is_some());
1714
1715 let installation_id = client.installation_public_key().to_vec();
1718 let mut revoke_installation_request = client
1719 .identity_updates()
1720 .revoke_installations(vec![installation_id])
1721 .await
1722 .unwrap();
1723
1724 let signature_text = revoke_installation_request.signature_text();
1727 let sig = original_wallet
1728 .sign_message(signature_text.as_bytes())
1729 .await
1730 .unwrap()
1731 .as_bytes()
1732 .to_vec();
1733 let unverified_sig = UnverifiedSignature::new_recoverable_ecdsa(sig);
1734 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
1735
1736 let attempt_to_revoke_with_original_wallet = revoke_installation_request
1737 .add_signature(unverified_sig, &scw_verifier)
1738 .await;
1739
1740 assert!(matches!(
1741 attempt_to_revoke_with_original_wallet,
1742 Err(SignatureRequestError::UnknownSigner)
1743 ));
1744
1745 let installation_id = client.installation_public_key().to_vec();
1747 let mut revoke_installation_request = client
1748 .identity_updates()
1749 .revoke_installations(vec![installation_id])
1750 .await
1751 .unwrap();
1752
1753 add_wallet_signature(&mut revoke_installation_request, &new_recovery_wallet).await;
1755
1756 client
1758 .identity_updates()
1759 .apply_signature_request(revoke_installation_request)
1760 .await
1761 .unwrap();
1762
1763 let association_state_final = get_association_state(&client, client.inbox_id()).await;
1765 assert_eq!(association_state_final.installation_ids().len(), 0);
1766 }
1767}
1768
1769#[cfg(test)]
1770mod conflict_tests {
1771 use super::*;
1772 use crate::tester;
1773 use xmtp_cryptography::utils::generate_local_wallet;
1774 use xmtp_id::associations::test_utils::{
1775 MockSmartContractSignatureVerifier, WalletTestExt, add_wallet_signature,
1776 };
1777 use xmtp_proto::{api::ApiClientError, backend_v1 as wire, types::Topic};
1778
1779 #[rstest::rstest]
1780 #[case(0)]
1781 #[case(1)]
1782 #[case(3)]
1783 #[case(4)]
1784 #[xmtp_common::test(unwrap_try = true)]
1785 async fn conflict_reloads_validates_and_bounds_identical_resends(#[case] conflicts: usize) {
1786 tester!(alix, disable_workers);
1787 let wallet = generate_local_wallet();
1788 let mut request = alix
1789 .identity_updates()
1790 .associate_identity(wallet.identifier())
1791 .await
1792 .unwrap();
1793 add_wallet_signature(&mut request, &wallet).await;
1794 let update = request.build_identity_update().unwrap();
1795 let expected: xmtp_proto::xmtp::identity::associations::IdentityUpdate =
1796 update.clone().into();
1797 let history = alix
1798 .context
1799 .api()
1800 .query_all(
1801 [(
1802 Topic::new_identity_update(hex::decode(alix.inbox_id()).unwrap()),
1803 Cursor(0),
1804 )]
1805 .into(),
1806 alix.context.api().limits().max_query_limit as u32,
1807 )
1808 .await
1809 .unwrap();
1810 let last_sequence = history
1811 .last()
1812 .unwrap()
1813 .meta
1814 .as_ref()
1815 .unwrap()
1816 .cursor
1817 .as_ref()
1818 .unwrap()
1819 .sequence_id;
1820 let mut mock = xmtp_api_backend::MockBackendClient::new();
1821 let mut calls = 0;
1822 mock.expect_publish()
1823 .times((conflicts + 1).min(4))
1824 .returning(move |request| {
1825 calls += 1;
1826 assert_eq!(request.envelopes.len(), 1);
1827 assert_eq!(
1828 request.envelopes[0].payload,
1829 Some(wire::client_envelope::Payload::IdentityUpdate(
1830 expected.clone()
1831 ))
1832 );
1833 if calls <= conflicts {
1834 return Err(ApiClientError::client(
1835 xmtp_api_grpc::error::GrpcError::Status(tonic::Status::aborted(
1836 "identity conflict",
1837 )),
1838 ));
1839 }
1840 let parsed =
1841 xmtp_mls_validation::parse_envelope(request.envelopes[0].clone()).unwrap();
1842 Ok(wire::PublishResponse {
1843 envelope_metas: vec![wire::EnvelopeMeta {
1844 cursor: Some(wire::Cursor {
1845 sequence_id: last_sequence + 1,
1846 }),
1847 topic: Some(wire::Topic {
1848 topic: parsed.topic.cloned_vec(),
1849 }),
1850 message_hash: Some(wire::MessageHash {
1851 hash: Some(wire::message_hash::Hash::Sha256(
1852 parsed.canonical.hash.to_vec(),
1853 )),
1854 }),
1855 ..Default::default()
1856 }],
1857 })
1858 });
1859 mock.expect_query()
1860 .times(conflicts.min(3))
1861 .returning(move |request| {
1862 assert_eq!(request.queries.len(), 1);
1863 let floor = request.queries[0].cursor.as_ref().unwrap().sequence_id;
1864 Ok(wire::QueryResponse {
1865 envelopes: history
1866 .iter()
1867 .filter(|entry| {
1868 entry
1869 .meta
1870 .as_ref()
1871 .unwrap()
1872 .cursor
1873 .as_ref()
1874 .unwrap()
1875 .sequence_id
1876 > floor
1877 })
1878 .cloned()
1879 .collect(),
1880 continuation: Some(wire::Continuation { has_more: false }),
1881 })
1882 });
1883 let api = ApiClientWrapper::new(mock, Default::default());
1884 let result = publish_with_conflict_retry(
1885 &api,
1886 &alix.context.db(),
1887 update,
1888 &MockSmartContractSignatureVerifier::new(true),
1889 )
1890 .await;
1891 if conflicts > 3 {
1892 assert!(matches!(
1893 result,
1894 Err(IdentityUpdateError::Api(
1895 xmtp_api::ApiError::IdentityUpdateConflict
1896 ))
1897 ));
1898 } else {
1899 assert_eq!(result.unwrap(), Cursor(last_sequence + 1));
1900 }
1901 }
1902
1903 #[xmtp_common::test(flavor = "multi_thread", worker_threads = 4, unwrap_try = true)]
1904 async fn two_clients_racing_identity_updates_keep_both_associations() {
1905 tester!(alix, disable_workers);
1906 tester!(alix2, from: alix, disable_workers);
1907 use xmtp_proto::api::HasStats;
1908 let first_stats = alix.context.api().api_client.mls_stats();
1909 let second_stats = alix2.context.api().api_client.mls_stats();
1910 const MAX_RACE_ROUNDS: usize = 16;
1911 const UPDATES_PER_RACE: usize = 2;
1912 for _ in 0..MAX_RACE_ROUNDS {
1913 let first_wallet = generate_local_wallet();
1914 let second_wallet = generate_local_wallet();
1915 let mut first = alix
1916 .identity_updates()
1917 .associate_identity(first_wallet.identifier())
1918 .await?;
1919 let mut second = alix2
1920 .identity_updates()
1921 .associate_identity(second_wallet.identifier())
1922 .await?;
1923 add_wallet_signature(&mut first, &first_wallet).await;
1924 add_wallet_signature(&mut second, &second_wallet).await;
1925 let first_updates = alix.identity_updates();
1926 let second_updates = alix2.identity_updates();
1927 first_stats.clear();
1928 second_stats.clear();
1929 let (first, second) = {
1930 xmtp_common::wasm_or_native! {
1931 native => {
1932 let barrier = std::sync::Barrier::new(UPDATES_PER_RACE);
1934 let runtime = tokio::runtime::Handle::current();
1935 std::thread::scope(|scope| {
1936 let first = scope.spawn(|| {
1937 barrier.wait();
1938 runtime.block_on(first_updates.apply_signature_request(first))
1939 });
1940 let second = scope.spawn(|| {
1941 barrier.wait();
1942 runtime.block_on(second_updates.apply_signature_request(second))
1943 });
1944 (first.join().unwrap(), second.join().unwrap())
1945 })
1946 },
1947 wasm => {
1948 futures::join!(
1949 first_updates.apply_signature_request(first),
1950 second_updates.apply_signature_request(second),
1951 )
1952 }
1953 }
1954 };
1955 first?;
1956 second?;
1957 let publishes = first_stats.publish.get_count() + second_stats.publish.get_count();
1958 let conn = alix.context.db();
1959 load_identity_updates(alix.context.api(), &conn, &[alix.inbox_id()]).await?;
1960 let state = alix
1961 .identity_updates()
1962 .get_association_state(&conn, alix.inbox_id(), None)
1963 .await?;
1964 assert!(state.get(&first_wallet.identifier().into()).is_some());
1965 assert!(state.get(&second_wallet.identifier().into()).is_some());
1966 if publishes > UPDATES_PER_RACE {
1967 tracing::info!(publishes, "Both identity updates survived a publish retry");
1968 return;
1969 }
1970 }
1971 panic!("No identity publish retry occurred in {MAX_RACE_ROUNDS} races");
1972 }
1973}