1use std::collections::HashMap;
6
7use super::member::{HasMemberKind, Identifier};
8use crate::scw_verifier::SmartContractSignatureVerifier;
9use thiserror::Error;
10use xmtp_common::ErrorCode;
11use xmtp_common::time::now_ns;
12
13use super::{
14 AccountId, MemberIdentifier, MemberKind, SignatureError,
15 unsigned_actions::{
16 SignatureTextCreator, UnsignedAction, UnsignedAddAssociation,
17 UnsignedChangeRecoveryAddress, UnsignedCreateInbox, UnsignedIdentityUpdate,
18 UnsignedRevokeAssociation,
19 },
20 unverified::{
21 NewUnverifiedSmartContractWalletSignature, UnverifiedAction, UnverifiedAddAssociation,
22 UnverifiedChangeRecoveryAddress, UnverifiedCreateInbox, UnverifiedIdentityUpdate,
23 UnverifiedRevokeAssociation, UnverifiedSignature, UnverifiedSmartContractWalletSignature,
24 },
25 verified_signature::VerifiedSignature,
26};
27
28#[derive(Clone, PartialEq, Hash, Eq, Debug)]
31enum SignatureField {
32 InitialAddress,
33 ExistingMember,
34 NewMember,
35 RecoveryAddress,
36}
37
38#[derive(Clone, Debug)]
39pub struct PendingIdentityAction {
40 unsigned_action: UnsignedAction,
41 pending_signatures: HashMap<SignatureField, MemberIdentifier>,
42}
43
44pub struct SignatureRequestBuilder {
49 inbox_id: String,
50 client_timestamp_ns: u64,
51 actions: Vec<PendingIdentityAction>,
52}
53
54impl SignatureRequestBuilder {
55 pub fn new<S: AsRef<str>>(inbox_id: S) -> Self {
57 Self {
58 inbox_id: inbox_id.as_ref().to_string(),
59 client_timestamp_ns: now_ns() as u64,
60 actions: vec![],
61 }
62 }
63
64 pub fn create_inbox(mut self, signer_identity: Identifier, nonce: u64) -> Self {
66 let pending_action = PendingIdentityAction {
67 unsigned_action: UnsignedAction::CreateInbox(UnsignedCreateInbox {
68 account_identifier: signer_identity.clone(),
69 nonce,
70 }),
71 pending_signatures: HashMap::from([(
72 SignatureField::InitialAddress,
73 signer_identity.into(),
74 )]),
75 };
76 self.actions.push(pending_action);
78
79 self
80 }
81
82 pub fn add_association(
84 mut self,
85 new_member_identifier: MemberIdentifier,
86 existing_member_identifier: MemberIdentifier,
87 ) -> Self {
88 self.actions.push(PendingIdentityAction {
89 unsigned_action: UnsignedAction::AddAssociation(UnsignedAddAssociation {
90 new_member_identifier: new_member_identifier.clone(),
91 }),
92 pending_signatures: HashMap::from([
93 (SignatureField::ExistingMember, existing_member_identifier),
94 (SignatureField::NewMember, new_member_identifier),
95 ]),
96 });
97
98 self
99 }
100
101 pub fn revoke_association(
102 mut self,
103 recovery_address_signer: MemberIdentifier,
104 revoked_member: MemberIdentifier,
105 ) -> Self {
106 self.actions.push(PendingIdentityAction {
107 pending_signatures: HashMap::from([(
108 SignatureField::RecoveryAddress,
109 recovery_address_signer,
110 )]),
111 unsigned_action: UnsignedAction::RevokeAssociation(UnsignedRevokeAssociation {
112 revoked_member,
113 }),
114 });
115
116 self
117 }
118
119 pub fn change_recovery_address(
120 mut self,
121 recovery_address_signer: MemberIdentifier,
122 new_recovery_identifier: Identifier,
123 ) -> Self {
124 self.actions.push(PendingIdentityAction {
125 pending_signatures: HashMap::from([(
126 SignatureField::RecoveryAddress,
127 recovery_address_signer,
128 )]),
129 unsigned_action: UnsignedAction::ChangeRecoveryAddress(UnsignedChangeRecoveryAddress {
130 new_recovery_identifier,
131 }),
132 });
133
134 self
135 }
136
137 pub fn build(self) -> SignatureRequest {
138 let unsigned_actions: Vec<UnsignedAction> = self
139 .actions
140 .iter()
141 .map(|pending_action| pending_action.unsigned_action.clone())
142 .collect();
143
144 let signature_text = get_signature_text(
145 unsigned_actions,
146 self.inbox_id.clone(),
147 self.client_timestamp_ns,
148 );
149
150 SignatureRequest::new(
151 self.actions,
152 signature_text,
153 self.inbox_id,
154 self.client_timestamp_ns,
155 )
156 }
157}
158
159#[derive(Debug, Error, ErrorCode)]
160pub enum SignatureRequestError {
161 #[error("Unknown signer")]
165 UnknownSigner,
166 #[error("Required signature was not provided")]
170 MissingSigner,
171 #[error("Signature error {0}")]
172 #[error_code(inherit)]
173 Signature(#[from] SignatureError),
174 #[error("Unable to get block number")]
178 BlockNumber,
179 #[error("the backend does not accept chain {chain}; it accepts {accepted:?}")]
184 ChainNotAccepted {
185 chain: String,
186 accepted: Vec<String>,
187 },
188}
189
190#[derive(Clone, Debug)]
195pub struct SignatureRequest {
196 pending_actions: Vec<PendingIdentityAction>,
197 signature_text: String,
198 signatures: HashMap<MemberIdentifier, UnverifiedSignature>,
199 accepted_chains: Option<std::sync::Arc<[String]>>,
204 client_timestamp_ns: u64,
205 inbox_id: String,
206}
207
208impl SignatureRequest {
209 pub fn new(
210 pending_actions: Vec<PendingIdentityAction>,
211 signature_text: String,
212 inbox_id: String,
213 client_timestamp_ns: u64,
214 ) -> Self {
215 Self {
216 inbox_id,
217 pending_actions,
218 signature_text,
219 signatures: HashMap::new(),
220 accepted_chains: None,
221 client_timestamp_ns,
222 }
223 }
224
225 pub fn missing_signatures(&self) -> Vec<&MemberIdentifier> {
226 self.pending_actions
227 .iter()
228 .flat_map(|pending_action| pending_action.pending_signatures.values())
229 .filter(|ident| !self.signatures.contains_key(ident))
230 .collect()
231 }
232
233 pub fn missing_address_signatures(&self) -> Vec<&MemberIdentifier> {
234 self.missing_signatures()
235 .into_iter()
236 .filter(|member| matches!(member.kind(), MemberKind::Ethereum | MemberKind::Passkey))
237 .collect()
238 }
239
240 pub fn restrict_chains(&mut self, chains: std::sync::Arc<[String]>) {
247 self.accepted_chains = Some(chains);
248 }
249
250 pub fn accepted_chains(&self) -> Option<&[String]> {
254 self.accepted_chains.as_deref()
255 }
256
257 fn check_chain(&self, account_id: &AccountId) -> Result<(), SignatureRequestError> {
261 let Some(accepted) = self.accepted_chains.as_ref() else {
262 return Ok(());
263 };
264 let chain = account_id.get_chain_id();
265 if accepted.iter().any(|accepted| accepted == chain) {
266 return Ok(());
267 }
268 Err(SignatureRequestError::ChainNotAccepted {
269 chain: chain.to_string(),
270 accepted: accepted.to_vec(),
271 })
272 }
273
274 pub async fn add_new_unverified_smart_contract_signature(
275 &mut self,
276 mut signature: NewUnverifiedSmartContractWalletSignature,
277 scw_verifier: impl SmartContractSignatureVerifier,
278 ) -> Result<(), SignatureRequestError> {
279 self.check_chain(&signature.account_id)?;
280 let verified_signature = VerifiedSignature::from_smart_contract_wallet(
281 &self.signature_text,
282 scw_verifier,
283 &signature.signature_bytes,
284 signature.account_id.clone(),
285 &mut signature.block_number,
286 )
287 .await?;
288
289 let Some(block_number) = signature.block_number else {
290 return Err(SignatureRequestError::BlockNumber);
291 };
292
293 self.add_verified_signature(
294 UnverifiedSignature::SmartContractWallet(UnverifiedSmartContractWalletSignature {
295 account_id: signature.account_id,
296 block_number,
297 signature_bytes: signature.signature_bytes,
298 }),
299 verified_signature,
300 )
301 }
302
303 pub async fn add_signature(
304 &mut self,
305 signature: UnverifiedSignature,
306 scw_verifier: impl SmartContractSignatureVerifier,
307 ) -> Result<(), SignatureRequestError> {
308 if let UnverifiedSignature::SmartContractWallet(scw) = &signature {
309 self.check_chain(&scw.account_id)?;
310 }
311 let verified_signature = signature
312 .to_verified(self.signature_text.clone(), scw_verifier)
313 .await?;
314
315 self.add_verified_signature(signature, verified_signature)
316 }
317
318 fn add_verified_signature(
319 &mut self,
320 signature: UnverifiedSignature,
321 verified_signature: VerifiedSignature,
322 ) -> Result<(), SignatureRequestError> {
323 let signer_identity = &verified_signature.signer;
324
325 let missing_signatures = self.missing_signatures();
326 tracing::info!(
327 signer = %signer_identity,
328 missing_signatures=?missing_signatures,
329 "adding verified signature");
330
331 if !missing_signatures.contains(&signer_identity) {
333 return Err(SignatureRequestError::UnknownSigner);
334 }
335
336 self.signatures.insert(verified_signature.signer, signature);
337
338 Ok(())
339 }
340
341 pub fn is_ready(&self) -> bool {
342 self.missing_signatures().is_empty()
343 }
344
345 pub fn signature_text(&self) -> String {
346 self.signature_text.clone()
347 }
348
349 pub fn build_identity_update(self) -> Result<UnverifiedIdentityUpdate, SignatureRequestError> {
350 if !self.is_ready() {
351 return Err(SignatureRequestError::MissingSigner);
352 }
353
354 let actions = self
355 .pending_actions
356 .clone()
357 .into_iter()
358 .map(|pending_action| build_action(pending_action, &self.signatures))
359 .collect::<Result<Vec<UnverifiedAction>, SignatureRequestError>>()?;
360
361 Ok(UnverifiedIdentityUpdate::new(
362 self.inbox_id,
363 self.client_timestamp_ns,
364 actions,
365 ))
366 }
367
368 pub fn inbox_id(&self) -> crate::InboxIdRef<'_> {
369 &self.inbox_id
370 }
371}
372
373fn build_action(
374 pending_action: PendingIdentityAction,
375 signatures: &HashMap<MemberIdentifier, UnverifiedSignature>,
376) -> Result<UnverifiedAction, SignatureRequestError> {
377 match pending_action.unsigned_action {
378 UnsignedAction::CreateInbox(unsigned_action) => {
379 let signer_identity = pending_action
380 .pending_signatures
381 .get(&SignatureField::InitialAddress)
382 .ok_or(SignatureRequestError::MissingSigner)?;
383 let initial_identifier_signature = signatures
384 .get(signer_identity)
385 .cloned()
386 .ok_or(SignatureRequestError::MissingSigner)?;
387
388 Ok(UnverifiedAction::CreateInbox(UnverifiedCreateInbox {
389 unsigned_action,
390 initial_identifier_signature,
391 }))
392 }
393 UnsignedAction::AddAssociation(unsigned_action) => {
394 let existing_member_signer_identity = pending_action
395 .pending_signatures
396 .get(&SignatureField::ExistingMember)
397 .ok_or(SignatureRequestError::MissingSigner)?;
398 let new_member_signer_identity = pending_action
399 .pending_signatures
400 .get(&SignatureField::NewMember)
401 .ok_or(SignatureRequestError::MissingSigner)?;
402
403 let existing_member_signature = signatures
404 .get(existing_member_signer_identity)
405 .cloned()
406 .ok_or(SignatureRequestError::MissingSigner)?;
407
408 let new_member_signature = signatures
409 .get(new_member_signer_identity)
410 .cloned()
411 .ok_or(SignatureRequestError::MissingSigner)?;
412
413 Ok(UnverifiedAction::AddAssociation(UnverifiedAddAssociation {
414 unsigned_action,
415 existing_member_signature,
416 new_member_signature,
417 }))
418 }
419 UnsignedAction::RevokeAssociation(unsigned_action) => {
420 let signer_identity = pending_action
421 .pending_signatures
422 .get(&SignatureField::RecoveryAddress)
423 .ok_or(SignatureRequestError::MissingSigner)?;
424 let recovery_address_signature = signatures
425 .get(signer_identity)
426 .cloned()
427 .ok_or(SignatureRequestError::MissingSigner)?;
428
429 Ok(UnverifiedAction::RevokeAssociation(
430 UnverifiedRevokeAssociation {
431 recovery_identifier_signature: recovery_address_signature,
432 unsigned_action,
433 },
434 ))
435 }
436 UnsignedAction::ChangeRecoveryAddress(unsigned_action) => {
437 let signer_identity = pending_action
438 .pending_signatures
439 .get(&SignatureField::RecoveryAddress)
440 .ok_or(SignatureRequestError::MissingSigner)?;
441
442 let recovery_identifier_signature = signatures
443 .get(signer_identity)
444 .cloned()
445 .ok_or(SignatureRequestError::MissingSigner)?;
446
447 Ok(UnverifiedAction::ChangeRecoveryAddress(
448 UnverifiedChangeRecoveryAddress {
449 recovery_identifier_signature,
450 unsigned_action,
451 },
452 ))
453 }
454 }
455}
456
457fn get_signature_text(
458 actions: Vec<UnsignedAction>,
459 inbox_id: String,
460 client_timestamp_ns: u64,
461) -> String {
462 let identity_update = UnsignedIdentityUpdate {
463 client_timestamp_ns,
464 actions,
465 inbox_id,
466 };
467
468 identity_update.signature_text()
469}
470
471#[cfg(test)]
472pub(crate) mod tests {
473 use alloy::signers::{Signer, local::PrivateKeySigner};
474 use xmtp_cryptography::XmtpInstallationCredential;
475
476 use crate::{
477 InboxOwner,
478 associations::{
479 IdentityUpdate, get_state,
480 test_utils::{
481 MockSmartContractSignatureVerifier, WalletTestExt, add_installation_key_signature,
482 add_wallet_signature,
483 },
484 unverified::UnverifiedRecoverableEcdsaSignature,
485 },
486 };
487
488 use super::*;
489
490 async fn convert_to_verified(identity_update: &UnverifiedIdentityUpdate) -> IdentityUpdate {
491 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
492 identity_update
493 .to_verified(&scw_verifier)
494 .await
495 .expect("should be valid")
496 }
497
498 #[xmtp_common::test]
499 async fn create_inbox() {
500 let wallet = PrivateKeySigner::random();
501 let account_ident = wallet.get_identifier().unwrap();
502 let nonce = 0;
503 let inbox_id = wallet.get_inbox_id(nonce);
504
505 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
506 .create_inbox(account_ident, nonce)
507 .build();
508
509 add_wallet_signature(&mut signature_request, &wallet).await;
510
511 let identity_update = signature_request
512 .build_identity_update()
513 .expect("should be valid");
514
515 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
516 }
517
518 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
519 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
520 async fn create_and_add_identity() {
521 let wallet = PrivateKeySigner::random();
522 let installation_key = XmtpInstallationCredential::new();
523 let account_address = wallet.get_identifier().unwrap();
524 let nonce = 0;
525 let inbox_id = wallet.get_inbox_id(nonce);
526 let ident = Identifier::eth(&account_address).unwrap();
527 let new_member_identifier =
528 MemberIdentifier::installation(installation_key.public_bytes().to_vec());
529
530 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
531 .create_inbox(ident.clone(), nonce)
532 .add_association(new_member_identifier, ident.into())
533 .build();
534
535 add_wallet_signature(&mut signature_request, &wallet).await;
536 add_installation_key_signature(&mut signature_request, &installation_key).await;
537
538 let identity_update = signature_request
539 .build_identity_update()
540 .expect("should be valid");
541
542 let state =
543 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
544 assert_eq!(state.members().len(), 2);
545 }
546
547 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
548 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
549 async fn create_and_revoke() {
550 let wallet = PrivateKeySigner::random();
551 let nonce = 0;
552 let inbox_id = wallet.get_inbox_id(nonce);
553 let existing_member_identifier = wallet.identifier();
554
555 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
556 .create_inbox(existing_member_identifier.clone(), nonce)
557 .revoke_association(
558 existing_member_identifier.clone().into(),
559 existing_member_identifier.clone().into(),
560 )
561 .build();
562
563 add_wallet_signature(&mut signature_request, &wallet).await;
564
565 let identity_update = signature_request
566 .build_identity_update()
567 .expect("should be valid");
568
569 let state =
570 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
571
572 assert_eq!(state.members().len(), 0);
573 }
574
575 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
576 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
577 async fn attempt_adding_unknown_signer() {
578 let account_address = "0x1234567890abcdef1234567890abcdef12345678".to_string();
579 let nonce = 0;
580 let ident = Identifier::eth(&account_address).unwrap();
581 let inbox_id = ident.inbox_id(nonce).unwrap();
582
583 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
584 .create_inbox(ident, nonce)
585 .build();
586
587 let rand_wallet = PrivateKeySigner::random();
588
589 let signature_text = signature_request.signature_text();
590 let sig = rand_wallet
591 .sign_message(signature_text.as_bytes())
592 .await
593 .unwrap();
594 let unverified_sig = UnverifiedSignature::RecoverableEcdsa(
595 UnverifiedRecoverableEcdsaSignature::new(sig.into()),
596 );
597 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
598
599 let attempt_to_add_random_member = signature_request
600 .add_signature(unverified_sig, &scw_verifier)
601 .await;
602
603 assert!(matches!(
604 attempt_to_add_random_member,
605 Err(SignatureRequestError::UnknownSigner)
606 ));
607 }
608
609 fn scw_request(chains: Option<Vec<&str>>) -> (SignatureRequest, String) {
612 let wallet = PrivateKeySigner::random();
613 let account_ident = wallet.get_identifier().unwrap();
614 let inbox_id = wallet.get_inbox_id(0);
615 let mut request = SignatureRequestBuilder::new(inbox_id)
616 .create_inbox(account_ident, 0)
617 .build();
618 if let Some(chains) = chains {
619 let chains: Vec<String> = chains.into_iter().map(str::to_owned).collect();
620 request.restrict_chains(std::sync::Arc::from(chains));
621 }
622 (request, wallet.address().to_string())
623 }
624
625 async fn add_scw_on(
626 request: &mut SignatureRequest,
627 address: &str,
628 chain: &str,
629 ) -> Result<(), SignatureRequestError> {
630 let signature = NewUnverifiedSmartContractWalletSignature::new(
631 vec![1, 2, 3],
632 AccountId::new(chain.to_owned(), address.to_owned()),
633 Some(1),
634 );
635 request
636 .add_new_unverified_smart_contract_signature(
637 signature,
638 &MockSmartContractSignatureVerifier::new(true),
639 )
640 .await
641 }
642
643 #[xmtp_common::test(unwrap_try = true)]
646 async fn a_chain_outside_the_accepted_list_is_refused() {
647 let (mut request, address) = scw_request(Some(vec!["eip155:1", "eip155:8453"]));
648 let error = add_scw_on(&mut request, &address, "eip155:137")
649 .await
650 .unwrap_err();
651 let SignatureRequestError::ChainNotAccepted { chain, accepted } = error else {
652 panic!("expected ChainNotAccepted, got {error}");
653 };
654 assert_eq!(chain, "eip155:137");
655 assert_eq!(accepted, vec!["eip155:1", "eip155:8453"]);
656 assert!(request.signatures.is_empty());
658 }
659
660 #[xmtp_common::test(unwrap_try = true)]
663 async fn a_chain_inside_the_accepted_list_is_admitted() {
664 let (mut request, address) = scw_request(Some(vec!["eip155:1"]));
665 add_scw_on(&mut request, &address, "eip155:1").await?;
666 assert_eq!(request.signatures.len(), 1);
667 }
668
669 #[xmtp_common::test(unwrap_try = true)]
671 async fn an_empty_accepted_list_refuses_every_chain() {
672 let (mut request, address) = scw_request(Some(vec![]));
673 for chain in ["eip155:1", "eip155:8453", "solana:mainnet"] {
674 let error = add_scw_on(&mut request, &address, chain).await.unwrap_err();
675 let SignatureRequestError::ChainNotAccepted { accepted, .. } = error else {
676 panic!("expected ChainNotAccepted for {chain}, got {error}");
677 };
678 assert!(accepted.is_empty());
679 }
680 }
681
682 #[xmtp_common::test(unwrap_try = true)]
685 async fn an_unrestricted_request_accepts_any_chain() {
686 let (mut request, address) = scw_request(None);
687 add_scw_on(&mut request, &address, "eip155:424242").await?;
688 assert_eq!(request.signatures.len(), 1);
689 }
690
691 #[xmtp_common::test(unwrap_try = true)]
695 async fn an_unknown_chain_stays_retryable_during_ordered_processing() {
696 use crate::scw_verifier::{
697 MultiSmartContractSignatureVerifier, SmartContractSignatureVerifier, VerifierError,
698 };
699 use xmtp_common::RetryableError;
700
701 let verifier = MultiSmartContractSignatureVerifier::new(Default::default())?;
702 let Err(error) = verifier
703 .is_valid_signature(
704 AccountId::new(
705 "eip155:424242".to_owned(),
706 PrivateKeySigner::random().address().to_string(),
707 ),
708 [0u8; 32],
709 vec![1, 2, 3].into(),
710 None,
711 )
712 .await
713 else {
714 panic!("a chain with no route must not verify");
715 };
716 assert!(
717 matches!(error, VerifierError::NoVerifier(_)),
718 "ordered processing must see a missing route, got {error}"
719 );
720 assert!(error.is_retryable(), "STR-076: a missing route retries");
721 }
722}