1use crate::{
2 XmtpApi,
3 client::ClientError,
4 context::XmtpSharedContext,
5 groups::group_membership::{GroupMembership, MembershipDiff},
6 identity::{IdentityError, IdentityExt},
7 subscriptions::SyncWorkerEvent,
8};
9use futures::future::try_join_all;
10use std::collections::{HashMap, HashSet};
11use thiserror::Error;
12use xmtp_common::{Event, Retry, RetryableError, retry_async, retryable};
13use xmtp_cryptography::CredentialSign;
14use xmtp_db::StorageError;
15use xmtp_db::XmtpDb;
16use xmtp_db::prelude::*;
17use xmtp_db::{db_connection::DbConnection, identity_update::StoredIdentityUpdate};
18use xmtp_id::associations::verify_updates;
19use xmtp_id::{
20 AsIdRef, InboxIdRef,
21 associations::{
22 AssociationError, AssociationState, Identifier, IdentityAction, InstallationKeyContext,
23 MemberIdentifier,
24 builder::{SignatureRequest, SignatureRequestBuilder, SignatureRequestError},
25 get_state,
26 unverified::{
27 UnverifiedIdentityUpdate, UnverifiedInstallationKeySignature, UnverifiedSignature,
28 },
29 },
30 scw_verifier::SmartContractSignatureVerifier,
31};
32use xmtp_macro::log_event;
33use xmtp_proto::{
34 ShortHex,
35 api_client::XmtpBackendClient,
36 types::{Cursor, GroupId},
37};
38
39use xmtp_api::{ApiClientWrapper, GetIdentityUpdatesV2Filter};
40use xmtp_id::InboxUpdate;
41
42mod dependencies;
43pub use dependencies::{IdentityDependencyError, IdentityRequirement, IdentityResolutionRegistry};
44pub(crate) use dependencies::{
45 require_association_state, resolve_identity_requirement, resolve_identity_requirements,
46};
47
48#[derive(Debug, Error)]
49pub enum IdentityUpdateError {
50 #[error(transparent)]
51 InvalidSignatureRequest(#[from] SignatureRequestError),
52 #[error(transparent)]
53 Api(#[from] xmtp_api::ApiError),
54 #[error(transparent)]
55 Validation(#[from] xmtp_mls_validation::ValidationError),
56 #[error(transparent)]
57 Load(Box<ClientError>),
58}
59
60const IDENTITY_UPDATE_CONFLICT_RETRIES: usize = 3;
61
62pub(crate) async fn publish_with_conflict_retry<ApiClient: XmtpApi>(
65 api_client: &ApiClientWrapper<ApiClient>,
66 conn: &impl DbQuery,
67 update: UnverifiedIdentityUpdate,
68 verifier: &impl SmartContractSignatureVerifier,
69) -> Result<Cursor, IdentityUpdateError> {
70 for attempt in 0..=IDENTITY_UPDATE_CONFLICT_RETRIES {
71 match api_client.publish_identity_update(update.clone()).await {
72 Ok(cursor) => return Ok(cursor),
73 Err(xmtp_api::ApiError::IdentityUpdateConflict)
74 if attempt < IDENTITY_UPDATE_CONFLICT_RETRIES =>
75 {
76 load_identity_updates(api_client, conn, &[update.inbox_id.as_str()])
77 .await
78 .map_err(|error| IdentityUpdateError::Load(Box::new(error)))?;
79 let history = conn
80 .get_identity_updates(&update.inbox_id, None, None)
81 .map_err(|error| IdentityUpdateError::Load(Box::new(error.into())))?;
82 let history = history
83 .into_iter()
84 .map(|stored| stored.to_unverified().map(Into::into))
85 .collect::<Result<Vec<_>, _>>()
86 .map_err(|error| IdentityUpdateError::Load(Box::new(error.into())))?;
87 xmtp_mls_validation::validate_identity_updates(
88 history,
89 vec![update.clone().into()],
90 verifier,
91 )
92 .await?;
93 }
94 Err(error) => return Err(error.into()),
95 }
96 }
97 unreachable!("the final publish attempt returns its result")
98}
99
100#[derive(Debug)]
101pub struct InstallationDiff {
102 pub added_installations: HashSet<Vec<u8>>,
103 pub removed_installations: HashSet<Vec<u8>>,
104}
105
106#[derive(Debug, Error)]
107pub enum InstallationDiffError {
108 #[error(transparent)]
109 IdentityDependency(#[from] IdentityDependencyError),
110 #[error(transparent)]
111 Client(#[from] ClientError),
112 #[error(transparent)]
113 Db(#[from] xmtp_db::ConnectionError),
114 #[error(transparent)]
115 Storage(#[from] StorageError),
116}
117
118impl RetryableError for InstallationDiffError {
119 fn is_retryable(&self) -> bool {
120 match self {
121 InstallationDiffError::IdentityDependency(error) => retryable!(error),
122 InstallationDiffError::Client(client_error) => retryable!(client_error),
123 InstallationDiffError::Storage(e) => retryable!(e),
124 InstallationDiffError::Db(e) => retryable!(e),
125 }
126 }
127}
128
129pub struct IdentityUpdates<Context> {
130 context: Context,
131}
132
133impl<Context> IdentityUpdates<Context> {
134 pub fn new(context: Context) -> Self {
135 Self { context }
136 }
137}
138
139pub async fn get_association_state_with_verifier(
142 conn: &impl DbQuery,
143 inbox_id: &str,
144 to_sequence_id: Option<i64>,
145 scw_verifier: &impl SmartContractSignatureVerifier,
146) -> Result<AssociationState, ClientError> {
147 let updates = conn.get_identity_updates(inbox_id, None, to_sequence_id)?;
148 let last_sequence_id = updates
149 .last()
150 .ok_or::<ClientError>(AssociationError::MissingIdentityUpdate.into())?
151 .sequence_id;
152 if let Some(to_sequence_id) = to_sequence_id
153 && to_sequence_id != last_sequence_id
154 {
155 return Err(AssociationError::MissingIdentityUpdate.into());
156 }
157
158 if let Some(association_state) = conn.read_from_cache(inbox_id, last_sequence_id)? {
159 return Ok(association_state.try_into().map_err(StorageError::from)?);
160 }
161
162 let unverified_updates = updates
163 .into_iter()
164 .map(StoredIdentityUpdate::to_unverified)
166 .collect::<Result<Vec<UnverifiedIdentityUpdate>, AssociationError>>()?;
167 let updates = verify_updates(unverified_updates, scw_verifier).await?;
168
169 let association_state = get_state(updates)?;
170
171 conn.write_to_cache(
172 inbox_id.to_owned(),
173 last_sequence_id,
174 association_state.clone().into(),
175 )?;
176
177 Ok(association_state)
178}
179
180pub fn revoke_installations_with_verifier(
182 identifier: &Identifier,
183 inbox_id: &str,
184 installation_ids: Vec<Vec<u8>>,
185) -> Result<SignatureRequest, ClientError> {
186 let mut builder = SignatureRequestBuilder::new(inbox_id);
187
188 for installation_id in installation_ids {
189 builder = builder.revoke_association(
190 identifier.clone().into(),
191 MemberIdentifier::installation(installation_id),
192 )
193 }
194
195 Ok(builder.build())
196}
197
198pub async fn apply_signature_request_with_verifier<ApiClient: XmtpApi>(
205 api_client: &ApiClientWrapper<ApiClient>,
206 conn: &impl DbQuery,
207 signature_request: SignatureRequest,
208 scw_verifier: &impl SmartContractSignatureVerifier,
209) -> Result<(), ClientError> {
210 let identity_update = signature_request
212 .build_identity_update()
213 .map_err(IdentityUpdateError::from)?;
214
215 identity_update.to_verified(scw_verifier).await?;
216
217 publish_with_conflict_retry(api_client, conn, identity_update, scw_verifier).await?;
218
219 Ok(())
220}
221
222pub async fn batch_get_association_state_with_verifier(
226 conn: &impl DbQuery,
227 identifiers: &[(impl AsIdRef, Option<i64>)],
228 scw_verifier: &impl SmartContractSignatureVerifier,
229) -> Result<Vec<AssociationState>, ClientError> {
230 let association_states = try_join_all(
231 identifiers
232 .iter()
233 .map(|(inbox_id, to_sequence_id)| {
234 get_association_state_with_verifier(
235 conn,
236 inbox_id.as_ref(),
237 *to_sequence_id,
238 scw_verifier,
239 )
240 })
241 .collect::<Vec<_>>(),
242 )
243 .await?;
244
245 Ok(association_states)
246}
247
248impl<'a, Context> IdentityUpdates<Context>
249where
250 Context: XmtpSharedContext,
251{
252 pub async fn batch_get_association_state(
256 &self,
257 conn: &impl DbQuery,
258 identifiers: &[(impl AsIdRef, Option<i64>)],
259 ) -> Result<Vec<AssociationState>, ClientError> {
260 let verifier = self.context.scw_verifier();
261 batch_get_association_state_with_verifier(conn, identifiers, &verifier).await
262 }
263
264 #[tracing::instrument(level = "trace", skip_all)]
266 pub async fn get_latest_association_state(
267 &self,
268 conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
269 inbox_id: InboxIdRef<'a>,
270 ) -> Result<AssociationState, ClientError> {
271 load_identity_updates(self.context.api(), conn, &[inbox_id]).await?;
272
273 self.get_association_state(conn, inbox_id, None).await
274 }
275
276 pub async fn get_association_state(
279 &self,
280 conn: &impl xmtp_db::DbQuery,
281 inbox_id: InboxIdRef<'_>,
282 to_sequence_id: Option<i64>,
283 ) -> Result<AssociationState, ClientError> {
284 let verifier = self.context.scw_verifier();
285 get_association_state_with_verifier(conn, inbox_id, to_sequence_id, &verifier).await
286 }
287
288 #[tracing::instrument(level = "trace", skip_all)]
291 pub async fn create_inbox(
292 &self,
293 identifier: Identifier,
294 maybe_nonce: Option<u64>,
295 ) -> Result<SignatureRequest, ClientError> {
296 let nonce = maybe_nonce.unwrap_or(0);
297 let inbox_id = identifier.inbox_id(nonce)?;
298 let installation_public_key = self.context.identity().installation_keys.verifying_key();
299
300 let builder = SignatureRequestBuilder::new(inbox_id);
301 let mut signature_request = builder
302 .create_inbox(identifier.clone(), nonce)
303 .add_association(
304 MemberIdentifier::installation(installation_public_key.as_bytes().to_vec()),
305 identifier.into(),
306 )
307 .build();
308
309 let sig_bytes = self
310 .context
311 .identity()
312 .sign_identity_update(signature_request.signature_text())?
313 .to_vec();
314 signature_request
316 .add_signature(
317 UnverifiedSignature::InstallationKey(UnverifiedInstallationKeySignature::new(
318 sig_bytes,
319 installation_public_key,
320 )),
321 &self.context.scw_verifier(),
322 )
323 .await?;
324
325 self.context
328 .server_configuration()
329 .restrict(&mut signature_request);
330
331 Ok(signature_request)
332 }
333
334 #[tracing::instrument(level = "trace", skip_all)]
336 pub async fn associate_identity(
337 &self,
338 new_identifier: Identifier,
339 ) -> Result<SignatureRequest, ClientError> {
340 tracing::info!(
341 "Associating new wallet with inbox_id {}",
342 self.context.inbox_id()
343 );
344 let inbox_id = self.context.inbox_id();
345 let builder = SignatureRequestBuilder::new(inbox_id);
346 let installation_public_key = self.context.identity().installation_keys.verifying_key();
347
348 let mut signature_request = builder
349 .add_association(new_identifier.into(), installation_public_key.into())
350 .build();
351
352 let signature = self
353 .context
354 .identity()
355 .installation_keys
356 .credential_sign::<InstallationKeyContext>(signature_request.signature_text())?;
357
358 signature_request
359 .add_signature(
360 UnverifiedSignature::new_installation_key(signature, installation_public_key),
361 &self.context.scw_verifier(),
362 )
363 .await?;
364
365 self.context
368 .server_configuration()
369 .restrict(&mut signature_request);
370
371 Ok(signature_request)
372 }
373
374 pub async fn revoke_identities(
376 &self,
377 identities_to_revoke: Vec<Identifier>,
378 ) -> Result<SignatureRequest, ClientError> {
379 let inbox_id = self.context.inbox_id();
380 let current_state = retry_async!(
381 Retry::default(),
382 (async {
383 self.get_association_state(&self.context.db(), inbox_id, None)
384 .await
385 })
386 )?;
387 let mut builder = SignatureRequestBuilder::new(inbox_id);
388
389 for ident in identities_to_revoke {
390 builder = builder.revoke_association(
391 current_state.recovery_identifier().clone().into(),
392 ident.into(),
393 )
394 }
395
396 let mut signature_request = builder.build();
397 self.context
398 .server_configuration()
399 .restrict(&mut signature_request);
400 Ok(signature_request)
401 }
402
403 pub async fn revoke_installations(
405 &self,
406 installation_ids: Vec<Vec<u8>>,
407 ) -> Result<SignatureRequest, ClientError> {
408 let inbox_id = self.context.inbox_id();
409 let current_state = retry_async!(
410 Retry::default(),
411 (async {
412 self.get_association_state(&self.context.db(), inbox_id, None)
413 .await
414 })
415 )?;
416
417 let mut result = revoke_installations_with_verifier(
418 ¤t_state.recovery_identifier().clone(),
419 inbox_id,
420 installation_ids,
421 )?;
422 self.context.server_configuration().restrict(&mut result);
426
427 let _ = self
428 .context
429 .worker_events()
430 .send(SyncWorkerEvent::CycleHMAC);
431
432 Ok(result)
433 }
434
435 pub async fn change_recovery_identifier(
437 &self,
438 new_recovery_identifier: Identifier,
439 ) -> Result<SignatureRequest, ClientError> {
440 let inbox_id = self.context.inbox_id();
441 let current_state = retry_async!(
442 Retry::default(),
443 (async {
444 self.get_association_state(&self.context.db(), inbox_id, None)
445 .await
446 })
447 )?;
448 let mut builder = SignatureRequestBuilder::new(inbox_id);
449 let member_identifier: MemberIdentifier =
450 current_state.recovery_identifier().clone().into();
451 builder = builder.change_recovery_address(member_identifier, new_recovery_identifier);
452 let mut signature_request = builder.build();
453 self.context
454 .server_configuration()
455 .restrict(&mut signature_request);
456 Ok(signature_request)
457 }
458
459 pub async fn apply_signature_request(
466 &self,
467 signature_request: SignatureRequest,
468 ) -> Result<(), ClientError> {
469 self.context.server_configuration().check()?;
471 let inbox_id = signature_request.inbox_id().to_string();
472
473 apply_signature_request_with_verifier(
474 self.context.api(),
475 &self.context.db(),
476 signature_request,
477 &self.context.scw_verifier(),
478 )
479 .await?;
480
481 retry_async!(
483 Retry::default(),
484 (async {
485 load_identity_updates(self.context.api(), &self.context.db(), &[inbox_id.as_str()])
486 .await
487 })
488 )?;
489
490 Ok(())
491 }
492
493 #[tracing::instrument(level = "trace", skip_all)]
496 pub async fn get_installation_diff(
497 &self,
498 conn: &impl DbQuery,
499 group_id: &GroupId, old_group_membership: &GroupMembership,
501 new_group_membership: &GroupMembership,
502 membership_diff: &MembershipDiff<'_>,
503 ) -> Result<InstallationDiff, InstallationDiffError> {
504 let diff = loop {
505 match get_installation_diff_local(
506 conn,
507 old_group_membership,
508 new_group_membership,
509 membership_diff,
510 ) {
511 Ok(diff) => break diff,
512 Err(IdentityDependencyError::Need(requirement)) => {
513 resolve_identity_requirement(&self.context, &requirement).await?;
514 }
515 Err(error) => return Err(error.into()),
516 }
517 };
518 let InstallationDiff {
519 added_installations,
520 removed_installations,
521 } = diff;
522
523 if !added_installations.is_empty() || !removed_installations.is_empty() {
524 let added_installations: Vec<_> =
525 added_installations.iter().map(|i| i.short_hex()).collect();
526 let removed_installations: Vec<_> = removed_installations
527 .iter()
528 .map(|i| i.short_hex())
529 .collect();
530 log_event!(
531 Event::UpdatedGroupMembership,
532 self.context.installation_id(),
533 #group_id,
534 ?added_installations,
535 ?removed_installations
536 );
537 }
538
539 Ok(InstallationDiff {
540 added_installations,
541 removed_installations,
542 })
543 }
544}
545
546pub(crate) fn get_installation_diff_local(
550 conn: &impl DbQuery,
551 old_membership: &GroupMembership,
552 new_membership: &GroupMembership,
553 membership_diff: &MembershipDiff<'_>,
554) -> Result<InstallationDiff, IdentityDependencyError> {
555 let mut added_installations = HashSet::new();
556 let mut removed_installations = HashSet::new();
557 for inbox_id in membership_diff
558 .added_inboxes
559 .iter()
560 .chain(&membership_diff.updated_inboxes)
561 {
562 let final_state = require_association_state(
563 conn,
564 &IdentityRequirement {
565 inbox_id: (*inbox_id).clone(),
566 sequence_id: new_membership.get(inbox_id).copied().unwrap_or(0),
567 },
568 )?;
569 let diff = match old_membership.get(inbox_id) {
570 None | Some(0) => final_state.as_diff(),
571 Some(sequence_id) => require_association_state(
572 conn,
573 &IdentityRequirement {
574 inbox_id: (*inbox_id).clone(),
575 sequence_id: *sequence_id,
576 },
577 )?
578 .diff(&final_state),
579 };
580 added_installations.extend(diff.new_installations());
581 removed_installations.extend(diff.removed_installations());
582 }
583 for inbox_id in &membership_diff.removed_inboxes {
584 let state = require_association_state(
585 conn,
586 &IdentityRequirement {
587 inbox_id: (*inbox_id).clone(),
588 sequence_id: old_membership.get(inbox_id).copied().unwrap_or(0),
589 },
590 )?;
591 removed_installations.extend(state.installation_ids());
592 }
593 Ok(InstallationDiff {
594 added_installations,
595 removed_installations,
596 })
597}
598
599#[tracing::instrument(level = "trace", skip_all)]
602pub async fn load_identity_updates<ApiClient: XmtpApi>(
603 api_client: &ApiClientWrapper<ApiClient>,
604 conn: &impl xmtp_db::DbQuery,
605 inbox_ids: &[&str],
606) -> Result<HashMap<String, Vec<InboxUpdate>>, ClientError> {
607 if inbox_ids.is_empty() {
608 return Ok(HashMap::new());
609 }
610 tracing::debug!("Fetching identity updates for: {:?}", inbox_ids);
611
612 let existing_sequence_ids = conn.get_latest_sequence_id(inbox_ids)?;
613 let filters: Vec<GetIdentityUpdatesV2Filter> = inbox_ids
614 .iter()
615 .map(|inbox_id| GetIdentityUpdatesV2Filter {
616 sequence_id: existing_sequence_ids.get(*inbox_id).map(|i| *i as u64),
617 inbox_id: inbox_id.to_string(),
618 })
619 .collect();
620
621 let updates = api_client.get_identity_updates_v2(filters).await?;
622 let updates = updates
623 .into_iter()
624 .map(|(inbox_id, entries)| {
625 let entries = entries
626 .into_iter()
627 .map(|entry| {
628 Ok(InboxUpdate {
629 sequence_id: entry
630 .meta
631 .cursor
632 .ok_or(xmtp_api::ApiError::InvalidResponse("identity cursor"))?
633 .sequence_id,
634 server_timestamp_ns: entry.meta.server_ns,
635 update: entry.update.try_into()?,
636 })
637 })
638 .collect::<Result<Vec<_>, ClientError>>()?;
639 Ok((inbox_id, entries))
640 })
641 .collect::<Result<HashMap<_, _>, ClientError>>()?;
642 let to_store = updates
643 .iter()
644 .flat_map(move |(inbox_id, updates)| {
645 updates.iter().map(move |update| StoredIdentityUpdate {
646 inbox_id: inbox_id.clone(),
647 sequence_id: update.sequence_id as i64,
648 server_timestamp_ns: update.server_timestamp_ns as i64,
649 payload: update.update.clone().into(),
650 })
651 })
652 .collect::<Vec<StoredIdentityUpdate>>();
653
654 conn.insert_or_ignore_identity_updates(&to_store)?;
655 Ok(updates)
656}
657
658pub async fn is_member_of_association_state<Client>(
660 api_client: &ApiClientWrapper<Client>,
661 inbox_id: &str,
662 identifier: &MemberIdentifier,
663 scw_verifier: Option<Box<dyn SmartContractSignatureVerifier>>,
664) -> Result<bool, ClientError>
665where
666 Client: XmtpBackendClient + Clone,
667{
668 let filters = vec![GetIdentityUpdatesV2Filter {
669 inbox_id: inbox_id.to_string(),
670 sequence_id: None,
671 }];
672 let mut updates = api_client.get_identity_updates_v2(filters).await?;
673
674 let Some(updates) = updates.remove(inbox_id) else {
675 return Err(ClientError::Generic(
676 "Unable to find provided inbox_id".to_string(),
677 ));
678 };
679 let updates: Vec<UnverifiedIdentityUpdate> = updates
680 .into_iter()
681 .map(|u| u.update.try_into())
682 .collect::<Result<_, _>>()?;
683
684 let mut association_state = None;
685
686 let scw_verifier = scw_verifier
687 .unwrap_or_else(|| Box::new(api_client.clone()) as Box<dyn SmartContractSignatureVerifier>);
688
689 let updates: Vec<_> = updates
690 .iter()
691 .map(|u| u.to_verified(&scw_verifier))
692 .collect();
693 let updates = try_join_all(updates).await?;
694
695 for update in updates {
696 association_state =
697 Some(update.update_state(association_state, update.client_timestamp_ns)?);
698 }
699 let association_state = association_state.ok_or(ClientError::Generic(
700 "Unable to create association state".to_string(),
701 ))?;
702
703 Ok(association_state.get(identifier).is_some())
704}
705
706#[tracing::instrument(level = "trace", skip_all)]
707pub async fn get_creation_signature_kind(
708 conn: &impl xmtp_db::DbQuery,
709 scw_verifier: impl SmartContractSignatureVerifier,
710 inbox_id: InboxIdRef<'_>,
711) -> Result<Option<xmtp_id::associations::SignatureKind>, ClientError> {
712 let updates = conn.get_identity_updates(inbox_id, None, None)?;
713
714 let first_update = updates
715 .first()
716 .ok_or_else(|| ClientError::Identity(IdentityError::RequiredIdentityNotFound))?;
717
718 let unverified_update: UnverifiedIdentityUpdate = first_update.clone().to_unverified()?;
719
720 let verified = unverified_update.to_verified(scw_verifier).await?;
721
722 Ok(verified.creation_signature_kind())
723}
724
725#[cfg(test)]
726pub(crate) mod tests {
727 #![allow(unused)] use crate::{
729 Client, XmtpApi,
730 builder::ClientBuilder,
731 context::XmtpSharedContext,
732 groups::group_membership::GroupMembership,
733 identity_updates::IdentityUpdates,
734 tester,
735 utils::{FullXmtpClient, Tester},
736 };
737 use alloy::signers::Signer;
738 use xmtp_cryptography::utils::generate_local_wallet;
739 use xmtp_id::{
740 InboxOwner,
741 associations::{
742 AssociationState, MemberIdentifier,
743 builder::{SignatureRequest, SignatureRequestError},
744 test_utils::{MockSmartContractSignatureVerifier, WalletTestExt, add_wallet_signature},
745 unverified::UnverifiedSignature,
746 },
747 };
748
749 use xmtp_db::{
750 ConnectionExt, db_connection::DbConnection, identity_update::StoredIdentityUpdate,
751 prelude::*,
752 };
753 use xmtp_proto::types::GroupId;
754
755 use xmtp_common::rand_vec;
756
757 use super::{is_member_of_association_state, load_identity_updates};
758
759 async fn get_association_state<Context>(
760 client: &Client<Context>,
761 inbox_id: &str,
762 ) -> AssociationState
763 where
764 Context: XmtpSharedContext,
765 {
766 let conn = client.context.db();
767 load_identity_updates(client.context.api(), &conn, &[inbox_id])
768 .await
769 .unwrap();
770
771 IdentityUpdates::new(&client.context)
772 .get_association_state(&conn, inbox_id, None)
773 .await
774 .unwrap()
775 }
776
777 fn insert_identity_update<C>(conn: &DbConnection<C>, inbox_id: &str, sequence_id: i64)
778 where
779 C: ConnectionExt,
780 {
781 let identity_update =
782 StoredIdentityUpdate::new(inbox_id.to_string(), sequence_id, 0, rand_vec::<24>());
783
784 conn.insert_or_ignore_identity_updates(&[identity_update])
785 .expect("insert should succeed");
786 }
787
788 #[rstest::rstest]
789 #[xmtp_common::test]
790 async fn test_is_member_of_association_state() {
791 let wallet = generate_local_wallet();
792 let client = ClientBuilder::new_test_client(&wallet).await;
793
794 let wallet2 = generate_local_wallet();
795 let client_identity_updates = IdentityUpdates::new(&client.context);
796
797 let mut request = client_identity_updates
798 .associate_identity(wallet2.identifier())
799 .await
800 .unwrap();
801 add_wallet_signature(&mut request, &wallet2).await;
802 client_identity_updates
803 .apply_signature_request(request)
804 .await
805 .unwrap();
806
807 let conn = client.context.db();
808
809 xmtp_common::wait_for_eq(
812 || async {
813 client_identity_updates
814 .get_latest_association_state(&conn, client.inbox_id())
815 .await
816 .unwrap()
817 .members()
818 .len()
819 },
820 3,
821 )
822 .await
823 .unwrap();
824
825 let api_client = client.context.api();
826
827 let is_member = is_member_of_association_state(
829 api_client,
830 client.inbox_id(),
831 &wallet2.member_identifier(),
832 None,
833 )
834 .await
835 .unwrap();
836
837 assert!(is_member);
838 }
839
840 #[rstest::rstest]
841 #[xmtp_common::test]
842 async fn create_inbox_round_trip() {
843 let wallet = generate_local_wallet();
844 let wallet_ident = wallet.identifier();
845 let client = ClientBuilder::new_test_client(&wallet).await;
846
847 let mut signature_request: SignatureRequest = client
848 .identity_updates()
849 .create_inbox(wallet_ident.clone(), None)
850 .await
851 .unwrap();
852 let inbox_id = signature_request.inbox_id().to_string();
853
854 add_wallet_signature(&mut signature_request, &wallet).await;
855
856 client
857 .identity_updates()
858 .apply_signature_request(signature_request)
859 .await
860 .unwrap();
861
862 let association_state = get_association_state(&client, &inbox_id).await;
863
864 assert_eq!(association_state.members().len(), 2);
865 assert_eq!(association_state.recovery_identifier(), &wallet_ident);
866 assert!(association_state.get(&wallet_ident.into()).is_some())
867 }
868
869 #[rstest::rstest]
870 #[xmtp_common::test]
871 async fn add_association() {
872 let wallet_2 = generate_local_wallet();
873 let wallet2_ident = wallet_2.identifier();
874
875 tester!(client);
876
877 let mut add_association_request = client
878 .identity_updates()
879 .associate_identity(wallet2_ident.clone())
880 .await
881 .unwrap();
882
883 add_wallet_signature(&mut add_association_request, &wallet_2).await;
884
885 client
886 .identity_updates()
887 .apply_signature_request(add_association_request)
888 .await
889 .unwrap();
890 let association_state = get_association_state(&client, client.inbox_id()).await;
891
892 let members = association_state
893 .members_by_parent(&client.builder.owner.get_identifier().unwrap().into());
894 for member in members {
896 assert!(member.client_timestamp_ns.is_some());
897 }
898
899 assert_eq!(association_state.members().len(), 3);
900 assert_eq!(
901 *association_state.recovery_identifier(),
902 client.builder.owner.get_identifier().unwrap()
903 );
904 assert!(association_state.get(&wallet2_ident.into()).is_some());
905 }
906
907 #[cfg_attr(not(target_arch = "wasm32"), test)]
908 #[cfg(not(target_arch = "wasm32"))]
909 fn cache_association_state() {
910 use std::sync::Arc;
911
912 use xmtp_common::assert_logged;
913
914 use crate::{
915 utils::LocalTester, worker::device_sync::DeviceSyncClient,
916 worker::metrics::WorkerMetrics,
917 };
918
919 xmtp_common::traced_test!(async {
920 let client = Tester::new().await;
921 let inbox_id = client.inbox_id();
922 let metrics = WorkerMetrics::new(client.context.installation_id());
923 let device_sync = DeviceSyncClient::new(&client.context, Arc::new(metrics));
924 device_sync.wait_for_sync_worker_init().await;
925
926 let wallet_2 = generate_local_wallet();
927
928 get_association_state(&client, inbox_id).await;
929
930 assert_logged!("Loaded association", 0);
931 assert_logged!("Wrote association", 1);
933
934 let association_state = get_association_state(&client, inbox_id).await;
935
936 assert_eq!(association_state.members().len(), 2);
937 assert_eq!(
938 association_state.recovery_identifier(),
939 &client.builder.owner.identifier()
940 );
941 assert!(
942 association_state
943 .get(&client.builder.owner.identifier().into())
944 .is_some()
945 );
946
947 assert_logged!("Loaded association", 1);
948 assert_logged!("Wrote association", 1);
949
950 let mut add_association_request = client
951 .identity_updates()
952 .associate_identity(wallet_2.identifier())
953 .await
954 .unwrap();
955
956 add_wallet_signature(&mut add_association_request, &wallet_2).await;
957
958 client
959 .identity_updates()
960 .apply_signature_request(add_association_request)
961 .await
962 .unwrap();
963
964 get_association_state(&client, inbox_id).await;
965
966 assert_logged!("Loaded association", 1);
967 assert_logged!("Wrote association", 2);
968
969 let association_state = get_association_state(&client, inbox_id).await;
970
971 assert_logged!("Loaded association", 2);
972 assert_logged!("Wrote association", 2);
973
974 assert_eq!(association_state.members().len(), 3);
975 assert_eq!(
976 association_state.recovery_identifier(),
977 &client.builder.owner.identifier()
978 );
979 assert!(
980 association_state
981 .get(&wallet_2.member_identifier())
982 .is_some()
983 );
984 });
985 }
986
987 #[rstest::rstest]
988 #[xmtp_common::test]
989 async fn load_identity_updates_if_needed() {
990 let wallet = generate_local_wallet();
991 let client = ClientBuilder::new_test_client(&wallet).await;
992 let conn = client.context.db();
993
994 insert_identity_update(&conn, "inbox_1", 1);
995 insert_identity_update(&conn, "inbox_2", 2);
996 insert_identity_update(&conn, "inbox_3", 3);
997
998 let filtered =
999 crate::groups::filter_inbox_ids_needing_updates(&conn, &[("inbox_1", 3), ("inbox_2", 2), ("inbox_3", 2)]);
1002 assert_eq!(filtered.unwrap(), vec!["inbox_1"]);
1003 }
1004
1005 #[rstest::rstest]
1006 #[xmtp_common::test]
1007 async fn get_installation_diff() {
1008 let wallet_1 = generate_local_wallet();
1009 let wallet_2 = generate_local_wallet();
1010 let wallet_3 = generate_local_wallet();
1011
1012 let client_1 = ClientBuilder::new_test_client(&wallet_1).await;
1013 let client_2 = ClientBuilder::new_test_client(&wallet_2).await;
1014 let client_3 = ClientBuilder::new_test_client(&wallet_3).await;
1015
1016 let client_2_installation_key = client_2.installation_public_key().to_vec();
1017 let client_3_installation_key = client_3.installation_public_key().to_vec();
1018
1019 let mut inbox_ids: Vec<String> = vec![];
1020
1021 for (client, wallet) in [
1023 (client_1, wallet_1),
1024 (client_2, wallet_2),
1025 (client_3, wallet_3),
1026 ] {
1027 let mut signature_request: SignatureRequest = client
1028 .identity_updates()
1029 .create_inbox(wallet.identifier(), None)
1030 .await
1031 .unwrap();
1032 let inbox_id = signature_request.inbox_id().to_string();
1033 inbox_ids.push(inbox_id);
1034
1035 add_wallet_signature(&mut signature_request, &wallet).await;
1036 client
1037 .identity_updates()
1038 .apply_signature_request(signature_request)
1039 .await
1040 .unwrap();
1041 let new_wallet = generate_local_wallet();
1042 let mut add_association_request = client
1043 .identity_updates()
1044 .associate_identity(new_wallet.identifier())
1045 .await
1046 .unwrap();
1047
1048 add_wallet_signature(&mut add_association_request, &new_wallet).await;
1049
1050 client
1051 .identity_updates()
1052 .apply_signature_request(add_association_request)
1053 .await
1054 .unwrap();
1055 }
1056
1057 let other_client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
1059 let other_conn = other_client.context.db();
1060 let ids = inbox_ids.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
1061 load_identity_updates(other_client.context.api(), &other_conn, ids.as_slice())
1063 .await
1064 .expect("load should succeed");
1065
1066 let latest_sequence_ids = other_conn.get_latest_sequence_id(ids.as_slice()).unwrap();
1068
1069 let inbox_1_first_sequence_id = other_conn
1070 .get_identity_updates(inbox_ids[0].clone(), None, None)
1071 .unwrap()
1072 .first()
1073 .unwrap()
1074 .sequence_id;
1075
1076 let mut original_group_membership = GroupMembership::new();
1077 original_group_membership.add(inbox_ids[0].to_string(), inbox_1_first_sequence_id as u64);
1078 original_group_membership.add(
1079 inbox_ids[1].to_string(),
1080 *latest_sequence_ids.get(&inbox_ids[1]).unwrap() as u64,
1081 );
1082
1083 let mut new_group_membership = original_group_membership.clone();
1084 new_group_membership.add(
1086 inbox_ids[0].to_string(),
1087 *latest_sequence_ids.get(&inbox_ids[0]).unwrap() as u64,
1088 );
1089 new_group_membership.add(
1090 inbox_ids[2].to_string(),
1091 *latest_sequence_ids.get(&inbox_ids[2]).unwrap() as u64,
1092 );
1093 new_group_membership.remove(&inbox_ids[1]);
1094
1095 let membership_diff = original_group_membership.diff(&new_group_membership);
1096
1097 let installation_diff = other_client
1098 .identity_updates()
1099 .get_installation_diff(
1100 &other_conn,
1101 &GroupId::default(),
1102 &original_group_membership,
1103 &new_group_membership,
1104 &membership_diff,
1105 )
1106 .await
1107 .unwrap();
1108
1109 assert_eq!(installation_diff.added_installations.len(), 1);
1110 assert!(
1111 installation_diff
1112 .added_installations
1113 .contains(&client_3_installation_key.to_vec()),
1114 );
1115 assert_eq!(installation_diff.removed_installations.len(), 1);
1116 assert!(
1117 installation_diff
1118 .removed_installations
1119 .contains(&client_2_installation_key.to_vec())
1120 );
1121 }
1122
1123 #[rstest::rstest]
1127 #[xmtp_common::test]
1128 async fn get_installation_diff_rejects_added_inbox_at_sequence_zero() {
1129 let wallet = generate_local_wallet();
1130 let client = ClientBuilder::new_test_client(&wallet).await;
1131
1132 let mut signature_request: SignatureRequest = client
1133 .identity_updates()
1134 .create_inbox(wallet.identifier(), None)
1135 .await
1136 .unwrap();
1137 let inbox_id = signature_request.inbox_id().to_string();
1138 add_wallet_signature(&mut signature_request, &wallet).await;
1139 client
1140 .identity_updates()
1141 .apply_signature_request(signature_request)
1142 .await
1143 .unwrap();
1144
1145 let other_client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
1148 let other_conn = other_client.context.db();
1149 load_identity_updates(
1150 other_client.context.api(),
1151 &other_conn,
1152 &[inbox_id.as_str()],
1153 )
1154 .await
1155 .expect("load should succeed");
1156 let latest_sequence_id = *other_conn
1157 .get_latest_sequence_id(&[inbox_id.as_str()])
1158 .unwrap()
1159 .get(&inbox_id)
1160 .unwrap();
1161
1162 let old_membership = GroupMembership::new();
1163
1164 let mut honest_membership = GroupMembership::new();
1166 honest_membership.add(inbox_id.clone(), latest_sequence_id as u64);
1167 other_client
1168 .identity_updates()
1169 .get_installation_diff(
1170 &other_conn,
1171 &GroupId::default(),
1172 &old_membership,
1173 &honest_membership,
1174 &old_membership.diff(&honest_membership),
1175 )
1176 .await
1177 .expect("an add at a real sequence id is valid");
1178
1179 let mut crafted_membership = GroupMembership::new();
1181 crafted_membership.add(inbox_id.clone(), 0);
1182 let crafted_diff = old_membership.diff(&crafted_membership);
1183 assert_eq!(crafted_diff.added_inboxes, vec![&inbox_id]);
1184
1185 let result = other_client
1186 .identity_updates()
1187 .get_installation_diff(
1188 &other_conn,
1189 &GroupId::default(),
1190 &old_membership,
1191 &crafted_membership,
1192 &crafted_diff,
1193 )
1194 .await;
1195
1196 assert!(
1197 result.is_err(),
1198 "an inbox added at sequence_id 0 must be rejected, not resolved at latest"
1199 );
1200 }
1201
1202 #[rstest::rstest]
1203 #[xmtp_common::test]
1204 pub async fn revoke_wallet() {
1205 let recovery_wallet = generate_local_wallet();
1206 let second_wallet = generate_local_wallet();
1207 let client = ClientBuilder::new_test_client(&recovery_wallet).await;
1208
1209 let mut add_wallet_signature_request = client
1210 .identity_updates()
1211 .associate_identity(second_wallet.identifier())
1212 .await
1213 .unwrap();
1214
1215 add_wallet_signature(&mut add_wallet_signature_request, &second_wallet).await;
1216
1217 client
1218 .identity_updates()
1219 .apply_signature_request(add_wallet_signature_request)
1220 .await
1221 .unwrap();
1222
1223 let association_state_after_add = get_association_state(&client, client.inbox_id()).await;
1224 assert_eq!(association_state_after_add.identifiers().len(), 2);
1225
1226 let inbox_ids = client
1228 .context
1229 .api()
1230 .get_inbox_ids(vec![second_wallet.identifier().into()])
1231 .await
1232 .unwrap();
1233 assert_eq!(inbox_ids.len(), 1);
1234
1235 let mut revoke_signature_request = client
1238 .identity_updates()
1239 .revoke_identities(vec![second_wallet.identifier()])
1240 .await
1241 .unwrap();
1242 add_wallet_signature(&mut revoke_signature_request, &recovery_wallet).await;
1243
1244 client
1245 .identity_updates()
1246 .apply_signature_request(revoke_signature_request)
1247 .await
1248 .unwrap();
1249
1250 let association_state_after_revoke =
1252 get_association_state(&client, client.inbox_id()).await;
1253 assert_eq!(association_state_after_revoke.identifiers().len(), 1);
1254
1255 let inbox_ids = client
1257 .context
1258 .api()
1259 .get_inbox_ids(vec![second_wallet.identifier().into()])
1260 .await
1261 .unwrap();
1262 assert_eq!(inbox_ids, vec![None]);
1263 }
1264
1265 #[rstest::rstest]
1266 #[xmtp_common::test]
1267 pub async fn revoke_installation() {
1268 let wallet = generate_local_wallet();
1269 let client1: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1270 let client2: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1271
1272 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1273 assert_eq!(association_state.installation_ids().len(), 2);
1275
1276 let mut revoke_installation_request = client1
1278 .identity_updates()
1279 .revoke_installations(vec![client2.installation_public_key().to_vec()])
1280 .await
1281 .unwrap();
1282 add_wallet_signature(&mut revoke_installation_request, &wallet).await;
1283 client1
1284 .identity_updates()
1285 .apply_signature_request(revoke_installation_request)
1286 .await
1287 .unwrap();
1288
1289 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1291 assert_eq!(association_state.installation_ids().len(), 1);
1292 }
1293
1294 #[cfg(not(target_arch = "wasm32"))]
1295 #[tokio::test(flavor = "multi_thread")]
1296 pub async fn revoke_installation_with_malformed_keypackage() {
1297 use crate::utils::test_mocks_helpers::set_test_mode_upload_malformed_keypackage;
1298
1299 let wallet = generate_local_wallet();
1300 let client1: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1301 let client2: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1302
1303 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1304 assert_eq!(association_state.installation_ids().len(), 2);
1306
1307 set_test_mode_upload_malformed_keypackage(
1308 true,
1309 Some(vec![client2.installation_public_key().to_vec()]),
1310 );
1311
1312 let mut revoke_installation_request = client1
1314 .identity_updates()
1315 .revoke_installations(vec![client2.installation_public_key().to_vec()])
1316 .await
1317 .unwrap();
1318 add_wallet_signature(&mut revoke_installation_request, &wallet).await;
1319 client1
1320 .identity_updates()
1321 .apply_signature_request(revoke_installation_request)
1322 .await
1323 .unwrap();
1324
1325 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1327 assert_eq!(association_state.installation_ids().len(), 1);
1328 }
1329
1330 #[cfg(not(target_arch = "wasm32"))]
1331 #[tokio::test(flavor = "multi_thread")]
1332 pub async fn revoke_good_installation_with_other_malformed_keypackage() {
1333 use crate::utils::test_mocks_helpers::set_test_mode_upload_malformed_keypackage;
1334
1335 let wallet = generate_local_wallet();
1336 let client1: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1337 let client2: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1338 let client3: FullXmtpClient = ClientBuilder::new_test_client(&wallet).await;
1339
1340 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1341 assert_eq!(association_state.installation_ids().len(), 3);
1343
1344 set_test_mode_upload_malformed_keypackage(
1345 true,
1346 Some(vec![client2.installation_public_key().to_vec()]),
1347 );
1348
1349 let mut revoke_installation_request = client1
1351 .identity_updates()
1352 .revoke_installations(vec![client3.installation_public_key().to_vec()])
1353 .await
1354 .unwrap();
1355 add_wallet_signature(&mut revoke_installation_request, &wallet).await;
1356 client1
1357 .identity_updates()
1358 .apply_signature_request(revoke_installation_request)
1359 .await
1360 .unwrap();
1361
1362 let association_state = get_association_state(&client1, client1.inbox_id()).await;
1364 assert_eq!(association_state.installation_ids().len(), 2);
1365 }
1366
1367 #[rstest::rstest]
1368 #[xmtp_common::test]
1369 pub async fn change_recovery_address() {
1370 let original_wallet = generate_local_wallet();
1371 let new_recovery_wallet = generate_local_wallet();
1372 let client = ClientBuilder::new_test_client(&original_wallet).await;
1373
1374 let association_state_before = get_association_state(&client, client.inbox_id()).await;
1376 assert_eq!(
1377 association_state_before.recovery_identifier(),
1378 &original_wallet.identifier()
1379 );
1380
1381 assert!(association_state_before.members().len() == 2);
1383 let binding = association_state_before.members();
1385 let recovery_member = binding
1386 .iter()
1387 .find(|m| m.identifier == original_wallet.identifier());
1388 assert!(recovery_member.is_some());
1389 let recovery_member_timestamp = recovery_member.unwrap().client_timestamp_ns;
1390 assert!(recovery_member_timestamp.is_none());
1392 let installation_member = binding
1394 .iter()
1395 .find(|m| matches!(m.identifier, MemberIdentifier::Installation(_)));
1396 assert!(installation_member.is_some());
1397 assert!(
1398 installation_member
1399 .unwrap()
1400 .identifier
1401 .installation_key()
1402 .unwrap()
1403 == client.installation_public_key().to_vec()
1404 );
1405 let installation_member_timestamp = installation_member.unwrap().client_timestamp_ns;
1406 assert!(installation_member_timestamp.is_some());
1407
1408 let mut change_recovery_request = client
1410 .identity_updates()
1411 .change_recovery_identifier(new_recovery_wallet.identifier())
1412 .await
1413 .unwrap();
1414
1415 add_wallet_signature(&mut change_recovery_request, &original_wallet).await;
1417
1418 client
1420 .identity_updates()
1421 .apply_signature_request(change_recovery_request)
1422 .await
1423 .unwrap();
1424
1425 let association_state_after = get_association_state(&client, client.inbox_id()).await;
1427 assert_eq!(
1428 association_state_after.recovery_identifier(),
1429 &new_recovery_wallet.identifier()
1430 );
1431
1432 assert!(association_state_after.members().len() == 2);
1434 let binding = association_state_after.members();
1436 let recovery_member = binding
1437 .iter()
1438 .find(|m| m.identifier == original_wallet.identifier());
1439 assert!(recovery_member.is_some());
1440 let recovery_member_timestamp = recovery_member.unwrap().client_timestamp_ns;
1441 assert!(recovery_member_timestamp.is_none());
1443 let installation_member = binding
1445 .iter()
1446 .find(|m| matches!(m.identifier, MemberIdentifier::Installation(_)));
1447 assert!(installation_member.is_some());
1448 assert!(
1449 installation_member
1450 .unwrap()
1451 .identifier
1452 .installation_key()
1453 .unwrap()
1454 == client.installation_public_key().to_vec()
1455 );
1456 let installation_member_timestamp = installation_member.unwrap().client_timestamp_ns;
1457 assert!(installation_member_timestamp.is_some());
1458
1459 let installation_id = client.installation_public_key().to_vec();
1462 let mut revoke_installation_request = client
1463 .identity_updates()
1464 .revoke_installations(vec![installation_id])
1465 .await
1466 .unwrap();
1467
1468 let signature_text = revoke_installation_request.signature_text();
1471 let sig = original_wallet
1472 .sign_message(signature_text.as_bytes())
1473 .await
1474 .unwrap()
1475 .as_bytes()
1476 .to_vec();
1477 let unverified_sig = UnverifiedSignature::new_recoverable_ecdsa(sig);
1478 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
1479
1480 let attempt_to_revoke_with_original_wallet = revoke_installation_request
1481 .add_signature(unverified_sig, &scw_verifier)
1482 .await;
1483
1484 assert!(matches!(
1485 attempt_to_revoke_with_original_wallet,
1486 Err(SignatureRequestError::UnknownSigner)
1487 ));
1488
1489 let installation_id = client.installation_public_key().to_vec();
1491 let mut revoke_installation_request = client
1492 .identity_updates()
1493 .revoke_installations(vec![installation_id])
1494 .await
1495 .unwrap();
1496
1497 add_wallet_signature(&mut revoke_installation_request, &new_recovery_wallet).await;
1499
1500 client
1502 .identity_updates()
1503 .apply_signature_request(revoke_installation_request)
1504 .await
1505 .unwrap();
1506
1507 let association_state_final = get_association_state(&client, client.inbox_id()).await;
1509 assert_eq!(association_state_final.installation_ids().len(), 0);
1510 }
1511}
1512
1513#[cfg(test)]
1514mod conflict_tests {
1515 use super::*;
1516 use crate::tester;
1517 use xmtp_cryptography::utils::generate_local_wallet;
1518 use xmtp_id::associations::test_utils::{
1519 MockSmartContractSignatureVerifier, WalletTestExt, add_wallet_signature,
1520 };
1521 use xmtp_proto::{api::ApiClientError, backend_v1 as wire, types::Topic};
1522
1523 #[rstest::rstest]
1524 #[case(0)]
1525 #[case(1)]
1526 #[case(3)]
1527 #[case(4)]
1528 #[xmtp_common::test(unwrap_try = true)]
1529 async fn conflict_reloads_validates_and_bounds_identical_resends(#[case] conflicts: usize) {
1530 tester!(alix, disable_workers);
1531 let wallet = generate_local_wallet();
1532 let mut request = alix
1533 .identity_updates()
1534 .associate_identity(wallet.identifier())
1535 .await
1536 .unwrap();
1537 add_wallet_signature(&mut request, &wallet).await;
1538 let update = request.build_identity_update().unwrap();
1539 let expected: xmtp_proto::xmtp::identity::associations::IdentityUpdate =
1540 update.clone().into();
1541 let history = alix
1542 .context
1543 .api()
1544 .query_all(
1545 [(
1546 Topic::new_identity_update(hex::decode(alix.inbox_id()).unwrap()),
1547 Cursor(0),
1548 )]
1549 .into(),
1550 alix.context.api().limits().max_query_limit as u32,
1551 )
1552 .await
1553 .unwrap();
1554 let last_sequence = history
1555 .last()
1556 .unwrap()
1557 .meta
1558 .as_ref()
1559 .unwrap()
1560 .cursor
1561 .as_ref()
1562 .unwrap()
1563 .sequence_id;
1564 let mut mock = xmtp_api_backend::MockBackendClient::new();
1565 let mut calls = 0;
1566 mock.expect_publish()
1567 .times((conflicts + 1).min(4))
1568 .returning(move |request| {
1569 calls += 1;
1570 assert_eq!(request.envelopes.len(), 1);
1571 assert_eq!(
1572 request.envelopes[0].payload,
1573 Some(wire::client_envelope::Payload::IdentityUpdate(
1574 expected.clone()
1575 ))
1576 );
1577 if calls <= conflicts {
1578 return Err(ApiClientError::client(
1579 xmtp_api_grpc::error::GrpcError::Status(tonic::Status::aborted(
1580 "identity conflict",
1581 )),
1582 ));
1583 }
1584 let parsed =
1585 xmtp_mls_validation::parse_envelope(request.envelopes[0].clone()).unwrap();
1586 Ok(wire::PublishResponse {
1587 envelope_metas: vec![wire::EnvelopeMeta {
1588 cursor: Some(wire::Cursor {
1589 sequence_id: last_sequence + 1,
1590 }),
1591 topic: Some(wire::Topic {
1592 topic: parsed.topic.cloned_vec(),
1593 }),
1594 message_hash: Some(wire::MessageHash {
1595 hash: Some(wire::message_hash::Hash::Sha256(
1596 parsed.canonical.hash.to_vec(),
1597 )),
1598 }),
1599 ..Default::default()
1600 }],
1601 })
1602 });
1603 mock.expect_query()
1604 .times(conflicts.min(3))
1605 .returning(move |request| {
1606 assert_eq!(request.queries.len(), 1);
1607 let floor = request.queries[0].cursor.as_ref().unwrap().sequence_id;
1608 Ok(wire::QueryResponse {
1609 envelopes: history
1610 .iter()
1611 .filter(|entry| {
1612 entry
1613 .meta
1614 .as_ref()
1615 .unwrap()
1616 .cursor
1617 .as_ref()
1618 .unwrap()
1619 .sequence_id
1620 > floor
1621 })
1622 .cloned()
1623 .collect(),
1624 continuation: Some(wire::Continuation { has_more: false }),
1625 })
1626 });
1627 let api = ApiClientWrapper::new(mock, Default::default());
1628 let result = publish_with_conflict_retry(
1629 &api,
1630 &alix.context.db(),
1631 update,
1632 &MockSmartContractSignatureVerifier::new(true),
1633 )
1634 .await;
1635 if conflicts > 3 {
1636 assert!(matches!(
1637 result,
1638 Err(IdentityUpdateError::Api(
1639 xmtp_api::ApiError::IdentityUpdateConflict
1640 ))
1641 ));
1642 } else {
1643 assert_eq!(result.unwrap(), Cursor(last_sequence + 1));
1644 }
1645 }
1646
1647 #[xmtp_common::test(flavor = "multi_thread", worker_threads = 4, unwrap_try = true)]
1648 async fn two_clients_racing_identity_updates_keep_both_associations() {
1649 tester!(alix, disable_workers);
1650 tester!(alix2, from: alix, disable_workers);
1651 use xmtp_proto::api::HasStats;
1652 let first_stats = alix.context.api().api_client.as_ref().mls_stats();
1653 let second_stats = alix2.context.api().api_client.as_ref().mls_stats();
1654 const MAX_RACE_ROUNDS: usize = 16;
1655 const UPDATES_PER_RACE: usize = 2;
1656 for _ in 0..MAX_RACE_ROUNDS {
1657 let first_wallet = generate_local_wallet();
1658 let second_wallet = generate_local_wallet();
1659 let mut first = alix
1660 .identity_updates()
1661 .associate_identity(first_wallet.identifier())
1662 .await?;
1663 let mut second = alix2
1664 .identity_updates()
1665 .associate_identity(second_wallet.identifier())
1666 .await?;
1667 add_wallet_signature(&mut first, &first_wallet).await;
1668 add_wallet_signature(&mut second, &second_wallet).await;
1669 let first_updates = alix.identity_updates();
1670 let second_updates = alix2.identity_updates();
1671 first_stats.clear();
1672 second_stats.clear();
1673 let (first, second) = {
1674 xmtp_common::wasm_or_native! {
1675 native => {
1676 let barrier = std::sync::Barrier::new(UPDATES_PER_RACE);
1678 let runtime = tokio::runtime::Handle::current();
1679 std::thread::scope(|scope| {
1680 let first = scope.spawn(|| {
1681 barrier.wait();
1682 runtime.block_on(first_updates.apply_signature_request(first))
1683 });
1684 let second = scope.spawn(|| {
1685 barrier.wait();
1686 runtime.block_on(second_updates.apply_signature_request(second))
1687 });
1688 (first.join().unwrap(), second.join().unwrap())
1689 })
1690 },
1691 wasm => {
1692 futures::join!(
1693 first_updates.apply_signature_request(first),
1694 second_updates.apply_signature_request(second),
1695 )
1696 }
1697 }
1698 };
1699 first?;
1700 second?;
1701 let publishes = first_stats.publish.get_count() + second_stats.publish.get_count();
1702 let conn = alix.context.db();
1703 load_identity_updates(alix.context.api(), &conn, &[alix.inbox_id()]).await?;
1704 let state = alix
1705 .identity_updates()
1706 .get_association_state(&conn, alix.inbox_id(), None)
1707 .await?;
1708 assert!(state.get(&first_wallet.identifier().into()).is_some());
1709 assert!(state.get(&second_wallet.identifier().into()).is_some());
1710 if publishes > UPDATES_PER_RACE {
1711 tracing::info!(publishes, "Both identity updates survived a publish retry");
1712 return;
1713 }
1714 }
1715 panic!("No identity publish retry occurred in {MAX_RACE_ROUNDS} races");
1716 }
1717}