1use super::member::{HasMemberKind, Identifier, Member, MemberIdentifier, MemberKind};
2use super::serialization::DeserializationError;
3use super::signature::{SignatureError, SignatureKind};
4use super::state::AssociationState;
5use super::verified_signature::VerifiedSignature;
6use thiserror::Error;
7use xmtp_common::ErrorCode;
8
9#[derive(Debug, Error, ErrorCode)]
10pub enum AssociationError {
11 #[error("Error creating association {0}")]
15 Generic(String),
16 #[error("Multiple create operations detected")]
20 MultipleCreate,
21 #[error("XID not yet created")]
25 NotCreated,
26 #[error("Signature validation failed {0}")]
27 #[error_code(inherit)]
28 Signature(#[from] SignatureError),
29 #[error("Member of kind {0} not allowed to add {1}")]
33 MemberNotAllowed(MemberKind, MemberKind),
34 #[error("Missing existing member")]
38 MissingExistingMember,
39 #[error("Legacy key is only allowed to be associated using a legacy signature with nonce 0")]
43 LegacySignatureReuse,
44 #[error("The new member identifier does not match the signer")]
48 NewMemberIdSignatureMismatch,
49 #[error("Wrong inbox_id specified on association")]
53 WrongInboxId,
54 #[error("Signature not allowed for role {0:?} {1:?}")]
58 SignatureNotAllowed(String, String),
59 #[error("Replay detected")]
63 Replay,
64 #[error("Deserialization error {0}")]
65 #[error_code(inherit)]
66 Deserialization(#[from] DeserializationError),
67 #[error("Missing identity update")]
71 MissingIdentityUpdate,
72 #[error("Wrong chain id. Initially added with {0} but now signing from {1}")]
76 ChainIdMismatch(u64, u64),
77 #[error("Invalid account address: Must be 42 hex characters, starting with '0x'.")]
81 InvalidAccountAddress,
82 #[error("{0} are not a public identifier")]
86 NotIdentifier(String),
87 #[error(transparent)]
88 #[error_code(inherit)]
89 Convert(#[from] xmtp_proto::ConversionError),
90}
91
92pub trait IdentityAction: Send {
93 fn update_state(
94 &self,
95 existing_state: Option<AssociationState>,
96 client_timestamp_ns: u64,
97 ) -> Result<AssociationState, AssociationError>;
98 fn signatures(&self) -> Vec<Vec<u8>>;
99 fn replay_check(&self, state: &AssociationState) -> Result<(), AssociationError> {
100 let signatures = self.signatures();
101 for signature in signatures {
102 if state.has_seen(&signature) {
103 return Err(AssociationError::Replay);
104 }
105 }
106
107 Ok(())
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct CreateInbox {
114 pub nonce: u64,
115 pub account_identifier: Identifier,
116 pub initial_identifier_signature: VerifiedSignature,
117}
118
119impl IdentityAction for CreateInbox {
120 fn update_state(
121 &self,
122 existing_state: Option<AssociationState>,
123 _client_timestamp_ns: u64,
124 ) -> Result<AssociationState, AssociationError> {
125 if existing_state.is_some() {
126 return Err(AssociationError::MultipleCreate);
127 }
128
129 let account_address = self.account_identifier.clone();
130 let recovered_signer = self.initial_identifier_signature.signer.clone();
131 if recovered_signer != account_address {
132 return Err(AssociationError::MissingExistingMember);
133 }
134
135 allowed_signature_for_kind(
136 &self.account_identifier.kind(),
137 &self.initial_identifier_signature.kind,
138 )?;
139
140 if self.initial_identifier_signature.kind == SignatureKind::LegacyDelegated
141 && self.nonce != 0
142 {
143 return Err(AssociationError::LegacySignatureReuse);
144 }
145
146 AssociationState::new(
147 account_address,
148 self.nonce,
149 self.initial_identifier_signature.chain_id,
150 )
151 }
152
153 fn signatures(&self) -> Vec<Vec<u8>> {
154 vec![self.initial_identifier_signature.raw_bytes.clone()]
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct AddAssociation {
161 pub new_member_signature: VerifiedSignature,
162 pub new_member_identifier: MemberIdentifier,
163 pub existing_member_signature: VerifiedSignature,
164}
165
166impl IdentityAction for AddAssociation {
167 fn update_state(
168 &self,
169 maybe_existing_state: Option<AssociationState>,
170 client_timestamp_ns: u64,
171 ) -> Result<AssociationState, AssociationError> {
172 let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
173 self.replay_check(&existing_state)?;
174
175 let new_member_address = &self.new_member_signature.signer;
177 let existing_member_identifier = &self.existing_member_signature.signer;
179
180 if new_member_address.ne(&self.new_member_identifier) {
181 return Err(AssociationError::NewMemberIdSignatureMismatch);
182 }
183
184 if new_member_address == existing_member_identifier {
186 return Err(AssociationError::Generic("tried to add self".to_string()));
187 }
188
189 let existing_member_identifier = existing_member_identifier.clone();
192 let identifier: Option<Identifier> = existing_member_identifier.clone().into();
193 if let Some(identifier) = identifier
194 && (is_legacy_signature(&self.new_member_signature)
195 || is_legacy_signature(&self.existing_member_signature))
196 && existing_state.inbox_id() != identifier.inbox_id(0)?
197 {
198 return Err(AssociationError::LegacySignatureReuse);
199 }
200
201 allowed_signature_for_kind(
202 &self.new_member_identifier.kind(),
203 &self.new_member_signature.kind,
204 )?;
205
206 let existing_member = existing_state.get(&existing_member_identifier);
207
208 if let Some(member) = existing_member {
209 verify_chain_id_matches(member, &self.existing_member_signature)?;
210 }
211
212 let existing_entity_id = match existing_member {
213 Some(member) => member.identifier.clone(),
215 None => {
216 let recovery_identifier = existing_state.recovery_identifier().clone().into();
218
219 if existing_member_identifier != recovery_identifier {
221 return Err(AssociationError::MissingExistingMember);
222 }
223 if is_legacy_signature(&self.existing_member_signature) {
225 return Err(AssociationError::LegacySignatureReuse);
226 }
227 recovery_identifier
229 }
230 };
231
232 allowed_signature_for_kind(
234 &existing_entity_id.kind(),
235 &self.existing_member_signature.kind,
236 )?;
237
238 allowed_association(
240 existing_member_identifier.kind(),
241 self.new_member_identifier.kind(),
242 )?;
243
244 let new_member = Member::new(
245 new_member_address.clone(),
246 Some(existing_entity_id),
247 Some(client_timestamp_ns),
248 self.new_member_signature.chain_id,
249 );
250
251 Ok(existing_state.add(new_member))
252 }
253
254 fn signatures(&self) -> Vec<Vec<u8>> {
255 vec![
256 self.existing_member_signature.raw_bytes.clone(),
257 self.new_member_signature.raw_bytes.clone(),
258 ]
259 }
260}
261
262#[derive(Debug, Clone)]
264pub struct RevokeAssociation {
265 pub recovery_identifier_signature: VerifiedSignature,
266 pub revoked_member: MemberIdentifier,
267}
268
269impl IdentityAction for RevokeAssociation {
270 fn update_state(
271 &self,
272 maybe_existing_state: Option<AssociationState>,
273 _client_timestamp_ns: u64,
274 ) -> Result<AssociationState, AssociationError> {
275 let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
276 self.replay_check(&existing_state)?;
277
278 let existing_member = existing_state.get(&self.recovery_identifier_signature.signer);
280 if let Some(member) = existing_member {
281 verify_chain_id_matches(member, &self.recovery_identifier_signature)?;
282 }
283
284 if is_legacy_signature(&self.recovery_identifier_signature) {
285 return Err(AssociationError::SignatureNotAllowed(
286 MemberKind::Ethereum.to_string(),
287 SignatureKind::LegacyDelegated.to_string(),
288 ));
289 }
290 let recovery_signer = &self.recovery_identifier_signature.signer;
292 let state_recovery_identifier: MemberIdentifier =
294 existing_state.recovery_identifier.clone().into();
295
296 if *recovery_signer != state_recovery_identifier {
297 return Err(AssociationError::MissingExistingMember);
298 }
299
300 let installations_to_remove: Vec<Member> = existing_state
301 .members_by_parent(&self.revoked_member)
302 .into_iter()
303 .filter(|child| child.kind() == MemberKind::Installation)
305 .collect();
306
307 let new_state = existing_state.remove(&self.revoked_member);
309
310 Ok(installations_to_remove
311 .iter()
312 .fold(new_state, |state, installation| {
313 state.remove(&installation.identifier)
314 }))
315 }
316
317 fn signatures(&self) -> Vec<Vec<u8>> {
318 vec![self.recovery_identifier_signature.raw_bytes.clone()]
319 }
320}
321
322#[derive(Debug, Clone)]
324pub struct ChangeRecoveryIdentity {
325 pub recovery_identifier_signature: VerifiedSignature,
326 pub new_recovery_identifier: Identifier,
327}
328
329impl IdentityAction for ChangeRecoveryIdentity {
330 fn update_state(
331 &self,
332 existing_state: Option<AssociationState>,
333 _client_timestamp_ns: u64,
334 ) -> Result<AssociationState, AssociationError> {
335 let existing_state = existing_state.ok_or(AssociationError::NotCreated)?;
336 self.replay_check(&existing_state)?;
337
338 let existing_member = existing_state.get(&self.recovery_identifier_signature.signer);
339 if let Some(member) = existing_member {
340 verify_chain_id_matches(member, &self.recovery_identifier_signature)?;
341 }
342
343 if is_legacy_signature(&self.recovery_identifier_signature) {
344 return Err(AssociationError::SignatureNotAllowed(
345 MemberKind::Ethereum.to_string(),
346 SignatureKind::LegacyDelegated.to_string(),
347 ));
348 }
349
350 let recovery_signer = &self.recovery_identifier_signature.signer;
351 if existing_state.recovery_identifier() != recovery_signer {
352 return Err(AssociationError::MissingExistingMember);
353 }
354
355 Ok(existing_state.set_recovery_identifier(self.new_recovery_identifier.clone()))
356 }
357
358 fn signatures(&self) -> Vec<Vec<u8>> {
359 vec![self.recovery_identifier_signature.raw_bytes.clone()]
360 }
361}
362
363#[derive(Debug, Clone)]
365pub enum Action {
366 CreateInbox(CreateInbox),
367 AddAssociation(AddAssociation),
368 RevokeAssociation(RevokeAssociation),
369 ChangeRecoveryIdentity(ChangeRecoveryIdentity),
370}
371
372impl IdentityAction for Action {
373 fn update_state(
374 &self,
375 existing_state: Option<AssociationState>,
376 client_timestamp_ns: u64,
377 ) -> Result<AssociationState, AssociationError> {
378 match self {
379 Action::CreateInbox(event) => event.update_state(existing_state, client_timestamp_ns),
380 Action::AddAssociation(event) => {
381 event.update_state(existing_state, client_timestamp_ns)
382 }
383 Action::RevokeAssociation(event) => {
384 event.update_state(existing_state, client_timestamp_ns)
385 }
386 Action::ChangeRecoveryIdentity(event) => {
387 event.update_state(existing_state, client_timestamp_ns)
388 }
389 }
390 }
391
392 fn signatures(&self) -> Vec<Vec<u8>> {
393 match self {
394 Action::CreateInbox(event) => event.signatures(),
395 Action::AddAssociation(event) => event.signatures(),
396 Action::RevokeAssociation(event) => event.signatures(),
397 Action::ChangeRecoveryIdentity(event) => event.signatures(),
398 }
399 }
400}
401
402#[derive(Debug, Clone)]
404pub struct IdentityUpdate {
405 pub inbox_id: String,
406 pub client_timestamp_ns: u64,
407 pub actions: Vec<Action>,
408}
409
410impl IdentityUpdate {
411 pub fn new(actions: Vec<Action>, inbox_id: String, client_timestamp_ns: u64) -> Self {
412 Self {
413 inbox_id,
414 actions,
415 client_timestamp_ns,
416 }
417 }
418
419 pub fn creation_signature_kind(&self) -> Option<SignatureKind> {
425 self.actions.iter().find_map(|action| match action {
426 Action::CreateInbox(create_inbox) => {
427 Some(create_inbox.initial_identifier_signature.kind.clone())
428 }
429 _ => None,
430 })
431 }
432}
433
434impl IdentityAction for IdentityUpdate {
435 fn update_state(
436 &self,
437 existing_state: Option<AssociationState>,
438 _client_timestamp_ns: u64,
439 ) -> Result<AssociationState, AssociationError> {
440 let mut state = existing_state;
441 for action in &self.actions {
442 state = Some(action.update_state(state, self.client_timestamp_ns)?);
443 }
444
445 let new_state = state.ok_or(AssociationError::NotCreated)?;
446 if new_state.inbox_id().ne(&self.inbox_id) {
447 tracing::error!(
448 "state inbox id mismatch, old: {}, new: {}",
449 self.inbox_id,
450 new_state.inbox_id()
451 );
452 return Err(AssociationError::WrongInboxId);
453 }
454
455 Ok(new_state.add_seen_signatures(self.signatures()))
458 }
459
460 fn signatures(&self) -> Vec<Vec<u8>> {
461 self.actions
462 .iter()
463 .flat_map(|action| action.signatures())
464 .collect()
465 }
466}
467
468#[allow(clippy::borrowed_box)]
469fn is_legacy_signature(signature: &VerifiedSignature) -> bool {
470 signature.kind == SignatureKind::LegacyDelegated
471}
472
473fn allowed_association(
474 existing_member_kind: MemberKind,
475 new_member_kind: MemberKind,
476) -> Result<(), AssociationError> {
477 if existing_member_kind == MemberKind::Installation
479 && new_member_kind == MemberKind::Installation
480 {
481 return Err(AssociationError::MemberNotAllowed(
482 existing_member_kind,
483 new_member_kind,
484 ));
485 }
486
487 Ok(())
488}
489
490fn allowed_signature_for_kind(
492 role: &MemberKind,
493 signature_kind: &SignatureKind,
494) -> Result<(), AssociationError> {
495 let is_ok = match role {
496 MemberKind::Ethereum => matches!(
497 signature_kind,
498 SignatureKind::Erc191 | SignatureKind::Erc1271 | SignatureKind::LegacyDelegated
499 ),
500 MemberKind::Installation => matches!(signature_kind, SignatureKind::InstallationKey),
501 MemberKind::Passkey => matches!(signature_kind, SignatureKind::P256),
502 };
503
504 if !is_ok {
505 return Err(AssociationError::SignatureNotAllowed(
506 role.to_string(),
507 signature_kind.to_string(),
508 ));
509 }
510
511 Ok(())
512}
513
514fn verify_chain_id_matches(
515 member: &Member,
516 signature: &VerifiedSignature,
517) -> Result<(), AssociationError> {
518 if member.added_on_chain_id != signature.chain_id {
519 return Err(AssociationError::ChainIdMismatch(
520 member.added_on_chain_id.unwrap_or(0),
521 signature.chain_id.unwrap_or(0),
522 ));
523 }
524
525 Ok(())
526}