1use crate::identity_ext::IdentityExt;
2use futures::{StreamExt, TryStreamExt, stream};
3use std::collections::{HashMap, HashSet};
4use std::pin::Pin;
5use thiserror::Error;
6use xmtp_common::{ErrorCode, Event, Retry, RetryableError, retry_async, retryable};
7use xmtp_configuration::{IDENTITY_DEPENDENCY_CONCURRENCY, MAX_IDENTITY_UPDATE_ACTIONS};
8use xmtp_cryptography::CredentialSign;
9use xmtp_cryptography::signature::IdentifierValidationError;
10use xmtp_db::StorageError;
11use xmtp_db::XmtpDb;
12use xmtp_db::prelude::*;
13use xmtp_db::{db_connection::DbConnection, identity_update::StoredIdentityUpdate};
14use xmtp_id::{
15 AsIdRef, BlockNumber, Bytes, InboxIdRef,
16 associations::{
17 AccountId, AssociationError, AssociationState, Handle, Identifier, IdentityAction,
18 InstallationKeyContext, MemberIdentifier, SignatureError,
19 builder::{SignatureRequest, SignatureRequestBuilder, SignatureRequestError},
20 unverified::{
21 UnverifiedIdentityUpdate, UnverifiedInstallationKeySignature, UnverifiedSignature,
22 },
23 verify_updates,
24 },
25 scw_verifier::{SmartContractSignatureVerifier, ValidationResponse, VerifierError},
26};
27use xmtp_macro::log_event;
28use xmtp_mls_core::context::CoreContext;
29use xmtp_mls_core::identity::IdentityError;
30use xmtp_mls_validation::group_membership::{GroupMembership, MembershipDiff};
31use xmtp_proto::api_client::XmtpApi;
32use xmtp_proto::{
33 ShortHex,
34 api_client::XmtpBackendClient,
35 types::{Cursor, GroupId, IncomingBatchLimits},
36};
37
38use xmtp_api::{ApiClientWrapper, GetIdentityUpdatesV2Filter};
39
40pub mod dependencies;
41#[cfg(test)]
42pub(crate) mod paged_load_tests;
43mod state_store;
44#[cfg(any(test, feature = "test-utils"))]
45#[doc(hidden)]
46pub mod test_support;
47pub use dependencies::{IdentityDependencyError, IdentityRequirement, IdentityResolutionRegistry};
48
49pub trait IdentityFeatures {
51 fn identity_resolutions(&self) -> &IdentityResolutionRegistry;
52}
53
54pub trait IdentityContext: CoreContext<Features: IdentityFeatures> {
56 fn identity_resolution_registry(&self) -> &IdentityResolutionRegistry {
58 self.features().identity_resolutions()
59 }
60}
61
62impl<T: CoreContext<Features: IdentityFeatures>> IdentityContext for T {}
63pub use dependencies::{
64 MissingProofs, OwnInstallationEvents, require_association_state, resolve_identity_requirements,
65};
66use state_store::{ResolvedState, resolve_association_state};
67pub use state_store::{get_association_state_for_client, store_association_state};
68
69#[derive(Debug, Error, ErrorCode)]
71pub enum IdentityUpdateError {
72 #[error(transparent)]
77 InvalidSignatureRequest(#[from] SignatureRequestError),
78 #[error(transparent)]
82 Api(#[from] xmtp_api::ApiError),
83 #[error(transparent)]
87 Validation(#[from] xmtp_mls_validation::ValidationError),
88 #[error("storage error: {0}")]
92 Storage(#[from] StorageError),
93 #[error(transparent)]
97 Db(#[from] xmtp_db::ConnectionError),
98 #[error("Association error: {0}")]
102 Association(#[from] AssociationError),
103 #[error("signature validation error: {0}")]
108 SignatureValidation(#[from] SignatureError),
109 #[error("identity error: {0}")]
113 Identity(#[from] IdentityError),
114 #[error(transparent)]
118 Conversion(#[from] xmtp_proto::ConversionError),
119 #[error(transparent)]
120 #[error_code(inherit)]
121 AddressValidation(#[from] IdentifierValidationError),
122 #[error(transparent)]
123 #[error_code(inherit)]
124 Handle(#[from] crate::inbox_handles::HandleError),
125 #[error(transparent)]
127 #[error_code(inherit)]
128 Blocked(#[from] xmtp_mls_server_config::server_configuration::BlockedConnection),
129 #[error("Unable to find provided inbox_id")]
133 InboxNotFound,
134 #[error("Unable to create association state")]
139 EmptyIdentityLog,
140 #[error(transparent)]
145 Publish(Box<IdentityUpdateError>),
146}
147
148impl From<xmtp_db::NotFound> for IdentityUpdateError {
149 fn from(value: xmtp_db::NotFound) -> Self {
150 Self::Storage(StorageError::NotFound(value))
151 }
152}
153
154impl RetryableError for IdentityUpdateError {
155 fn is_retryable(&self) -> bool {
156 match self {
157 Self::Api(error) => retryable!(error),
158 Self::Storage(error) => retryable!(error),
159 Self::Db(error) => retryable!(error),
160 Self::SignatureValidation(error) => retryable!(error),
164 Self::InvalidSignatureRequest(_)
165 | Self::Validation(_)
166 | Self::Association(_)
167 | Self::Identity(_)
168 | Self::Conversion(_)
169 | Self::AddressValidation(_)
170 | Self::Handle(_)
171 | Self::Blocked(_)
172 | Self::InboxNotFound
173 | Self::EmptyIdentityLog
174 | Self::Publish(_) => false,
175 }
176 }
177
178 fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
179 match self {
180 Self::Api(error) => error.rate_limit_delay(),
181 Self::SignatureValidation(error) => error.rate_limit_delay(),
182 _ => None,
183 }
184 }
185}
186
187impl xmtp_mls_core::worker::NeedsDbReconnect for IdentityUpdateError {
188 #[deny(clippy::wildcard_enum_match_arm)]
189 fn needs_db_reconnect(&self) -> bool {
190 match self {
191 Self::Storage(error) => error.db_needs_connection(),
192 Self::Db(error) => error.db_needs_connection(),
193 Self::Identity(error) => error.needs_db_reconnect(),
194 Self::Publish(error) => error.needs_db_reconnect(),
195 Self::InvalidSignatureRequest(_)
196 | Self::Api(_)
197 | Self::Validation(_)
198 | Self::Association(_)
199 | Self::SignatureValidation(_)
200 | Self::Conversion(_)
201 | Self::AddressValidation(_)
202 | Self::Handle(_)
203 | Self::Blocked(_)
204 | Self::InboxNotFound
205 | Self::EmptyIdentityLog => false,
206 }
207 }
208}
209
210const IDENTITY_UPDATE_CONFLICT_RETRIES: usize = 3;
211
212pub async fn publish_with_conflict_retry<ApiClient: XmtpApi>(
215 api_client: &ApiClientWrapper<ApiClient>,
216 conn: &impl DbQuery,
217 update: UnverifiedIdentityUpdate,
218 verifier: &impl SmartContractSignatureVerifier,
219) -> Result<Cursor, IdentityUpdateError> {
220 for attempt in 0..=IDENTITY_UPDATE_CONFLICT_RETRIES {
221 match api_client.publish_identity_update(update.clone()).await {
222 Ok(cursor) => return Ok(cursor),
223 Err(xmtp_api::ApiError::IdentityUpdateConflict)
224 if attempt < IDENTITY_UPDATE_CONFLICT_RETRIES =>
225 {
226 load_identity_updates(api_client, conn, &[update.inbox_id.as_str()]).await?;
227 let history = conn.get_identity_updates(&update.inbox_id, None, None)?;
228 let history = history
229 .into_iter()
230 .map(|stored| stored.to_unverified().map(Into::into))
231 .collect::<Result<Vec<_>, _>>()?;
232 xmtp_mls_validation::validate_identity_updates(
233 history,
234 vec![update.clone().into()],
235 verifier,
236 )
237 .await?;
238 }
239 Err(error) => return Err(error.into()),
240 }
241 }
242 unreachable!("the final publish attempt returns its result")
243}
244
245#[derive(Debug)]
246pub struct InstallationDiff {
247 pub added_installations: HashMap<Vec<u8>, HashSet<String>>,
249 pub removed_installations: HashSet<Vec<u8>>,
250}
251
252#[derive(Debug, Error)]
253pub enum InstallationDiffError {
254 #[error(transparent)]
255 IdentityDependency(#[from] IdentityDependencyError),
256 #[error(transparent)]
257 IdentityUpdate(#[from] IdentityUpdateError),
258 #[error(transparent)]
259 Db(#[from] xmtp_db::ConnectionError),
260 #[error(transparent)]
261 Storage(#[from] StorageError),
262}
263
264impl RetryableError for InstallationDiffError {
265 fn is_retryable(&self) -> bool {
266 match self {
267 InstallationDiffError::IdentityDependency(error) => retryable!(error),
268 InstallationDiffError::IdentityUpdate(error) => retryable!(error),
269 InstallationDiffError::Storage(e) => retryable!(e),
270 InstallationDiffError::Db(e) => retryable!(e),
271 }
272 }
273
274 fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
275 match self {
276 InstallationDiffError::IdentityDependency(error) => error.rate_limit_delay(),
277 InstallationDiffError::IdentityUpdate(error) => error.rate_limit_delay(),
278 _ => None,
279 }
280 }
281}
282
283impl xmtp_mls_core::worker::NeedsDbReconnect for InstallationDiffError {
284 #[deny(clippy::wildcard_enum_match_arm)]
285 fn needs_db_reconnect(&self) -> bool {
286 match self {
287 InstallationDiffError::IdentityDependency(error) => error.needs_db_reconnect(),
288 InstallationDiffError::IdentityUpdate(error) => error.needs_db_reconnect(),
289 InstallationDiffError::Db(error) => error.db_needs_connection(),
290 InstallationDiffError::Storage(error) => error.db_needs_connection(),
291 }
292 }
293}
294
295pub struct IdentityUpdates<Context> {
296 context: Context,
297}
298
299impl<Context> IdentityUpdates<Context> {
300 pub fn new(context: Context) -> Self {
301 Self { context }
302 }
303}
304
305pub async fn get_association_state_with_verifier(
311 conn: &impl DbQuery,
312 inbox_id: &str,
313 to_sequence_id: Option<i64>,
314 scw_verifier: &impl SmartContractSignatureVerifier,
315) -> Result<AssociationState, IdentityUpdateError> {
316 match resolve_association_state(conn, inbox_id, to_sequence_id, scw_verifier).await? {
317 ResolvedState::Cached(state) => Ok(state),
318 ResolvedState::Computed { sequence_id, state } => {
319 conn.write_to_cache(inbox_id.to_owned(), sequence_id, state.clone().into())?;
320 Ok(state)
321 }
322 }
323}
324
325pub(crate) fn state_holds_identifier(state: &AssociationState, identifier: &Identifier) -> bool {
328 identifier
329 .clone()
330 .sanitize()
331 .is_ok_and(|identifier| state.get(&identifier.into()).is_some())
332}
333
334pub async fn identifier_membership(
339 conn: &impl DbQuery,
340 inbox_id: &str,
341 identifier: &Identifier,
342 scw_verifier: &impl SmartContractSignatureVerifier,
343) -> Result<Option<bool>, IdentityUpdateError> {
344 match get_association_state_with_verifier(conn, inbox_id, None, scw_verifier).await {
345 Ok(state) => Ok(Some(state_holds_identifier(&state, identifier))),
346 Err(IdentityUpdateError::Association(AssociationError::MissingIdentityUpdate)) => Ok(None),
347 Err(error) => Err(error),
348 }
349}
350
351pub async fn stored_identifier_membership(
356 conn: &impl DbQuery,
357 inbox_id: &str,
358 identifier: &Identifier,
359) -> Result<Option<bool>, IdentityUpdateError> {
360 match identifier_membership(conn, inbox_id, identifier, &NoRequestVerifier).await {
361 Err(IdentityUpdateError::SignatureValidation(SignatureError::VerifierError(_))) => {
362 Ok(Some(false))
363 }
364 membership => membership,
365 }
366}
367
368struct NoRequestVerifier;
371
372#[xmtp_common::async_trait]
373impl SmartContractSignatureVerifier for NoRequestVerifier {
374 async fn is_valid_signature(
375 &self,
376 _account_id: AccountId,
377 _hash: [u8; 32],
378 _signature: Bytes,
379 _block_number: Option<BlockNumber>,
380 ) -> Result<ValidationResponse, VerifierError> {
381 Err(std::io::Error::other(
382 "a check that makes no request verifies no smart contract wallet",
383 )
384 .into())
385 }
386}
387
388pub fn identifier_opens_inbox(
392 membership: Option<bool>,
393 identifier: &Identifier,
394 nonce: u64,
395 inbox_id: &str,
396) -> bool {
397 membership.unwrap_or_else(|| identifier.inbox_id(nonce).is_ok_and(|own| own == inbox_id))
398}
399
400fn check_revocation_count(count: usize) -> Result<(), IdentityUpdateError> {
403 if count > MAX_IDENTITY_UPDATE_ACTIONS {
404 return Err(SignatureRequestError::TooManyActions(count).into());
405 }
406 Ok(())
407}
408
409pub fn revoke_installations_with_verifier(
412 identifier: &Identifier,
413 inbox_id: &str,
414 installation_ids: Vec<Vec<u8>>,
415) -> Result<SignatureRequest, IdentityUpdateError> {
416 check_revocation_count(installation_ids.len())?;
417 let mut builder = SignatureRequestBuilder::new(inbox_id);
418
419 for installation_id in installation_ids {
420 builder = builder.revoke_association(
421 identifier.clone().into(),
422 MemberIdentifier::installation(installation_id),
423 )
424 }
425
426 Ok(builder.build())
427}
428
429pub async fn apply_signature_request_with_verifier<ApiClient: XmtpApi>(
436 api_client: &ApiClientWrapper<ApiClient>,
437 conn: &impl DbQuery,
438 signature_request: SignatureRequest,
439 scw_verifier: &impl SmartContractSignatureVerifier,
440) -> Result<(), IdentityUpdateError> {
441 let identity_update = signature_request
443 .build_identity_update()
444 .map_err(IdentityUpdateError::from)?;
445
446 identity_update.to_verified(scw_verifier).await?;
447
448 publish_with_conflict_retry(api_client, conn, identity_update, scw_verifier)
449 .await
450 .map_err(|error| IdentityUpdateError::Publish(Box::new(error)))?;
451
452 Ok(())
453}
454
455pub async fn batch_get_association_state_with_verifier(
462 conn: &impl DbQuery,
463 identifiers: &[(impl AsIdRef, Option<i64>)],
464 scw_verifier: &impl SmartContractSignatureVerifier,
465) -> Result<Vec<AssociationState>, IdentityUpdateError> {
466 let reads: Vec<_> = identifiers
470 .iter()
471 .map(|(inbox_id, to_sequence_id)| {
472 get_association_state_with_verifier(
473 conn,
474 inbox_id.as_ref(),
475 *to_sequence_id,
476 scw_verifier,
477 )
478 })
479 .collect();
480 stream::iter(reads)
481 .buffered(IDENTITY_DEPENDENCY_CONCURRENCY)
482 .try_collect()
483 .await
484}
485
486impl<'a, Context> IdentityUpdates<Context>
487where
488 Context: IdentityContext,
489{
490 pub async fn batch_get_association_state(
497 &self,
498 conn: &impl DbQuery,
499 identifiers: &[(impl AsIdRef, Option<i64>)],
500 ) -> Result<Vec<AssociationState>, IdentityUpdateError> {
501 let reads: Vec<_> = identifiers
504 .iter()
505 .map(|(inbox_id, to_sequence_id)| {
506 get_association_state_for_client(
507 &self.context,
508 conn,
509 inbox_id.as_ref(),
510 *to_sequence_id,
511 )
512 })
513 .collect();
514 stream::iter(reads)
515 .buffered(IDENTITY_DEPENDENCY_CONCURRENCY)
516 .try_collect()
517 .await
518 }
519
520 #[tracing::instrument(level = "trace", skip_all)]
522 pub async fn get_latest_association_state(
523 &self,
524 conn: &DbConnection<<Context::Db as XmtpDb>::Connection>,
525 inbox_id: InboxIdRef<'a>,
526 ) -> Result<AssociationState, IdentityUpdateError> {
527 load_identity_updates_for_client(&self.context, conn, &[inbox_id]).await?;
528 self.get_association_state(conn, inbox_id, None).await
529 }
530
531 fn emit_own_installation_changes(
535 &self,
536 conn: &impl DbQuery,
537 before: &AssociationState,
538 after: &AssociationState,
539 revoked_at_ns: i64,
540 ) -> Result<(), IdentityUpdateError> {
541 use xmtp_db::consent_record::Owed;
542 use xmtp_db::user_preferences::{RootKeyWrite, write_root_key};
543 use xmtp_events::{ClientEvent, EventWriter, InstallationRef, InstallationRevoked};
544 let old: HashSet<_> = before.installation_ids().into_iter().collect();
545 let new: HashSet<_> = after.installation_ids().into_iter().collect();
546 for installation_key in new.difference(&old) {
547 self.context.events().emit(
548 Some(ClientEvent::IdentityOwnInstallationAdded(InstallationRef {
549 installation_key: installation_key.clone(),
550 })),
551 None,
552 );
553 }
554 for installation_key in old.difference(&new) {
555 self.context.events().emit(
556 Some(ClientEvent::IdentityOwnInstallationRevoked(
557 InstallationRevoked {
558 installation_key: installation_key.clone(),
559 is_this_installation: installation_key.as_slice()
560 == self.context.installation_id().to_vec().as_slice(),
561 },
562 )),
563 None,
564 );
565 }
566 let me = self.context.installation_id().to_vec();
568 if new.contains(&me)
569 && old.difference(&new).next().is_some()
570 && write_root_key(conn, RootKeyWrite::revocation(revoked_at_ns), Owed::Mark)?
571 {
572 xmtp_mls_core::events::emit_root_key_changed(self.context.events(), conn, true)?;
573 }
574 Ok(())
575 }
576
577 pub async fn with_own_installation_events<T>(
587 &self,
588 conn: &impl DbQuery,
589 load: Pin<Box<impl Future<Output = Result<T, IdentityUpdateError>>>>,
590 ) -> Result<T, IdentityUpdateError> {
591 let mut events = self
592 .context
593 .identity_resolution_registry()
594 .own_inbox_refresh()
595 .lock()
596 .await;
597 if events.emitted_through.is_none() {
598 events.emitted_through = self.latest_own_sequence(conn)?;
599 }
600 let loaded = load.await;
601 let emitted = self.emit_own_installation_events(conn, &mut events).await;
602 match (loaded, emitted) {
603 (Ok(value), emitted) => emitted.map(|()| value),
604 (Err(error), Ok(())) => Err(error),
605 (Err(error), Err(emit_error)) => {
606 tracing::warn!(
607 %emit_error,
608 "own-installation events wait for the next own-inbox load"
609 );
610 Err(error)
611 }
612 }
613 }
614
615 fn latest_own_sequence(&self, conn: &impl DbQuery) -> Result<Option<i64>, IdentityUpdateError> {
616 let inbox_id = self.context.inbox_id();
617 Ok(conn
618 .get_latest_sequence_id(&[inbox_id])?
619 .get(inbox_id)
620 .copied())
621 }
622
623 async fn emit_own_installation_events(
628 &self,
629 conn: &impl DbQuery,
630 events: &mut OwnInstallationEvents,
631 ) -> Result<(), IdentityUpdateError> {
632 let inbox_id = self.context.inbox_id();
633 let Some(through) = events.emitted_through else {
634 events.emitted_through = self.latest_own_sequence(conn)?;
635 return Ok(());
636 };
637 let updates = conn.get_identity_updates(inbox_id, Some(through), None)?;
638 if updates.is_empty() {
639 return Ok(());
640 }
641 let mut state = self
642 .get_association_state(conn, inbox_id, Some(through))
643 .await?;
644 for update in updates {
645 let next = self
646 .get_association_state(conn, inbox_id, Some(update.sequence_id))
647 .await?;
648 self.emit_own_installation_changes(conn, &state, &next, update.server_timestamp_ns)?;
649 events.emitted_through = Some(update.sequence_id);
650 state = next;
651 }
652 Ok(())
653 }
654
655 pub async fn get_association_state(
658 &self,
659 conn: &impl xmtp_db::DbQuery,
660 inbox_id: InboxIdRef<'_>,
661 to_sequence_id: Option<i64>,
662 ) -> Result<AssociationState, IdentityUpdateError> {
663 get_association_state_for_client(&self.context, conn, inbox_id, to_sequence_id).await
664 }
665
666 #[tracing::instrument(level = "trace", skip_all)]
669 pub async fn create_inbox(
670 &self,
671 identifier: Identifier,
672 maybe_nonce: Option<u64>,
673 ) -> Result<SignatureRequest, IdentityUpdateError> {
674 let nonce = maybe_nonce.unwrap_or(0);
675 let inbox_id = identifier.inbox_id(nonce)?;
676 let installation_public_key = self.context.identity().installation_keys.verifying_key();
677
678 let builder = SignatureRequestBuilder::new(inbox_id);
679 let mut signature_request = builder
680 .create_inbox(identifier.clone(), nonce)
681 .add_association(
682 MemberIdentifier::installation(installation_public_key.as_bytes().to_vec()),
683 identifier.into(),
684 )
685 .build();
686
687 let sig_bytes = self
688 .context
689 .identity()
690 .sign_identity_update(signature_request.signature_text())?
691 .to_vec();
692 signature_request
694 .add_signature(
695 UnverifiedSignature::InstallationKey(UnverifiedInstallationKeySignature::new(
696 sig_bytes,
697 installation_public_key,
698 )),
699 &self.context.scw_verifier(),
700 )
701 .await?;
702
703 self.context
706 .server_configuration()
707 .restrict(&mut signature_request);
708
709 Ok(signature_request)
710 }
711
712 #[tracing::instrument(level = "trace", skip_all)]
714 pub async fn associate_identity(
715 &self,
716 new_identifier: Identifier,
717 ) -> Result<SignatureRequest, IdentityUpdateError> {
718 tracing::info!(
719 "Associating new wallet with inbox_id {}",
720 self.context.inbox_id()
721 );
722 let inbox_id = self.context.inbox_id();
723 let builder = SignatureRequestBuilder::new(inbox_id);
724 let installation_public_key = self.context.identity().installation_keys.verifying_key();
725
726 let mut signature_request = builder
727 .add_association(new_identifier.into(), installation_public_key.into())
728 .build();
729
730 let signature = self
731 .context
732 .identity()
733 .installation_keys
734 .credential_sign::<InstallationKeyContext>(signature_request.signature_text())?;
735
736 signature_request
737 .add_signature(
738 UnverifiedSignature::new_installation_key(signature, installation_public_key),
739 &self.context.scw_verifier(),
740 )
741 .await?;
742
743 self.context
746 .server_configuration()
747 .restrict(&mut signature_request);
748
749 Ok(signature_request)
750 }
751
752 pub async fn claim_handle_request(
755 &self,
756 handle: Handle,
757 ) -> Result<SignatureRequest, IdentityUpdateError> {
758 let installation_keys = &self.context.identity().installation_keys;
759 let installation_public_key = installation_keys.verifying_key();
760 let mut signature_request = SignatureRequestBuilder::new(self.context.inbox_id())
761 .claim_handle(handle, installation_public_key.into())
762 .build();
763 let signature = installation_keys
764 .credential_sign::<InstallationKeyContext>(signature_request.signature_text())?;
765 signature_request
766 .add_signature(
767 UnverifiedSignature::new_installation_key(signature, installation_public_key),
768 &self.context.scw_verifier(),
769 )
770 .await?;
771 Ok(signature_request)
772 }
773
774 pub async fn revoke_identities(
777 &self,
778 identities_to_revoke: Vec<Identifier>,
779 ) -> Result<SignatureRequest, IdentityUpdateError> {
780 check_revocation_count(identities_to_revoke.len())?;
781 let inbox_id = self.context.inbox_id();
782 let current_state = retry_async!(
783 Retry::default(),
784 (async {
785 self.get_association_state(&self.context.db(), inbox_id, None)
786 .await
787 })
788 )?;
789 let mut builder = SignatureRequestBuilder::new(inbox_id);
790
791 for ident in identities_to_revoke {
792 builder = builder.revoke_association(
793 current_state.recovery_identifier().clone().into(),
794 ident.into(),
795 )
796 }
797
798 let mut signature_request = builder.build();
799 self.context
800 .server_configuration()
801 .restrict(&mut signature_request);
802 Ok(signature_request)
803 }
804
805 pub async fn revoke_installations(
807 &self,
808 installation_ids: Vec<Vec<u8>>,
809 ) -> Result<SignatureRequest, IdentityUpdateError> {
810 let inbox_id = self.context.inbox_id();
811 let current_state = retry_async!(
812 Retry::default(),
813 (async {
814 self.get_association_state(&self.context.db(), inbox_id, None)
815 .await
816 })
817 )?;
818
819 let mut result = revoke_installations_with_verifier(
820 ¤t_state.recovery_identifier().clone(),
821 inbox_id,
822 installation_ids,
823 )?;
824 self.context.server_configuration().restrict(&mut result);
828
829 Ok(result)
830 }
831
832 pub async fn change_recovery_identifier(
834 &self,
835 new_recovery_identifier: Identifier,
836 ) -> Result<SignatureRequest, IdentityUpdateError> {
837 let inbox_id = self.context.inbox_id();
838 let current_state = retry_async!(
839 Retry::default(),
840 (async {
841 self.get_association_state(&self.context.db(), inbox_id, None)
842 .await
843 })
844 )?;
845 let mut builder = SignatureRequestBuilder::new(inbox_id);
846 let member_identifier: MemberIdentifier =
847 current_state.recovery_identifier().clone().into();
848 builder = builder.change_recovery_address(member_identifier, new_recovery_identifier);
849 let mut signature_request = builder.build();
850 self.context
851 .server_configuration()
852 .restrict(&mut signature_request);
853 Ok(signature_request)
854 }
855
856 pub async fn apply_signature_request(
863 &self,
864 signature_request: SignatureRequest,
865 ) -> Result<(), IdentityUpdateError> {
866 self.context.server_configuration().check()?;
868 let inbox_id = signature_request.inbox_id().to_string();
869 let apply = async {
870 apply_signature_request_with_verifier(
871 self.context.api(),
872 &self.context.db(),
873 signature_request,
874 &self.context.scw_verifier(),
875 )
876 .await?;
877 retry_async!(
880 Retry::default(),
881 (async {
882 load_identity_updates(
883 self.context.api(),
884 &self.context.db(),
885 &[inbox_id.as_str()],
886 )
887 .await
888 })
889 )
890 };
891 if inbox_id == self.context.inbox_id() {
892 self.with_own_installation_events(&self.context.db(), Box::pin(apply))
893 .await
894 } else {
895 apply.await
896 }
897 }
898
899 #[tracing::instrument(level = "trace", skip_all)]
902 pub async fn get_installation_diff(
903 &self,
904 conn: &impl DbQuery,
905 group_id: &GroupId, old_group_membership: &GroupMembership,
907 new_group_membership: &GroupMembership,
908 membership_diff: &MembershipDiff<'_>,
909 ) -> Result<InstallationDiff, InstallationDiffError> {
910 let diff = loop {
911 match get_installation_diff_local(
912 conn,
913 old_group_membership,
914 new_group_membership,
915 membership_diff,
916 ) {
917 Ok(diff) => break diff,
918 Err(IdentityDependencyError::Need(requirements)) => {
919 resolve_identity_requirements(&self.context, &requirements).await?;
920 }
921 Err(error) => return Err(error.into()),
922 }
923 };
924 let InstallationDiff {
925 added_installations,
926 removed_installations,
927 } = diff;
928
929 if !added_installations.is_empty() || !removed_installations.is_empty() {
930 let added_installations: Vec<_> =
931 added_installations.keys().map(|i| i.short_hex()).collect();
932 let removed_installations: Vec<_> = removed_installations
933 .iter()
934 .map(|i| i.short_hex())
935 .collect();
936 log_event!(
937 Event::UpdatedGroupMembership,
938 self.context.installation_id(),
939 #group_id,
940 ?added_installations,
941 ?removed_installations
942 );
943 }
944
945 Ok(InstallationDiff {
946 added_installations,
947 removed_installations,
948 })
949 }
950}
951
952pub fn get_installation_diff_local(
957 conn: &impl DbQuery,
958 old_membership: &GroupMembership,
959 new_membership: &GroupMembership,
960 membership_diff: &MembershipDiff<'_>,
961) -> Result<InstallationDiff, IdentityDependencyError> {
962 let mut added_installations = HashMap::new();
963 let mut removed_installations = HashSet::new();
964 let mut missing = MissingProofs::default();
965 for inbox_id in membership_diff
966 .added_inboxes
967 .iter()
968 .chain(&membership_diff.updated_inboxes)
969 {
970 let final_state = missing.read(
971 conn,
972 &IdentityRequirement {
973 inbox_id: (*inbox_id).clone(),
974 sequence_id: new_membership.get(inbox_id).copied().unwrap_or(0),
975 },
976 )?;
977 let initial_state = match old_membership.get(inbox_id) {
978 None | Some(0) => None,
979 Some(sequence_id) => Some(missing.read(
980 conn,
981 &IdentityRequirement {
982 inbox_id: (*inbox_id).clone(),
983 sequence_id: *sequence_id,
984 },
985 )?),
986 };
987 let diff = match (final_state, initial_state) {
988 (Some(final_state), None) => final_state.as_diff(),
989 (Some(final_state), Some(Some(initial_state))) => initial_state.diff(&final_state),
990 (None, _) | (_, Some(None)) => continue,
991 };
992 for installation in diff.new_installations() {
993 added_installations
994 .entry(installation)
995 .or_insert_with(HashSet::new)
996 .insert((*inbox_id).clone());
997 }
998 removed_installations.extend(diff.removed_installations());
999 }
1000 for inbox_id in &membership_diff.removed_inboxes {
1001 let Some(state) = missing.read(
1002 conn,
1003 &IdentityRequirement {
1004 inbox_id: (*inbox_id).clone(),
1005 sequence_id: old_membership.get(inbox_id).copied().unwrap_or(0),
1006 },
1007 )?
1008 else {
1009 continue;
1010 };
1011 removed_installations.extend(state.installation_ids());
1012 }
1013 missing.check()?;
1014 Ok(InstallationDiff {
1015 added_installations,
1016 removed_installations,
1017 })
1018}
1019
1020pub fn required_leaf_removals(
1024 conn: &impl DbQuery,
1025 old: &GroupMembership,
1026 new: &GroupMembership,
1027 diff_removals: &HashSet<Vec<u8>>,
1028 leaf_owners: &HashMap<Vec<u8>, String>,
1029) -> Result<HashSet<Vec<u8>>, IdentityDependencyError> {
1030 let mut required: HashSet<_> = diff_removals
1031 .iter()
1032 .filter(|key| leaf_owners.contains_key(*key))
1033 .cloned()
1034 .collect();
1035 let mut by_inbox: HashMap<&str, Vec<&Vec<u8>>> = HashMap::new();
1036 for (key, inbox) in leaf_owners {
1037 by_inbox.entry(inbox).or_default().push(key);
1038 }
1039 let mut missing = MissingProofs::default();
1040 for (inbox, keys) in by_inbox {
1041 match (old.get(inbox), new.get(inbox)) {
1042 (Some(_), None) => required.extend(keys.into_iter().cloned()),
1043 (before, Some(after)) if *after != 0 && before != Some(after) => {
1044 let Some(state) = missing.read(
1045 conn,
1046 &IdentityRequirement {
1047 inbox_id: inbox.to_string(),
1048 sequence_id: *after,
1049 },
1050 )?
1051 else {
1052 continue;
1053 };
1054 let associated: HashSet<_> = state.installation_ids().into_iter().collect();
1055 required.extend(
1056 keys.into_iter()
1057 .filter(|key| !associated.contains(*key))
1058 .cloned(),
1059 );
1060 }
1061 _ => {}
1062 }
1063 }
1064 missing.check()?;
1065 Ok(required)
1066}
1067
1068pub async fn load_identity_updates_for_client<Context: IdentityContext>(
1072 context: &Context,
1073 conn: &impl DbQuery,
1074 inbox_ids: &[&str],
1075) -> Result<(), IdentityUpdateError> {
1076 let load = Box::pin(store_identity_update_pages(
1077 context.api(),
1078 conn,
1079 inbox_ids,
1080 context.stream_policy().fetched_limits(),
1081 ));
1082 if inbox_ids.contains(&context.inbox_id()) {
1083 IdentityUpdates::new(context)
1084 .with_own_installation_events(conn, load)
1085 .await
1086 } else {
1087 load.await
1088 }
1089}
1090
1091#[tracing::instrument(level = "trace", skip_all)]
1094pub async fn load_identity_updates<ApiClient: XmtpApi>(
1095 api_client: &ApiClientWrapper<ApiClient>,
1096 conn: &impl xmtp_db::DbQuery,
1097 inbox_ids: &[&str],
1098) -> Result<(), IdentityUpdateError> {
1099 store_identity_update_pages(
1100 api_client,
1101 conn,
1102 inbox_ids,
1103 IncomingBatchLimits::STREAM_BATCH,
1104 )
1105 .await
1106}
1107
1108async fn store_identity_update_pages<ApiClient: XmtpApi>(
1113 api_client: &ApiClientWrapper<ApiClient>,
1114 conn: &impl DbQuery,
1115 inbox_ids: &[&str],
1116 limits: IncomingBatchLimits,
1117) -> Result<(), IdentityUpdateError> {
1118 if inbox_ids.is_empty() {
1119 return Ok(());
1120 }
1121 tracing::debug!("Fetching identity updates for: {:?}", inbox_ids);
1122
1123 let existing_sequence_ids = conn.get_latest_sequence_id(inbox_ids)?;
1124 let filters: Vec<GetIdentityUpdatesV2Filter> = inbox_ids
1125 .iter()
1126 .map(|inbox_id| GetIdentityUpdatesV2Filter {
1127 sequence_id: existing_sequence_ids.get(*inbox_id).map(|i| *i as u64),
1128 inbox_id: inbox_id.to_string(),
1129 })
1130 .collect();
1131 api_client
1132 .fold_identity_updates(filters, limits, |page| {
1133 let page = page
1134 .into_iter()
1135 .map(|entry| {
1136 let sequence_id = entry
1137 .meta
1138 .cursor
1139 .ok_or(xmtp_api::ApiError::InvalidResponse("identity cursor"))?
1140 .sequence_id;
1141 let inbox_id = entry.update.inbox_id.clone();
1142 let update: UnverifiedIdentityUpdate = entry.update.try_into()?;
1143 Ok(StoredIdentityUpdate {
1144 inbox_id,
1145 sequence_id: sequence_id as i64,
1146 server_timestamp_ns: entry.meta.server_ns as i64,
1147 payload: update.into(),
1148 })
1149 })
1150 .collect::<Result<Vec<_>, IdentityUpdateError>>()?;
1151 conn.insert_or_ignore_identity_updates(&page)?;
1152 Ok(())
1153 })
1154 .await
1155}
1156
1157pub async fn is_member_of_association_state<Client>(
1159 api_client: &ApiClientWrapper<Client>,
1160 inbox_id: &str,
1161 identifier: &MemberIdentifier,
1162 scw_verifier: Option<Box<dyn SmartContractSignatureVerifier>>,
1163) -> Result<bool, IdentityUpdateError>
1164where
1165 Client: XmtpBackendClient + Clone,
1166{
1167 let filters = vec![GetIdentityUpdatesV2Filter {
1168 inbox_id: inbox_id.to_string(),
1169 sequence_id: None,
1170 }];
1171 let mut updates = api_client.get_identity_updates_v2(filters).await?;
1172
1173 let Some(updates) = updates.remove(inbox_id) else {
1174 return Err(IdentityUpdateError::InboxNotFound);
1175 };
1176 let updates: Vec<UnverifiedIdentityUpdate> = updates
1177 .into_iter()
1178 .map(|u| u.update.try_into())
1179 .collect::<Result<_, _>>()?;
1180
1181 let mut association_state = None;
1182
1183 let scw_verifier = scw_verifier
1184 .unwrap_or_else(|| Box::new(api_client.clone()) as Box<dyn SmartContractSignatureVerifier>);
1185
1186 let updates = verify_updates(updates, &scw_verifier).await?;
1187
1188 for update in updates {
1189 association_state =
1190 Some(update.update_state(association_state, update.client_timestamp_ns)?);
1191 }
1192 let association_state = association_state.ok_or(IdentityUpdateError::EmptyIdentityLog)?;
1193
1194 Ok(association_state.get(identifier).is_some())
1195}
1196
1197#[tracing::instrument(level = "trace", skip_all)]
1198pub async fn get_creation_signature_kind(
1199 conn: &impl xmtp_db::DbQuery,
1200 scw_verifier: impl SmartContractSignatureVerifier,
1201 inbox_id: InboxIdRef<'_>,
1202) -> Result<Option<xmtp_id::associations::SignatureKind>, IdentityUpdateError> {
1203 let updates = conn.get_identity_updates(inbox_id, None, None)?;
1204
1205 let first_update = updates
1206 .first()
1207 .ok_or_else(|| IdentityUpdateError::Identity(IdentityError::RequiredIdentityNotFound))?;
1208
1209 let unverified_update: UnverifiedIdentityUpdate = first_update.clone().to_unverified()?;
1210
1211 let verified = unverified_update.to_verified(scw_verifier).await?;
1212
1213 Ok(verified.creation_signature_kind())
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218 use crate::identity_updates::IdentityUpdates;
1219 use xmtp_db::prelude::*;
1220 use xmtp_id::associations::test_utils::add_wallet_signature;
1221
1222 #[xmtp_common::test(unwrap_try = true)]
1224 async fn required_leaf_removals_include_stale_leaves() {
1225 use super::{
1226 IdentityDependencyError, IdentityRequirement, get_installation_diff_local,
1227 required_leaf_removals,
1228 };
1229 use crate::identity_lifecycle::test_support::IdentityTestPeer;
1230 use std::collections::{BTreeSet, HashMap, HashSet};
1231 use xmtp_mls_validation::group_membership::GroupMembership;
1232 let alix = IdentityTestPeer::new().await;
1233 let alix2 = alix.new_installation().await;
1234 let mut revoke = IdentityUpdates::new(&alix.context)
1235 .revoke_installations(vec![alix2.installation_public_key().to_vec()])
1236 .await?;
1237 add_wallet_signature(&mut revoke, &alix.owner).await;
1238 IdentityUpdates::new(&alix.context)
1239 .apply_signature_request(revoke)
1240 .await?;
1241 IdentityUpdates::new(&alix.context)
1242 .get_latest_association_state(&alix.context.db(), alix.inbox_id())
1243 .await?;
1244 let sequence2 = alix
1245 .context
1246 .db()
1247 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
1248 let alix3 = alix.new_installation().await;
1249 IdentityUpdates::new(&alix.context)
1250 .get_latest_association_state(&alix.context.db(), alix.inbox_id())
1251 .await?;
1252 let sequence3 = alix
1253 .context
1254 .db()
1255 .get_latest_sequence_id_for_inbox(alix.inbox_id())? as u64;
1256 let mut old = GroupMembership::new();
1257 old.add(alix.inbox_id().into(), sequence2);
1258 let mut new = GroupMembership::new();
1259 new.add(alix.inbox_id().into(), sequence3);
1260 let owners = HashMap::from([
1261 (
1262 alix.installation_public_key().to_vec(),
1263 alix.inbox_id().into(),
1264 ),
1265 (
1266 alix2.installation_public_key().to_vec(),
1267 alix.inbox_id().into(),
1268 ),
1269 ]);
1270 let missing =
1273 required_leaf_removals(&alix2.context.db(), &old, &new, &HashSet::new(), &owners)
1274 .expect_err("the exact S3 proof is not cached on Alix2");
1275 assert!(matches!(
1276 missing,
1277 IdentityDependencyError::Need(needed)
1278 if needed == BTreeSet::from([IdentityRequirement {
1279 inbox_id: alix.inbox_id().into(),
1280 sequence_id: sequence3,
1281 }])
1282 ));
1283
1284 let db = alix.context.db();
1285 let diff = get_installation_diff_local(&db, &old, &new, &old.diff(&new))?;
1286 assert!(diff.removed_installations.is_empty());
1287 let stale = HashSet::from([alix2.installation_public_key().to_vec()]);
1288 assert_eq!(
1289 required_leaf_removals(&db, &old, &new, &diff.removed_installations, &owners)?,
1290 stale
1291 );
1292 assert_eq!(
1293 required_leaf_removals(&db, &old, &GroupMembership::new(), &HashSet::new(), &owners)?,
1294 owners.keys().cloned().collect()
1295 );
1296 old.add(alix.inbox_id().into(), 0);
1297 assert_eq!(
1298 required_leaf_removals(&db, &old, &new, &HashSet::new(), &owners)?,
1299 stale
1300 );
1301 assert!(required_leaf_removals(&db, &new, &new, &HashSet::new(), &owners)?.is_empty());
1302 drop(alix3);
1303 }
1304}
1305
1306#[cfg(test)]
1307mod conflict_tests {
1308 use super::*;
1309 use crate::identity_lifecycle::test_support::IdentityTestPeer;
1310 use xmtp_cryptography::utils::generate_local_wallet;
1311 use xmtp_id::associations::test_utils::{
1312 MockSmartContractSignatureVerifier, WalletTestExt, add_wallet_signature,
1313 };
1314 use xmtp_proto::{api::ApiClientError, backend_v1 as wire, types::Topic};
1315
1316 #[rstest::rstest]
1317 #[case(0)]
1318 #[case(1)]
1319 #[case(3)]
1320 #[case(4)]
1321 #[xmtp_common::test(unwrap_try = true)]
1322 async fn conflict_reloads_validates_and_bounds_identical_resends(#[case] conflicts: usize) {
1323 let alix = IdentityTestPeer::new().await;
1324 let wallet = generate_local_wallet();
1325 let mut request = IdentityUpdates::new(&alix.context)
1326 .associate_identity(wallet.identifier())
1327 .await
1328 .unwrap();
1329 add_wallet_signature(&mut request, &wallet).await;
1330 let update = request.build_identity_update().unwrap();
1331 let expected: xmtp_proto::xmtp::identity::associations::IdentityUpdate =
1332 update.clone().into();
1333 let history = alix
1334 .context
1335 .api()
1336 .query_all(
1337 [(
1338 Topic::new_identity_update(hex::decode(alix.inbox_id()).unwrap()),
1339 Cursor(0),
1340 )]
1341 .into(),
1342 alix.context.api().limits().max_query_limit as u32,
1343 )
1344 .await
1345 .unwrap();
1346 let last_sequence = history
1347 .last()
1348 .unwrap()
1349 .meta
1350 .as_ref()
1351 .unwrap()
1352 .cursor
1353 .as_ref()
1354 .unwrap()
1355 .sequence_id;
1356 let mut mock = xmtp_api_backend::MockBackendClient::new();
1357 let mut calls = 0;
1358 mock.expect_publish()
1359 .times((conflicts + 1).min(4))
1360 .returning(move |request| {
1361 calls += 1;
1362 assert_eq!(request.envelopes.len(), 1);
1363 assert_eq!(
1364 request.envelopes[0].payload,
1365 Some(wire::client_envelope::Payload::IdentityUpdate(
1366 expected.clone()
1367 ))
1368 );
1369 if calls <= conflicts {
1370 return Err(ApiClientError::client(
1371 xmtp_api_grpc::error::GrpcError::Status(tonic::Status::aborted(
1372 "identity conflict",
1373 )),
1374 ));
1375 }
1376 let parsed =
1377 xmtp_mls_validation::parse_envelope(request.envelopes[0].clone()).unwrap();
1378 Ok(wire::PublishResponse {
1379 envelope_metas: vec![wire::EnvelopeMeta {
1380 cursor: Some(wire::Cursor {
1381 sequence_id: last_sequence + 1,
1382 }),
1383 topic: Some(wire::Topic {
1384 topic: parsed.topic.cloned_vec(),
1385 }),
1386 message_hash: Some(wire::MessageHash {
1387 hash: Some(wire::message_hash::Hash::Sha256(
1388 parsed.canonical.hash.to_vec(),
1389 )),
1390 }),
1391 ..Default::default()
1392 }],
1393 })
1394 });
1395 mock.expect_query_raw_envelopes()
1396 .times(conflicts.min(3))
1397 .returning(move |request| {
1398 assert_eq!(request.queries.len(), 1);
1399 let floor = request.queries[0].cursor.as_ref().unwrap().sequence_id;
1400 Ok(wire::QueryResponse {
1401 envelopes: history
1402 .iter()
1403 .filter(|entry| {
1404 entry
1405 .meta
1406 .as_ref()
1407 .unwrap()
1408 .cursor
1409 .as_ref()
1410 .unwrap()
1411 .sequence_id
1412 > floor
1413 })
1414 .cloned()
1415 .collect(),
1416 continuation: Some(wire::Continuation { has_more: false }),
1417 }
1418 .into())
1419 });
1420 let api = ApiClientWrapper::new(mock, Default::default());
1421 let result = publish_with_conflict_retry(
1422 &api,
1423 &alix.context.db(),
1424 update,
1425 &MockSmartContractSignatureVerifier::new(true),
1426 )
1427 .await;
1428 if conflicts > 3 {
1429 assert!(matches!(
1430 result,
1431 Err(IdentityUpdateError::Api(
1432 xmtp_api::ApiError::IdentityUpdateConflict
1433 ))
1434 ));
1435 } else {
1436 assert_eq!(result.unwrap(), Cursor(last_sequence + 1));
1437 }
1438 }
1439}