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    /// Missing block.
61    ///
62    /// The chain did not return a block at or below its reported head. Retryable.
63    #[error("chain did not return block {0}")]
64    MissingBlock(BlockNumber),
65    /// Invalid hash.
66    ///
67    /// Hash has invalid length or format. Not retryable.
68    #[error("hash was invalid length or otherwise malformed")]
69    InvalidHash(Vec<u8>),
70    /// Other error.
71    ///
72    /// Unclassified verifier error. May be retryable.
73    #[error("{0}")]
74    Other(Box<dyn RetryableError>),
75}
76
77impl RetryableError for VerifierError {
78    fn is_retryable(&self) -> bool {
79        use VerifierError::*;
80        match self {
81            Io(_) => true,
82            NoVerifier(_) => true,
83            MissingBlock(_) => true,
84            Provider(_) => true,
85            Other(o) => o.is_retryable(),
86            _ => false,
87        }
88    }
89}
90
91#[xmtp_common::async_trait]
92pub trait SmartContractSignatureVerifier: MaybeSend + MaybeSync {
93    /// Verifies an ERC-6492<https://eips.ethereum.org/EIPS/eip-6492> signature.
94    ///
95    /// # Arguments
96    ///
97    /// * `signer` - can be the smart wallet address or EOA address.
98    /// * `hash` - Message digest for the signature.
99    /// * `signature` - Could be encoded smart wallet signature or raw ECDSA signature.
100    async fn is_valid_signature(
101        &self,
102        account_id: AccountId,
103        hash: [u8; 32],
104        signature: Bytes,
105        block_number: Option<BlockNumber>,
106    ) -> Result<ValidationResponse, VerifierError>;
107}
108
109#[xmtp_common::async_trait]
110impl<T> SmartContractSignatureVerifier for Arc<T>
111where
112    T: SmartContractSignatureVerifier,
113{
114    async fn is_valid_signature(
115        &self,
116        account_id: AccountId,
117        hash: [u8; 32],
118        signature: Bytes,
119        block_number: Option<BlockNumber>,
120    ) -> Result<ValidationResponse, VerifierError> {
121        (**self)
122            .is_valid_signature(account_id, hash, signature, block_number)
123            .await
124    }
125}
126
127#[xmtp_common::async_trait]
128impl<T> SmartContractSignatureVerifier for &T
129where
130    T: SmartContractSignatureVerifier,
131{
132    async fn is_valid_signature(
133        &self,
134        account_id: AccountId,
135        hash: [u8; 32],
136        signature: Bytes,
137        block_number: Option<BlockNumber>,
138    ) -> Result<ValidationResponse, VerifierError> {
139        (*self)
140            .is_valid_signature(account_id, hash, signature, block_number)
141            .await
142    }
143}
144
145#[xmtp_common::async_trait]
146impl<T> SmartContractSignatureVerifier for Box<T>
147where
148    T: SmartContractSignatureVerifier + ?Sized,
149{
150    async fn is_valid_signature(
151        &self,
152        account_id: AccountId,
153        hash: [u8; 32],
154        signature: Bytes,
155        block_number: Option<BlockNumber>,
156    ) -> Result<ValidationResponse, VerifierError> {
157        (**self)
158            .is_valid_signature(account_id, hash, signature, block_number)
159            .await
160    }
161}
162
163/// A block's number and timestamp, in seconds, as a chain reports them.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct BlockStamp {
166    pub number: BlockNumber,
167    pub timestamp: u64,
168}
169
170/// Read access to chain blocks, keyed by `eip155:<chain>` identifier.
171///
172/// Admission uses it to judge how fresh a signature's stated block is. An
173/// unrouted chain or failed call is a retryable [`VerifierError`], never a
174/// verdict on the signature.
175#[xmtp_common::async_trait]
176pub trait ChainBlocks: MaybeSend + MaybeSync {
177    /// The chain's head block.
178    async fn head(&self, chain_id: &str) -> Result<BlockStamp, VerifierError>;
179    /// The timestamp of block `number`, which the caller has seen at or below the head.
180    async fn timestamp(&self, chain_id: &str, number: BlockNumber) -> Result<u64, VerifierError>;
181}
182
183#[derive(Clone)]
184/// Result of one smart-contract-wallet signature check.
185///
186/// A negative verdict is a successful check and is represented by
187/// `is_valid = false`. Provider failures use `VerifierError` instead.
188pub struct ValidationResponse {
189    /// Whether the signature is valid for the requested account and block.
190    pub is_valid: bool,
191    /// The block used by the provider, when it can report one.
192    pub block_number: Option<u64>,
193    /// Provider detail for a negative verdict, when available.
194    pub error: Option<String>,
195}
196
197/// Routes signature checks to a verifier selected by chain ID.
198///
199/// Each configured key is an `eip155:<chain>` identifier. A missing route is a
200/// retryable configuration/provider error so callers can distinguish it from a
201/// bad signature.
202pub struct MultiSmartContractSignatureVerifier {
203    verifiers: HashMap<String, RpcSmartContractWalletVerifier>,
204}
205
206impl std::fmt::Debug for MultiSmartContractSignatureVerifier {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("MultiSmartContractSignatureVerifier")
209            .field("verifiers", &self.verifiers.keys().collect::<Vec<_>>())
210            .finish()
211    }
212}
213
214impl MultiSmartContractSignatureVerifier {
215    /// Build RPC verifiers from chain IDs and endpoint URLs.
216    ///
217    /// URL parsing and provider construction errors are returned before a
218    /// partially configured verifier is created.
219    pub fn new(urls: HashMap<String, url::Url>) -> Result<Self, VerifierError> {
220        let verifiers = urls
221            .into_iter()
222            .map(|(chain_id, url)| {
223                Ok::<_, VerifierError>((
224                    chain_id,
225                    RpcSmartContractWalletVerifier::new(url.to_string())?,
226                ))
227            })
228            .collect::<Result<HashMap<_, _>, _>>()?;
229
230        Ok(Self { verifiers })
231    }
232
233    /// Build RPC verifiers from already-created providers.
234    ///
235    /// The provider map is consumed and keyed by the caller's chain IDs.
236    pub fn new_providers(providers: HashMap<String, DynProvider>) -> Result<Self, VerifierError> {
237        let verifiers = providers
238            .into_iter()
239            .map(|(chain_id, provider)| {
240                (
241                    chain_id,
242                    RpcSmartContractWalletVerifier::new_from_provider(provider),
243                )
244            })
245            .collect();
246        Ok(Self { verifiers })
247    }
248
249    /// Load the default chain routes, apply environment overrides, and add Anvil.
250    pub fn new_from_env() -> Result<Self, VerifierError> {
251        let urls: HashMap<String, Url> = serde_json::from_str(DEFAULT_CHAIN_URLS)?;
252        Self::new(urls)?.upgrade()
253    }
254
255    /// Load chain routes from a JSON file.
256    ///
257    /// The file must contain a map from chain ID to URL. Environment upgrades
258    /// are not applied by this constructor.
259    pub fn new_from_file(path: impl AsRef<Path>) -> Result<Self, VerifierError> {
260        let json = fs::read_to_string(path.as_ref())?;
261        let urls: HashMap<String, Url> = serde_json::from_str(&json)?;
262
263        Self::new(urls)
264    }
265
266    /// Replace default routes with environment overrides when present.
267    ///
268    /// This also registers the configured Anvil endpoint. A malformed chain ID
269    /// or endpoint prevents the verifier from being returned.
270    pub fn upgrade(mut self) -> Result<Self, VerifierError> {
271        for (id, verifier) in self.verifiers.iter_mut() {
272            // TODO: coda - update the chain id env var ids to preceded with "EIP155_"
273            let eip_id = id.split(":").nth(1).ok_or(VerifierError::MalformedEipUrl)?;
274            if let Ok(url) = std::env::var(format!("CHAIN_RPC_{eip_id}")) {
275                *verifier = RpcSmartContractWalletVerifier::new(url)?;
276            } else {
277                info!("No upgraded chain url for chain {id}, using default.");
278            };
279        }
280
281        if let Ok(url) = std::env::var("ANVIL_URL") {
282            info!("Adding anvil from env to the verifiers: {url}");
283            self.add_anvil(url)?;
284        } else {
285            use xmtp_configuration::DockerUrls;
286            let url = DockerUrls::anvil();
287            info!("adding default anvil url @{url}");
288            self.add_anvil(url)?;
289        }
290        Ok(self)
291    }
292
293    /// The verifier routed for `chain_id`, or a retryable error when none is.
294    fn route(&self, chain_id: &str) -> Result<&RpcSmartContractWalletVerifier, VerifierError> {
295        self.verifiers
296            .get(chain_id)
297            .ok_or_else(|| VerifierError::NoVerifier(chain_id.to_string()))
298    }
299
300    /// Add or replace one chain verifier backed by an RPC URL.
301    pub fn add_verifier(&mut self, id: String, url: String) -> Result<(), VerifierError> {
302        self.verifiers
303            .insert(id, RpcSmartContractWalletVerifier::new(url)?);
304        Ok(())
305    }
306
307    /// Add or replace the local Anvil verifier route.
308    pub fn add_anvil(&mut self, url: String) -> Result<(), VerifierError> {
309        self.verifiers.insert(
310            "eip155:31337".to_string(),
311            RpcSmartContractWalletVerifier::new(url)?,
312        );
313        Ok(())
314    }
315}
316
317#[xmtp_common::async_trait]
318impl SmartContractSignatureVerifier for MultiSmartContractSignatureVerifier {
319    async fn is_valid_signature(
320        &self,
321        account_id: AccountId,
322        hash: [u8; 32],
323        signature: Bytes,
324        block_number: Option<BlockNumber>,
325    ) -> Result<ValidationResponse, VerifierError> {
326        self.route(&account_id.chain_id)?
327            .is_valid_signature(account_id, hash, signature, block_number)
328            .await
329    }
330}
331
332#[xmtp_common::async_trait]
333impl ChainBlocks for MultiSmartContractSignatureVerifier {
334    async fn head(&self, chain_id: &str) -> Result<BlockStamp, VerifierError> {
335        self.route(chain_id)?.head(chain_id).await
336    }
337
338    async fn timestamp(&self, chain_id: &str, number: BlockNumber) -> Result<u64, VerifierError> {
339        self.route(chain_id)?.timestamp(chain_id, number).await
340    }
341}