1use crate::associations::AccountId;
3use crate::scw_verifier::{BlockStamp, ChainBlocks, SmartContractSignatureVerifier};
4use alloy::eips::BlockNumberOrTag;
5use alloy::network::TransactionBuilder;
6use alloy::primitives::{Address, BlockNumber, Bytes, FixedBytes};
7use alloy::providers::{DynProvider, Provider, ProviderBuilder};
8use alloy::{sol, sol_types::SolConstructor};
9use hex::FromHexError;
10use std::sync::Arc;
11
12use super::{ValidationResponse, VerifierError};
13
14const VALIDATE_SIG_OFFCHAIN_BYTECODE: &str = include_str!("signature_validation.hex");
19
20sol!(
21 contract VerifySig {
22 constructor (
23 address _signer,
24 bytes32 _hash,
25 bytes memory _signature
26 );
27 }
28);
29
30#[derive(Debug, Clone)]
31pub struct RpcSmartContractWalletVerifier {
32 provider: Arc<DynProvider>,
33}
34
35impl RpcSmartContractWalletVerifier {
36 pub fn new(provider_url: String) -> Result<Self, VerifierError> {
37 Ok(Self {
38 provider: Arc::new(
39 ProviderBuilder::new()
40 .connect_http(provider_url.parse()?)
41 .erased(),
42 ),
43 })
44 }
45
46 pub fn new_from_provider(provider: impl Provider + 'static) -> Self {
47 Self {
48 provider: Arc::new(DynProvider::new(provider)),
49 }
50 }
51}
52
53#[xmtp_common::async_trait]
54impl SmartContractSignatureVerifier for RpcSmartContractWalletVerifier {
55 async fn is_valid_signature(
56 &self,
57 signer: AccountId,
58 hash: [u8; 32],
59 signature: Bytes,
60 block_number: Option<BlockNumber>,
61 ) -> Result<ValidationResponse, VerifierError> {
62 let code = hex::decode(VALIDATE_SIG_OFFCHAIN_BYTECODE.trim())?;
63 let account_address: Address = signer
64 .account_address
65 .parse()
66 .map_err(|_| FromHexError::InvalidStringLength)?;
67 let call = VerifySig::constructorCall::new((
68 account_address,
69 FixedBytes::<32>::new(hash),
70 signature,
71 ));
72
73 let data = call.abi_encode();
74 let data = [code, data].concat();
75 let block_number = match block_number {
76 Some(bn) => bn,
77 None => self
78 .provider
79 .get_block_number()
80 .await
81 .map_err(VerifierError::Provider)?,
82 };
83 let mut tx = self.provider.transaction_request();
84 tx.set_input(data);
85 let result = self.provider.call(tx).block(block_number.into()).await?;
86
87 let expected_valid = Bytes::from_static(&[0x01]);
89 let is_valid = result == expected_valid;
90
91 Ok(ValidationResponse {
92 is_valid,
93 block_number: Some(block_number),
94 error: None,
95 })
96 }
97}
98
99impl RpcSmartContractWalletVerifier {
100 async fn block(&self, number: BlockNumberOrTag) -> Result<BlockStamp, VerifierError> {
102 let block =
103 self.provider
104 .get_block_by_number(number)
105 .await?
106 .ok_or(VerifierError::MissingBlock(
107 number.as_number().unwrap_or_default(),
108 ))?;
109 Ok(BlockStamp {
110 number: block.header.number,
111 timestamp: block.header.timestamp,
112 })
113 }
114}
115
116#[xmtp_common::async_trait]
118impl ChainBlocks for RpcSmartContractWalletVerifier {
119 async fn head(&self, _: &str) -> Result<BlockStamp, VerifierError> {
120 self.block(BlockNumberOrTag::Latest).await
121 }
122
123 async fn timestamp(&self, _: &str, number: BlockNumber) -> Result<u64, VerifierError> {
124 Ok(self.block(number.into()).await?.timestamp)
125 }
126}
127
128#[cfg(all(test, not(target_arch = "wasm32")))]
131pub(crate) mod tests {
132 #![allow(clippy::unwrap_used)]
133 use crate::utils::test::{
134 EthereumProvider, SignatureWithNonce, SmartWalletContext, docker_provider,
135 docker_smart_wallet,
136 };
137
138 use super::*;
139 use alloy::dyn_abi::SolType;
140 use alloy::primitives::{B256, U256};
141 use alloy::providers::ext::AnvilApi;
142 use alloy::signers::Signer;
143 use std::time::Duration;
144 use xmtp_common::RetryableError;
145
146 #[rstest::rstest]
151 #[xmtp_common::timeout(Duration::from_secs(30))]
152 #[tokio::test]
153 async fn chain_blocks_report_the_head_and_past_timestamps(
154 #[future] docker_provider: EthereumProvider,
155 ) {
156 let provider = docker_provider.await.provider;
157 provider.anvil_mine(Some(2), Some(100)).await.unwrap();
158 let verifier = RpcSmartContractWalletVerifier::new_from_provider(provider.clone());
159 let head = verifier.head("eip155:31337").await.unwrap();
160 assert!(head.number <= provider.get_block_number().await.unwrap());
161 let block = provider
162 .get_block_by_number(head.number.into())
163 .await
164 .unwrap()
165 .unwrap();
166 assert_eq!(head.timestamp, block.header.timestamp);
167 let earlier = verifier
168 .timestamp("eip155:31337", head.number - 1)
169 .await
170 .unwrap();
171 assert!(earlier <= head.timestamp);
172 let missing = verifier
173 .timestamp("eip155:31337", head.number + 1_000_000)
174 .await
175 .unwrap_err();
176 assert!(matches!(missing, VerifierError::MissingBlock(_)));
177 assert!(missing.is_retryable());
178 }
179
180 #[rstest::rstest]
181 #[xmtp_common::timeout(Duration::from_secs(30))]
182 #[tokio::test]
183 async fn test_coinbase_smart_wallet(#[future] docker_smart_wallet: SmartWalletContext) {
184 let SmartWalletContext {
185 factory,
186 sw,
187 owner0,
188 owner1,
189 sw_address,
190 } = docker_smart_wallet.await;
191 let provider = factory.provider();
192 let chain_id = provider.get_chain_id().await.unwrap();
193 let hash = B256::random();
194 let replay_safe_hash = sw.replaySafeHash(hash).call().await.unwrap();
195 let verifier = RpcSmartContractWalletVerifier::new_from_provider(provider.clone());
196 let sig0 = owner0.sign_hash(&replay_safe_hash).await.unwrap();
197 let account_id = AccountId::new_evm(chain_id, format!("{}", sw_address));
198
199 let res = verifier
200 .is_valid_signature(
201 account_id.clone(),
202 *hash,
203 SignatureWithNonce::abi_encode(&(U256::from(0), Bytes::from(sig0.as_bytes())))
204 .into(),
205 None,
206 )
207 .await
208 .unwrap();
209 assert!(res.is_valid);
210
211 let sig1 = owner1.sign_hash(&replay_safe_hash).await.unwrap();
213 let res = verifier
214 .is_valid_signature(
215 account_id.clone(),
216 *hash,
217 SignatureWithNonce::abi_encode(&(U256::from(1), Bytes::from(sig1.as_bytes())))
218 .into(),
219 None,
220 )
221 .await
222 .unwrap();
223 assert!(res.is_valid);
224
225 let res = verifier
227 .is_valid_signature(
228 account_id.clone(),
229 *hash,
230 SignatureWithNonce::abi_encode(&(U256::from(1), Bytes::from(sig0.as_bytes())))
231 .into(),
232 None,
233 )
234 .await
235 .unwrap();
236 assert!(!res.is_valid);
237 }
238
239 #[rstest::rstest]
240 #[xmtp_common::timeout(Duration::from_secs(60))]
241 #[tokio::test]
242 async fn test_smart_wallet_time_travel(#[future] docker_smart_wallet: SmartWalletContext) {
244 let SmartWalletContext {
245 factory,
246 sw,
247 owner1,
248 sw_address,
249 ..
250 } = docker_smart_wallet.await;
251
252 let provider = factory.provider();
253 let verifier = RpcSmartContractWalletVerifier::new_from_provider(provider.clone());
254 let chain_id = provider.get_chain_id().await.unwrap();
255 let hash = B256::random();
256 let replay_safe_hash = sw.replaySafeHash(hash).call().await.unwrap();
257 let sig1 = owner1.sign_hash(&replay_safe_hash).await.unwrap();
258 let account_id = AccountId::new_evm(chain_id, format!("{}", sw_address));
259 let block_number = provider.get_block_number().await.unwrap();
260 println!("{}", block_number);
261 provider.anvil_mine(Some(50), None).await.unwrap();
262 println!("{}", provider.get_block_number().await.unwrap());
263 let _tx = sw
265 .removeOwnerAtIndex(U256::from(1))
266 .from(owner1.address())
267 .send()
268 .await
269 .unwrap()
270 .get_receipt()
271 .await
272 .unwrap();
273
274 let res = verifier
275 .is_valid_signature(
276 account_id.clone(),
277 *hash,
278 SignatureWithNonce::abi_encode(&(U256::from(1), sig1.as_bytes())).into(),
279 None,
280 )
281 .await;
282 assert!(res.is_err());
283 let res = verifier
287 .is_valid_signature(
288 account_id.clone(),
289 *hash,
290 SignatureWithNonce::abi_encode(&(U256::from(1), sig1.as_bytes())).into(),
291 Some(block_number),
292 )
293 .await
294 .unwrap();
295 assert!(res.is_valid);
296 }
297
298 #[rstest::rstest]
300 #[xmtp_common::timeout(Duration::from_secs(60))]
301 #[tokio::test]
302 async fn test_is_valid_signature(#[future] docker_smart_wallet: SmartWalletContext) {
303 let SmartWalletContext {
304 factory,
305 sw,
306 owner0: owner,
307 sw_address,
308 ..
309 } = docker_smart_wallet.await;
310 let provider = factory.provider();
311 let chain_id = provider.get_chain_id().await.unwrap();
312 let hash = B256::random();
313 let replay_safe_hash = sw.replaySafeHash(hash).call().await.unwrap();
314 let verifier = RpcSmartContractWalletVerifier::new_from_provider(provider.clone());
315 let signature = owner.sign_hash(&replay_safe_hash).await.unwrap();
316 let signature: Bytes =
317 SignatureWithNonce::abi_encode(&(U256::from(0), signature.as_bytes())).into();
318 let account_id = AccountId::new_evm(chain_id, format!("{}", sw_address));
319
320 assert!(
322 verifier
323 .is_valid_signature(account_id.clone(), *hash, signature.clone(), None)
324 .await
325 .unwrap()
326 .is_valid
327 );
328
329 assert!(
330 !verifier
331 .is_valid_signature(account_id.clone(), *B256::random(), signature, None)
332 .await
333 .unwrap()
334 .is_valid
335 );
336
337 let signature = owner.sign_hash(&hash).await.unwrap();
339 let owner_account_id = AccountId::new_evm(chain_id, format!("{:?}", owner.address()));
340 assert!(
341 verifier
342 .is_valid_signature(
343 owner_account_id.clone(),
344 *hash,
345 signature.as_bytes().into(),
346 None
347 )
348 .await
349 .unwrap()
350 .is_valid
351 );
352
353 assert!(
354 !verifier
355 .is_valid_signature(
356 owner_account_id,
357 *B256::random(),
358 signature.as_bytes().into(),
359 None
360 )
361 .await
362 .unwrap()
363 .is_valid
364 );
365 }
366}