Skip to main content

xmtp_mls_identity/
identity_updates.rs

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
49/// Identity's feature slot: the registry that coalesces exact identity lookups.
50pub trait IdentityFeatures {
51    fn identity_resolutions(&self) -> &IdentityResolutionRegistry;
52}
53
54/// A context the identity code runs on: core and identity resolution.
55pub trait IdentityContext: CoreContext<Features: IdentityFeatures> {
56    /// Coalesces exact identity lookups without treating a newer snapshot as the requested one.
57    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/// A failure to read, verify, build, or publish identity updates.
70#[derive(Debug, Error, ErrorCode)]
71pub enum IdentityUpdateError {
72    /// Signature request error.
73    ///
74    /// The signature request is incomplete, or asks for more actions than
75    /// one identity update may carry. Not retryable.
76    #[error(transparent)]
77    InvalidSignatureRequest(#[from] SignatureRequestError),
78    /// API error.
79    ///
80    /// Network request to XMTP backend failed. Retryable when the request is.
81    #[error(transparent)]
82    Api(#[from] xmtp_api::ApiError),
83    /// Validation error.
84    ///
85    /// The identity update did not validate against its history. Not retryable.
86    #[error(transparent)]
87    Validation(#[from] xmtp_mls_validation::ValidationError),
88    /// Storage error.
89    ///
90    /// Database operation failed. May be retryable.
91    #[error("storage error: {0}")]
92    Storage(#[from] StorageError),
93    /// Database connection error.
94    ///
95    /// Connection to database failed. Retryable.
96    #[error(transparent)]
97    Db(#[from] xmtp_db::ConnectionError),
98    /// Association error.
99    ///
100    /// Identity association operation failed. Not retryable.
101    #[error("Association error: {0}")]
102    Association(#[from] AssociationError),
103    /// Signature validation error.
104    ///
105    /// A signature failed verification. Retryable when the verifier failed
106    /// transiently.
107    #[error("signature validation error: {0}")]
108    SignatureValidation(#[from] SignatureError),
109    /// Identity error.
110    ///
111    /// Problem with identity operations. Not retryable.
112    #[error("identity error: {0}")]
113    Identity(#[from] IdentityError),
114    /// Conversion error.
115    ///
116    /// Data type failed to convert. Not retryable.
117    #[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    /// The connection is blocked. Not retryable.
126    #[error(transparent)]
127    #[error_code(inherit)]
128    Blocked(#[from] xmtp_mls_server_config::server_configuration::BlockedConnection),
129    /// Inbox not found.
130    ///
131    /// The backend holds no identity update for the inbox. Not retryable.
132    #[error("Unable to find provided inbox_id")]
133    InboxNotFound,
134    /// Empty identity log.
135    ///
136    /// The inbox's identity updates produced no association state. Not
137    /// retryable.
138    #[error("Unable to create association state")]
139    EmptyIdentityLog,
140    /// Publish failed.
141    ///
142    /// Publishing a signed identity update failed after its conflict retries.
143    /// Not retryable: the caller signs and applies a new request.
144    #[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            // SCW verification errors carry retryability through SignatureError;
161            // transient RPC provider failures must not advance the welcome cursor.
162            // See xmtp/libxmtp#3394.
163            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
212/// Reload and validate the signed update after a commit-time conflict.
213/// Retry the same signed bytes at most three times. Other errors are terminal.
214pub 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    /// Each added installation key, mapped to the inboxes whose identity states add it.
248    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
305/// Get the association state for a given inbox_id up to the (and inclusive of) the `to_sequence_id`
306/// If no `to_sequence_id` is provided, use the latest value in the database
307///
308/// This path has no client, so storing a computed state emits no event. A
309/// client stores through [`IdentityUpdates::get_association_state`].
310pub 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
325/// Whether `state` holds `identifier`. An Ethereum address matches in any
326/// letter case, and a passkey by its public key bytes.
327pub(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
334/// Whether the association state of the inbox in the database holds the
335/// identifier, an Ethereum address in any letter case, or `None` when the
336/// database has no identity update of the inbox. Computing the state caches
337/// it in the database.
338pub 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
351/// [`identifier_membership`] for a caller that may make no request, such as an
352/// offline build. It reads only the database: the cached state, or one
353/// derived from the stored log. A state whose derivation needs a smart
354/// contract wallet verdict counts as not holding the identifier.
355pub 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
368/// A verifier that makes no request, so it fails every smart contract wallet
369/// check.
370struct 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
388/// Whether the identifier may open the database of the inbox, given its
389/// [`identifier_membership`]. Only the identity that creates an inbox belongs
390/// to it before its first identity update.
391pub 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
400/// Refuse to revoke more members than one identity update may carry, before
401/// any request is built or signed: every validator would reject that update.
402fn 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
409/// Revoke the given installations from the association state for the client's inbox.
410/// More installations than one identity update may revoke fail before a request is built.
411pub 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
429/**
430 * Apply a signature request to the client's inbox by publishing the identity update to the network.
431 *
432 * This will error if the signature request is missing signatures, if the signatures are invalid,
433 * if the update fails other verifications, or if the update fails to be published to the network.
434 **/
435pub 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    // If the signature request isn't completed, this will error
442    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
455/// Get the association state for all provided `inbox_id`/optional `sequence_id` tuples, using the cache when available
456/// If the association state is not available in the cache, this falls back to reconstructing the association state
457/// from Identity Updates in the network.
458///
459/// At most [`IDENTITY_DEPENDENCY_CONCURRENCY`] states are derived at once, and
460/// the states are returned in the order of `identifiers`.
461pub 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    // Each read is a future that starts only when `buffered` polls it. They
467    // are collected first: a stream that maps the borrowed identifiers is not
468    // `Send` for every lifetime that callers such as the SDK need.
469    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    /// Get the association state for all provided `inbox_id`/optional `sequence_id` tuples, using the cache when available
491    /// If the association state is not available in the cache, this falls back to reconstructing the association state
492    /// from Identity Updates in the network.
493    ///
494    /// At most [`IDENTITY_DEPENDENCY_CONCURRENCY`] states are derived at once,
495    /// and the states are returned in the order of `identifiers`.
496    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        // Collected first for the reason given in
502        // `batch_get_association_state_with_verifier`.
503        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    /// Get the latest association state available on the network for the given `inbox_id`
521    #[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    /// Emit the installation changes of one own identity update. When it
532    /// revokes another installation, cycle the root key. The
533    /// revoked installation does not cycle on its own revocation.
534    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        // implements: SYNC-026
567        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    /// Run `load`, which can store updates of this client's own inbox, with
578    /// own-inbox loads serialized. Then, whether `load` succeeded or failed,
579    /// emit the own-installation changes of every own-inbox update stored
580    /// since the last emission. A load that stores pages and then fails,
581    /// including the reload after a publish conflict, therefore still reports
582    /// what it stored. Updates whose emission does not happen, because `load`
583    /// was dropped or the emission failed, are emitted after the next
584    /// own-inbox load. `load` is boxed to keep callers' futures small: SDK
585    /// worker futures have a size limit.
586    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    /// Emit the changes of each own-inbox update stored after
624    /// `emitted_through`, in sequence order, and move `emitted_through` past
625    /// each one emitted. With nothing emitted through yet, the latest stored
626    /// update becomes the starting point and nothing is emitted.
627    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    /// Get the association state for a given inbox_id up to the (and inclusive of) the `to_sequence_id`
656    /// If no `to_sequence_id` is provided, use the latest value in the database
657    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    /// Generate a `CreateInbox` signature request for the given wallet address.
667    /// If no nonce is provided, use 0
668    #[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        // We can pre-sign the request with an installation key signature, since we have access to the key
693        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        // The app fills this request in, so bind it to
704        // the chains the deployment accepts before it leaves the client.
705        self.context
706            .server_configuration()
707            .restrict(&mut signature_request);
708
709        Ok(signature_request)
710    }
711
712    /// Generate a `AssociateWallet` signature request using an existing wallet and a new wallet address
713    #[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        // the app fills this request in, so bind it to
744        // the chains the deployment accepts before it leaves the client.
745        self.context
746            .server_configuration()
747            .restrict(&mut signature_request);
748
749        Ok(signature_request)
750    }
751
752    /// Generate a `ClaimHandle` signature request for `handle`, signed by this
753    /// installation's key.
754    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    /// Revoke the given identities from the association state for the client's inbox.
775    /// More identities than one identity update may revoke fail before any network read.
776    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    /// Revoke the given installations from the association state for the client's inbox
806    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            &current_state.recovery_identifier().clone(),
821            inbox_id,
822            installation_ids,
823        )?;
824        // Every request this client hands back is bound to the
825        // deployment's accepted chains, so an app cannot sign it from a chain
826        // the deployment refuses.
827        self.context.server_configuration().restrict(&mut result);
828
829        Ok(result)
830    }
831
832    /// Generate a `ChangeRecoveryAddress` signature request using a new identifier
833    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    /**
857     * Apply a signature request to the client's inbox by publishing the identity update to the network.
858     *
859     * This will error if the signature request is missing signatures, if the signatures are invalid,
860     * if the update fails other verifications, or if the update fails to be published to the network.
861     **/
862    pub async fn apply_signature_request(
863        &self,
864        signature_request: SignatureRequest,
865    ) -> Result<(), IdentityUpdateError> {
866        // A client with a blocked connection publishes no identity update.
867        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            // Load the identity updates for the inbox so that we have a
878            // record in our DB.
879            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    /// Given two group memberships and the diff, get the list of installations that were added or removed
900    /// between the two membership states.
901    #[tracing::instrument(level = "trace", skip_all)]
902    pub async fn get_installation_diff(
903        &self,
904        conn: &impl DbQuery,
905        group_id: &GroupId, // used for logging
906        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
952/// Compare exact verified snapshots without network or signature-verifier calls.
953/// A creation placeholder has an empty identity baseline. It never selects the
954/// receiver's latest identity state. New membership references must be nonzero.
955/// A missing proof does not stop the scan, so `Need` names every missing proof.
956pub 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
1020/// Required removals include leafed identity removals and stale leaves of
1021/// every inbox whose membership entry advances or disappears.
1022// implements: GMOD-046
1023pub 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
1068/// Load identity updates. A load that includes this client's own inbox
1069/// reports the changes to its own installations of every update it stored,
1070/// even when it fails after storing some pages.
1071pub 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/// Store every identity update of `inbox_ids` newer than the latest stored
1092/// one of each inbox.
1093#[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
1108/// Read the updates of `inbox_ids` after the latest stored one of each inbox,
1109/// one bounded page at a time, and store each page before the next request.
1110/// A failure leaves each inbox with a stored prefix of its log, and the next
1111/// load resumes after it.
1112async 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
1157/// A static lookup method to verify if an identity is a member of an inbox
1158pub 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    // verifies: GMOD-046
1223    #[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        // Alix2 was created before S3 and has not loaded that proof. A
1271        // missing proof must not produce an incomplete set of removals.
1272        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}