Skip to main content

xmtp_id/scw_verifier/
mod.rs

1mod cached;
2pub use cached::CachedSmartContractSignatureVerifier;
3mod chain_rpc_verifier;
4mod remote_signature_verifier;
5use crate::associations::AccountId;
6use alloy::{
7    primitives::{BlockNumber, Bytes},
8    providers::DynProvider,
9};
10pub use chain_rpc_verifier::*;
11pub use remote_signature_verifier::*;
12use std::{collections::HashMap, fs, path::Path, sync::Arc};
13use thiserror::Error;
14use tracing::info;
15use url::Url;
16use xmtp_common::{ErrorCode, MaybeSend, MaybeSync, RetryableError};
17
18static DEFAULT_CHAIN_URLS: &str = include_str!("chain_urls_default.json");
19
20#[derive(Debug, Error, ErrorCode)]
21pub enum VerifierError {
22    /// Unexpected ERC-6492 result.
23    ///
24    /// Smart contract wallet signature verification returned unexpected result. Not retryable.
25    #[error("unexpected result from ERC-6492 {0}")]
26    UnexpectedERC6492Result(String),
27    #[error(transparent)]
28    #[error_code(inherit)]
29    FromHex(#[from] hex::FromHexError),
30    /// Provider error.
31    ///
32    /// Ethereum RPC provider error. Retryable.
33    #[error(transparent)]
34    Provider(#[from] alloy::transports::RpcError<alloy::transports::TransportErrorKind>),
35    /// URL parse error.
36    ///
37    /// Verifier URL is malformed. Not retryable.
38    #[error(transparent)]
39    Url(#[from] url::ParseError),
40    /// I/O error.
41    ///
42    /// I/O operation failed. May be retryable.
43    #[error(transparent)]
44    Io(#[from] std::io::Error),
45    /// Serialization error.
46    ///
47    /// JSON serialization/deserialization failed. Not retryable.
48    #[error(transparent)]
49    Serde(#[from] serde_json::Error),
50    /// Malformed chain ID.
51    ///
52    /// Chain ID string lacks expected eip155: prefix. Not retryable.
53    #[error("Chain IDs must be preceded with eip155:")]
54    MalformedEipUrl,
55    /// No verifier.
56    ///
57    /// Verifier not configured for the given chain ID. Retryable.
58    #[error("verifier not present for chain ID {0}")]
59    NoVerifier(String),
60    /// Invalid hash.
61    ///
62    /// Hash has invalid length or format. Not retryable.
63    #[error("hash was invalid length or otherwise malformed")]
64    InvalidHash(Vec<u8>),
65    /// Other error.
66    ///
67    /// Unclassified verifier error. May be retryable.
68    #[error("{0}")]
69    Other(Box<dyn RetryableError>),
70}
71
72impl RetryableError for VerifierError {
73    fn is_retryable(&self) -> bool {
74        use VerifierError::*;
75        match self {
76            Io(_) => true,
77            NoVerifier(_) => true,
78            Provider(_) => true,
79            Other(o) => o.is_retryable(),
80            _ => false,
81        }
82    }
83}
84
85#[xmtp_common::async_trait]
86pub trait SmartContractSignatureVerifier: MaybeSend + MaybeSync {
87    /// Verifies an ERC-6492<https://eips.ethereum.org/EIPS/eip-6492> signature.
88    ///
89    /// # Arguments
90    ///
91    /// * `signer` - can be the smart wallet address or EOA address.
92    /// * `hash` - Message digest for the signature.
93    /// * `signature` - Could be encoded smart wallet signature or raw ECDSA signature.
94    async fn is_valid_signature(
95        &self,
96        account_id: AccountId,
97        hash: [u8; 32],
98        signature: Bytes,
99        block_number: Option<BlockNumber>,
100    ) -> Result<ValidationResponse, VerifierError>;
101}
102
103#[xmtp_common::async_trait]
104impl<T> SmartContractSignatureVerifier for Arc<T>
105where
106    T: SmartContractSignatureVerifier,
107{
108    async fn is_valid_signature(
109        &self,
110        account_id: AccountId,
111        hash: [u8; 32],
112        signature: Bytes,
113        block_number: Option<BlockNumber>,
114    ) -> Result<ValidationResponse, VerifierError> {
115        (**self)
116            .is_valid_signature(account_id, hash, signature, block_number)
117            .await
118    }
119}
120
121#[xmtp_common::async_trait]
122impl<T> SmartContractSignatureVerifier for &T
123where
124    T: SmartContractSignatureVerifier,
125{
126    async fn is_valid_signature(
127        &self,
128        account_id: AccountId,
129        hash: [u8; 32],
130        signature: Bytes,
131        block_number: Option<BlockNumber>,
132    ) -> Result<ValidationResponse, VerifierError> {
133        (*self)
134            .is_valid_signature(account_id, hash, signature, block_number)
135            .await
136    }
137}
138
139#[xmtp_common::async_trait]
140impl<T> SmartContractSignatureVerifier for Box<T>
141where
142    T: SmartContractSignatureVerifier + ?Sized,
143{
144    async fn is_valid_signature(
145        &self,
146        account_id: AccountId,
147        hash: [u8; 32],
148        signature: Bytes,
149        block_number: Option<BlockNumber>,
150    ) -> Result<ValidationResponse, VerifierError> {
151        (**self)
152            .is_valid_signature(account_id, hash, signature, block_number)
153            .await
154    }
155}
156
157#[derive(Clone)]
158/// Result of one smart-contract-wallet signature check.
159///
160/// A negative verdict is a successful check and is represented by
161/// `is_valid = false`. Provider failures use `VerifierError` instead.
162pub struct ValidationResponse {
163    /// Whether the signature is valid for the requested account and block.
164    pub is_valid: bool,
165    /// The block used by the provider, when it can report one.
166    pub block_number: Option<u64>,
167    /// Provider detail for a negative verdict, when available.
168    pub error: Option<String>,
169}
170
171/// Routes signature checks to a verifier selected by chain ID.
172///
173/// Each configured key is an `eip155:<chain>` identifier. A missing route is a
174/// retryable configuration/provider error so callers can distinguish it from a
175/// bad signature.
176pub struct MultiSmartContractSignatureVerifier {
177    verifiers: HashMap<String, Box<dyn SmartContractSignatureVerifier>>,
178}
179
180impl std::fmt::Debug for MultiSmartContractSignatureVerifier {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.debug_struct("MultiSmartContractSignatureVerifier")
183            .field("verifiers", &self.verifiers.keys().collect::<Vec<_>>())
184            .finish()
185    }
186}
187
188impl MultiSmartContractSignatureVerifier {
189    /// Build RPC verifiers from chain IDs and endpoint URLs.
190    ///
191    /// URL parsing and provider construction errors are returned before a
192    /// partially configured verifier is created.
193    pub fn new(urls: HashMap<String, url::Url>) -> Result<Self, VerifierError> {
194        let verifiers = urls
195            .into_iter()
196            .map(|(chain_id, url)| {
197                Ok::<_, VerifierError>((
198                    chain_id,
199                    Box::new(RpcSmartContractWalletVerifier::new(url.to_string())?) as Box<_>,
200                ))
201            })
202            .collect::<Result<HashMap<_, _>, _>>()?;
203
204        Ok(Self { verifiers })
205    }
206
207    /// Build RPC verifiers from already-created providers.
208    ///
209    /// The provider map is consumed and keyed by the caller's chain IDs.
210    pub fn new_providers(providers: HashMap<String, DynProvider>) -> Result<Self, VerifierError> {
211        let verifiers = providers
212            .into_iter()
213            .map(|(chain_id, provider)| {
214                (
215                    chain_id,
216                    Box::new(RpcSmartContractWalletVerifier::new_from_provider(provider)) as Box<_>,
217                )
218            })
219            .collect();
220        Ok(Self { verifiers })
221    }
222
223    /// Load the default chain routes, apply environment overrides, and add Anvil.
224    pub fn new_from_env() -> Result<Self, VerifierError> {
225        let urls: HashMap<String, Url> = serde_json::from_str(DEFAULT_CHAIN_URLS)?;
226        Self::new(urls)?.upgrade()
227    }
228
229    /// Load chain routes from a JSON file.
230    ///
231    /// The file must contain a map from chain ID to URL. Environment upgrades
232    /// are not applied by this constructor.
233    pub fn new_from_file(path: impl AsRef<Path>) -> Result<Self, VerifierError> {
234        let json = fs::read_to_string(path.as_ref())?;
235        let urls: HashMap<String, Url> = serde_json::from_str(&json)?;
236
237        Self::new(urls)
238    }
239
240    /// Replace default routes with environment overrides when present.
241    ///
242    /// This also registers the configured Anvil endpoint. A malformed chain ID
243    /// or endpoint prevents the verifier from being returned.
244    pub fn upgrade(mut self) -> Result<Self, VerifierError> {
245        for (id, verifier) in self.verifiers.iter_mut() {
246            // TODO: coda - update the chain id env var ids to preceded with "EIP155_"
247            let eip_id = id.split(":").nth(1).ok_or(VerifierError::MalformedEipUrl)?;
248            if let Ok(url) = std::env::var(format!("CHAIN_RPC_{eip_id}")) {
249                *verifier = Box::new(RpcSmartContractWalletVerifier::new(url)?);
250            } else {
251                info!("No upgraded chain url for chain {id}, using default.");
252            };
253        }
254
255        if let Ok(url) = std::env::var("ANVIL_URL") {
256            info!("Adding anvil from env to the verifiers: {url}");
257            self.add_anvil(url)?;
258        } else {
259            use xmtp_configuration::DockerUrls;
260            let url = DockerUrls::anvil();
261            info!("adding default anvil url @{url}");
262            self.add_anvil(url)?;
263        }
264        Ok(self)
265    }
266
267    /// Add or replace one chain verifier backed by an RPC URL.
268    pub fn add_verifier(&mut self, id: String, url: String) -> Result<(), VerifierError> {
269        self.verifiers
270            .insert(id, Box::new(RpcSmartContractWalletVerifier::new(url)?));
271        Ok(())
272    }
273
274    /// Add or replace the local Anvil verifier route.
275    pub fn add_anvil(&mut self, url: String) -> Result<(), VerifierError> {
276        self.verifiers.insert(
277            "eip155:31337".to_string(),
278            Box::new(RpcSmartContractWalletVerifier::new(url)?),
279        );
280        Ok(())
281    }
282}
283
284#[xmtp_common::async_trait]
285impl SmartContractSignatureVerifier for MultiSmartContractSignatureVerifier {
286    async fn is_valid_signature(
287        &self,
288        account_id: AccountId,
289        hash: [u8; 32],
290        signature: Bytes,
291        block_number: Option<BlockNumber>,
292    ) -> Result<ValidationResponse, VerifierError> {
293        if let Some(verifier) = self.verifiers.get(&account_id.chain_id) {
294            return verifier
295                .is_valid_signature(account_id, hash, signature, block_number)
296                .await;
297        }
298
299        Err(VerifierError::NoVerifier(account_id.chain_id))
300    }
301}