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;
7
8#[derive(Debug, Error)]
9pub enum AssociationError {
10 #[error("Error creating association {0}")]
11 Generic(String),
12 #[error("Multiple create operations detected")]
13 MultipleCreate,
14 #[error("XID not yet created")]
15 NotCreated,
16 #[error("Signature validation failed {0}")]
17 Signature(#[from] SignatureError),
18 #[error("Member of kind {0} not allowed to add {1}")]
19 MemberNotAllowed(MemberKind, MemberKind),
20 #[error("Missing existing member")]
21 MissingExistingMember,
22 #[error("Legacy key is only allowed to be associated using a legacy signature with nonce 0")]
23 LegacySignatureReuse,
24 #[error("The new member identifier does not match the signer")]
25 NewMemberIdSignatureMismatch,
26 #[error("Wrong inbox_id specified on association")]
27 WrongInboxId,
28 #[error("Signature not allowed for role {0:?} {1:?}")]
29 SignatureNotAllowed(String, String),
30 #[error("Replay detected")]
31 Replay,
32 #[error("Deserialization error {0}")]
33 Deserialization(#[from] DeserializationError),
34 #[error("Missing identity update")]
35 MissingIdentityUpdate,
36 #[error("Wrong chain id. Initially added with {0} but now signing from {1}")]
37 ChainIdMismatch(u64, u64),
38 #[error("Invalid account address: Must be 42 hex characters, starting with '0x'.")]
39 InvalidAccountAddress,
40 #[error("{0} are not a public identifier")]
41 NotIdentifier(String),
42 #[error(transparent)]
43 Convert(#[from] xmtp_proto::ConversionError),
44}
45
46pub trait IdentityAction: Send {
47 fn update_state(
48 &self,
49 existing_state: Option<AssociationState>,
50 client_timestamp_ns: u64,
51 ) -> Result<AssociationState, AssociationError>;
52 fn signatures(&self) -> Vec<Vec<u8>>;
53 fn replay_check(&self, state: &AssociationState) -> Result<(), AssociationError> {
54 let signatures = self.signatures();
55 for signature in signatures {
56 if state.has_seen(&signature) {
57 return Err(AssociationError::Replay);
58 }
59 }
60
61 Ok(())
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct CreateInbox {
68 pub nonce: u64,
69 pub account_identifier: Identifier,
70 pub initial_identifier_signature: VerifiedSignature,
71}
72
73impl IdentityAction for CreateInbox {
74 fn update_state(
75 &self,
76 existing_state: Option<AssociationState>,
77 _client_timestamp_ns: u64,
78 ) -> Result<AssociationState, AssociationError> {
79 if existing_state.is_some() {
80 return Err(AssociationError::MultipleCreate);
81 }
82
83 let account_address = self.account_identifier.clone();
84 let recovered_signer = self.initial_identifier_signature.signer.clone();
85 if recovered_signer != account_address {
86 return Err(AssociationError::MissingExistingMember);
87 }
88
89 allowed_signature_for_kind(
90 &self.account_identifier.kind(),
91 &self.initial_identifier_signature.kind,
92 )?;
93
94 if self.initial_identifier_signature.kind == SignatureKind::LegacyDelegated
95 && self.nonce != 0
96 {
97 return Err(AssociationError::LegacySignatureReuse);
98 }
99
100 AssociationState::new(
101 account_address,
102 self.nonce,
103 self.initial_identifier_signature.chain_id,
104 )
105 }
106
107 fn signatures(&self) -> Vec<Vec<u8>> {
108 vec![self.initial_identifier_signature.raw_bytes.clone()]
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct AddAssociation {
115 pub new_member_signature: VerifiedSignature,
116 pub new_member_identifier: MemberIdentifier,
117 pub existing_member_signature: VerifiedSignature,
118}
119
120impl IdentityAction for AddAssociation {
121 fn update_state(
122 &self,
123 maybe_existing_state: Option<AssociationState>,
124 client_timestamp_ns: u64,
125 ) -> Result<AssociationState, AssociationError> {
126 let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
127 self.replay_check(&existing_state)?;
128
129 let new_member_address = &self.new_member_signature.signer;
131 let existing_member_identifier = &self.existing_member_signature.signer;
133
134 if new_member_address.ne(&self.new_member_identifier) {
135 return Err(AssociationError::NewMemberIdSignatureMismatch);
136 }
137
138 if new_member_address == existing_member_identifier {
140 return Err(AssociationError::Generic("tried to add self".to_string()));
141 }
142
143 let existing_member_identifier = existing_member_identifier.clone();
146 let identifier: Option<Identifier> = existing_member_identifier.clone().into();
147 if let Some(identifier) = identifier
148 && (is_legacy_signature(&self.new_member_signature)
149 || is_legacy_signature(&self.existing_member_signature))
150 && existing_state.inbox_id() != identifier.inbox_id(0)?
151 {
152 return Err(AssociationError::LegacySignatureReuse);
153 }
154
155 allowed_signature_for_kind(
156 &self.new_member_identifier.kind(),
157 &self.new_member_signature.kind,
158 )?;
159
160 let existing_member = existing_state.get(&existing_member_identifier);
161
162 if let Some(member) = existing_member {
163 verify_chain_id_matches(member, &self.existing_member_signature)?;
164 }
165
166 let existing_entity_id = match existing_member {
167 Some(member) => member.identifier.clone(),
169 None => {
170 let recovery_identifier = existing_state.recovery_identifier().clone().into();
172
173 if existing_member_identifier != recovery_identifier {
175 return Err(AssociationError::MissingExistingMember);
176 }
177 if is_legacy_signature(&self.existing_member_signature) {
179 return Err(AssociationError::LegacySignatureReuse);
180 }
181 recovery_identifier
183 }
184 };
185
186 allowed_signature_for_kind(
188 &existing_entity_id.kind(),
189 &self.existing_member_signature.kind,
190 )?;
191
192 allowed_association(
194 existing_member_identifier.kind(),
195 self.new_member_identifier.kind(),
196 )?;
197
198 let new_member = Member::new(
199 new_member_address.clone(),
200 Some(existing_entity_id),
201 Some(client_timestamp_ns),
202 self.new_member_signature.chain_id,
203 );
204
205 Ok(existing_state.add(new_member))
206 }
207
208 fn signatures(&self) -> Vec<Vec<u8>> {
209 vec![
210 self.existing_member_signature.raw_bytes.clone(),
211 self.new_member_signature.raw_bytes.clone(),
212 ]
213 }
214}
215
216#[derive(Debug, Clone)]
218pub struct RevokeAssociation {
219 pub recovery_identifier_signature: VerifiedSignature,
220 pub revoked_member: MemberIdentifier,
221}
222
223impl IdentityAction for RevokeAssociation {
224 fn update_state(
225 &self,
226 maybe_existing_state: Option<AssociationState>,
227 _client_timestamp_ns: u64,
228 ) -> Result<AssociationState, AssociationError> {
229 let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
230 self.replay_check(&existing_state)?;
231
232 let existing_member = existing_state.get(&self.recovery_identifier_signature.signer);
234 if let Some(member) = existing_member {
235 verify_chain_id_matches(member, &self.recovery_identifier_signature)?;
236 }
237
238 if is_legacy_signature(&self.recovery_identifier_signature) {
239 return Err(AssociationError::SignatureNotAllowed(
240 MemberKind::Ethereum.to_string(),
241 SignatureKind::LegacyDelegated.to_string(),
242 ));
243 }
244 let recovery_signer = &self.recovery_identifier_signature.signer;
246 let state_recovery_identifier: MemberIdentifier =
248 existing_state.recovery_identifier.clone().into();
249
250 if *recovery_signer != state_recovery_identifier {
251 return Err(AssociationError::MissingExistingMember);
252 }
253
254 let installations_to_remove: Vec<Member> = existing_state
255 .members_by_parent(&self.revoked_member)
256 .into_iter()
257 .filter(|child| child.kind() == MemberKind::Installation)
259 .collect();
260
261 let new_state = existing_state.remove(&self.revoked_member);
263
264 Ok(installations_to_remove
265 .iter()
266 .fold(new_state, |state, installation| {
267 state.remove(&installation.identifier)
268 }))
269 }
270
271 fn signatures(&self) -> Vec<Vec<u8>> {
272 vec![self.recovery_identifier_signature.raw_bytes.clone()]
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct ChangeRecoveryIdentity {
279 pub recovery_identifier_signature: VerifiedSignature,
280 pub new_recovery_identifier: Identifier,
281}
282
283impl IdentityAction for ChangeRecoveryIdentity {
284 fn update_state(
285 &self,
286 existing_state: Option<AssociationState>,
287 _client_timestamp_ns: u64,
288 ) -> Result<AssociationState, AssociationError> {
289 let existing_state = existing_state.ok_or(AssociationError::NotCreated)?;
290 self.replay_check(&existing_state)?;
291
292 let existing_member = existing_state.get(&self.recovery_identifier_signature.signer);
293 if let Some(member) = existing_member {
294 verify_chain_id_matches(member, &self.recovery_identifier_signature)?;
295 }
296
297 if is_legacy_signature(&self.recovery_identifier_signature) {
298 return Err(AssociationError::SignatureNotAllowed(
299 MemberKind::Ethereum.to_string(),
300 SignatureKind::LegacyDelegated.to_string(),
301 ));
302 }
303
304 let recovery_signer = &self.recovery_identifier_signature.signer;
305 if existing_state.recovery_identifier() != recovery_signer {
306 return Err(AssociationError::MissingExistingMember);
307 }
308
309 Ok(existing_state.set_recovery_identifier(self.new_recovery_identifier.clone()))
310 }
311
312 fn signatures(&self) -> Vec<Vec<u8>> {
313 vec![self.recovery_identifier_signature.raw_bytes.clone()]
314 }
315}
316
317#[derive(Debug, Clone)]
319pub enum Action {
320 CreateInbox(CreateInbox),
321 AddAssociation(AddAssociation),
322 RevokeAssociation(RevokeAssociation),
323 ChangeRecoveryIdentity(ChangeRecoveryIdentity),
324}
325
326impl IdentityAction for Action {
327 fn update_state(
328 &self,
329 existing_state: Option<AssociationState>,
330 client_timestamp_ns: u64,
331 ) -> Result<AssociationState, AssociationError> {
332 match self {
333 Action::CreateInbox(event) => event.update_state(existing_state, client_timestamp_ns),
334 Action::AddAssociation(event) => {
335 event.update_state(existing_state, client_timestamp_ns)
336 }
337 Action::RevokeAssociation(event) => {
338 event.update_state(existing_state, client_timestamp_ns)
339 }
340 Action::ChangeRecoveryIdentity(event) => {
341 event.update_state(existing_state, client_timestamp_ns)
342 }
343 }
344 }
345
346 fn signatures(&self) -> Vec<Vec<u8>> {
347 match self {
348 Action::CreateInbox(event) => event.signatures(),
349 Action::AddAssociation(event) => event.signatures(),
350 Action::RevokeAssociation(event) => event.signatures(),
351 Action::ChangeRecoveryIdentity(event) => event.signatures(),
352 }
353 }
354}
355
356#[derive(Debug, Clone)]
358pub struct IdentityUpdate {
359 pub inbox_id: String,
360 pub client_timestamp_ns: u64,
361 pub actions: Vec<Action>,
362}
363
364impl IdentityUpdate {
365 pub fn new(actions: Vec<Action>, inbox_id: String, client_timestamp_ns: u64) -> Self {
366 Self {
367 inbox_id,
368 actions,
369 client_timestamp_ns,
370 }
371 }
372
373 pub fn creation_signature_kind(&self) -> Option<SignatureKind> {
379 self.actions.iter().find_map(|action| match action {
380 Action::CreateInbox(create_inbox) => {
381 Some(create_inbox.initial_identifier_signature.kind.clone())
382 }
383 _ => None,
384 })
385 }
386}
387
388impl IdentityAction for IdentityUpdate {
389 fn update_state(
390 &self,
391 existing_state: Option<AssociationState>,
392 _client_timestamp_ns: u64,
393 ) -> Result<AssociationState, AssociationError> {
394 let mut state = existing_state;
395 for action in &self.actions {
396 state = Some(action.update_state(state, self.client_timestamp_ns)?);
397 }
398
399 let new_state = state.ok_or(AssociationError::NotCreated)?;
400 if new_state.inbox_id().ne(&self.inbox_id) {
401 tracing::error!(
402 "state inbox id mismatch, old: {}, new: {}",
403 self.inbox_id,
404 new_state.inbox_id()
405 );
406 return Err(AssociationError::WrongInboxId);
407 }
408
409 Ok(new_state.add_seen_signatures(self.signatures()))
412 }
413
414 fn signatures(&self) -> Vec<Vec<u8>> {
415 self.actions
416 .iter()
417 .flat_map(|action| action.signatures())
418 .collect()
419 }
420}
421
422#[allow(clippy::borrowed_box)]
423fn is_legacy_signature(signature: &VerifiedSignature) -> bool {
424 signature.kind == SignatureKind::LegacyDelegated
425}
426
427fn allowed_association(
428 existing_member_kind: MemberKind,
429 new_member_kind: MemberKind,
430) -> Result<(), AssociationError> {
431 if existing_member_kind == MemberKind::Installation
433 && new_member_kind == MemberKind::Installation
434 {
435 return Err(AssociationError::MemberNotAllowed(
436 existing_member_kind,
437 new_member_kind,
438 ));
439 }
440
441 Ok(())
442}
443
444fn allowed_signature_for_kind(
446 role: &MemberKind,
447 signature_kind: &SignatureKind,
448) -> Result<(), AssociationError> {
449 let is_ok = match role {
450 MemberKind::Ethereum => matches!(
451 signature_kind,
452 SignatureKind::Erc191 | SignatureKind::Erc1271 | SignatureKind::LegacyDelegated
453 ),
454 MemberKind::Installation => matches!(signature_kind, SignatureKind::InstallationKey),
455 MemberKind::Passkey => matches!(signature_kind, SignatureKind::P256),
456 };
457
458 if !is_ok {
459 return Err(AssociationError::SignatureNotAllowed(
460 role.to_string(),
461 signature_kind.to_string(),
462 ));
463 }
464
465 Ok(())
466}
467
468fn verify_chain_id_matches(
469 member: &Member,
470 signature: &VerifiedSignature,
471) -> Result<(), AssociationError> {
472 if member.added_on_chain_id != signature.chain_id {
473 return Err(AssociationError::ChainIdMismatch(
474 member.added_on_chain_id.unwrap_or(0),
475 signature.chain_id.unwrap_or(0),
476 ));
477 }
478
479 Ok(())
480}