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 #[error("unexpected result from ERC-6492 {0}")]
26 UnexpectedERC6492Result(String),
27 #[error(transparent)]
28 #[error_code(inherit)]
29 FromHex(#[from] hex::FromHexError),
30 #[error(transparent)]
34 Provider(#[from] alloy::transports::RpcError<alloy::transports::TransportErrorKind>),
35 #[error(transparent)]
39 Url(#[from] url::ParseError),
40 #[error(transparent)]
44 Io(#[from] std::io::Error),
45 #[error(transparent)]
49 Serde(#[from] serde_json::Error),
50 #[error("Chain IDs must be preceded with eip155:")]
54 MalformedEipUrl,
55 #[error("verifier not present for chain ID {0}")]
59 NoVerifier(String),
60 #[error("chain did not return block {0}")]
64 MissingBlock(BlockNumber),
65 #[error("hash was invalid length or otherwise malformed")]
69 InvalidHash(Vec<u8>),
70 #[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct BlockStamp {
166 pub number: BlockNumber,
167 pub timestamp: u64,
168}
169
170#[xmtp_common::async_trait]
176pub trait ChainBlocks: MaybeSend + MaybeSync {
177 async fn head(&self, chain_id: &str) -> Result<BlockStamp, VerifierError>;
179 async fn timestamp(&self, chain_id: &str, number: BlockNumber) -> Result<u64, VerifierError>;
181}
182
183#[derive(Clone)]
184pub struct ValidationResponse {
189 pub is_valid: bool,
191 pub block_number: Option<u64>,
193 pub error: Option<String>,
195}
196
197pub 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 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 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 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 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 pub fn upgrade(mut self) -> Result<Self, VerifierError> {
271 for (id, verifier) in self.verifiers.iter_mut() {
272 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 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 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 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}