Skip to content

Commit 7d6b8b1

Browse files
authored
feat(cache): add in-memory lru cache for signature verification (#1625)
* feat(cache): add in-memory lru cache for signature verification * apply cargo fmt * format Cargo.toml * Wrap MultiSmartContractVerifier into CachedContractSignatureVerifier - Create a CacheContractSifnatureVerifier which implements SmartContractSignatureVerifier - Move all the logic to mls_validation_service * make linter happy * pass NonZeroUsize as cache_size * Make cache key 32B long * remove unnecessary clone() * config accepts only NonZeroUint as cache_size * Use parking_lot mutex * rename test * test: add happy path
1 parent c40bda5 commit 7d6b8b1

File tree

6 files changed

+303
-9
lines changed

6 files changed

+303
-9
lines changed

Cargo.lock

+30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mls_validation_service/Cargo.toml

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[package]
2+
build = "build.rs"
23
edition = "2021"
4+
license.workspace = true
35
name = "mls_validation_service"
46
version = "0.1.4"
5-
build = "build.rs"
6-
license.workspace = true
77

88
[[bin]] # Bin to run the Validation Service
99
name = "mls-validation-service"
@@ -13,17 +13,21 @@ path = "src/main.rs"
1313
vergen-git2 = { workspace = true, features = ["build"] }
1414

1515
[dependencies]
16+
async-trait.workspace = true
1617
clap = { version = "4.4.6", features = ["derive"] }
1718
ethers = { workspace = true }
1819
futures = { workspace = true }
1920
hex = { workspace = true }
21+
lru = "0.13.0"
2022
openmls = { workspace = true }
2123
openmls_rust_crypto = { workspace = true }
24+
parking_lot.workspace = true
2225
thiserror.workspace = true
2326
tokio = { workspace = true, features = ["signal", "rt-multi-thread"] }
2427
tonic = { workspace = true }
2528
tracing.workspace = true
2629
tracing-subscriber = { workspace = true, features = ["env-filter", "ansi"] }
30+
url.workspace = true
2731
warp = "0.3.6"
2832
xmtp_cryptography = { path = "../xmtp_cryptography" }
2933
xmtp_id.workspace = true
@@ -34,9 +38,9 @@ xmtp_proto = { path = "../xmtp_proto", features = ["proto_full", "convert"] }
3438
anyhow.workspace = true
3539
ethers.workspace = true
3640
rand = { workspace = true }
41+
xmtp_common = { workspace = true, features = ["test-utils"] }
3742
xmtp_id = { workspace = true, features = ["test-utils"] }
3843
xmtp_mls = { workspace = true, features = ["test-utils"] }
39-
xmtp_common = { workspace = true, features = ["test-utils"] }
4044

4145
[features]
4246
test-utils = ["xmtp_id/test-utils"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
use lru::LruCache;
2+
use parking_lot::Mutex;
3+
use std::num::NonZeroUsize;
4+
5+
use ethers::types::{BlockNumber, Bytes};
6+
use xmtp_id::associations::AccountId;
7+
use xmtp_id::scw_verifier::{SmartContractSignatureVerifier, ValidationResponse, VerifierError};
8+
9+
type CacheKey = [u8; 32];
10+
11+
/// A cached smart contract verifier.
12+
///
13+
/// This wraps MultiSmartContractSignatureVerifier (or any other verifier
14+
/// implementing SmartContractSignatureVerifier) and adds an in-memory LRU cache.
15+
pub struct CachedSmartContractSignatureVerifier {
16+
verifier: Box<dyn SmartContractSignatureVerifier>,
17+
cache: Mutex<LruCache<CacheKey, ValidationResponse>>,
18+
}
19+
20+
impl CachedSmartContractSignatureVerifier {
21+
pub fn new(
22+
verifier: impl SmartContractSignatureVerifier + 'static,
23+
cache_size: NonZeroUsize,
24+
) -> Result<Self, VerifierError> {
25+
Ok(Self {
26+
verifier: Box::new(verifier),
27+
cache: Mutex::new(LruCache::new(cache_size)),
28+
})
29+
}
30+
}
31+
32+
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
33+
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
34+
impl SmartContractSignatureVerifier for CachedSmartContractSignatureVerifier {
35+
async fn is_valid_signature(
36+
&self,
37+
account_id: AccountId,
38+
hash: [u8; 32],
39+
signature: Bytes,
40+
block_number: Option<BlockNumber>,
41+
) -> Result<ValidationResponse, VerifierError> {
42+
if let Some(cached_response) = {
43+
let mut cache = self.cache.lock();
44+
cache.get(&hash).cloned()
45+
} {
46+
return Ok(cached_response);
47+
}
48+
49+
let response = self
50+
.verifier
51+
.is_valid_signature(account_id, hash, signature, block_number)
52+
.await?;
53+
54+
let mut cache = self.cache.lock();
55+
cache.put(hash, response.clone());
56+
57+
Ok(response)
58+
}
59+
}
60+
61+
#[cfg(test)]
62+
mod tests {
63+
use super::*;
64+
use ethers::{
65+
abi::{self, Token},
66+
middleware::MiddlewareBuilder,
67+
providers::{Http, Middleware, Provider},
68+
signers::{LocalWallet, Signer as _},
69+
types::{H256, U256},
70+
};
71+
use std::{collections::HashMap, sync::Arc};
72+
use xmtp_id::scw_verifier::{
73+
MultiSmartContractSignatureVerifier, SmartContractSignatureVerifier, ValidationResponse,
74+
VerifierError,
75+
};
76+
use xmtp_id::utils::test::{with_smart_contracts, CoinbaseSmartWallet};
77+
78+
#[tokio::test]
79+
async fn test_is_valid_signature() {
80+
with_smart_contracts(|anvil, _provider, client, smart_contracts| async move {
81+
let owner: LocalWallet = anvil.keys()[0].clone().into();
82+
let owners_addresses = vec![Bytes::from(H256::from(owner.address()).0.to_vec())];
83+
let factory = smart_contracts.coinbase_smart_wallet_factory();
84+
let nonce = U256::from(0);
85+
let smart_wallet_address = factory
86+
.get_address(owners_addresses.clone(), nonce)
87+
.await
88+
.unwrap();
89+
let contract_call = factory.create_account(owners_addresses.clone(), nonce);
90+
let pending_tx = contract_call.send().await.unwrap();
91+
pending_tx.await.unwrap();
92+
93+
// Check that the smart contract is deployed
94+
let provider: Provider<Http> = Provider::new(anvil.endpoint().parse().unwrap());
95+
let code = provider.get_code(smart_wallet_address, None).await.unwrap();
96+
assert!(!code.is_empty());
97+
98+
// Generate the signature for coinbase smart wallet
99+
let smart_wallet = CoinbaseSmartWallet::new(
100+
smart_wallet_address,
101+
Arc::new(client.with_signer(owner.clone().with_chain_id(anvil.chain_id()))),
102+
);
103+
let hash: [u8; 32] = H256::random().into();
104+
let replay_safe_hash = smart_wallet.replay_safe_hash(hash).call().await.unwrap();
105+
let signature = owner.sign_hash(replay_safe_hash.into()).unwrap();
106+
let signature: Bytes = abi::encode(&[Token::Tuple(vec![
107+
Token::Uint(U256::from(0)),
108+
Token::Bytes(signature.to_vec()),
109+
])])
110+
.into();
111+
112+
// Create the verifiers map
113+
let mut verifiers = HashMap::new();
114+
verifiers.insert(
115+
"eip155:31337".to_string(),
116+
anvil.endpoint().parse().unwrap(),
117+
);
118+
119+
// Create the cached verifier
120+
let verifier = CachedSmartContractSignatureVerifier::new(
121+
MultiSmartContractSignatureVerifier::new(verifiers).unwrap(),
122+
NonZeroUsize::new(5).unwrap(),
123+
)
124+
.unwrap();
125+
126+
let account_id =
127+
AccountId::new_evm(anvil.chain_id(), format!("{:?}", smart_wallet_address));
128+
129+
// Testing ERC-6492 signatures with deployed ERC-1271.
130+
assert!(
131+
verifier
132+
.is_valid_signature(account_id.clone(), hash, signature.clone(), None)
133+
.await
134+
.unwrap()
135+
.is_valid
136+
);
137+
138+
assert!(
139+
!verifier
140+
.is_valid_signature(account_id.clone(), H256::random().into(), signature, None)
141+
.await
142+
.unwrap()
143+
.is_valid
144+
);
145+
146+
// Testing if EOA wallet signature is valid on ERC-6492
147+
let signature = owner.sign_hash(hash.into()).unwrap();
148+
let owner_account_id =
149+
AccountId::new_evm(anvil.chain_id(), format!("{:?}", owner.address()));
150+
assert!(
151+
verifier
152+
.is_valid_signature(
153+
owner_account_id.clone(),
154+
hash,
155+
signature.to_vec().into(),
156+
None
157+
)
158+
.await
159+
.unwrap()
160+
.is_valid
161+
);
162+
163+
assert!(
164+
!verifier
165+
.is_valid_signature(
166+
owner_account_id,
167+
H256::random().into(),
168+
signature.to_vec().into(),
169+
None
170+
)
171+
.await
172+
.unwrap()
173+
.is_valid
174+
);
175+
})
176+
.await;
177+
}
178+
179+
#[tokio::test]
180+
async fn test_cache_eviction() {
181+
let mut cache: LruCache<CacheKey, ValidationResponse> =
182+
LruCache::new(NonZeroUsize::new(1).unwrap());
183+
let key1 = [0u8; 32];
184+
let key2 = [1u8; 32];
185+
186+
assert_ne!(key1, key2);
187+
188+
let val1: ValidationResponse = ValidationResponse {
189+
is_valid: true,
190+
block_number: Some(1),
191+
error: None,
192+
};
193+
let val2: ValidationResponse = ValidationResponse {
194+
is_valid: true,
195+
block_number: Some(2),
196+
error: None,
197+
};
198+
199+
cache.put(key1, val1.clone());
200+
let response = cache.get(&key1).unwrap();
201+
202+
// key1 is correctly cached
203+
assert_eq!(response.is_valid, val1.is_valid);
204+
assert_eq!(response.block_number, val1.block_number);
205+
206+
cache.put(key2, val2.clone());
207+
208+
// key1 is evicted, shouldn't exist
209+
assert!(cache.get(&key1).is_none());
210+
211+
// And key2 is correctly cached
212+
let response2 = cache.get(&key2).unwrap();
213+
assert_eq!(response2.is_valid, val2.is_valid);
214+
assert_eq!(response2.block_number, val2.block_number);
215+
}
216+
217+
#[tokio::test]
218+
async fn test_missing_verifier() {
219+
//
220+
let verifiers = std::collections::HashMap::new();
221+
let multi_verifier = MultiSmartContractSignatureVerifier::new(verifiers).unwrap();
222+
let cached_verifier = CachedSmartContractSignatureVerifier::new(
223+
multi_verifier,
224+
NonZeroUsize::new(1).unwrap(),
225+
)
226+
.unwrap();
227+
228+
let account_id = AccountId::new("missing".to_string(), "account1".to_string());
229+
let hash = [0u8; 32];
230+
let signature = Bytes::from(vec![1, 2, 3]);
231+
let block_number = Some(BlockNumber::Number(1.into()));
232+
233+
let result = cached_verifier
234+
.is_valid_signature(account_id, hash, signature, block_number)
235+
.await;
236+
assert!(result.is_err());
237+
238+
match result {
239+
Err(VerifierError::Provider(provider_error)) => {
240+
assert_eq!(
241+
provider_error.to_string(),
242+
"custom error: Verifier not present"
243+
);
244+
}
245+
_ => {
246+
panic!("Expected a VerifierError::Provider error.");
247+
}
248+
}
249+
}
250+
}

mls_validation_service/src/config.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use clap::Parser;
2+
use std::num::NonZeroUsize;
23

34
// Gather the command line arguments into a struct
45
#[derive(Parser, Debug)]
@@ -18,4 +19,8 @@ pub(crate) struct Args {
1819
// A path to a json file in the same format as chain_urls_default.json in the codebase.
1920
#[arg(long)]
2021
pub(crate) chain_urls: Option<String>,
22+
23+
// The size of the cache to use for the smart contract signature verifier.
24+
#[arg(long, default_value_t = NonZeroUsize::new(10000).expect("Set to positive number"))]
25+
pub(crate) cache_size: NonZeroUsize,
2126
}

0 commit comments

Comments
 (0)