From 0d5687761cfe17f00db0403f7876575310146cb0 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:43:04 +0530 Subject: [PATCH 01/48] in-memory cache implementation --- crates/provider/src/layers/cache.rs | 150 ++++++++++++++++++++++++++++ crates/provider/src/layers/mod.rs | 3 + 2 files changed, 153 insertions(+) create mode 100644 crates/provider/src/layers/cache.rs diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs new file mode 100644 index 00000000000..6a668338190 --- /dev/null +++ b/crates/provider/src/layers/cache.rs @@ -0,0 +1,150 @@ +use crate::{Provider, ProviderLayer, RootProvider}; +use alloy_json_rpc::{Request, RpcParam}; +use alloy_network::Ethereum; +use alloy_primitives::B256; +use alloy_rpc_types_eth::{Block, BlockNumberOrTag}; +use alloy_transport::{Transport, TransportErrorKind, TransportResult}; +use lru::LruCache; +use std::{marker::PhantomData, num::NonZeroUsize, path::PathBuf}; +use tokio::sync::Mutex; + +// TODO: Populate load cache from file on initialization. +// TODO: Add method to dump cache to file. +/// A provider layer that caches RPC responses and serves them on subsequent requests. +#[derive(Debug, Clone)] +pub struct CacheLayer { + path: PathBuf, + max_items: usize, +} + +impl CacheLayer { + /// Instantiate a new cache layer. + pub const fn new(path: PathBuf, max_items: usize) -> Self { + Self { path, max_items } + } +} + +impl ProviderLayer for CacheLayer +where + P: Provider, + T: Transport + Clone, +{ + type Provider = CacheProvider; + + fn layer(&self, inner: P) -> Self::Provider { + CacheProvider::new(inner, self.path.clone(), self.max_items) + } +} + +/// A provider that caches responses to RPC requests. +#[derive(Debug)] +pub struct CacheProvider { + /// Inner provider. + inner: P, + /// In-memory LRU cache, mapping requests to responses. + cache: Mutex>, + /// Path to the cache file. + path: PathBuf, + /// Phantom data + _pd: PhantomData, +} + +impl CacheProvider +where + P: Provider, + T: Transport + Clone, +{ + /// Instantiate a new cache provider. + pub fn new(inner: P, path: PathBuf, max_items: usize) -> Self { + let cache = + Mutex::new(LruCache::::new(NonZeroUsize::new(max_items).unwrap())); + Self { inner, path, cache, _pd: PhantomData } + } + + /// `keccak256` hash the request params. + pub fn hash_request(&self, req: Request) -> TransportResult { + let ser_req = req.serialize().map_err(TransportErrorKind::custom)?; + + Ok(ser_req.params_hash()) + } + + /// Gets the path to the cache file. + pub fn path(&self) -> PathBuf { + self.path.clone() + } + + /// Puts a value into the cache, and returns the old value if it existed. + pub async fn put(&self, key: B256, value: String) -> TransportResult> { + let mut cache = self.cache.lock().await; + Ok(cache.put(key, value)) + } + + /// Gets a value from the cache, if it exists. + pub async fn get(&self, key: &B256) -> TransportResult> { + let mut cache = self.cache.lock().await; + let val = cache.get(key).cloned(); + Ok(val) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Provider for CacheProvider +where + P: Provider, + T: Transport + Clone, +{ + #[inline(always)] + fn root(&self) -> &RootProvider { + self.inner.root() + } + + async fn get_block_by_number( + &self, + number: BlockNumberOrTag, + hydrate: bool, + ) -> TransportResult> { + let req = self.inner.client().make_request("eth_getBlockByNumber", (number, hydrate)); + + let hash = self.hash_request(req)?; + + // Try to get from cache + if let Some(block) = self.get(&hash).await? { + println!("Returned from cache!"); + let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; + return Ok(Some(block)); + } + + let rpc_res = self.inner.get_block_by_number(number, hydrate).await?; + println!("Returned from RPC!"); + if let Some(ref block) = rpc_res { + let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; + let old_val = self.put(hash, json_str).await?; + } + + Ok(rpc_res) + } + + // TODO: Add other commonly used methods such as eth_getTransactionByHash, eth_getProof, + // eth_getStorage etc. +} + +#[cfg(test)] +mod tests { + use crate::ProviderBuilder; + use alloy_node_bindings::Anvil; + + use super::*; + + #[tokio::test] + async fn test_cache_provider() { + let cache = CacheLayer::new(PathBuf::new(), 100); + let anvil = Anvil::new().spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + + let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); + let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); + + assert_eq!(blk, blk2); + } +} diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index 6baaf09e411..521c3e133a2 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -9,3 +9,6 @@ pub use anvil::{AnvilLayer, AnvilProvider}; mod chain; pub use chain::ChainLayer; + +mod cache; +pub use cache::{CacheLayer, CacheProvider}; From df7d861316878e0369aecc838f1f8273456660ba Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:21:37 +0530 Subject: [PATCH 02/48] load and dump cache from fs --- crates/provider/src/layers/cache.rs | 58 ++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 6a668338190..750a5484dd9 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -5,9 +5,9 @@ use alloy_primitives::B256; use alloy_rpc_types_eth::{Block, BlockNumberOrTag}; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; -use std::{marker::PhantomData, num::NonZeroUsize, path::PathBuf}; +use serde::{Deserialize, Serialize}; +use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf}; use tokio::sync::Mutex; - // TODO: Populate load cache from file on initialization. // TODO: Add method to dump cache to file. /// A provider layer that caches RPC responses and serves them on subsequent requests. @@ -85,6 +85,36 @@ where let val = cache.get(key).cloned(); Ok(val) } + + pub async fn dump_cache(&self) -> TransportResult<()> { + let cache = self.cache.lock().await; + let file = std::fs::File::create(self.path.clone()).map_err(TransportErrorKind::custom)?; + + // Iterate over the cache and dump to the file. + let entries = cache + .iter() + .map(|(key, value)| FsCacheEntry { key: key.clone(), value: value.clone() }) + .collect::>(); + serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; + Ok(()) + } + + pub async fn load_cache(&self) -> TransportResult<()> { + if !self.path.exists() { + return Ok(()); + }; + let file = std::fs::File::open(self.path.clone()).map_err(TransportErrorKind::custom)?; + let file = BufReader::new(file); + let entries: Vec = + serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; + let mut cache = self.cache.lock().await; + for entry in entries { + cache.put(entry.key, entry.value); + } + + println!("Loaded {} cache entries", cache.len()); + Ok(()) + } } #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] @@ -119,7 +149,7 @@ where println!("Returned from RPC!"); if let Some(ref block) = rpc_res { let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; - let old_val = self.put(hash, json_str).await?; + let _old_val = self.put(hash, json_str).await?; } Ok(rpc_res) @@ -129,8 +159,18 @@ where // eth_getStorage etc. } +#[derive(Debug, Serialize, Deserialize)] +struct FsCacheEntry { + /// Hash of the request params + key: B256, + /// Serialized response to the request from which the hash was computed. + value: String, +} + #[cfg(test)] mod tests { + use std::str::FromStr; + use crate::ProviderBuilder; use alloy_node_bindings::Anvil; @@ -138,13 +178,21 @@ mod tests { #[tokio::test] async fn test_cache_provider() { - let cache = CacheLayer::new(PathBuf::new(), 100); - let anvil = Anvil::new().spawn(); + let cache = CacheLayer::new(PathBuf::from_str("./rpc-cache.txt").unwrap(), 100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + provider.load_cache().await.unwrap(); let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + let latest_block_num = provider.get_block_number().await.unwrap(); + let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); + let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); + + provider.dump_cache().await.unwrap(); assert_eq!(blk, blk2); + assert_eq!(blk3, blk4); } } From cc8b11d3c1fbcaaa21c2661e2ac78e25dd5a0818 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:37:08 +0530 Subject: [PATCH 03/48] use RwLock --- crates/provider/src/layers/cache.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 750a5484dd9..dd6f2e9acb1 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -7,7 +7,7 @@ use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; use serde::{Deserialize, Serialize}; use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf}; -use tokio::sync::Mutex; +use tokio::sync::RwLock; // TODO: Populate load cache from file on initialization. // TODO: Add method to dump cache to file. /// A provider layer that caches RPC responses and serves them on subsequent requests. @@ -42,7 +42,7 @@ pub struct CacheProvider { /// Inner provider. inner: P, /// In-memory LRU cache, mapping requests to responses. - cache: Mutex>, + cache: RwLock>, /// Path to the cache file. path: PathBuf, /// Phantom data @@ -57,7 +57,7 @@ where /// Instantiate a new cache provider. pub fn new(inner: P, path: PathBuf, max_items: usize) -> Self { let cache = - Mutex::new(LruCache::::new(NonZeroUsize::new(max_items).unwrap())); + RwLock::new(LruCache::::new(NonZeroUsize::new(max_items).unwrap())); Self { inner, path, cache, _pd: PhantomData } } @@ -75,19 +75,20 @@ where /// Puts a value into the cache, and returns the old value if it existed. pub async fn put(&self, key: B256, value: String) -> TransportResult> { - let mut cache = self.cache.lock().await; + let mut cache = self.cache.write().await; Ok(cache.put(key, value)) } /// Gets a value from the cache, if it exists. pub async fn get(&self, key: &B256) -> TransportResult> { - let mut cache = self.cache.lock().await; + // Need to acquire a write guard to change the order of keys in LRU cache. + let mut cache = self.cache.write().await; let val = cache.get(key).cloned(); Ok(val) } pub async fn dump_cache(&self) -> TransportResult<()> { - let cache = self.cache.lock().await; + let cache = self.cache.read().await; let file = std::fs::File::create(self.path.clone()).map_err(TransportErrorKind::custom)?; // Iterate over the cache and dump to the file. @@ -107,7 +108,7 @@ where let file = BufReader::new(file); let entries: Vec = serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; - let mut cache = self.cache.lock().await; + let mut cache = self.cache.write().await; for entry in entries { cache.put(entry.key, entry.value); } From 8a1e3535e6b186a14da0cf0a3e0893f38346ec9e Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:45:21 +0530 Subject: [PATCH 04/48] doc nits --- crates/provider/src/layers/cache.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index dd6f2e9acb1..ca9a2441a26 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -87,6 +87,9 @@ where Ok(val) } + /// Dumps the cache to a file specified by the path. + /// If the files does not exist, it creates one. + /// If the file exists, it overwrites it. pub async fn dump_cache(&self) -> TransportResult<()> { let cache = self.cache.read().await; let file = std::fs::File::create(self.path.clone()).map_err(TransportErrorKind::custom)?; @@ -100,6 +103,8 @@ where Ok(()) } + /// Loads the cache from a file specified by the path. + /// If the file does not exist, it returns without error. pub async fn load_cache(&self) -> TransportResult<()> { if !self.path.exists() { return Ok(()); From 03c50c5a90037bd183a02ac446349304b2cb2b57 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:34:03 +0530 Subject: [PATCH 05/48] feat: CacheConfig, load/save at specific paths --- crates/provider/src/layers/cache.rs | 86 ++++++++++++++++++++--------- crates/provider/src/layers/mod.rs | 2 +- 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index ca9a2441a26..7e3a9dbe95c 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -11,16 +11,49 @@ use tokio::sync::RwLock; // TODO: Populate load cache from file on initialization. // TODO: Add method to dump cache to file. /// A provider layer that caches RPC responses and serves them on subsequent requests. +/// +/// In order to initialize the caching layer, the path to the cache file is provided along with the +/// max number of items that are stored in the in-memory LRU cache. +/// +/// One can load the cache from the file system by calling `load_cache` and save the cache to the +/// file system by calling `save_cache`. +/// +/// Example usage: +/// ``` +/// use alloy_node_bindings::Anvil; +/// use alloy_provider::ProviderBuilder; +/// use std::path::PathBuf; +/// +/// let cache = CacheLayer::new(100); +/// let anvil = Anvil::new().block_time_f64(0.3).spawn(); +/// let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); +/// let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); +/// provider.load_cache(path).await.unwrap(); // Load cache from file if it exists. +/// +/// let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from RPC and saved to in-memory cache +/// +/// let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from in-memory cache +/// assert_eq!(blk, blk2); +/// +/// provider.save_cache(path).await.unwrap(); // Save cache to file +/// ``` #[derive(Debug, Clone)] pub struct CacheLayer { - path: PathBuf, - max_items: usize, + config: CacheConfig, } impl CacheLayer { - /// Instantiate a new cache layer. - pub const fn new(path: PathBuf, max_items: usize) -> Self { - Self { path, max_items } + /// Instantiate a new cache layer with the the maximum number of + /// items to store. + #[inline] + pub const fn new(max_items: usize) -> Self { + Self { config: CacheConfig { max_items } } + } + + /// Returns the maximum number of items that can be stored in the cache, set at initialization. + #[inline] + pub const fn max_items(&self) -> usize { + self.config.max_items } } @@ -32,7 +65,7 @@ where type Provider = CacheProvider; fn layer(&self, inner: P) -> Self::Provider { - CacheProvider::new(inner, self.path.clone(), self.max_items) + CacheProvider::new(inner, self.max_items()) } } @@ -43,8 +76,6 @@ pub struct CacheProvider { inner: P, /// In-memory LRU cache, mapping requests to responses. cache: RwLock>, - /// Path to the cache file. - path: PathBuf, /// Phantom data _pd: PhantomData, } @@ -55,10 +86,11 @@ where T: Transport + Clone, { /// Instantiate a new cache provider. - pub fn new(inner: P, path: PathBuf, max_items: usize) -> Self { + pub fn new(inner: P, max_items: usize) -> Self { let cache = RwLock::new(LruCache::::new(NonZeroUsize::new(max_items).unwrap())); - Self { inner, path, cache, _pd: PhantomData } + let provider = Self { inner, cache, _pd: PhantomData }; + provider } /// `keccak256` hash the request params. @@ -68,11 +100,6 @@ where Ok(ser_req.params_hash()) } - /// Gets the path to the cache file. - pub fn path(&self) -> PathBuf { - self.path.clone() - } - /// Puts a value into the cache, and returns the old value if it existed. pub async fn put(&self, key: B256, value: String) -> TransportResult> { let mut cache = self.cache.write().await; @@ -87,17 +114,17 @@ where Ok(val) } - /// Dumps the cache to a file specified by the path. + /// Saves the cache to a file specified by the path. /// If the files does not exist, it creates one. /// If the file exists, it overwrites it. - pub async fn dump_cache(&self) -> TransportResult<()> { + pub async fn save_cache(&self, path: PathBuf) -> TransportResult<()> { let cache = self.cache.read().await; - let file = std::fs::File::create(self.path.clone()).map_err(TransportErrorKind::custom)?; + let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; // Iterate over the cache and dump to the file. let entries = cache .iter() - .map(|(key, value)| FsCacheEntry { key: key.clone(), value: value.clone() }) + .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() }) .collect::>(); serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; Ok(()) @@ -105,11 +132,11 @@ where /// Loads the cache from a file specified by the path. /// If the file does not exist, it returns without error. - pub async fn load_cache(&self) -> TransportResult<()> { - if !self.path.exists() { + pub async fn load_cache(&self, path: PathBuf) -> TransportResult<()> { + if !path.exists() { return Ok(()); }; - let file = std::fs::File::open(self.path.clone()).map_err(TransportErrorKind::custom)?; + let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; let file = BufReader::new(file); let entries: Vec = serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; @@ -173,6 +200,14 @@ struct FsCacheEntry { value: String, } +/// Configuration for the cache layer. +/// For future extensibility of the configurations. +#[derive(Debug, Clone)] +pub struct CacheConfig { + /// Maximum number of items to store in the cache. + pub max_items: usize, +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -184,11 +219,12 @@ mod tests { #[tokio::test] async fn test_cache_provider() { - let cache = CacheLayer::new(PathBuf::from_str("./rpc-cache.txt").unwrap(), 100); + let cache = CacheLayer::new(100); let anvil = Anvil::new().block_time_f64(0.3).spawn(); let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - provider.load_cache().await.unwrap(); + let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); + provider.load_cache(path.clone()).await.unwrap(); let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); @@ -197,7 +233,7 @@ mod tests { let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); - provider.dump_cache().await.unwrap(); + provider.save_cache(path).await.unwrap(); assert_eq!(blk, blk2); assert_eq!(blk3, blk4); } diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index 521c3e133a2..92e5d1d0714 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -11,4 +11,4 @@ mod chain; pub use chain::ChainLayer; mod cache; -pub use cache::{CacheLayer, CacheProvider}; +pub use cache::{CacheConfig, CacheLayer, CacheProvider}; From 0fc93a1e4b152a61316ce081c785ecac0db0c897 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:33:26 +0530 Subject: [PATCH 06/48] RequestType enum --- crates/provider/src/layers/cache.rs | 30 +++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 7e3a9dbe95c..9da023695a2 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -2,6 +2,7 @@ use crate::{Provider, ProviderLayer, RootProvider}; use alloy_json_rpc::{Request, RpcParam}; use alloy_network::Ethereum; use alloy_primitives::B256; +use alloy_rpc_client::ClientRef; use alloy_rpc_types_eth::{Block, BlockNumberOrTag}; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; @@ -89,8 +90,7 @@ where pub fn new(inner: P, max_items: usize) -> Self { let cache = RwLock::new(LruCache::::new(NonZeroUsize::new(max_items).unwrap())); - let provider = Self { inner, cache, _pd: PhantomData }; - provider + Self { inner, cache, _pd: PhantomData } } /// `keccak256` hash the request params. @@ -145,7 +145,6 @@ where cache.put(entry.key, entry.value); } - println!("Loaded {} cache entries", cache.len()); Ok(()) } } @@ -167,19 +166,16 @@ where number: BlockNumberOrTag, hydrate: bool, ) -> TransportResult> { - let req = self.inner.client().make_request("eth_getBlockByNumber", (number, hydrate)); - + let req = + RequestType::GetBlockByNumber((number, hydrate)).make_request(self.inner.client()); let hash = self.hash_request(req)?; - // Try to get from cache if let Some(block) = self.get(&hash).await? { - println!("Returned from cache!"); let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; return Ok(Some(block)); } let rpc_res = self.inner.get_block_by_number(number, hydrate).await?; - println!("Returned from RPC!"); if let Some(ref block) = rpc_res { let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; let _old_val = self.put(hash, json_str).await?; @@ -192,6 +188,24 @@ where // eth_getStorage etc. } +/// Enum representing different RPC requests. +/// +/// Useful for handling hashing of various request parameters. +enum RequestType { + /// Get block by number. + GetBlockByNumber(Params), +} + +impl RequestType { + fn make_request(&self, client: ClientRef<'_, T>) -> Request { + match self { + Self::GetBlockByNumber(params) => { + client.make_request("eth_getBlockByNumber", params.to_owned()) + } + } + } +} + #[derive(Debug, Serialize, Deserialize)] struct FsCacheEntry { /// Hash of the request params From 9b95833fd34394fde8c00545bd4f661c1647f6d2 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:41:05 +0530 Subject: [PATCH 07/48] params hash --- crates/provider/src/layers/cache.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 9da023695a2..d6b39d33f98 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -166,9 +166,8 @@ where number: BlockNumberOrTag, hydrate: bool, ) -> TransportResult> { - let req = - RequestType::GetBlockByNumber((number, hydrate)).make_request(self.inner.client()); - let hash = self.hash_request(req)?; + let hash = + RequestType::GetBlockByNumber((number, hydrate)).params_hash(self.inner.client())?; // Try to get from cache if let Some(block) = self.get(&hash).await? { let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; @@ -204,6 +203,13 @@ impl RequestType { } } } + + fn params_hash(&self, client: ClientRef<'_, T>) -> TransportResult { + let req = self.make_request(client); + let ser_req = req.serialize().map_err(TransportErrorKind::custom)?; + + Ok(ser_req.params_hash()) + } } #[derive(Debug, Serialize, Deserialize)] From f79a12870b3198ef8f18c980a2890239f90c67d6 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:47:38 +0530 Subject: [PATCH 08/48] clone and arc `CacheProvider` --- crates/provider/src/layers/cache.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index d6b39d33f98..495cd4b96c8 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -7,7 +7,7 @@ use alloy_rpc_types_eth::{Block, BlockNumberOrTag}; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; use serde::{Deserialize, Serialize}; -use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf}; +use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf, sync::Arc}; use tokio::sync::RwLock; // TODO: Populate load cache from file on initialization. // TODO: Add method to dump cache to file. @@ -71,12 +71,12 @@ where } /// A provider that caches responses to RPC requests. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CacheProvider { /// Inner provider. inner: P, /// In-memory LRU cache, mapping requests to responses. - cache: RwLock>, + cache: Arc>>, /// Phantom data _pd: PhantomData, } @@ -88,18 +88,12 @@ where { /// Instantiate a new cache provider. pub fn new(inner: P, max_items: usize) -> Self { - let cache = - RwLock::new(LruCache::::new(NonZeroUsize::new(max_items).unwrap())); + let cache = Arc::new(RwLock::new(LruCache::::new( + NonZeroUsize::new(max_items).unwrap(), + ))); Self { inner, cache, _pd: PhantomData } } - /// `keccak256` hash the request params. - pub fn hash_request(&self, req: Request) -> TransportResult { - let ser_req = req.serialize().map_err(TransportErrorKind::custom)?; - - Ok(ser_req.params_hash()) - } - /// Puts a value into the cache, and returns the old value if it existed. pub async fn put(&self, key: B256, value: String) -> TransportResult> { let mut cache = self.cache.write().await; @@ -177,7 +171,7 @@ where let rpc_res = self.inner.get_block_by_number(number, hydrate).await?; if let Some(ref block) = rpc_res { let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; - let _old_val = self.put(hash, json_str).await?; + let _ = self.put(hash, json_str).await?; } Ok(rpc_res) @@ -204,6 +198,7 @@ impl RequestType { } } + /// `keccak256` hash the request params. fn params_hash(&self, client: ClientRef<'_, T>) -> TransportResult { let req = self.make_request(client); let ser_req = req.serialize().map_err(TransportErrorKind::custom)?; From e6da7f7d390ff8e145a042af4cfcd74ddb3ee20d Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:16:33 +0530 Subject: [PATCH 09/48] add: get_block_by_hash --- crates/provider/src/layers/cache.rs | 101 ++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 495cd4b96c8..5d2fa01d829 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,9 +1,9 @@ use crate::{Provider, ProviderLayer, RootProvider}; use alloy_json_rpc::{Request, RpcParam}; use alloy_network::Ethereum; -use alloy_primitives::B256; +use alloy_primitives::{BlockHash, B256}; use alloy_rpc_client::ClientRef; -use alloy_rpc_types_eth::{Block, BlockNumberOrTag}; +use alloy_rpc_types_eth::{Block, BlockId, BlockNumberOrTag, BlockTransactionsKind}; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; use serde::{Deserialize, Serialize}; @@ -162,19 +162,62 @@ where ) -> TransportResult> { let hash = RequestType::GetBlockByNumber((number, hydrate)).params_hash(self.inner.client())?; - // Try to get from cache + if let Some(block) = self.get(&hash).await? { let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; + println!("Cache hit"); return Ok(Some(block)); } - let rpc_res = self.inner.get_block_by_number(number, hydrate).await?; - if let Some(ref block) = rpc_res { + println!("Cache miss"); + let block = self.inner.get_block_by_number(number, hydrate).await?; + if let Some(ref block) = block { let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; let _ = self.put(hash, json_str).await?; } - Ok(rpc_res) + Ok(block) + } + + /// Gets a block by its [BlockHash], with full transactions or only hashes. + async fn get_block_by_hash( + &self, + hash: BlockHash, + kind: BlockTransactionsKind, + ) -> TransportResult> { + let full = match kind { + BlockTransactionsKind::Full => true, + BlockTransactionsKind::Hashes => false, + }; + + let req_hash = + RequestType::GetBlockByHash((hash, full)).params_hash(self.inner.client())?; + + if let Some(block) = self.get(&req_hash).await? { + let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; + println!("Cache hit"); + return Ok(Some(block)); + } + + println!("Cache miss"); + let block = self + .client() + .request::<_, Option>("eth_getBlockByHash", (hash, full)) + .await? + .map(|mut block| { + if !full { + // this ensures an empty response for `Hashes` has the expected form + // this is required because deserializing [] is ambiguous + block.transactions.convert_to_hashes(); + } + block + }); + if let Some(ref block) = block { + let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; + let _ = self.put(req_hash, json_str).await?; + } + + Ok(block) } // TODO: Add other commonly used methods such as eth_getTransactionByHash, eth_getProof, @@ -187,15 +230,17 @@ where enum RequestType { /// Get block by number. GetBlockByNumber(Params), + /// Get block by hash. + GetBlockByHash(Params), } impl RequestType { fn make_request(&self, client: ClientRef<'_, T>) -> Request { - match self { - Self::GetBlockByNumber(params) => { - client.make_request("eth_getBlockByNumber", params.to_owned()) - } - } + let (method, params) = match self { + Self::GetBlockByNumber(params) => ("eth_getBlockByNumber", params), + Self::GetBlockByHash(params) => ("eth_getBlockByHash", params), + }; + client.make_request(method, params.to_owned()) } /// `keccak256` hash the request params. @@ -240,16 +285,46 @@ mod tests { let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); provider.load_cache(path.clone()).await.unwrap(); + let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); + assert_eq!(blk, blk2); tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + let latest_block_num = provider.get_block_number().await.unwrap(); let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); + assert_eq!(blk3, blk4); + + provider.save_cache(path).await.unwrap(); + } + + #[tokio::test] + async fn test_get_block() { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + + let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); + provider.load_cache(path.clone()).await.unwrap(); + + let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block, block2); + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + let latest_block = + provider.get_block(BlockId::latest(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let latest_hash = latest_block.unwrap().header.hash.unwrap(); + + let block3 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block4 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block3, block4); provider.save_cache(path).await.unwrap(); - assert_eq!(blk, blk2); - assert_eq!(blk3, blk4); } } From 902a24b16c055fa041bed454260eb3c3e2f4d1de Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:33:32 +0530 Subject: [PATCH 10/48] todos --- crates/provider/src/layers/cache.rs | 53 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 5d2fa01d829..8166bc823d6 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,9 +1,11 @@ -use crate::{Provider, ProviderLayer, RootProvider}; +use crate::{Provider, ProviderLayer, RootProvider, RpcWithBlock}; use alloy_json_rpc::{Request, RpcParam}; use alloy_network::Ethereum; -use alloy_primitives::{BlockHash, B256}; +use alloy_primitives::{Address, BlockHash, StorageKey, StorageValue, B256, U256}; use alloy_rpc_client::ClientRef; -use alloy_rpc_types_eth::{Block, BlockId, BlockNumberOrTag, BlockTransactionsKind}; +use alloy_rpc_types_eth::{ + Block, BlockId, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, +}; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; use serde::{Deserialize, Serialize}; @@ -200,18 +202,7 @@ where } println!("Cache miss"); - let block = self - .client() - .request::<_, Option>("eth_getBlockByHash", (hash, full)) - .await? - .map(|mut block| { - if !full { - // this ensures an empty response for `Hashes` has the expected form - // this is required because deserializing [] is ambiguous - block.transactions.convert_to_hashes(); - } - block - }); + let block = self.inner.get_block_by_hash(hash, kind).await?; if let Some(ref block) = block { let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; let _ = self.put(req_hash, json_str).await?; @@ -220,8 +211,30 @@ where Ok(block) } - // TODO: Add other commonly used methods such as eth_getTransactionByHash, eth_getProof, - // eth_getStorage etc. + // TODO: Add other commonly used methods such as eth_getTransactionByHash + // TODO: Any methods returning RpcWithBlock or RpcCall are blocked by https://github.com/alloy-rs/alloy/pull/788 + + /// Get the account and storage values of the specified account including the merkle proofs. + /// + /// This call can be used to verify that the data has not been tampered with. + fn get_proof( + &self, + address: Address, + keys: Vec, + ) -> RpcWithBlock), EIP1186AccountProofResponse> { + todo!() + // Blocked by https://github.com/alloy-rs/alloy/pull/788 + } + + /// Gets the specified storage value from [Address]. + fn get_storage_at( + &self, + address: Address, + key: U256, + ) -> RpcWithBlock { + todo!() + // Blocked by https://github.com/alloy-rs/alloy/pull/788 + } } /// Enum representing different RPC requests. @@ -232,6 +245,10 @@ enum RequestType { GetBlockByNumber(Params), /// Get block by hash. GetBlockByHash(Params), + /// Get proof. + GetProof(Params), + /// Get storage at. + GetStorageAt(Params), } impl RequestType { @@ -239,6 +256,8 @@ impl RequestType { let (method, params) = match self { Self::GetBlockByNumber(params) => ("eth_getBlockByNumber", params), Self::GetBlockByHash(params) => ("eth_getBlockByHash", params), + Self::GetProof(params) => ("eth_getProof", params), + Self::GetStorageAt(params) => ("eth_getStorageAt", params), }; client.make_request(method, params.to_owned()) } From bb0554484c8f4e3c5aecc60382a3921bd5693ead Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:03:55 +0530 Subject: [PATCH 11/48] refactor: port to transport layer --- crates/json-rpc/src/packet.rs | 9 ++ crates/provider/src/layers/cache.rs | 3 +- crates/provider/src/provider/trait.rs | 21 +++ crates/transport/Cargo.toml | 2 + crates/transport/src/layers/cache.rs | 214 ++++++++++++++++++++++++++ crates/transport/src/layers/mod.rs | 5 + 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 crates/transport/src/layers/cache.rs diff --git a/crates/json-rpc/src/packet.rs b/crates/json-rpc/src/packet.rs index 38f1edabe26..adcf4e11ea0 100644 --- a/crates/json-rpc/src/packet.rs +++ b/crates/json-rpc/src/packet.rs @@ -275,6 +275,15 @@ impl ResponsePacket { Self::Batch(batch) => batch.iter().filter(|res| ids.contains(&res.id)).collect(), } } + + /// Returns the underlying response if ResponsePacket is of type Single, + /// otherwise returns `None`. + pub const fn single_response(&self) -> Option<&Response> { + match self { + Self::Single(single) => Some(single), + Self::Batch(_) => None, + } + } } /// An Iterator over the [ErrorPayload]s in a [ResponsePacket]. diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 8166bc823d6..29a275c11e2 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -4,7 +4,7 @@ use alloy_network::Ethereum; use alloy_primitives::{Address, BlockHash, StorageKey, StorageValue, B256, U256}; use alloy_rpc_client::ClientRef; use alloy_rpc_types_eth::{ - Block, BlockId, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, + Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, }; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; @@ -293,6 +293,7 @@ mod tests { use crate::ProviderBuilder; use alloy_node_bindings::Anvil; + use alloy_rpc_types_eth::BlockId; use super::*; diff --git a/crates/provider/src/provider/trait.rs b/crates/provider/src/provider/trait.rs index 6af73c6744b..1730e1c6f0a 100644 --- a/crates/provider/src/provider/trait.rs +++ b/crates/provider/src/provider/trait.rs @@ -989,7 +989,9 @@ mod tests { use alloy_network::AnyNetwork; use alloy_node_bindings::Anvil; use alloy_primitives::{address, b256, bytes, keccak256}; + use alloy_rpc_client::ClientBuilder; use alloy_rpc_types_eth::request::TransactionRequest; + use alloy_transport::layers::CacheLayer; fn init_tracing() { let _ = tracing_subscriber::fmt::try_init(); @@ -1022,6 +1024,25 @@ mod tests { assert_eq!(0, num); } + #[tokio::test] + async fn test_transport_cache_layer() { + init_tracing(); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let cache_layer = CacheLayer::new(10, "./transport-rpc-cache.txt".into()); + let client = ClientBuilder::default().layer(cache_layer).http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().on_client(client); + let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); + let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); + assert_eq!(blk, blk2); + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + let latest_block_num = provider.get_block_number().await.unwrap(); + let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); + let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); + assert_eq!(blk3, blk4); + } + #[cfg(feature = "reqwest")] #[tokio::test] async fn object_safety() { diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index d6e013f308d..b6485b7e077 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -20,6 +20,7 @@ workspace = true [dependencies] alloy-json-rpc.workspace = true +alloy-primitives.workspace = true base64.workspace = true futures-util.workspace = true @@ -31,6 +32,7 @@ tower.workspace = true url.workspace = true tracing.workspace = true tokio = { workspace = true, features = ["rt", "time"] } +lru = "0.12" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = { version = "0.4", optional = true } diff --git a/crates/transport/src/layers/cache.rs b/crates/transport/src/layers/cache.rs new file mode 100644 index 00000000000..76514e93f5c --- /dev/null +++ b/crates/transport/src/layers/cache.rs @@ -0,0 +1,214 @@ +use crate::{TransportError, TransportErrorKind, TransportFut, TransportResult}; +use alloy_json_rpc::{RequestPacket, Response, ResponsePacket, ResponsePayload}; +use alloy_primitives::B256; +use lru::LruCache; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use std::{ + io::BufReader, + num::NonZeroUsize, + path::PathBuf, + sync::{Arc, RwLock}, + task::{Context, Poll}, +}; +use tower::{Layer, Service}; +/// Cache Layer +#[derive(Debug, Clone)] +pub struct CacheLayer { + /// Config for the cache layer. + config: CacheConfig, +} + +impl CacheLayer { + /// Instantiate a new cache layer with the the maximum number of + /// items to store. + #[inline] + pub const fn new(max_items: usize, path: PathBuf) -> Self { + Self { config: CacheConfig { max_items, path } } + } + + /// Returns the maximum number of items that can be stored in the cache, set at initialization. + #[inline] + pub const fn max_items(&self) -> usize { + self.config.max_items + } +} + +/// Configuration for the cache layer. +/// For future extensibility of the configurations. +#[derive(Debug, Clone)] +pub struct CacheConfig { + /// Maximum number of items to store in the cache. + pub max_items: usize, + /// Path of the cache file. + pub path: PathBuf, +} + +impl Layer for CacheLayer { + type Service = CachingService; + + fn layer(&self, inner: S) -> Self::Service { + CachingService::new(inner, self.config.clone()) + } +} + +/// Caching service. +#[derive(Debug, Clone)] +pub struct CachingService { + /// Inner transport service. + inner: S, + /// Config for the cache layer. + config: CacheConfig, + /// In-memory LRU cache, mapping requests to responses. + cache: Arc>>, +} + +impl Drop for CachingService { + fn drop(&mut self) { + let _ = self.save_cache(); + } +} + +impl CachingService { + /// Instantiate a new cache service. + pub fn new(inner: S, config: CacheConfig) -> Self { + let cache = Arc::new(RwLock::new(LruCache::::new( + NonZeroUsize::new(config.max_items).unwrap(), + ))); + let service = Self { inner, config, cache }; + + let loaded = service.load_cache(); + + match loaded { + Ok(_) => { + tracing::info!("Loaded cache"); + } + Err(e) => { + tracing::info!("Error loading cache: {:?}", e); + } + } + service + } + + /// Puts a value into the cache, and returns the old value if it existed. + pub fn put(&self, key: B256, value: String) -> TransportResult> { + let mut cache = self.cache.write().unwrap(); + Ok(cache.put(key, value)) + } + + /// Gets a value from the cache, if it exists. + pub fn get(&self, key: &B256) -> TransportResult> { + // Need to acquire a write guard to change the order of keys in LRU cache. + let mut cache = self.cache.write().unwrap(); + let val = cache.get(key).cloned(); + Ok(val) + } + + /// Saves the cache to a file specified by the path. + /// If the files does not exist, it creates one. + /// If the file exists, it overwrites it. + pub fn save_cache(&self) -> TransportResult<()> { + let path = self.config.path.clone(); + let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; + let cache = self.cache.read().unwrap(); + + // Iterate over the cache and dump to the file. + let entries = cache + .iter() + .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() }) + .collect::>(); + serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; + Ok(()) + } + + /// Loads the cache from a file specified by the path. + /// If the file does not exist, it returns without error. + pub fn load_cache(&self) -> TransportResult<()> { + println!("Loading cache..."); + let path = self.config.path.clone(); + if !path.exists() { + println!("Cache file does not exist."); + return Ok(()); + }; + let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; + let file = BufReader::new(file); + let entries: Vec = + serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; + let mut cache = self.cache.write().unwrap(); + for entry in entries { + cache.put(entry.key, entry.value); + } + + println!("Loaded from Cache"); + Ok(()) + } +} + +impl Service for CachingService +where + S: Service + + Send + + 'static + + Clone, + S::Future: Send + 'static, +{ + type Response = ResponsePacket; + type Error = TransportError; + type Future = TransportFut<'static>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: RequestPacket) -> Self::Future { + let mut inner = self.inner.clone(); + let this = self.clone(); + match req.clone() { + RequestPacket::Single(ser_req) => { + let params_hash = ser_req.params_hash(); + let resp = this.get(¶ms_hash); + match resp { + Ok(Some(resp)) => { + println!("Cache hit!"); + let raw = RawValue::from_string(resp).unwrap(); + let payload: ResponsePayload, Box> = + ResponsePayload::Success(raw); + let response = Response { id: ser_req.id().clone(), payload }; + + Box::pin(async move { Ok(ResponsePacket::Single(response)) }) + } + Ok(None) => { + println!("Cache miss!"); + Box::pin(async move { + let res = inner.call(req).await; + match res { + Ok(resp) => { + // Store success response into cache. + if let Some(res) = resp.single_response() { + let ser = res.payload.as_success().unwrap().to_string(); + let _ = this.put(params_hash, ser); + } + + Ok(resp) + } + Err(e) => Err(e), + } + }) + } + Err(e) => Box::pin(async move { Err(e) }), + } + } + RequestPacket::Batch(_reqs) => { + todo!() + } + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct FsCacheEntry { + /// Hash of the request params + key: B256, + /// Serialized response to the request from which the hash was computed. + value: String, +} diff --git a/crates/transport/src/layers/mod.rs b/crates/transport/src/layers/mod.rs index a6a1ccbf7b4..5a90269a523 100644 --- a/crates/transport/src/layers/mod.rs +++ b/crates/transport/src/layers/mod.rs @@ -4,3 +4,8 @@ mod retry; /// RetryBackoffLayer pub use retry::{RateLimitRetryPolicy, RetryBackoffLayer, RetryBackoffService, RetryPolicy}; + +mod cache; + +/// CacheLayer +pub use cache::{CacheConfig, CacheLayer, CachingService}; From 5fa27f44aba5c82be40768055031dff457bb77d4 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:06:04 +0530 Subject: [PATCH 12/48] rm provider cache layer --- crates/provider/src/layers/cache.rs | 350 ---------------------------- crates/provider/src/layers/mod.rs | 3 - 2 files changed, 353 deletions(-) delete mode 100644 crates/provider/src/layers/cache.rs diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs deleted file mode 100644 index 29a275c11e2..00000000000 --- a/crates/provider/src/layers/cache.rs +++ /dev/null @@ -1,350 +0,0 @@ -use crate::{Provider, ProviderLayer, RootProvider, RpcWithBlock}; -use alloy_json_rpc::{Request, RpcParam}; -use alloy_network::Ethereum; -use alloy_primitives::{Address, BlockHash, StorageKey, StorageValue, B256, U256}; -use alloy_rpc_client::ClientRef; -use alloy_rpc_types_eth::{ - Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, -}; -use alloy_transport::{Transport, TransportErrorKind, TransportResult}; -use lru::LruCache; -use serde::{Deserialize, Serialize}; -use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf, sync::Arc}; -use tokio::sync::RwLock; -// TODO: Populate load cache from file on initialization. -// TODO: Add method to dump cache to file. -/// A provider layer that caches RPC responses and serves them on subsequent requests. -/// -/// In order to initialize the caching layer, the path to the cache file is provided along with the -/// max number of items that are stored in the in-memory LRU cache. -/// -/// One can load the cache from the file system by calling `load_cache` and save the cache to the -/// file system by calling `save_cache`. -/// -/// Example usage: -/// ``` -/// use alloy_node_bindings::Anvil; -/// use alloy_provider::ProviderBuilder; -/// use std::path::PathBuf; -/// -/// let cache = CacheLayer::new(100); -/// let anvil = Anvil::new().block_time_f64(0.3).spawn(); -/// let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); -/// let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); -/// provider.load_cache(path).await.unwrap(); // Load cache from file if it exists. -/// -/// let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from RPC and saved to in-memory cache -/// -/// let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from in-memory cache -/// assert_eq!(blk, blk2); -/// -/// provider.save_cache(path).await.unwrap(); // Save cache to file -/// ``` -#[derive(Debug, Clone)] -pub struct CacheLayer { - config: CacheConfig, -} - -impl CacheLayer { - /// Instantiate a new cache layer with the the maximum number of - /// items to store. - #[inline] - pub const fn new(max_items: usize) -> Self { - Self { config: CacheConfig { max_items } } - } - - /// Returns the maximum number of items that can be stored in the cache, set at initialization. - #[inline] - pub const fn max_items(&self) -> usize { - self.config.max_items - } -} - -impl ProviderLayer for CacheLayer -where - P: Provider, - T: Transport + Clone, -{ - type Provider = CacheProvider; - - fn layer(&self, inner: P) -> Self::Provider { - CacheProvider::new(inner, self.max_items()) - } -} - -/// A provider that caches responses to RPC requests. -#[derive(Debug, Clone)] -pub struct CacheProvider { - /// Inner provider. - inner: P, - /// In-memory LRU cache, mapping requests to responses. - cache: Arc>>, - /// Phantom data - _pd: PhantomData, -} - -impl CacheProvider -where - P: Provider, - T: Transport + Clone, -{ - /// Instantiate a new cache provider. - pub fn new(inner: P, max_items: usize) -> Self { - let cache = Arc::new(RwLock::new(LruCache::::new( - NonZeroUsize::new(max_items).unwrap(), - ))); - Self { inner, cache, _pd: PhantomData } - } - - /// Puts a value into the cache, and returns the old value if it existed. - pub async fn put(&self, key: B256, value: String) -> TransportResult> { - let mut cache = self.cache.write().await; - Ok(cache.put(key, value)) - } - - /// Gets a value from the cache, if it exists. - pub async fn get(&self, key: &B256) -> TransportResult> { - // Need to acquire a write guard to change the order of keys in LRU cache. - let mut cache = self.cache.write().await; - let val = cache.get(key).cloned(); - Ok(val) - } - - /// Saves the cache to a file specified by the path. - /// If the files does not exist, it creates one. - /// If the file exists, it overwrites it. - pub async fn save_cache(&self, path: PathBuf) -> TransportResult<()> { - let cache = self.cache.read().await; - let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; - - // Iterate over the cache and dump to the file. - let entries = cache - .iter() - .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() }) - .collect::>(); - serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; - Ok(()) - } - - /// Loads the cache from a file specified by the path. - /// If the file does not exist, it returns without error. - pub async fn load_cache(&self, path: PathBuf) -> TransportResult<()> { - if !path.exists() { - return Ok(()); - }; - let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; - let file = BufReader::new(file); - let entries: Vec = - serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; - let mut cache = self.cache.write().await; - for entry in entries { - cache.put(entry.key, entry.value); - } - - Ok(()) - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl Provider for CacheProvider -where - P: Provider, - T: Transport + Clone, -{ - #[inline(always)] - fn root(&self) -> &RootProvider { - self.inner.root() - } - - async fn get_block_by_number( - &self, - number: BlockNumberOrTag, - hydrate: bool, - ) -> TransportResult> { - let hash = - RequestType::GetBlockByNumber((number, hydrate)).params_hash(self.inner.client())?; - - if let Some(block) = self.get(&hash).await? { - let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; - println!("Cache hit"); - return Ok(Some(block)); - } - - println!("Cache miss"); - let block = self.inner.get_block_by_number(number, hydrate).await?; - if let Some(ref block) = block { - let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; - let _ = self.put(hash, json_str).await?; - } - - Ok(block) - } - - /// Gets a block by its [BlockHash], with full transactions or only hashes. - async fn get_block_by_hash( - &self, - hash: BlockHash, - kind: BlockTransactionsKind, - ) -> TransportResult> { - let full = match kind { - BlockTransactionsKind::Full => true, - BlockTransactionsKind::Hashes => false, - }; - - let req_hash = - RequestType::GetBlockByHash((hash, full)).params_hash(self.inner.client())?; - - if let Some(block) = self.get(&req_hash).await? { - let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; - println!("Cache hit"); - return Ok(Some(block)); - } - - println!("Cache miss"); - let block = self.inner.get_block_by_hash(hash, kind).await?; - if let Some(ref block) = block { - let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; - let _ = self.put(req_hash, json_str).await?; - } - - Ok(block) - } - - // TODO: Add other commonly used methods such as eth_getTransactionByHash - // TODO: Any methods returning RpcWithBlock or RpcCall are blocked by https://github.com/alloy-rs/alloy/pull/788 - - /// Get the account and storage values of the specified account including the merkle proofs. - /// - /// This call can be used to verify that the data has not been tampered with. - fn get_proof( - &self, - address: Address, - keys: Vec, - ) -> RpcWithBlock), EIP1186AccountProofResponse> { - todo!() - // Blocked by https://github.com/alloy-rs/alloy/pull/788 - } - - /// Gets the specified storage value from [Address]. - fn get_storage_at( - &self, - address: Address, - key: U256, - ) -> RpcWithBlock { - todo!() - // Blocked by https://github.com/alloy-rs/alloy/pull/788 - } -} - -/// Enum representing different RPC requests. -/// -/// Useful for handling hashing of various request parameters. -enum RequestType { - /// Get block by number. - GetBlockByNumber(Params), - /// Get block by hash. - GetBlockByHash(Params), - /// Get proof. - GetProof(Params), - /// Get storage at. - GetStorageAt(Params), -} - -impl RequestType { - fn make_request(&self, client: ClientRef<'_, T>) -> Request { - let (method, params) = match self { - Self::GetBlockByNumber(params) => ("eth_getBlockByNumber", params), - Self::GetBlockByHash(params) => ("eth_getBlockByHash", params), - Self::GetProof(params) => ("eth_getProof", params), - Self::GetStorageAt(params) => ("eth_getStorageAt", params), - }; - client.make_request(method, params.to_owned()) - } - - /// `keccak256` hash the request params. - fn params_hash(&self, client: ClientRef<'_, T>) -> TransportResult { - let req = self.make_request(client); - let ser_req = req.serialize().map_err(TransportErrorKind::custom)?; - - Ok(ser_req.params_hash()) - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct FsCacheEntry { - /// Hash of the request params - key: B256, - /// Serialized response to the request from which the hash was computed. - value: String, -} - -/// Configuration for the cache layer. -/// For future extensibility of the configurations. -#[derive(Debug, Clone)] -pub struct CacheConfig { - /// Maximum number of items to store in the cache. - pub max_items: usize, -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use crate::ProviderBuilder; - use alloy_node_bindings::Anvil; - use alloy_rpc_types_eth::BlockId; - - use super::*; - - #[tokio::test] - async fn test_cache_provider() { - let cache = CacheLayer::new(100); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - - let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); - provider.load_cache(path.clone()).await.unwrap(); - - let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); - let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); - assert_eq!(blk, blk2); - - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - - let latest_block_num = provider.get_block_number().await.unwrap(); - let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); - let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); - assert_eq!(blk3, blk4); - - provider.save_cache(path).await.unwrap(); - } - - #[tokio::test] - async fn test_get_block() { - let cache = CacheLayer::new(100); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - - let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); - provider.load_cache(path.clone()).await.unwrap(); - - let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. - let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. - assert_eq!(block, block2); - - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - - let latest_block = - provider.get_block(BlockId::latest(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. - let latest_hash = latest_block.unwrap().header.hash.unwrap(); - - let block3 = - provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. - let block4 = - provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. - assert_eq!(block3, block4); - - provider.save_cache(path).await.unwrap(); - } -} diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index 92e5d1d0714..6baaf09e411 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -9,6 +9,3 @@ pub use anvil::{AnvilLayer, AnvilProvider}; mod chain; pub use chain::ChainLayer; - -mod cache; -pub use cache::{CacheConfig, CacheLayer, CacheProvider}; From 12a76c3b27bf990fe182b2125e27f99d002000a6 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:47:50 +0530 Subject: [PATCH 13/48] use parking_lot::RwLock + tracing nits --- crates/transport/Cargo.toml | 1 + crates/transport/src/layers/cache.rs | 31 +++++++++++----------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index b6485b7e077..a84ca4dab92 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -33,6 +33,7 @@ url.workspace = true tracing.workspace = true tokio = { workspace = true, features = ["rt", "time"] } lru = "0.12" +parking_lot = "0.12.3" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = { version = "0.4", optional = true } diff --git a/crates/transport/src/layers/cache.rs b/crates/transport/src/layers/cache.rs index 76514e93f5c..e877c9bf829 100644 --- a/crates/transport/src/layers/cache.rs +++ b/crates/transport/src/layers/cache.rs @@ -2,16 +2,18 @@ use crate::{TransportError, TransportErrorKind, TransportFut, TransportResult}; use alloy_json_rpc::{RequestPacket, Response, ResponsePacket, ResponsePayload}; use alloy_primitives::B256; use lru::LruCache; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use std::{ io::BufReader, num::NonZeroUsize, path::PathBuf, - sync::{Arc, RwLock}, + sync::Arc, task::{Context, Poll}, }; use tower::{Layer, Service}; +use tracing::trace; /// Cache Layer #[derive(Debug, Clone)] pub struct CacheLayer { @@ -77,29 +79,23 @@ impl CachingService { ))); let service = Self { inner, config, cache }; - let loaded = service.load_cache(); + let _loaded = service.load_cache().inspect_err(|e| { + trace!(?e, "Error loading cache"); + }); - match loaded { - Ok(_) => { - tracing::info!("Loaded cache"); - } - Err(e) => { - tracing::info!("Error loading cache: {:?}", e); - } - } service } /// Puts a value into the cache, and returns the old value if it existed. pub fn put(&self, key: B256, value: String) -> TransportResult> { - let mut cache = self.cache.write().unwrap(); + let mut cache = self.cache.write(); Ok(cache.put(key, value)) } /// Gets a value from the cache, if it exists. pub fn get(&self, key: &B256) -> TransportResult> { // Need to acquire a write guard to change the order of keys in LRU cache. - let mut cache = self.cache.write().unwrap(); + let mut cache = self.cache.write(); let val = cache.get(key).cloned(); Ok(val) } @@ -110,7 +106,7 @@ impl CachingService { pub fn save_cache(&self) -> TransportResult<()> { let path = self.config.path.clone(); let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; - let cache = self.cache.read().unwrap(); + let cache = self.cache.read(); // Iterate over the cache and dump to the file. let entries = cache @@ -124,22 +120,21 @@ impl CachingService { /// Loads the cache from a file specified by the path. /// If the file does not exist, it returns without error. pub fn load_cache(&self) -> TransportResult<()> { - println!("Loading cache..."); + trace!("Loading cache..."); let path = self.config.path.clone(); if !path.exists() { - println!("Cache file does not exist."); + trace!(?path, "Cache file does not exist."); return Ok(()); }; let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; let file = BufReader::new(file); let entries: Vec = serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; - let mut cache = self.cache.write().unwrap(); + let mut cache = self.cache.write(); for entry in entries { cache.put(entry.key, entry.value); } - println!("Loaded from Cache"); Ok(()) } } @@ -169,7 +164,6 @@ where let resp = this.get(¶ms_hash); match resp { Ok(Some(resp)) => { - println!("Cache hit!"); let raw = RawValue::from_string(resp).unwrap(); let payload: ResponsePayload, Box> = ResponsePayload::Success(raw); @@ -178,7 +172,6 @@ where Box::pin(async move { Ok(ResponsePacket::Single(response)) }) } Ok(None) => { - println!("Cache miss!"); Box::pin(async move { let res = inner.call(req).await; match res { From 64343b0f39d104eaf7c75395bfe54e390f0690cd Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:16:28 +0530 Subject: [PATCH 14/48] cleanup nits --- crates/transport/src/layers/cache.rs | 51 ++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/crates/transport/src/layers/cache.rs b/crates/transport/src/layers/cache.rs index e877c9bf829..c37d77dfd34 100644 --- a/crates/transport/src/layers/cache.rs +++ b/crates/transport/src/layers/cache.rs @@ -1,5 +1,7 @@ use crate::{TransportError, TransportErrorKind, TransportFut, TransportResult}; -use alloy_json_rpc::{RequestPacket, Response, ResponsePacket, ResponsePayload}; +use alloy_json_rpc::{ + Id, RequestPacket, Response, ResponsePacket, ResponsePayload, RpcError, SerializedRequest, +}; use alloy_primitives::B256; use lru::LruCache; use parking_lot::RwLock; @@ -100,6 +102,27 @@ impl CachingService { Ok(val) } + /// Resolves a `SerializedRequest` into a `RawValue` if it exists in the cache. + pub fn resolve(&self, req: SerializedRequest) -> TransportResult>> { + let key = req.params_hash(); + let value = self.get(&key)?; + + match value { + Some(value) => { + let raw = RawValue::from_string(value).map_err(RpcError::ser_err)?; + Ok(Some(raw)) + } + None => Ok(None), + } + } + + /// Handles a cache hit. + fn handle_cache_hit(&self, id: Id, raw: Box) -> ResponsePacket { + let payload = ResponsePayload::Success(raw); + let response = Response { id, payload }; + ResponsePacket::Single(response) + } + /// Saves the cache to a file specified by the path. /// If the files does not exist, it creates one. /// If the file exists, it overwrites it. @@ -161,20 +184,14 @@ where match req.clone() { RequestPacket::Single(ser_req) => { let params_hash = ser_req.params_hash(); - let resp = this.get(¶ms_hash); - match resp { - Ok(Some(resp)) => { - let raw = RawValue::from_string(resp).unwrap(); - let payload: ResponsePayload, Box> = - ResponsePayload::Success(raw); - let response = Response { id: ser_req.id().clone(), payload }; - - Box::pin(async move { Ok(ResponsePacket::Single(response)) }) + match this.resolve(ser_req) { + Ok(Some(raw)) => { + let resp = this.handle_cache_hit(ser_req.id().to_owned(), raw); + Box::pin(async move { Ok(resp) }) } Ok(None) => { Box::pin(async move { - let res = inner.call(req).await; - match res { + match inner.call(req).await { Ok(resp) => { // Store success response into cache. if let Some(res) = resp.single_response() { @@ -191,9 +208,13 @@ where Err(e) => Box::pin(async move { Err(e) }), } } - RequestPacket::Batch(_reqs) => { - todo!() - } + RequestPacket::Batch(reqs) => Box::pin(async move { + // Ignores cache, forwards request. + match inner.call(req).await { + Ok(resp) => Ok(resp), + Err(e) => Err(e), + } + }), } } } From 9fee4413dca288c9ec99d36b4d3c606400fff73b Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:18:03 +0530 Subject: [PATCH 15/48] nit --- crates/transport/src/layers/cache.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/transport/src/layers/cache.rs b/crates/transport/src/layers/cache.rs index c37d77dfd34..c7acb2d14a9 100644 --- a/crates/transport/src/layers/cache.rs +++ b/crates/transport/src/layers/cache.rs @@ -103,7 +103,7 @@ impl CachingService { } /// Resolves a `SerializedRequest` into a `RawValue` if it exists in the cache. - pub fn resolve(&self, req: SerializedRequest) -> TransportResult>> { + pub fn resolve(&self, req: &SerializedRequest) -> TransportResult>> { let key = req.params_hash(); let value = self.get(&key)?; @@ -184,7 +184,7 @@ where match req.clone() { RequestPacket::Single(ser_req) => { let params_hash = ser_req.params_hash(); - match this.resolve(ser_req) { + match this.resolve(&ser_req) { Ok(Some(raw)) => { let resp = this.handle_cache_hit(ser_req.id().to_owned(), raw); Box::pin(async move { Ok(resp) }) @@ -208,7 +208,7 @@ where Err(e) => Box::pin(async move { Err(e) }), } } - RequestPacket::Batch(reqs) => Box::pin(async move { + RequestPacket::Batch(_) => Box::pin(async move { // Ignores cache, forwards request. match inner.call(req).await { Ok(resp) => Ok(resp), From cb1c1b5b752f4c751742c0ee6704dff850caa40c Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:01:00 +0530 Subject: [PATCH 16/48] move cache instance to layer --- Cargo.toml | 2 + crates/transport/Cargo.toml | 4 +- crates/transport/src/layers/cache.rs | 160 ++++++++++++++------------- 3 files changed, 85 insertions(+), 81 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 590aa9fc7a8..7ddb2bd29a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,8 @@ thiserror = "1.0" thiserror-no-std = "2.0.2" url = "2.5" derive_more = "0.99.17" +lru = "0.12" +parking_lot = "0.12.3" ## serde serde = { version = "1.0", default-features = false, features = [ diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index a84ca4dab92..eb9fa2dff5c 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -32,8 +32,8 @@ tower.workspace = true url.workspace = true tracing.workspace = true tokio = { workspace = true, features = ["rt", "time"] } -lru = "0.12" -parking_lot = "0.12.3" +lru.workspace = true +parking_lot.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = { version = "0.4", optional = true } diff --git a/crates/transport/src/layers/cache.rs b/crates/transport/src/layers/cache.rs index c7acb2d14a9..43f242a8a33 100644 --- a/crates/transport/src/layers/cache.rs +++ b/crates/transport/src/layers/cache.rs @@ -21,14 +21,24 @@ use tracing::trace; pub struct CacheLayer { /// Config for the cache layer. config: CacheConfig, + /// In-memory LRU cache, mapping requests to responses. + cache: Arc>>, } impl CacheLayer { /// Instantiate a new cache layer with the the maximum number of /// items to store. - #[inline] - pub const fn new(max_items: usize, path: PathBuf) -> Self { - Self { config: CacheConfig { max_items, path } } + pub fn new(max_items: usize, path: PathBuf) -> Self { + let layer = Self { + config: CacheConfig { max_items, path: path.clone() }, + cache: Arc::new(RwLock::new(LruCache::new(NonZeroUsize::new(max_items).unwrap()))), + }; + + let _loaded = Self::load_cache(path, layer.cache.clone()).inspect_err(|e| { + trace!(?e, "Error loading cache"); + }); + + layer } /// Returns the maximum number of items that can be stored in the cache, set at initialization. @@ -36,57 +46,6 @@ impl CacheLayer { pub const fn max_items(&self) -> usize { self.config.max_items } -} - -/// Configuration for the cache layer. -/// For future extensibility of the configurations. -#[derive(Debug, Clone)] -pub struct CacheConfig { - /// Maximum number of items to store in the cache. - pub max_items: usize, - /// Path of the cache file. - pub path: PathBuf, -} - -impl Layer for CacheLayer { - type Service = CachingService; - - fn layer(&self, inner: S) -> Self::Service { - CachingService::new(inner, self.config.clone()) - } -} - -/// Caching service. -#[derive(Debug, Clone)] -pub struct CachingService { - /// Inner transport service. - inner: S, - /// Config for the cache layer. - config: CacheConfig, - /// In-memory LRU cache, mapping requests to responses. - cache: Arc>>, -} - -impl Drop for CachingService { - fn drop(&mut self) { - let _ = self.save_cache(); - } -} - -impl CachingService { - /// Instantiate a new cache service. - pub fn new(inner: S, config: CacheConfig) -> Self { - let cache = Arc::new(RwLock::new(LruCache::::new( - NonZeroUsize::new(config.max_items).unwrap(), - ))); - let service = Self { inner, config, cache }; - - let _loaded = service.load_cache().inspect_err(|e| { - trace!(?e, "Error loading cache"); - }); - - service - } /// Puts a value into the cache, and returns the old value if it existed. pub fn put(&self, key: B256, value: String) -> TransportResult> { @@ -102,32 +61,12 @@ impl CachingService { Ok(val) } - /// Resolves a `SerializedRequest` into a `RawValue` if it exists in the cache. - pub fn resolve(&self, req: &SerializedRequest) -> TransportResult>> { - let key = req.params_hash(); - let value = self.get(&key)?; - - match value { - Some(value) => { - let raw = RawValue::from_string(value).map_err(RpcError::ser_err)?; - Ok(Some(raw)) - } - None => Ok(None), - } - } - - /// Handles a cache hit. - fn handle_cache_hit(&self, id: Id, raw: Box) -> ResponsePacket { - let payload = ResponsePayload::Success(raw); - let response = Response { id, payload }; - ResponsePacket::Single(response) - } - /// Saves the cache to a file specified by the path. /// If the files does not exist, it creates one. /// If the file exists, it overwrites it. pub fn save_cache(&self) -> TransportResult<()> { let path = self.config.path.clone(); + trace!(?path, "saving cache"); let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; let cache = self.cache.read(); @@ -142,9 +81,11 @@ impl CachingService { /// Loads the cache from a file specified by the path. /// If the file does not exist, it returns without error. - pub fn load_cache(&self) -> TransportResult<()> { + fn load_cache( + path: PathBuf, + cache: Arc>>, + ) -> TransportResult<()> { trace!("Loading cache..."); - let path = self.config.path.clone(); if !path.exists() { trace!(?path, "Cache file does not exist."); return Ok(()); @@ -153,7 +94,7 @@ impl CachingService { let file = BufReader::new(file); let entries: Vec = serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; - let mut cache = self.cache.write(); + let mut cache = cache.write(); for entry in entries { cache.put(entry.key, entry.value); } @@ -162,6 +103,67 @@ impl CachingService { } } +/// Configuration for the cache layer. +/// For future extensibility of the configurations. +#[derive(Debug, Clone)] +pub struct CacheConfig { + /// Maximum number of items to store in the cache. + pub max_items: usize, + /// Path of the cache file. + pub path: PathBuf, +} + +impl Layer for CacheLayer { + type Service = CachingService; + + fn layer(&self, inner: S) -> Self::Service { + CachingService::new(inner, self.clone()) + } +} + +impl Drop for CacheLayer { + fn drop(&mut self) { + let _ = self.save_cache(); + } +} + +/// Caching service. +#[derive(Debug, Clone)] +pub struct CachingService { + /// Inner transport service. + inner: S, + /// Cache layer that houses the cache and it's config + layer: CacheLayer, +} + +impl CachingService { + /// Instantiate a new cache service. + pub fn new(inner: S, layer: CacheLayer) -> Self { + Self { inner, layer } + } + + /// Resolves a `SerializedRequest` into a `RawValue` if it exists in the cache. + pub fn resolve(&self, req: &SerializedRequest) -> TransportResult>> { + let key = req.params_hash(); + let value = self.layer.get(&key)?; + + match value { + Some(value) => { + let raw = RawValue::from_string(value).map_err(RpcError::ser_err)?; + Ok(Some(raw)) + } + None => Ok(None), + } + } + + /// Handles a cache hit. + fn handle_cache_hit(&self, id: Id, raw: Box) -> ResponsePacket { + let payload = ResponsePayload::Success(raw); + let response = Response { id, payload }; + ResponsePacket::Single(response) + } +} + impl Service for CachingService where S: Service @@ -196,7 +198,7 @@ where // Store success response into cache. if let Some(res) = resp.single_response() { let ser = res.payload.as_success().unwrap().to_string(); - let _ = this.put(params_hash, ser); + let _ = this.layer.put(params_hash, ser); } Ok(resp) From 5eb4af20432c3096005c53e0e79038c57a7954d1 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:34:53 +0530 Subject: [PATCH 17/48] Revert "refactor: port to transport layer" This reverts commit bb0554484c8f4e3c5aecc60382a3921bd5693ead. --- crates/json-rpc/src/packet.rs | 9 - crates/provider/src/layers/cache.rs | 349 ++++++++++++++++++++++++++ crates/provider/src/provider/trait.rs | 20 -- crates/transport/Cargo.toml | 3 - crates/transport/src/layers/cache.rs | 230 ----------------- crates/transport/src/layers/mod.rs | 5 - 6 files changed, 349 insertions(+), 267 deletions(-) create mode 100644 crates/provider/src/layers/cache.rs delete mode 100644 crates/transport/src/layers/cache.rs diff --git a/crates/json-rpc/src/packet.rs b/crates/json-rpc/src/packet.rs index cee41b4ac12..e3d7509aad8 100644 --- a/crates/json-rpc/src/packet.rs +++ b/crates/json-rpc/src/packet.rs @@ -276,15 +276,6 @@ impl ResponsePacket { Self::Batch(batch) => batch.iter().filter(|res| ids.contains(&res.id)).collect(), } } - - /// Returns the underlying response if ResponsePacket is of type Single, - /// otherwise returns `None`. - pub const fn single_response(&self) -> Option<&Response> { - match self { - Self::Single(single) => Some(single), - Self::Batch(_) => None, - } - } } /// An Iterator over the [ErrorPayload]s in a [ResponsePacket]. diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs new file mode 100644 index 00000000000..8166bc823d6 --- /dev/null +++ b/crates/provider/src/layers/cache.rs @@ -0,0 +1,349 @@ +use crate::{Provider, ProviderLayer, RootProvider, RpcWithBlock}; +use alloy_json_rpc::{Request, RpcParam}; +use alloy_network::Ethereum; +use alloy_primitives::{Address, BlockHash, StorageKey, StorageValue, B256, U256}; +use alloy_rpc_client::ClientRef; +use alloy_rpc_types_eth::{ + Block, BlockId, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, +}; +use alloy_transport::{Transport, TransportErrorKind, TransportResult}; +use lru::LruCache; +use serde::{Deserialize, Serialize}; +use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf, sync::Arc}; +use tokio::sync::RwLock; +// TODO: Populate load cache from file on initialization. +// TODO: Add method to dump cache to file. +/// A provider layer that caches RPC responses and serves them on subsequent requests. +/// +/// In order to initialize the caching layer, the path to the cache file is provided along with the +/// max number of items that are stored in the in-memory LRU cache. +/// +/// One can load the cache from the file system by calling `load_cache` and save the cache to the +/// file system by calling `save_cache`. +/// +/// Example usage: +/// ``` +/// use alloy_node_bindings::Anvil; +/// use alloy_provider::ProviderBuilder; +/// use std::path::PathBuf; +/// +/// let cache = CacheLayer::new(100); +/// let anvil = Anvil::new().block_time_f64(0.3).spawn(); +/// let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); +/// let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); +/// provider.load_cache(path).await.unwrap(); // Load cache from file if it exists. +/// +/// let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from RPC and saved to in-memory cache +/// +/// let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from in-memory cache +/// assert_eq!(blk, blk2); +/// +/// provider.save_cache(path).await.unwrap(); // Save cache to file +/// ``` +#[derive(Debug, Clone)] +pub struct CacheLayer { + config: CacheConfig, +} + +impl CacheLayer { + /// Instantiate a new cache layer with the the maximum number of + /// items to store. + #[inline] + pub const fn new(max_items: usize) -> Self { + Self { config: CacheConfig { max_items } } + } + + /// Returns the maximum number of items that can be stored in the cache, set at initialization. + #[inline] + pub const fn max_items(&self) -> usize { + self.config.max_items + } +} + +impl ProviderLayer for CacheLayer +where + P: Provider, + T: Transport + Clone, +{ + type Provider = CacheProvider; + + fn layer(&self, inner: P) -> Self::Provider { + CacheProvider::new(inner, self.max_items()) + } +} + +/// A provider that caches responses to RPC requests. +#[derive(Debug, Clone)] +pub struct CacheProvider { + /// Inner provider. + inner: P, + /// In-memory LRU cache, mapping requests to responses. + cache: Arc>>, + /// Phantom data + _pd: PhantomData, +} + +impl CacheProvider +where + P: Provider, + T: Transport + Clone, +{ + /// Instantiate a new cache provider. + pub fn new(inner: P, max_items: usize) -> Self { + let cache = Arc::new(RwLock::new(LruCache::::new( + NonZeroUsize::new(max_items).unwrap(), + ))); + Self { inner, cache, _pd: PhantomData } + } + + /// Puts a value into the cache, and returns the old value if it existed. + pub async fn put(&self, key: B256, value: String) -> TransportResult> { + let mut cache = self.cache.write().await; + Ok(cache.put(key, value)) + } + + /// Gets a value from the cache, if it exists. + pub async fn get(&self, key: &B256) -> TransportResult> { + // Need to acquire a write guard to change the order of keys in LRU cache. + let mut cache = self.cache.write().await; + let val = cache.get(key).cloned(); + Ok(val) + } + + /// Saves the cache to a file specified by the path. + /// If the files does not exist, it creates one. + /// If the file exists, it overwrites it. + pub async fn save_cache(&self, path: PathBuf) -> TransportResult<()> { + let cache = self.cache.read().await; + let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; + + // Iterate over the cache and dump to the file. + let entries = cache + .iter() + .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() }) + .collect::>(); + serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; + Ok(()) + } + + /// Loads the cache from a file specified by the path. + /// If the file does not exist, it returns without error. + pub async fn load_cache(&self, path: PathBuf) -> TransportResult<()> { + if !path.exists() { + return Ok(()); + }; + let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; + let file = BufReader::new(file); + let entries: Vec = + serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; + let mut cache = self.cache.write().await; + for entry in entries { + cache.put(entry.key, entry.value); + } + + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl Provider for CacheProvider +where + P: Provider, + T: Transport + Clone, +{ + #[inline(always)] + fn root(&self) -> &RootProvider { + self.inner.root() + } + + async fn get_block_by_number( + &self, + number: BlockNumberOrTag, + hydrate: bool, + ) -> TransportResult> { + let hash = + RequestType::GetBlockByNumber((number, hydrate)).params_hash(self.inner.client())?; + + if let Some(block) = self.get(&hash).await? { + let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; + println!("Cache hit"); + return Ok(Some(block)); + } + + println!("Cache miss"); + let block = self.inner.get_block_by_number(number, hydrate).await?; + if let Some(ref block) = block { + let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; + let _ = self.put(hash, json_str).await?; + } + + Ok(block) + } + + /// Gets a block by its [BlockHash], with full transactions or only hashes. + async fn get_block_by_hash( + &self, + hash: BlockHash, + kind: BlockTransactionsKind, + ) -> TransportResult> { + let full = match kind { + BlockTransactionsKind::Full => true, + BlockTransactionsKind::Hashes => false, + }; + + let req_hash = + RequestType::GetBlockByHash((hash, full)).params_hash(self.inner.client())?; + + if let Some(block) = self.get(&req_hash).await? { + let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; + println!("Cache hit"); + return Ok(Some(block)); + } + + println!("Cache miss"); + let block = self.inner.get_block_by_hash(hash, kind).await?; + if let Some(ref block) = block { + let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; + let _ = self.put(req_hash, json_str).await?; + } + + Ok(block) + } + + // TODO: Add other commonly used methods such as eth_getTransactionByHash + // TODO: Any methods returning RpcWithBlock or RpcCall are blocked by https://github.com/alloy-rs/alloy/pull/788 + + /// Get the account and storage values of the specified account including the merkle proofs. + /// + /// This call can be used to verify that the data has not been tampered with. + fn get_proof( + &self, + address: Address, + keys: Vec, + ) -> RpcWithBlock), EIP1186AccountProofResponse> { + todo!() + // Blocked by https://github.com/alloy-rs/alloy/pull/788 + } + + /// Gets the specified storage value from [Address]. + fn get_storage_at( + &self, + address: Address, + key: U256, + ) -> RpcWithBlock { + todo!() + // Blocked by https://github.com/alloy-rs/alloy/pull/788 + } +} + +/// Enum representing different RPC requests. +/// +/// Useful for handling hashing of various request parameters. +enum RequestType { + /// Get block by number. + GetBlockByNumber(Params), + /// Get block by hash. + GetBlockByHash(Params), + /// Get proof. + GetProof(Params), + /// Get storage at. + GetStorageAt(Params), +} + +impl RequestType { + fn make_request(&self, client: ClientRef<'_, T>) -> Request { + let (method, params) = match self { + Self::GetBlockByNumber(params) => ("eth_getBlockByNumber", params), + Self::GetBlockByHash(params) => ("eth_getBlockByHash", params), + Self::GetProof(params) => ("eth_getProof", params), + Self::GetStorageAt(params) => ("eth_getStorageAt", params), + }; + client.make_request(method, params.to_owned()) + } + + /// `keccak256` hash the request params. + fn params_hash(&self, client: ClientRef<'_, T>) -> TransportResult { + let req = self.make_request(client); + let ser_req = req.serialize().map_err(TransportErrorKind::custom)?; + + Ok(ser_req.params_hash()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct FsCacheEntry { + /// Hash of the request params + key: B256, + /// Serialized response to the request from which the hash was computed. + value: String, +} + +/// Configuration for the cache layer. +/// For future extensibility of the configurations. +#[derive(Debug, Clone)] +pub struct CacheConfig { + /// Maximum number of items to store in the cache. + pub max_items: usize, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::ProviderBuilder; + use alloy_node_bindings::Anvil; + + use super::*; + + #[tokio::test] + async fn test_cache_provider() { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + + let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); + provider.load_cache(path.clone()).await.unwrap(); + + let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); + let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); + assert_eq!(blk, blk2); + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + let latest_block_num = provider.get_block_number().await.unwrap(); + let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); + let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); + assert_eq!(blk3, blk4); + + provider.save_cache(path).await.unwrap(); + } + + #[tokio::test] + async fn test_get_block() { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + + let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); + provider.load_cache(path.clone()).await.unwrap(); + + let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block, block2); + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + let latest_block = + provider.get_block(BlockId::latest(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let latest_hash = latest_block.unwrap().header.hash.unwrap(); + + let block3 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block4 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block3, block4); + + provider.save_cache(path).await.unwrap(); + } +} diff --git a/crates/provider/src/provider/trait.rs b/crates/provider/src/provider/trait.rs index e12a90967bf..e072712698a 100644 --- a/crates/provider/src/provider/trait.rs +++ b/crates/provider/src/provider/trait.rs @@ -1025,7 +1025,6 @@ mod tests { use alloy_primitives::{address, b256, bytes, keccak256}; use alloy_rpc_client::ClientBuilder; use alloy_rpc_types_eth::{request::TransactionRequest, Block}; - use alloy_transport::layers::CacheLayer; // For layer transport tests #[cfg(feature = "hyper")] use alloy_transport_http::{ @@ -1182,25 +1181,6 @@ mod tests { assert_eq!(0, num); } - #[tokio::test] - async fn test_transport_cache_layer() { - init_tracing(); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let cache_layer = CacheLayer::new(10, "./transport-rpc-cache.txt".into()); - let client = ClientBuilder::default().layer(cache_layer).http(anvil.endpoint_url()); - let provider = ProviderBuilder::new().on_client(client); - let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); - let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); - assert_eq!(blk, blk2); - - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - - let latest_block_num = provider.get_block_number().await.unwrap(); - let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); - let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); - assert_eq!(blk3, blk4); - } - #[cfg(feature = "reqwest")] #[tokio::test] async fn object_safety() { diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml index eb9fa2dff5c..d6e013f308d 100644 --- a/crates/transport/Cargo.toml +++ b/crates/transport/Cargo.toml @@ -20,7 +20,6 @@ workspace = true [dependencies] alloy-json-rpc.workspace = true -alloy-primitives.workspace = true base64.workspace = true futures-util.workspace = true @@ -32,8 +31,6 @@ tower.workspace = true url.workspace = true tracing.workspace = true tokio = { workspace = true, features = ["rt", "time"] } -lru.workspace = true -parking_lot.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = { version = "0.4", optional = true } diff --git a/crates/transport/src/layers/cache.rs b/crates/transport/src/layers/cache.rs deleted file mode 100644 index 1233d116068..00000000000 --- a/crates/transport/src/layers/cache.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::{TransportError, TransportErrorKind, TransportFut, TransportResult}; -use alloy_json_rpc::{ - Id, RequestPacket, Response, ResponsePacket, ResponsePayload, RpcError, SerializedRequest, -}; -use alloy_primitives::B256; -use lru::LruCache; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use std::{ - io::BufReader, - num::NonZeroUsize, - path::PathBuf, - sync::Arc, - task::{Context, Poll}, -}; -use tower::{Layer, Service}; -use tracing::trace; -/// Cache Layer -#[derive(Debug, Clone)] -pub struct CacheLayer { - /// Config for the cache layer. - config: CacheConfig, - /// In-memory LRU cache, mapping requests to responses. - cache: Arc>>, -} - -impl CacheLayer { - /// Instantiate a new cache layer with the the maximum number of - /// items to store. - pub fn new(max_items: usize, path: PathBuf) -> Self { - let layer = Self { - config: CacheConfig { max_items, path: path.clone() }, - cache: Arc::new(RwLock::new(LruCache::new(NonZeroUsize::new(max_items).unwrap()))), - }; - - let _loaded = Self::load_cache(path, layer.cache.clone()).inspect_err(|e| { - trace!(?e, "Error loading cache"); - }); - - layer - } - - /// Returns the maximum number of items that can be stored in the cache, set at initialization. - #[inline] - pub const fn max_items(&self) -> usize { - self.config.max_items - } - - /// Puts a value into the cache, and returns the old value if it existed. - pub fn put(&self, key: B256, value: String) -> TransportResult> { - let mut cache = self.cache.write(); - Ok(cache.put(key, value)) - } - - /// Gets a value from the cache, if it exists. - pub fn get(&self, key: &B256) -> TransportResult> { - // Need to acquire a write guard to change the order of keys in LRU cache. - let mut cache = self.cache.write(); - let val = cache.get(key).cloned(); - Ok(val) - } - - /// Saves the cache to a file specified by the path. - /// If the files does not exist, it creates one. - /// If the file exists, it overwrites it. - pub fn save_cache(&self) -> TransportResult<()> { - let path = self.config.path.clone(); - trace!(?path, "saving cache"); - let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; - let cache = self.cache.read(); - - // Iterate over the cache and dump to the file. - let entries = cache - .iter() - .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() }) - .collect::>(); - serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; - Ok(()) - } - - /// Loads the cache from a file specified by the path. - /// If the file does not exist, it returns without error. - fn load_cache( - path: PathBuf, - cache: Arc>>, - ) -> TransportResult<()> { - trace!("Loading cache..."); - if !path.exists() { - trace!(?path, "Cache file does not exist."); - return Ok(()); - }; - let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; - let file = BufReader::new(file); - let entries: Vec = - serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; - let mut cache = cache.write(); - for entry in entries { - cache.put(entry.key, entry.value); - } - - Ok(()) - } -} - -/// Configuration for the cache layer. -/// For future extensibility of the configurations. -#[derive(Debug, Clone)] -pub struct CacheConfig { - /// Maximum number of items to store in the cache. - pub max_items: usize, - /// Path of the cache file. - pub path: PathBuf, -} - -impl Layer for CacheLayer { - type Service = CachingService; - - fn layer(&self, inner: S) -> Self::Service { - CachingService::new(inner, self.clone()) - } -} - -impl Drop for CacheLayer { - fn drop(&mut self) { - let _ = self.save_cache(); - } -} - -/// Caching service. -#[derive(Debug, Clone)] -pub struct CachingService { - /// Inner transport service. - inner: S, - /// Cache layer that houses the cache and it's config - layer: CacheLayer, -} - -impl CachingService { - /// Instantiate a new cache service. - pub const fn new(inner: S, layer: CacheLayer) -> Self { - Self { inner, layer } - } - - /// Resolves a `SerializedRequest` into a `RawValue` if it exists in the cache. - pub fn resolve(&self, req: &SerializedRequest) -> TransportResult>> { - let key = req.params_hash(); - let value = self.layer.get(&key)?; - - match value { - Some(value) => { - let raw = RawValue::from_string(value).map_err(RpcError::ser_err)?; - Ok(Some(raw)) - } - None => Ok(None), - } - } - - /// Handles a cache hit. - const fn handle_cache_hit(&self, id: Id, raw: Box) -> ResponsePacket { - let payload = ResponsePayload::Success(raw); - let response = Response { id, payload }; - ResponsePacket::Single(response) - } -} - -impl Service for CachingService -where - S: Service - + Send - + 'static - + Clone, - S::Future: Send + 'static, -{ - type Response = ResponsePacket; - type Error = TransportError; - type Future = TransportFut<'static>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: RequestPacket) -> Self::Future { - let mut inner = self.inner.clone(); - let this = self.clone(); - match req.clone() { - RequestPacket::Single(ser_req) => { - let params_hash = ser_req.params_hash(); - match this.resolve(&ser_req) { - Ok(Some(raw)) => { - let resp = this.handle_cache_hit(ser_req.id().to_owned(), raw); - Box::pin(async move { Ok(resp) }) - } - Ok(None) => { - Box::pin(async move { - match inner.call(req).await { - Ok(resp) => { - // Store success response into cache. - if let Some(res) = resp.single_response() { - let ser = res.payload.as_success().unwrap().to_string(); - let _ = this.layer.put(params_hash, ser); - } - - Ok(resp) - } - Err(e) => Err(e), - } - }) - } - Err(e) => Box::pin(async move { Err(e) }), - } - } - RequestPacket::Batch(_) => Box::pin(async move { - // Ignores cache, forwards request. - match inner.call(req).await { - Ok(resp) => Ok(resp), - Err(e) => Err(e), - } - }), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct FsCacheEntry { - /// Hash of the request params - key: B256, - /// Serialized response to the request from which the hash was computed. - value: String, -} diff --git a/crates/transport/src/layers/mod.rs b/crates/transport/src/layers/mod.rs index 5a90269a523..a6a1ccbf7b4 100644 --- a/crates/transport/src/layers/mod.rs +++ b/crates/transport/src/layers/mod.rs @@ -4,8 +4,3 @@ mod retry; /// RetryBackoffLayer pub use retry::{RateLimitRetryPolicy, RetryBackoffLayer, RetryBackoffService, RetryPolicy}; - -mod cache; - -/// CacheLayer -pub use cache::{CacheConfig, CacheLayer, CachingService}; From 3e9538efd9390b50560946ee9cd52ff469a355c5 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:37:25 +0530 Subject: [PATCH 18/48] use provider cache --- crates/provider/src/layers/cache.rs | 2 +- crates/provider/src/layers/mod.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 8166bc823d6..5d585c2584b 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -336,7 +336,7 @@ mod tests { let latest_block = provider.get_block(BlockId::latest(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. - let latest_hash = latest_block.unwrap().header.hash.unwrap(); + let latest_hash = latest_block.unwrap().header.hash; let block3 = provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index 6baaf09e411..92e5d1d0714 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -9,3 +9,6 @@ pub use anvil::{AnvilLayer, AnvilProvider}; mod chain; pub use chain::ChainLayer; + +mod cache; +pub use cache::{CacheConfig, CacheLayer, CacheProvider}; From efa027adf5218f915a3b98adb4e156d0fe6254fe Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:56:10 +0530 Subject: [PATCH 19/48] use macro --- crates/provider/src/layers/cache.rs | 49 +++++++++++++---------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 5d585c2584b..bba429011cd 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -145,6 +145,25 @@ where } } +macro_rules! cache_get_or_fetch { + ($self:expr, $hash:expr, $fetch_fn:expr) => {{ + if let Some(cached) = $self.get(&$hash).await? { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; + println!("Cache hit"); + return Ok(Some(result)); + } + + println!("Cache miss"); + let result = $fetch_fn.await?; + if let Some(ref data) = result { + let json_str = serde_json::to_string(data).map_err(TransportErrorKind::custom)?; + let _ = $self.put($hash, json_str).await?; + } + + Ok(result) + }}; +} + #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for CacheProvider @@ -165,20 +184,7 @@ where let hash = RequestType::GetBlockByNumber((number, hydrate)).params_hash(self.inner.client())?; - if let Some(block) = self.get(&hash).await? { - let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; - println!("Cache hit"); - return Ok(Some(block)); - } - - println!("Cache miss"); - let block = self.inner.get_block_by_number(number, hydrate).await?; - if let Some(ref block) = block { - let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; - let _ = self.put(hash, json_str).await?; - } - - Ok(block) + cache_get_or_fetch!(self, hash, self.inner.get_block_by_number(number, hydrate)) } /// Gets a block by its [BlockHash], with full transactions or only hashes. @@ -195,20 +201,7 @@ where let req_hash = RequestType::GetBlockByHash((hash, full)).params_hash(self.inner.client())?; - if let Some(block) = self.get(&req_hash).await? { - let block = serde_json::from_str(&block).map_err(TransportErrorKind::custom)?; - println!("Cache hit"); - return Ok(Some(block)); - } - - println!("Cache miss"); - let block = self.inner.get_block_by_hash(hash, kind).await?; - if let Some(ref block) = block { - let json_str = serde_json::to_string(block).map_err(TransportErrorKind::custom)?; - let _ = self.put(req_hash, json_str).await?; - } - - Ok(block) + cache_get_or_fetch!(self, req_hash, self.inner.get_block_by_hash(hash, kind)) } // TODO: Add other commonly used methods such as eth_getTransactionByHash From ab44b80f7da91dda9fe536add28d15ca535cdad9 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:50:43 +0530 Subject: [PATCH 20/48] cached get_proof --- crates/provider/src/layers/cache.rs | 113 +++++++++++++++++++-- crates/provider/src/provider/with_block.rs | 6 +- 2 files changed, 107 insertions(+), 12 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index bba429011cd..fcdd36f348a 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,10 +1,10 @@ -use crate::{Provider, ProviderLayer, RootProvider, RpcWithBlock}; +use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; use alloy_json_rpc::{Request, RpcParam}; use alloy_network::Ethereum; use alloy_primitives::{Address, BlockHash, StorageKey, StorageValue, B256, U256}; use alloy_rpc_client::ClientRef; use alloy_rpc_types_eth::{ - Block, BlockId, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, + Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, }; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; @@ -204,8 +204,8 @@ where cache_get_or_fetch!(self, req_hash, self.inner.get_block_by_hash(hash, kind)) } - // TODO: Add other commonly used methods such as eth_getTransactionByHash - // TODO: Any methods returning RpcWithBlock or RpcCall are blocked by https://github.com/alloy-rs/alloy/pull/788 + // TODO: Add other commonly used methods such as eth_getTransactionByHash, + // eth_getTransactionReceipt, etc. /// Get the account and storage values of the specified account including the merkle proofs. /// @@ -215,15 +215,53 @@ where address: Address, keys: Vec, ) -> RpcWithBlock), EIP1186AccountProofResponse> { - todo!() - // Blocked by https://github.com/alloy-rs/alloy/pull/788 + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + RpcWithBlock::new_provider(move |block_id| { + let block_id = block_id.clone(); + let keys = keys.clone(); + let client = client.clone(); + let cache = cache.clone(); + ProviderCall::BoxedFuture(Box::pin(async move { + let req = RequestType::GetProof((address, keys.clone(), block_id)); + + let client = client.upgrade().ok_or_else(|| { + TransportErrorKind::custom_str( + "RPC client was dropped before the request was made", + ) + })?; + + let hash = req.params_hash(&client)?; + + let mut cache = cache.write().await; + + if let Some(cached) = cache.get(&hash).cloned() { + let result = + serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; + return Ok(result); + } + + let result = client + .request("eth_getProof", (address, keys)) + .map_params(|params| ParamsWithBlock { params, block_id }) + .map_resp(EIP1186AccountProofResponse::from); + + let res = result.await?; + + let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; + + let _ = cache.put(hash, json_str); + + Ok(res) + })) + }) } /// Gets the specified storage value from [Address]. fn get_storage_at( &self, - address: Address, - key: U256, + _address: Address, + _key: U256, ) -> RpcWithBlock { todo!() // Blocked by https://github.com/alloy-rs/alloy/pull/788 @@ -285,7 +323,10 @@ mod tests { use std::str::FromStr; use crate::ProviderBuilder; + use alloy_network::TransactionBuilder; use alloy_node_bindings::Anvil; + use alloy_primitives::{Bytes, FixedBytes}; + use alloy_rpc_types_eth::TransactionRequest; use super::*; @@ -295,7 +336,7 @@ mod tests { let anvil = Anvil::new().block_time_f64(0.3).spawn(); let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); + let path = PathBuf::from_str("./rpc-cache-block-by-number.txt").unwrap(); provider.load_cache(path.clone()).await.unwrap(); let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); @@ -318,7 +359,7 @@ mod tests { let anvil = Anvil::new().block_time_f64(0.3).spawn(); let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); + let path = PathBuf::from_str("./rpc-cache-block-by-hash.txt").unwrap(); provider.load_cache(path.clone()).await.unwrap(); let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. @@ -339,4 +380,56 @@ mod tests { provider.save_cache(path).await.unwrap(); } + + #[tokio::test] + async fn test_get_proof() { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default() + // .with_recommended_fillers() - TODO: Issue here. Layer doesn't work with fillers. Fix. + .layer(cache) + .on_http(anvil.endpoint_url()); + + let from = anvil.addresses()[0]; + let path = PathBuf::from_str("./rpc-cache-proof.txt").unwrap(); + + provider.load_cache(path.clone()).await.unwrap(); + + let calldata: Bytes = "0x6080604052348015600f57600080fd5b506101f28061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80633fb5c1cb146100465780638381f58a14610062578063d09de08a14610080575b600080fd5b610060600480360381019061005b91906100ee565b61008a565b005b61006a610094565b604051610077919061012a565b60405180910390f35b61008861009a565b005b8060008190555050565b60005481565b6000808154809291906100ac90610174565b9190505550565b600080fd5b6000819050919050565b6100cb816100b8565b81146100d657600080fd5b50565b6000813590506100e8816100c2565b92915050565b600060208284031215610104576101036100b3565b5b6000610112848285016100d9565b91505092915050565b610124816100b8565b82525050565b600060208201905061013f600083018461011b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017f826100b8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101b1576101b0610145565b5b60018201905091905056fea264697066735822122067ac0f21f648b0cacd1b7260772852ad4a0f63e2cc174168c51a6887fd5197a964736f6c634300081a0033".parse().unwrap(); + + let tx = TransactionRequest::default() + .with_from(from) + .with_input(calldata) + .with_max_fee_per_gas(1_000_000_000) + .with_max_priority_fee_per_gas(1_000_000) + .with_gas_limit(1_000_000) + .with_nonce(0); + + let tx_receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + let counter_addr = tx_receipt.contract_address.unwrap(); + + let keys = vec![ + FixedBytes::with_last_byte(0), + FixedBytes::with_last_byte(0x1), + FixedBytes::with_last_byte(0x2), + FixedBytes::with_last_byte(0x3), + FixedBytes::with_last_byte(0x4), + ]; + + let start_t = std::time::Instant::now(); + let _proof = + provider.get_proof(counter_addr, keys.clone()).block_id(1.into()).await.unwrap(); + let end_t = std::time::Instant::now(); + + println!("Time taken: {:?}", end_t - start_t); + + let start_t = std::time::Instant::now(); + let _proof2 = provider.get_proof(counter_addr, keys).block_id(1.into()).await.unwrap(); + let end_t = std::time::Instant::now(); + + println!("Time taken: {:?}", end_t - start_t); + + provider.save_cache(path).await.unwrap(); + } } diff --git a/crates/provider/src/provider/with_block.rs b/crates/provider/src/provider/with_block.rs index d3fcd300957..c19b6f2ee8a 100644 --- a/crates/provider/src/provider/with_block.rs +++ b/crates/provider/src/provider/with_block.rs @@ -10,8 +10,10 @@ use crate::ProviderCall; /// Helper struct that houses the params along with the BlockId. #[derive(Debug, Clone)] pub struct ParamsWithBlock { - params: Params, - block_id: BlockId, + /// The params to be sent to the RPC call. + pub params: Params, + /// The block id to be used for the RPC call. + pub block_id: BlockId, } impl serde::Serialize for ParamsWithBlock { From ae9e123d20f1196fb6a276cfbc01b15790972d69 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:52:26 +0530 Subject: [PATCH 21/48] nit --- crates/provider/src/layers/cache.rs | 3 +-- crates/provider/src/provider/trait.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index fcdd36f348a..246200fa290 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -218,7 +218,6 @@ where let client = self.inner.weak_client(); let cache = self.cache.clone(); RpcWithBlock::new_provider(move |block_id| { - let block_id = block_id.clone(); let keys = keys.clone(); let client = client.clone(); let cache = cache.clone(); @@ -326,7 +325,7 @@ mod tests { use alloy_network::TransactionBuilder; use alloy_node_bindings::Anvil; use alloy_primitives::{Bytes, FixedBytes}; - use alloy_rpc_types_eth::TransactionRequest; + use alloy_rpc_types_eth::{BlockId, TransactionRequest}; use super::*; diff --git a/crates/provider/src/provider/trait.rs b/crates/provider/src/provider/trait.rs index e072712698a..d04fa821786 100644 --- a/crates/provider/src/provider/trait.rs +++ b/crates/provider/src/provider/trait.rs @@ -1023,7 +1023,6 @@ mod tests { use alloy_network::AnyNetwork; use alloy_node_bindings::Anvil; use alloy_primitives::{address, b256, bytes, keccak256}; - use alloy_rpc_client::ClientBuilder; use alloy_rpc_types_eth::{request::TransactionRequest, Block}; // For layer transport tests #[cfg(feature = "hyper")] From 9f646b2512527ff6aa7cb01d2cd7cf21e6c4217e Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:59:45 +0530 Subject: [PATCH 22/48] nit --- crates/provider/src/layers/cache.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 246200fa290..93e375cac95 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -182,7 +182,7 @@ where hydrate: bool, ) -> TransportResult> { let hash = - RequestType::GetBlockByNumber((number, hydrate)).params_hash(self.inner.client())?; + RequestType::BlockByNumber((number, hydrate)).params_hash(self.inner.client())?; cache_get_or_fetch!(self, hash, self.inner.get_block_by_number(number, hydrate)) } @@ -198,8 +198,7 @@ where BlockTransactionsKind::Hashes => false, }; - let req_hash = - RequestType::GetBlockByHash((hash, full)).params_hash(self.inner.client())?; + let req_hash = RequestType::BlockByHash((hash, full)).params_hash(self.inner.client())?; cache_get_or_fetch!(self, req_hash, self.inner.get_block_by_hash(hash, kind)) } @@ -222,7 +221,7 @@ where let client = client.clone(); let cache = cache.clone(); ProviderCall::BoxedFuture(Box::pin(async move { - let req = RequestType::GetProof((address, keys.clone(), block_id)); + let req = RequestType::Proof((address, keys.clone(), block_id)); let client = client.upgrade().ok_or_else(|| { TransportErrorKind::custom_str( @@ -272,22 +271,23 @@ where /// Useful for handling hashing of various request parameters. enum RequestType { /// Get block by number. - GetBlockByNumber(Params), + BlockByNumber(Params), /// Get block by hash. - GetBlockByHash(Params), + BlockByHash(Params), /// Get proof. - GetProof(Params), + Proof(Params), /// Get storage at. - GetStorageAt(Params), + #[allow(dead_code)] // todo + StorageAt(Params), } impl RequestType { fn make_request(&self, client: ClientRef<'_, T>) -> Request { let (method, params) = match self { - Self::GetBlockByNumber(params) => ("eth_getBlockByNumber", params), - Self::GetBlockByHash(params) => ("eth_getBlockByHash", params), - Self::GetProof(params) => ("eth_getProof", params), - Self::GetStorageAt(params) => ("eth_getStorageAt", params), + Self::BlockByNumber(params) => ("eth_getBlockByNumber", params), + Self::BlockByHash(params) => ("eth_getBlockByHash", params), + Self::Proof(params) => ("eth_getProof", params), + Self::StorageAt(params) => ("eth_getStorageAt", params), }; client.make_request(method, params.to_owned()) } From 947bd91bcf4894b90bf65c3b781685f3ba4042fe Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:04:21 +0530 Subject: [PATCH 23/48] use parking_lot --- crates/provider/Cargo.toml | 3 +- crates/provider/src/layers/cache.rs | 54 +++++++++++++++-------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index 12444152f42..d0b44847c89 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -51,6 +51,7 @@ dashmap = "6.0" futures-utils-wasm.workspace = true futures.workspace = true lru = "0.12" +parking_lot.workspace = true pin-project.workspace = true reqwest = { workspace = true, optional = true } serde_json.workspace = true @@ -72,7 +73,7 @@ alloy-transport-http = { workspace = true, features = ["reqwest"] } itertools.workspace = true reqwest.workspace = true -tokio = { workspace = true, features = ["macros"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tracing-subscriber = { workspace = true, features = ["fmt"] } tempfile.workspace = true tower.workspace = true diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 93e375cac95..824644e1071 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -8,11 +8,10 @@ use alloy_rpc_types_eth::{ }; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf, sync::Arc}; -use tokio::sync::RwLock; -// TODO: Populate load cache from file on initialization. -// TODO: Add method to dump cache to file. + /// A provider layer that caches RPC responses and serves them on subsequent requests. /// /// In order to initialize the caching layer, the path to the cache file is provided along with the @@ -24,21 +23,26 @@ use tokio::sync::RwLock; /// Example usage: /// ``` /// use alloy_node_bindings::Anvil; -/// use alloy_provider::ProviderBuilder; +/// use alloy_provider::{ProviderBuilder, Provider}; +/// use alloy_provider::layers::CacheLayer; /// use std::path::PathBuf; +/// use std::str::FromStr; /// +/// #[tokio::main] +/// async fn main() { /// let cache = CacheLayer::new(100); /// let anvil = Anvil::new().block_time_f64(0.3).spawn(); /// let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); /// let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); -/// provider.load_cache(path).await.unwrap(); // Load cache from file if it exists. +/// provider.load_cache(path.clone()).unwrap(); // Load cache from file if it exists. /// /// let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from RPC and saved to in-memory cache /// /// let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from in-memory cache /// assert_eq!(blk, blk2); /// -/// provider.save_cache(path).await.unwrap(); // Save cache to file +/// provider.save_cache(path).unwrap(); // Save cache to file +/// } /// ``` #[derive(Debug, Clone)] pub struct CacheLayer { @@ -97,15 +101,15 @@ where } /// Puts a value into the cache, and returns the old value if it existed. - pub async fn put(&self, key: B256, value: String) -> TransportResult> { - let mut cache = self.cache.write().await; + pub fn put(&self, key: B256, value: String) -> TransportResult> { + let mut cache = self.cache.write(); Ok(cache.put(key, value)) } /// Gets a value from the cache, if it exists. - pub async fn get(&self, key: &B256) -> TransportResult> { + pub fn get(&self, key: &B256) -> TransportResult> { // Need to acquire a write guard to change the order of keys in LRU cache. - let mut cache = self.cache.write().await; + let mut cache = self.cache.write(); let val = cache.get(key).cloned(); Ok(val) } @@ -113,8 +117,8 @@ where /// Saves the cache to a file specified by the path. /// If the files does not exist, it creates one. /// If the file exists, it overwrites it. - pub async fn save_cache(&self, path: PathBuf) -> TransportResult<()> { - let cache = self.cache.read().await; + pub fn save_cache(&self, path: PathBuf) -> TransportResult<()> { + let cache = self.cache.read(); let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; // Iterate over the cache and dump to the file. @@ -128,7 +132,7 @@ where /// Loads the cache from a file specified by the path. /// If the file does not exist, it returns without error. - pub async fn load_cache(&self, path: PathBuf) -> TransportResult<()> { + pub fn load_cache(&self, path: PathBuf) -> TransportResult<()> { if !path.exists() { return Ok(()); }; @@ -136,7 +140,7 @@ where let file = BufReader::new(file); let entries: Vec = serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; - let mut cache = self.cache.write().await; + let mut cache = self.cache.write(); for entry in entries { cache.put(entry.key, entry.value); } @@ -147,7 +151,7 @@ where macro_rules! cache_get_or_fetch { ($self:expr, $hash:expr, $fetch_fn:expr) => {{ - if let Some(cached) = $self.get(&$hash).await? { + if let Some(cached) = $self.get(&$hash)? { let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; println!("Cache hit"); return Ok(Some(result)); @@ -157,7 +161,7 @@ macro_rules! cache_get_or_fetch { let result = $fetch_fn.await?; if let Some(ref data) = result { let json_str = serde_json::to_string(data).map_err(TransportErrorKind::custom)?; - let _ = $self.put($hash, json_str).await?; + let _ = $self.put($hash, json_str)?; } Ok(result) @@ -231,9 +235,7 @@ where let hash = req.params_hash(&client)?; - let mut cache = cache.write().await; - - if let Some(cached) = cache.get(&hash).cloned() { + if let Some(cached) = cache.write().get(&hash).cloned() { let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; return Ok(result); @@ -248,7 +250,7 @@ where let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; - let _ = cache.put(hash, json_str); + let _ = cache.write().put(hash, json_str); Ok(res) })) @@ -336,7 +338,7 @@ mod tests { let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); let path = PathBuf::from_str("./rpc-cache-block-by-number.txt").unwrap(); - provider.load_cache(path.clone()).await.unwrap(); + provider.load_cache(path.clone()).unwrap(); let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); @@ -349,7 +351,7 @@ mod tests { let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); assert_eq!(blk3, blk4); - provider.save_cache(path).await.unwrap(); + provider.save_cache(path).unwrap(); } #[tokio::test] @@ -359,7 +361,7 @@ mod tests { let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); let path = PathBuf::from_str("./rpc-cache-block-by-hash.txt").unwrap(); - provider.load_cache(path.clone()).await.unwrap(); + provider.load_cache(path.clone()).unwrap(); let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. @@ -377,7 +379,7 @@ mod tests { provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. assert_eq!(block3, block4); - provider.save_cache(path).await.unwrap(); + provider.save_cache(path).unwrap(); } #[tokio::test] @@ -392,7 +394,7 @@ mod tests { let from = anvil.addresses()[0]; let path = PathBuf::from_str("./rpc-cache-proof.txt").unwrap(); - provider.load_cache(path.clone()).await.unwrap(); + provider.load_cache(path.clone()).unwrap(); let calldata: Bytes = "0x6080604052348015600f57600080fd5b506101f28061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80633fb5c1cb146100465780638381f58a14610062578063d09de08a14610080575b600080fd5b610060600480360381019061005b91906100ee565b61008a565b005b61006a610094565b604051610077919061012a565b60405180910390f35b61008861009a565b005b8060008190555050565b60005481565b6000808154809291906100ac90610174565b9190505550565b600080fd5b6000819050919050565b6100cb816100b8565b81146100d657600080fd5b50565b6000813590506100e8816100c2565b92915050565b600060208284031215610104576101036100b3565b5b6000610112848285016100d9565b91505092915050565b610124816100b8565b82525050565b600060208201905061013f600083018461011b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017f826100b8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101b1576101b0610145565b5b60018201905091905056fea264697066735822122067ac0f21f648b0cacd1b7260772852ad4a0f63e2cc174168c51a6887fd5197a964736f6c634300081a0033".parse().unwrap(); @@ -429,6 +431,6 @@ mod tests { println!("Time taken: {:?}", end_t - start_t); - provider.save_cache(path).await.unwrap(); + provider.save_cache(path).unwrap(); } } From c82901cd99930f830e40917103fc4c98d5df3c28 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:39:26 +0530 Subject: [PATCH 24/48] make params hash independent of client --- crates/provider/src/layers/cache.rs | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 824644e1071..a5ee20cfae9 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,8 +1,7 @@ use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; -use alloy_json_rpc::{Request, RpcParam}; +use alloy_json_rpc::{RpcError, RpcParam}; use alloy_network::Ethereum; -use alloy_primitives::{Address, BlockHash, StorageKey, StorageValue, B256, U256}; -use alloy_rpc_client::ClientRef; +use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256}; use alloy_rpc_types_eth::{ Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, }; @@ -153,11 +152,9 @@ macro_rules! cache_get_or_fetch { ($self:expr, $hash:expr, $fetch_fn:expr) => {{ if let Some(cached) = $self.get(&$hash)? { let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; - println!("Cache hit"); return Ok(Some(result)); } - println!("Cache miss"); let result = $fetch_fn.await?; if let Some(ref data) = result { let json_str = serde_json::to_string(data).map_err(TransportErrorKind::custom)?; @@ -185,8 +182,7 @@ where number: BlockNumberOrTag, hydrate: bool, ) -> TransportResult> { - let hash = - RequestType::BlockByNumber((number, hydrate)).params_hash(self.inner.client())?; + let hash = RequestType::BlockByNumber((number, hydrate)).params_hash()?; cache_get_or_fetch!(self, hash, self.inner.get_block_by_number(number, hydrate)) } @@ -202,7 +198,7 @@ where BlockTransactionsKind::Hashes => false, }; - let req_hash = RequestType::BlockByHash((hash, full)).params_hash(self.inner.client())?; + let req_hash = RequestType::BlockByHash((hash, full)).params_hash()?; cache_get_or_fetch!(self, req_hash, self.inner.get_block_by_hash(hash, kind)) } @@ -233,7 +229,7 @@ where ) })?; - let hash = req.params_hash(&client)?; + let hash = req.params_hash()?; if let Some(cached) = cache.write().get(&hash).cloned() { let result = @@ -284,22 +280,19 @@ enum RequestType { } impl RequestType { - fn make_request(&self, client: ClientRef<'_, T>) -> Request { - let (method, params) = match self { + fn params_hash(&self) -> TransportResult { + let (_method, params) = match self { Self::BlockByNumber(params) => ("eth_getBlockByNumber", params), Self::BlockByHash(params) => ("eth_getBlockByHash", params), Self::Proof(params) => ("eth_getProof", params), Self::StorageAt(params) => ("eth_getStorageAt", params), }; - client.make_request(method, params.to_owned()) - } - /// `keccak256` hash the request params. - fn params_hash(&self, client: ClientRef<'_, T>) -> TransportResult { - let req = self.make_request(client); - let ser_req = req.serialize().map_err(TransportErrorKind::custom)?; + let hash = serde_json::to_string(¶ms) + .map(|p| keccak256(p.as_bytes())) + .map_err(RpcError::ser_err)?; - Ok(ser_req.params_hash()) + Ok(hash) } } From 32a7beb44c246218b61ab22838423fb20da0ff3f Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:54:32 +0530 Subject: [PATCH 25/48] fix --- crates/provider/src/layers/cache.rs | 43 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index a5ee20cfae9..1bbf2c86451 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -217,25 +217,28 @@ where let client = self.inner.weak_client(); let cache = self.cache.clone(); RpcWithBlock::new_provider(move |block_id| { - let keys = keys.clone(); - let client = client.clone(); + let req = RequestType::Proof((address, keys.clone(), block_id)); + let hash = req.params_hash().ok(); + + if let Some(hash) = hash { + // let cache = cache.read(); + if let Some(cached) = cache.write().get(&hash) { + let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); + + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + let client = client.upgrade().ok_or_else(|| { + TransportErrorKind::custom_str("RPC client was dropped before the request was made") + }); let cache = cache.clone(); + let keys = keys.clone(); ProviderCall::BoxedFuture(Box::pin(async move { - let req = RequestType::Proof((address, keys.clone(), block_id)); - - let client = client.upgrade().ok_or_else(|| { - TransportErrorKind::custom_str( - "RPC client was dropped before the request was made", - ) - })?; - - let hash = req.params_hash()?; - - if let Some(cached) = cache.write().get(&hash).cloned() { - let result = - serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; - return Ok(result); - } + let client = client?; let result = client .request("eth_getProof", (address, keys)) @@ -244,9 +247,11 @@ where let res = result.await?; + // Insert into cache. let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; - - let _ = cache.write().put(hash, json_str); + let hash = req.params_hash()?; + let mut cache = cache.write(); + let _ = cache.put(hash, json_str); Ok(res) })) From 0478ded84eba28b483b0bcfe5fcec22798d8cc64 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:49:58 +0530 Subject: [PATCH 26/48] cache_rpc_call_with_block! --- crates/provider/src/layers/cache.rs | 118 ++++++++++++++++------------ 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 1bbf2c86451..6164d4f471d 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,4 +1,4 @@ -use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; +use crate::{Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; use alloy_json_rpc::{RpcError, RpcParam}; use alloy_network::Ethereum; use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256}; @@ -165,6 +165,42 @@ macro_rules! cache_get_or_fetch { }}; } +macro_rules! cache_rpc_call_with_block { + ($cache:expr, $client:expr, $req:expr) => {{ + let hash = $req.params_hash().ok(); + + if let Some(hash) = hash { + if let Some(cached) = $cache.write().get(&hash) { + let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + let client = + $client.upgrade().ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); + let cache = $cache.clone(); + // let params = $params.clone(); + ProviderCall::BoxedFuture(Box::pin(async move { + let client = client?; + + let result = client.request($req.method(), $req.params()); + + let res = result.await?; + + // Insert into cache. + let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; + let hash = $req.params_hash()?; + let mut cache = cache.write(); + let _ = cache.put(hash, json_str); + + Ok(res) + })) + }}; +} + #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for CacheProvider @@ -218,54 +254,22 @@ where let cache = self.cache.clone(); RpcWithBlock::new_provider(move |block_id| { let req = RequestType::Proof((address, keys.clone(), block_id)); - let hash = req.params_hash().ok(); - - if let Some(hash) = hash { - // let cache = cache.read(); - if let Some(cached) = cache.write().get(&hash) { - let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); - - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); - } - } - - let client = client.upgrade().ok_or_else(|| { - TransportErrorKind::custom_str("RPC client was dropped before the request was made") - }); - let cache = cache.clone(); - let keys = keys.clone(); - ProviderCall::BoxedFuture(Box::pin(async move { - let client = client?; - - let result = client - .request("eth_getProof", (address, keys)) - .map_params(|params| ParamsWithBlock { params, block_id }) - .map_resp(EIP1186AccountProofResponse::from); - - let res = result.await?; - - // Insert into cache. - let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; - let hash = req.params_hash()?; - let mut cache = cache.write(); - let _ = cache.put(hash, json_str); - - Ok(res) - })) + cache_rpc_call_with_block!(cache, client, req) }) } /// Gets the specified storage value from [Address]. fn get_storage_at( &self, - _address: Address, - _key: U256, + address: Address, + key: U256, ) -> RpcWithBlock { - todo!() - // Blocked by https://github.com/alloy-rs/alloy/pull/788 + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + RpcWithBlock::new_provider(move |block_id| { + let req = RequestType::StorageAt((address, key, block_id)); + cache_rpc_call_with_block!(cache, client, req) + }) } } @@ -285,13 +289,26 @@ enum RequestType { } impl RequestType { + fn method(&self) -> &'static str { + match self { + Self::BlockByNumber(_) => "eth_getBlockByNumber", + Self::BlockByHash(_) => "eth_getBlockByHash", + Self::Proof(_) => "eth_getProof", + Self::StorageAt(_) => "eth_getStorageAt", + } + } + + fn params(&self) -> Params { + match self { + Self::BlockByNumber(params) => params.clone(), + Self::BlockByHash(params) => params.clone(), + Self::Proof(params) => params.clone(), + Self::StorageAt(params) => params.clone(), + } + } + fn params_hash(&self) -> TransportResult { - let (_method, params) = match self { - Self::BlockByNumber(params) => ("eth_getBlockByNumber", params), - Self::BlockByHash(params) => ("eth_getBlockByHash", params), - Self::Proof(params) => ("eth_getProof", params), - Self::StorageAt(params) => ("eth_getStorageAt", params), - }; + let params = self.params(); let hash = serde_json::to_string(¶ms) .map(|p| keccak256(p.as_bytes())) @@ -384,10 +401,7 @@ mod tests { async fn test_get_proof() { let cache = CacheLayer::new(100); let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default() - // .with_recommended_fillers() - TODO: Issue here. Layer doesn't work with fillers. Fix. - .layer(cache) - .on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); let from = anvil.addresses()[0]; let path = PathBuf::from_str("./rpc-cache-proof.txt").unwrap(); From fa8346c827f0c9f2871a702f324a05bc5c2a1712 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:06:37 +0530 Subject: [PATCH 27/48] fix: request type --- crates/provider/src/layers/cache.rs | 88 ++++++++++++----------------- 1 file changed, 36 insertions(+), 52 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 6164d4f471d..505b1788d41 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,4 +1,5 @@ -use crate::{Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; +use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; +use alloy_eips::BlockId; use alloy_json_rpc::{RpcError, RpcParam}; use alloy_network::Ethereum; use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256}; @@ -149,8 +150,9 @@ where } macro_rules! cache_get_or_fetch { - ($self:expr, $hash:expr, $fetch_fn:expr) => {{ - if let Some(cached) = $self.get(&$hash)? { + ($self:expr, $req:expr, $fetch_fn:expr) => {{ + let hash = $req.params_hash()?; + if let Some(cached) = $self.get(&hash)? { let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; return Ok(Some(result)); } @@ -158,7 +160,7 @@ macro_rules! cache_get_or_fetch { let result = $fetch_fn.await?; if let Some(ref data) = result { let json_str = serde_json::to_string(data).map_err(TransportErrorKind::custom)?; - let _ = $self.put($hash, json_str)?; + let _ = $self.put(hash, json_str)?; } Ok(result) @@ -186,7 +188,9 @@ macro_rules! cache_rpc_call_with_block { ProviderCall::BoxedFuture(Box::pin(async move { let client = client?; - let result = client.request($req.method(), $req.params()); + let result = client.request($req.method(), $req.params()).map_params(|params| { + ParamsWithBlock { params, block_id: $req.block_id.unwrap_or(BlockId::latest()) } + }); let res = result.await?; @@ -218,7 +222,8 @@ where number: BlockNumberOrTag, hydrate: bool, ) -> TransportResult> { - let hash = RequestType::BlockByNumber((number, hydrate)).params_hash()?; + // let hash = RequestType::BlockByNumber((number, hydrate)).params_hash()?; + let hash = RequestType::new("eth_getBlockByNumber", (number, hydrate)); cache_get_or_fetch!(self, hash, self.inner.get_block_by_number(number, hydrate)) } @@ -234,14 +239,11 @@ where BlockTransactionsKind::Hashes => false, }; - let req_hash = RequestType::BlockByHash((hash, full)).params_hash()?; + let req_hash = RequestType::new("eth_getBlockByHash", (hash, full)); cache_get_or_fetch!(self, req_hash, self.inner.get_block_by_hash(hash, kind)) } - // TODO: Add other commonly used methods such as eth_getTransactionByHash, - // eth_getTransactionReceipt, etc. - /// Get the account and storage values of the specified account including the merkle proofs. /// /// This call can be used to verify that the data has not been tampered with. @@ -253,7 +255,8 @@ where let client = self.inner.weak_client(); let cache = self.cache.clone(); RpcWithBlock::new_provider(move |block_id| { - let req = RequestType::Proof((address, keys.clone(), block_id)); + let req = + RequestType::new("eth_getProof", (address, keys.clone())).with_block_id(block_id); cache_rpc_call_with_block!(cache, client, req) }) } @@ -267,55 +270,43 @@ where let client = self.inner.weak_client(); let cache = self.cache.clone(); RpcWithBlock::new_provider(move |block_id| { - let req = RequestType::StorageAt((address, key, block_id)); + let req = RequestType::new("eth_getStorageAt", (address, key)).with_block_id(block_id); cache_rpc_call_with_block!(cache, client, req) }) } } -/// Enum representing different RPC requests. -/// -/// Useful for handling hashing of various request parameters. -enum RequestType { - /// Get block by number. - BlockByNumber(Params), - /// Get block by hash. - BlockByHash(Params), - /// Get proof. - Proof(Params), - /// Get storage at. - #[allow(dead_code)] // todo - StorageAt(Params), +struct RequestType { + method: &'static str, + params: Params, + block_id: Option, } impl RequestType { - fn method(&self) -> &'static str { - match self { - Self::BlockByNumber(_) => "eth_getBlockByNumber", - Self::BlockByHash(_) => "eth_getBlockByHash", - Self::Proof(_) => "eth_getProof", - Self::StorageAt(_) => "eth_getStorageAt", - } + fn new(method: &'static str, params: Params) -> Self { + Self { method, params, block_id: None } } - fn params(&self) -> Params { - match self { - Self::BlockByNumber(params) => params.clone(), - Self::BlockByHash(params) => params.clone(), - Self::Proof(params) => params.clone(), - Self::StorageAt(params) => params.clone(), - } + fn with_block_id(mut self, block_id: BlockId) -> Self { + self.block_id = Some(block_id); + self } fn params_hash(&self) -> TransportResult { - let params = self.params(); - - let hash = serde_json::to_string(¶ms) + let hash = serde_json::to_string(&self.params()) .map(|p| keccak256(p.as_bytes())) .map_err(RpcError::ser_err)?; Ok(hash) } + + fn method(&self) -> &'static str { + self.method + } + + fn params(&self) -> Params { + self.params.clone() + } } #[derive(Debug, Serialize, Deserialize)] @@ -430,18 +421,11 @@ mod tests { FixedBytes::with_last_byte(0x4), ]; - let start_t = std::time::Instant::now(); - let _proof = + let proof = provider.get_proof(counter_addr, keys.clone()).block_id(1.into()).await.unwrap(); - let end_t = std::time::Instant::now(); - - println!("Time taken: {:?}", end_t - start_t); - - let start_t = std::time::Instant::now(); - let _proof2 = provider.get_proof(counter_addr, keys).block_id(1.into()).await.unwrap(); - let end_t = std::time::Instant::now(); + let proof2 = provider.get_proof(counter_addr, keys).block_id(1.into()).await.unwrap(); - println!("Time taken: {:?}", end_t - start_t); + assert_eq!(proof, proof2); provider.save_cache(path).unwrap(); } From 6e0c676ccf65873045e540d4d66f7fe6ebe54362 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:22:45 +0530 Subject: [PATCH 28/48] redirect reqs with block tags to rpc --- crates/provider/src/layers/cache.rs | 50 +++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 505b1788d41..895ff8f90c6 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -167,24 +167,11 @@ macro_rules! cache_get_or_fetch { }}; } -macro_rules! cache_rpc_call_with_block { +macro_rules! rpc_prov_call { ($cache:expr, $client:expr, $req:expr) => {{ - let hash = $req.params_hash().ok(); - - if let Some(hash) = hash { - if let Some(cached) = $cache.write().get(&hash) { - let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); - } - } - let client = $client.upgrade().ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); let cache = $cache.clone(); - // let params = $params.clone(); ProviderCall::BoxedFuture(Box::pin(async move { let client = client?; @@ -205,6 +192,28 @@ macro_rules! cache_rpc_call_with_block { }}; } +macro_rules! cache_rpc_call_with_block { + ($cache:expr, $client:expr, $req:expr) => {{ + if $req.has_block_tag() { + return rpc_prov_call!($cache, $client, $req); + } + + let hash = $req.params_hash().ok(); + + if let Some(hash) = hash { + if let Some(cached) = $cache.write().get(&hash) { + let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + rpc_prov_call!($cache, $client, $req) + }}; +} + #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for CacheProvider @@ -307,6 +316,19 @@ impl RequestType { fn params(&self) -> Params { self.params.clone() } + + /// Returns true if the BlockId has been set to a tag value such as "latest", "earliest", or + /// "pending". + fn has_block_tag(&self) -> bool { + if let Some(block_id) = self.block_id { + match block_id { + BlockId::Hash(_) => return false, + BlockId::Number(BlockNumberOrTag::Number(_)) => return false, + _ => return true, + } + } + false + } } #[derive(Debug, Serialize, Deserialize)] From 70bc5e17788b13022c898cda750ee7ca187b3a5a Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:26:57 +0530 Subject: [PATCH 29/48] nits --- crates/provider/src/layers/cache.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 895ff8f90c6..96a8df19e20 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -292,11 +292,11 @@ struct RequestType { } impl RequestType { - fn new(method: &'static str, params: Params) -> Self { + const fn new(method: &'static str, params: Params) -> Self { Self { method, params, block_id: None } } - fn with_block_id(mut self, block_id: BlockId) -> Self { + const fn with_block_id(mut self, block_id: BlockId) -> Self { self.block_id = Some(block_id); self } @@ -309,7 +309,7 @@ impl RequestType { Ok(hash) } - fn method(&self) -> &'static str { + const fn method(&self) -> &'static str { self.method } @@ -319,7 +319,7 @@ impl RequestType { /// Returns true if the BlockId has been set to a tag value such as "latest", "earliest", or /// "pending". - fn has_block_tag(&self) -> bool { + const fn has_block_tag(&self) -> bool { if let Some(block_id) = self.block_id { match block_id { BlockId::Hash(_) => return false, From f8d1ab6095265de9a9e85540191f16035b471b5d Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:04:49 +0530 Subject: [PATCH 30/48] get_accounts --- crates/provider/src/layers/cache.rs | 55 ++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 96a8df19e20..6289b4fab14 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -3,6 +3,7 @@ use alloy_eips::BlockId; use alloy_json_rpc::{RpcError, RpcParam}; use alloy_network::Ethereum; use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256}; +use alloy_rpc_client::NoParams; use alloy_rpc_types_eth::{ Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, }; @@ -226,12 +227,48 @@ where self.inner.root() } + /// Gets the accounts in the remote node. This is usually empty unless you're using a local + /// node. + fn get_accounts(&self) -> ProviderCall> { + let req = RequestType::new("eth_accounts", NoParams::default()); + + let hash = req.params_hash().ok(); + + if let Some(hash) = hash { + if let Some(cached) = self.get(&hash).unwrap() { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + let client = self + .inner + .weak_client() + .upgrade() + .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); + let cache = self.cache.clone(); + + ProviderCall::BoxedFuture(Box::pin(async move { + let client = client?; + + let res = client.request_noparams(req.method()).await?; + + let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; + let hash = req.params_hash()?; + let mut cache = cache.write(); + let _ = cache.put(hash, json_str); + Ok(res) + })) + } + async fn get_block_by_number( &self, number: BlockNumberOrTag, hydrate: bool, ) -> TransportResult> { - // let hash = RequestType::BlockByNumber((number, hydrate)).params_hash()?; let hash = RequestType::new("eth_getBlockByNumber", (number, hydrate)); cache_get_or_fetch!(self, hash, self.inner.get_block_by_number(number, hydrate)) @@ -359,6 +396,22 @@ mod tests { use super::*; + #[tokio::test] + async fn test_get_accounts() { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + + let path = PathBuf::from_str("./rpc-cache-accounts.txt").unwrap(); + provider.load_cache(path.clone()).unwrap(); + + let accounts = provider.get_accounts().await.unwrap(); + let accounts2 = provider.get_accounts().await.unwrap(); + assert_eq!(accounts, accounts2); + + provider.save_cache(path).unwrap(); + } + #[tokio::test] async fn test_cache_provider() { let cache = CacheLayer::new(100); From d8d4094d9563af27ba3c58644ac619293bb4c784 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:08:10 +0530 Subject: [PATCH 31/48] chain_id --- crates/provider/src/layers/cache.rs | 61 ++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 6289b4fab14..127b6825ce2 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,8 +1,10 @@ -use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; +use crate::{ + utils, ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock, +}; use alloy_eips::BlockId; use alloy_json_rpc::{RpcError, RpcParam}; use alloy_network::Ethereum; -use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256}; +use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256, U64}; use alloy_rpc_client::NoParams; use alloy_rpc_types_eth::{ Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, @@ -264,6 +266,45 @@ where })) } + /// Gets the chain ID. + fn get_chain_id(&self) -> ProviderCall { + let req = RequestType::new("eth_chainId", NoParams::default()); + + let hash = req.params_hash().ok(); + + if let Some(hash) = hash { + if let Some(cached) = self.get(&hash).unwrap() { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + let client = self + .inner + .weak_client() + .upgrade() + .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); + let cache = self.cache.clone(); + + ProviderCall::BoxedFuture(Box::pin(async move { + let client = client?; + + let res = client + .request_noparams(req.method()) + .map_resp(utils::convert_u64 as fn(U64) -> u64) + .await?; + + let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; + let hash = req.params_hash()?; + let mut cache = cache.write(); + let _ = cache.put(hash, json_str); + Ok(res) + })) + } + async fn get_block_by_number( &self, number: BlockNumberOrTag, @@ -412,6 +453,22 @@ mod tests { provider.save_cache(path).unwrap(); } + #[tokio::test] + async fn test_get_chain_id() { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + + let path = PathBuf::from_str("./rpc-cache-chain-id.txt").unwrap(); + provider.load_cache(path.clone()).unwrap(); + + let chain_id = provider.get_chain_id().await.unwrap(); + let chain_id2 = provider.get_chain_id().await.unwrap(); + assert_eq!(chain_id, chain_id2); + + provider.save_cache(path).unwrap(); + } + #[tokio::test] async fn test_cache_provider() { let cache = CacheLayer::new(100); From d67142c4b9d0e625859eb825a0e0ba332afbfefd Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:09:04 +0530 Subject: [PATCH 32/48] cfg gate wasm --- crates/provider/Cargo.toml | 4 +++- crates/provider/src/layers/cache.rs | 25 ------------------------- crates/provider/src/layers/mod.rs | 2 ++ 3 files changed, 5 insertions(+), 26 deletions(-) diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index def4a18ff7e..651d7ef1778 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -51,7 +51,6 @@ dashmap = "6.0" futures-utils-wasm.workspace = true futures.workspace = true lru = "0.12" -parking_lot.workspace = true pin-project.workspace = true reqwest = { workspace = true, optional = true } serde_json.workspace = true @@ -61,6 +60,9 @@ tokio = { workspace = true, features = ["sync", "macros"] } tracing.workspace = true url = { workspace = true, optional = true } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +parking_lot = { workspace = true } + [dev-dependencies] alloy-primitives = { workspace = true, features = ["rand"] } alloy-node-bindings.workspace = true diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 127b6825ce2..88e83db8f59 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -22,31 +22,6 @@ use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf, /// /// One can load the cache from the file system by calling `load_cache` and save the cache to the /// file system by calling `save_cache`. -/// -/// Example usage: -/// ``` -/// use alloy_node_bindings::Anvil; -/// use alloy_provider::{ProviderBuilder, Provider}; -/// use alloy_provider::layers::CacheLayer; -/// use std::path::PathBuf; -/// use std::str::FromStr; -/// -/// #[tokio::main] -/// async fn main() { -/// let cache = CacheLayer::new(100); -/// let anvil = Anvil::new().block_time_f64(0.3).spawn(); -/// let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); -/// let path = PathBuf::from_str("./rpc-cache.txt").unwrap(); -/// provider.load_cache(path.clone()).unwrap(); // Load cache from file if it exists. -/// -/// let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from RPC and saved to in-memory cache -/// -/// let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); // Fetched from in-memory cache -/// assert_eq!(blk, blk2); -/// -/// provider.save_cache(path).unwrap(); // Save cache to file -/// } -/// ``` #[derive(Debug, Clone)] pub struct CacheLayer { config: CacheConfig, diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index 92e5d1d0714..86b43c9d999 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -10,5 +10,7 @@ pub use anvil::{AnvilLayer, AnvilProvider}; mod chain; pub use chain::ChainLayer; +#[cfg(not(target_arch = "wasm32"))] mod cache; +#[cfg(not(target_arch = "wasm32"))] pub use cache::{CacheConfig, CacheLayer, CacheProvider}; From 2545bb02d83200df9cb507bf68571299f507f3c3 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:05:02 +0530 Subject: [PATCH 33/48] rm get_accounts and get_chain_id --- crates/provider/src/layers/cache.rs | 76 ----------------------------- 1 file changed, 76 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 88e83db8f59..e38c4d880b0 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -204,82 +204,6 @@ where self.inner.root() } - /// Gets the accounts in the remote node. This is usually empty unless you're using a local - /// node. - fn get_accounts(&self) -> ProviderCall> { - let req = RequestType::new("eth_accounts", NoParams::default()); - - let hash = req.params_hash().ok(); - - if let Some(hash) = hash { - if let Some(cached) = self.get(&hash).unwrap() { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); - } - } - - let client = self - .inner - .weak_client() - .upgrade() - .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); - let cache = self.cache.clone(); - - ProviderCall::BoxedFuture(Box::pin(async move { - let client = client?; - - let res = client.request_noparams(req.method()).await?; - - let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; - let hash = req.params_hash()?; - let mut cache = cache.write(); - let _ = cache.put(hash, json_str); - Ok(res) - })) - } - - /// Gets the chain ID. - fn get_chain_id(&self) -> ProviderCall { - let req = RequestType::new("eth_chainId", NoParams::default()); - - let hash = req.params_hash().ok(); - - if let Some(hash) = hash { - if let Some(cached) = self.get(&hash).unwrap() { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); - } - } - - let client = self - .inner - .weak_client() - .upgrade() - .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); - let cache = self.cache.clone(); - - ProviderCall::BoxedFuture(Box::pin(async move { - let client = client?; - - let res = client - .request_noparams(req.method()) - .map_resp(utils::convert_u64 as fn(U64) -> u64) - .await?; - - let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; - let hash = req.params_hash()?; - let mut cache = cache.write(); - let _ = cache.put(hash, json_str); - Ok(res) - })) - } - async fn get_block_by_number( &self, number: BlockNumberOrTag, From 3214f6362d75d023b9b81f8a86929bfdd9e7f707 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:05:32 +0530 Subject: [PATCH 34/48] rm related tests --- crates/provider/src/layers/cache.rs | 32 ----------------------------- 1 file changed, 32 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index e38c4d880b0..6c6ae810e67 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -336,38 +336,6 @@ mod tests { use super::*; - #[tokio::test] - async fn test_get_accounts() { - let cache = CacheLayer::new(100); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - - let path = PathBuf::from_str("./rpc-cache-accounts.txt").unwrap(); - provider.load_cache(path.clone()).unwrap(); - - let accounts = provider.get_accounts().await.unwrap(); - let accounts2 = provider.get_accounts().await.unwrap(); - assert_eq!(accounts, accounts2); - - provider.save_cache(path).unwrap(); - } - - #[tokio::test] - async fn test_get_chain_id() { - let cache = CacheLayer::new(100); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - - let path = PathBuf::from_str("./rpc-cache-chain-id.txt").unwrap(); - provider.load_cache(path.clone()).unwrap(); - - let chain_id = provider.get_chain_id().await.unwrap(); - let chain_id2 = provider.get_chain_id().await.unwrap(); - assert_eq!(chain_id, chain_id2); - - provider.save_cache(path).unwrap(); - } - #[tokio::test] async fn test_cache_provider() { let cache = CacheLayer::new(100); From dcfc38a1d136938023728e03be11c11caeb62268 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:21:15 +0530 Subject: [PATCH 35/48] tests: run_with_temp_dir --- crates/provider/src/layers/cache.rs | 124 ++++++++++++---------------- 1 file changed, 52 insertions(+), 72 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 6c6ae810e67..7f31f7e4702 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -326,106 +326,86 @@ pub struct CacheConfig { #[cfg(test)] mod tests { - use std::str::FromStr; - use crate::ProviderBuilder; use alloy_network::TransactionBuilder; - use alloy_node_bindings::Anvil; + use alloy_node_bindings::{utils::run_with_tempdir, Anvil}; use alloy_primitives::{Bytes, FixedBytes}; use alloy_rpc_types_eth::{BlockId, TransactionRequest}; use super::*; - #[tokio::test] - async fn test_cache_provider() { - let cache = CacheLayer::new(100); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - - let path = PathBuf::from_str("./rpc-cache-block-by-number.txt").unwrap(); - provider.load_cache(path.clone()).unwrap(); - - let blk = provider.get_block_by_number(0.into(), true).await.unwrap(); - let blk2 = provider.get_block_by_number(0.into(), true).await.unwrap(); - assert_eq!(blk, blk2); - - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - - let latest_block_num = provider.get_block_number().await.unwrap(); - let blk3 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); - let blk4 = provider.get_block_by_number(latest_block_num.into(), true).await.unwrap(); - assert_eq!(blk3, blk4); - - provider.save_cache(path).unwrap(); - } - #[tokio::test] async fn test_get_block() { - let cache = CacheLayer::new(100); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + run_with_tempdir("get-block", |dir| async move { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - let path = PathBuf::from_str("./rpc-cache-block-by-hash.txt").unwrap(); - provider.load_cache(path.clone()).unwrap(); + let path = dir.join("rpc-cache-block.txt"); + provider.load_cache(path.clone()).unwrap(); - let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. - let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. - assert_eq!(block, block2); + let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block, block2); - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - let latest_block = - provider.get_block(BlockId::latest(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. - let latest_hash = latest_block.unwrap().header.hash; + let latest_block = + provider.get_block(BlockId::latest(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let latest_hash = latest_block.unwrap().header.hash; - let block3 = - provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. - let block4 = - provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. - assert_eq!(block3, block4); + let block3 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block4 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block3, block4); - provider.save_cache(path).unwrap(); + provider.save_cache(path).unwrap(); + }) + .await; } #[tokio::test] async fn test_get_proof() { - let cache = CacheLayer::new(100); - let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + run_with_tempdir("get-proof", |dir| async move { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); - let from = anvil.addresses()[0]; - let path = PathBuf::from_str("./rpc-cache-proof.txt").unwrap(); + let from = anvil.addresses()[0]; + let path = dir.join("rpc-cache-proof.txt"); - provider.load_cache(path.clone()).unwrap(); + provider.load_cache(path.clone()).unwrap(); - let calldata: Bytes = "0x6080604052348015600f57600080fd5b506101f28061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80633fb5c1cb146100465780638381f58a14610062578063d09de08a14610080575b600080fd5b610060600480360381019061005b91906100ee565b61008a565b005b61006a610094565b604051610077919061012a565b60405180910390f35b61008861009a565b005b8060008190555050565b60005481565b6000808154809291906100ac90610174565b9190505550565b600080fd5b6000819050919050565b6100cb816100b8565b81146100d657600080fd5b50565b6000813590506100e8816100c2565b92915050565b600060208284031215610104576101036100b3565b5b6000610112848285016100d9565b91505092915050565b610124816100b8565b82525050565b600060208201905061013f600083018461011b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017f826100b8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101b1576101b0610145565b5b60018201905091905056fea264697066735822122067ac0f21f648b0cacd1b7260772852ad4a0f63e2cc174168c51a6887fd5197a964736f6c634300081a0033".parse().unwrap(); + let calldata: Bytes = "0x6080604052348015600f57600080fd5b506101f28061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80633fb5c1cb146100465780638381f58a14610062578063d09de08a14610080575b600080fd5b610060600480360381019061005b91906100ee565b61008a565b005b61006a610094565b604051610077919061012a565b60405180910390f35b61008861009a565b005b8060008190555050565b60005481565b6000808154809291906100ac90610174565b9190505550565b600080fd5b6000819050919050565b6100cb816100b8565b81146100d657600080fd5b50565b6000813590506100e8816100c2565b92915050565b600060208284031215610104576101036100b3565b5b6000610112848285016100d9565b91505092915050565b610124816100b8565b82525050565b600060208201905061013f600083018461011b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017f826100b8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101b1576101b0610145565b5b60018201905091905056fea264697066735822122067ac0f21f648b0cacd1b7260772852ad4a0f63e2cc174168c51a6887fd5197a964736f6c634300081a0033".parse().unwrap(); - let tx = TransactionRequest::default() - .with_from(from) - .with_input(calldata) - .with_max_fee_per_gas(1_000_000_000) - .with_max_priority_fee_per_gas(1_000_000) - .with_gas_limit(1_000_000) - .with_nonce(0); + let tx = TransactionRequest::default() + .with_from(from) + .with_input(calldata) + .with_max_fee_per_gas(1_000_000_000) + .with_max_priority_fee_per_gas(1_000_000) + .with_gas_limit(1_000_000) + .with_nonce(0); - let tx_receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + let tx_receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); - let counter_addr = tx_receipt.contract_address.unwrap(); + let counter_addr = tx_receipt.contract_address.unwrap(); - let keys = vec![ - FixedBytes::with_last_byte(0), - FixedBytes::with_last_byte(0x1), - FixedBytes::with_last_byte(0x2), - FixedBytes::with_last_byte(0x3), - FixedBytes::with_last_byte(0x4), - ]; + let keys = vec![ + FixedBytes::with_last_byte(0), + FixedBytes::with_last_byte(0x1), + FixedBytes::with_last_byte(0x2), + FixedBytes::with_last_byte(0x3), + FixedBytes::with_last_byte(0x4), + ]; - let proof = - provider.get_proof(counter_addr, keys.clone()).block_id(1.into()).await.unwrap(); - let proof2 = provider.get_proof(counter_addr, keys).block_id(1.into()).await.unwrap(); + let proof = + provider.get_proof(counter_addr, keys.clone()).block_id(1.into()).await.unwrap(); + let proof2 = provider.get_proof(counter_addr, keys).block_id(1.into()).await.unwrap(); - assert_eq!(proof, proof2); + assert_eq!(proof, proof2); - provider.save_cache(path).unwrap(); + provider.save_cache(path).unwrap(); + }).await; } } From 0c88fe9a5747b406fe30ce2c6bb44512b4cccd2e Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:32:36 +0530 Subject: [PATCH 36/48] feat: SharedCache --- crates/provider/src/layers/cache.rs | 67 ++++++++++++++++++----------- crates/provider/src/layers/mod.rs | 2 +- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 7f31f7e4702..6460759071b 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,11 +1,8 @@ -use crate::{ - utils, ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock, -}; +use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; use alloy_eips::BlockId; use alloy_json_rpc::{RpcError, RpcParam}; use alloy_network::Ethereum; -use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256, U64}; -use alloy_rpc_client::NoParams; +use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256}; use alloy_rpc_types_eth::{ Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, }; @@ -60,7 +57,7 @@ pub struct CacheProvider { /// Inner provider. inner: P, /// In-memory LRU cache, mapping requests to responses. - cache: Arc>>, + cache: SharedCache, /// Phantom data _pd: PhantomData, } @@ -72,26 +69,10 @@ where { /// Instantiate a new cache provider. pub fn new(inner: P, max_items: usize) -> Self { - let cache = Arc::new(RwLock::new(LruCache::::new( - NonZeroUsize::new(max_items).unwrap(), - ))); + let cache = SharedCache::new(max_items); Self { inner, cache, _pd: PhantomData } } - /// Puts a value into the cache, and returns the old value if it existed. - pub fn put(&self, key: B256, value: String) -> TransportResult> { - let mut cache = self.cache.write(); - Ok(cache.put(key, value)) - } - - /// Gets a value from the cache, if it exists. - pub fn get(&self, key: &B256) -> TransportResult> { - // Need to acquire a write guard to change the order of keys in LRU cache. - let mut cache = self.cache.write(); - let val = cache.get(key).cloned(); - Ok(val) - } - /// Saves the cache to a file specified by the path. /// If the files does not exist, it creates one. /// If the file exists, it overwrites it. @@ -130,7 +111,7 @@ where macro_rules! cache_get_or_fetch { ($self:expr, $req:expr, $fetch_fn:expr) => {{ let hash = $req.params_hash()?; - if let Some(cached) = $self.get(&hash)? { + if let Some(cached) = $self.cache.get(&hash)? { let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; return Ok(Some(result)); } @@ -138,7 +119,7 @@ macro_rules! cache_get_or_fetch { let result = $fetch_fn.await?; if let Some(ref data) = result { let json_str = serde_json::to_string(data).map_err(TransportErrorKind::custom)?; - let _ = $self.put(hash, json_str)?; + let _ = $self.cache.put(hash, json_str)?; } Ok(result) @@ -324,6 +305,42 @@ pub struct CacheConfig { pub max_items: usize, } +/// Shareable cache. +#[derive(Debug, Clone)] +pub struct SharedCache { + inner: Arc>>, +} + +impl SharedCache { + /// Instantiate a new shared cache. + pub fn new(max_items: usize) -> Self { + let cache = Arc::new(RwLock::new(LruCache::::new( + NonZeroUsize::new(max_items).unwrap(), + ))); + Self { inner: cache } + } + + /// Puts a value into the cache, and returns the old value if it existed. + pub fn put(&self, key: B256, value: String) -> TransportResult> { + Ok(self.inner.write().put(key, value)) + } + + /// Gets a value from the cache, if it exists. + pub fn get(&self, key: &B256) -> TransportResult> { + // Need to acquire a write guard to change the order of keys in LRU cache. + let val = self.inner.write().get(key).cloned(); + Ok(val) + } + + fn read(&self) -> parking_lot::RwLockReadGuard<'_, LruCache> { + self.inner.read() + } + + fn write(&self) -> parking_lot::RwLockWriteGuard<'_, LruCache> { + self.inner.write() + } +} + #[cfg(test)] mod tests { use crate::ProviderBuilder; diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index 86b43c9d999..c704b6737e0 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -13,4 +13,4 @@ pub use chain::ChainLayer; #[cfg(not(target_arch = "wasm32"))] mod cache; #[cfg(not(target_arch = "wasm32"))] -pub use cache::{CacheConfig, CacheLayer, CacheProvider}; +pub use cache::{CacheConfig, CacheLayer, CacheProvider, SharedCache}; From d1c5e0943ed956b204d5c9a2c46e19c83fd9e2c9 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 19:40:02 +0530 Subject: [PATCH 37/48] make CacheProvider generic over Network --- crates/provider/src/layers/cache.rs | 74 ++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 6460759071b..c34bf68b456 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,8 +1,10 @@ use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; use alloy_eips::BlockId; use alloy_json_rpc::{RpcError, RpcParam}; -use alloy_network::Ethereum; -use alloy_primitives::{keccak256, Address, BlockHash, StorageKey, StorageValue, B256, U256}; +use alloy_network::{Ethereum, Network}; +use alloy_primitives::{ + keccak256, Address, BlockHash, StorageKey, StorageValue, TxHash, B256, U256, +}; use alloy_rpc_types_eth::{ Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, }; @@ -39,12 +41,13 @@ impl CacheLayer { } } -impl ProviderLayer for CacheLayer +impl ProviderLayer for CacheLayer where - P: Provider, + P: Provider, T: Transport + Clone, + N: Network, { - type Provider = CacheProvider; + type Provider = CacheProvider; fn layer(&self, inner: P) -> Self::Provider { CacheProvider::new(inner, self.max_items()) @@ -53,19 +56,20 @@ where /// A provider that caches responses to RPC requests. #[derive(Debug, Clone)] -pub struct CacheProvider { +pub struct CacheProvider { /// Inner provider. inner: P, /// In-memory LRU cache, mapping requests to responses. cache: SharedCache, /// Phantom data - _pd: PhantomData, + _pd: PhantomData<(T, N)>, } -impl CacheProvider +impl CacheProvider where - P: Provider, + P: Provider, T: Transport + Clone, + N: Network, { /// Instantiate a new cache provider. pub fn new(inner: P, max_items: usize) -> Self { @@ -143,7 +147,6 @@ macro_rules! rpc_prov_call { // Insert into cache. let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; let hash = $req.params_hash()?; - let mut cache = cache.write(); let _ = cache.put(hash, json_str); Ok(res) @@ -175,13 +178,14 @@ macro_rules! cache_rpc_call_with_block { #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl Provider for CacheProvider +impl Provider for CacheProvider where - P: Provider, + P: Provider, T: Transport + Clone, + N: Network, { #[inline(always)] - fn root(&self) -> &RootProvider { + fn root(&self) -> &RootProvider { self.inner.root() } @@ -189,7 +193,7 @@ where &self, number: BlockNumberOrTag, hydrate: bool, - ) -> TransportResult> { + ) -> TransportResult> { let hash = RequestType::new("eth_getBlockByNumber", (number, hydrate)); cache_get_or_fetch!(self, hash, self.inner.get_block_by_number(number, hydrate)) @@ -200,7 +204,7 @@ where &self, hash: BlockHash, kind: BlockTransactionsKind, - ) -> TransportResult> { + ) -> TransportResult> { let full = match kind { BlockTransactionsKind::Full => true, BlockTransactionsKind::Hashes => false, @@ -344,7 +348,7 @@ impl SharedCache { #[cfg(test)] mod tests { use crate::ProviderBuilder; - use alloy_network::TransactionBuilder; + use alloy_network::{AnyNetwork, TransactionBuilder}; use alloy_node_bindings::{utils::run_with_tempdir, Anvil}; use alloy_primitives::{Bytes, FixedBytes}; use alloy_rpc_types_eth::{BlockId, TransactionRequest}; @@ -356,7 +360,41 @@ mod tests { run_with_tempdir("get-block", |dir| async move { let cache = CacheLayer::new(100); let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + + let path = dir.join("rpc-cache-block.txt"); + provider.load_cache(path.clone()).unwrap(); + + let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block, block2); + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + let latest_block = + provider.get_block(BlockId::latest(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let latest_hash = latest_block.unwrap().header.hash; + + let block3 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. + let block4 = + provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. + assert_eq!(block3, block4); + + provider.save_cache(path).unwrap(); + }) + .await; + } + + #[tokio::test] + async fn test_get_block_any_network() { + run_with_tempdir("get-block", |dir| async move { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::new() + .network::() + .layer(cache) + .on_http(anvil.endpoint_url()); let path = dir.join("rpc-cache-block.txt"); provider.load_cache(path.clone()).unwrap(); @@ -387,7 +425,7 @@ mod tests { run_with_tempdir("get-proof", |dir| async move { let cache = CacheLayer::new(100); let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::default().layer(cache).on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); let from = anvil.addresses()[0]; let path = dir.join("rpc-cache-proof.txt"); From cc0c1eb410ed68a8c04985ff13afb59ffab27fda Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:20:46 +0530 Subject: [PATCH 38/48] add more methods --- crates/provider/src/layers/cache.rs | 315 +++++++++++++++++++++++++++- 1 file changed, 305 insertions(+), 10 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index c34bf68b456..27f745d04eb 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -1,12 +1,14 @@ use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock}; use alloy_eips::BlockId; use alloy_json_rpc::{RpcError, RpcParam}; -use alloy_network::{Ethereum, Network}; +use alloy_network::Network; +use alloy_rpc_types_eth::{Filter, Log}; use alloy_primitives::{ - keccak256, Address, BlockHash, StorageKey, StorageValue, TxHash, B256, U256, + keccak256, Address, BlockHash, StorageKey, StorageValue, TxHash, B256, U256,Bytes }; +use alloy_primitives::U64; use alloy_rpc_types_eth::{ - Block, BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, + BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, }; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; @@ -130,12 +132,14 @@ macro_rules! cache_get_or_fetch { }}; } -macro_rules! rpc_prov_call { +macro_rules! rpc_call_with_block { ($cache:expr, $client:expr, $req:expr) => {{ let client = $client.upgrade().ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); let cache = $cache.clone(); ProviderCall::BoxedFuture(Box::pin(async move { + println!("Cache miss"); + let client = client?; let result = client.request($req.method(), $req.params()).map_params(|params| { @@ -157,13 +161,14 @@ macro_rules! rpc_prov_call { macro_rules! cache_rpc_call_with_block { ($cache:expr, $client:expr, $req:expr) => {{ if $req.has_block_tag() { - return rpc_prov_call!($cache, $client, $req); + return rpc_call_with_block!($cache, $client, $req); } let hash = $req.params_hash().ok(); if let Some(hash) = hash { if let Some(cached) = $cache.write().get(&hash) { + println!("Cache hit"); let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); return ProviderCall::BoxedFuture(Box::pin(async move { let res = result?; @@ -172,7 +177,7 @@ macro_rules! cache_rpc_call_with_block { } } - rpc_prov_call!($cache, $client, $req) + rpc_call_with_block!($cache, $client, $req) }}; } @@ -215,6 +220,45 @@ where cache_get_or_fetch!(self, req_hash, self.inner.get_block_by_hash(hash, kind)) } + fn get_block_receipts( + &self, + block: BlockId, + ) -> ProviderCall>> { + let req = RequestType::new("eth_getBlockReceipts", (block,)); + + // TODO: Redirect to RPC if BlockId is a tag. + + let params_hash = req.params_hash().ok(); + + if let Some(hash) = params_hash { + if let Ok(Some(cached)) = self.cache.get(&hash) { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + + ProviderCall::BoxedFuture(Box::pin(async move { + let client = client + .upgrade() + .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?; + + let result = client.request(req.method(), req.params()).await?; + + let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?; + let hash = req.params_hash()?; + let _ = cache.put(hash, json_str); + + Ok(result) + })) + } + + /// Get the account and storage values of the specified account including the merkle proofs. /// /// This call can be used to verify that the data has not been tampered with. @@ -245,6 +289,156 @@ where cache_rpc_call_with_block!(cache, client, req) }) } + + fn get_code_at(&self, address: Address) -> RpcWithBlock { + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + RpcWithBlock::new_provider(move |block_id| { + let req = RequestType::new("eth_getCode", address).with_block_id(block_id); + cache_rpc_call_with_block!(cache, client, req) + }) + } + + fn get_transaction_count( + &self, + address: Address, + ) -> RpcWithBlock { + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + RpcWithBlock::new_provider(move |block_id| { + let req = RequestType::new("eth_getTransactionCount", address).with_block_id(block_id); + + cache_rpc_call_with_block!(cache, client, req) + }) + } + + + async fn get_logs(&self, filter: &Filter) -> TransportResult> { + let req = RequestType::new("eth_getLogs", filter.clone()); + + let params_hash = req.params_hash().ok(); + + if let Some(hash) = params_hash { + if let Ok(Some(cached)) = self.cache.get(&hash) { + println!("Cache hit"); + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return result; + } + } + + println!("Cache miss"); + let result = self.inner.get_logs(filter).await?; + + let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?; + + let hash = req.params_hash()?; + let _ = self.cache.put(hash, json_str); + + Ok(result) + } + + fn get_transaction_by_hash( + &self, + hash: TxHash, + ) -> ProviderCall> { + let req = RequestType::new("eth_getTransactionByHash", (hash,)); + + let params_hash = req.params_hash().ok(); + + if let Some(hash) = params_hash { + if let Ok(Some(cached)) = self.cache.get(&hash) { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + ProviderCall::BoxedFuture(Box::pin(async move { + let client = client + .upgrade() + .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?; + let result = client.request(req.method(), req.params()).await?; + + let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?; + let hash = req.params_hash()?; + let _ = cache.put(hash, json_str); + + Ok(result) + })) + } + + fn get_raw_transaction_by_hash( + &self, + hash: TxHash, + ) -> ProviderCall> { + let req = RequestType::new("eth_getRawTransactionByHash", (hash,)); + + let params_hash = req.params_hash().ok(); + + if let Some(hash) = params_hash { + if let Ok(Some(cached)) = self.cache.get(&hash) { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + ProviderCall::BoxedFuture(Box::pin(async move { + let client = client + .upgrade() + .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?; + + let result = client.request(req.method(), req.params()).await?; + + let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?; + let hash = req.params_hash()?; + let _ = cache.put(hash, json_str); + + Ok(result) + })) + } + + fn get_transaction_receipt( + &self, + hash: TxHash, + ) -> ProviderCall> { + let req = RequestType::new("eth_getTransactionReceipt", (hash,)); + + let params_hash = req.params_hash().ok(); + + if let Some(hash) = params_hash { + if let Ok(Some(cached)) = self.cache.get(&hash) { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } + } + + let client = self.inner.weak_client(); + let cache = self.cache.clone(); + ProviderCall::BoxedFuture(Box::pin(async move { + let client = client + .upgrade() + .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?; + + let result = client.request(req.method(), req.params()).await?; + + let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?; + let hash = req.params_hash()?; + let _ = cache.put(hash, json_str); + + Ok(result) + })) + } } struct RequestType { @@ -264,8 +458,9 @@ impl RequestType { } fn params_hash(&self) -> TransportResult { + // Merge the method and params and hash them. let hash = serde_json::to_string(&self.params()) - .map(|p| keccak256(p.as_bytes())) + .map(|p| keccak256(format!("{}{}", self.method(), p).as_bytes())) .map_err(RpcError::ser_err)?; Ok(hash) @@ -347,14 +542,13 @@ impl SharedCache { #[cfg(test)] mod tests { + use super::*; use crate::ProviderBuilder; use alloy_network::{AnyNetwork, TransactionBuilder}; use alloy_node_bindings::{utils::run_with_tempdir, Anvil}; - use alloy_primitives::{Bytes, FixedBytes}; + use alloy_primitives::{bytes, hex, Bytes, FixedBytes}; use alloy_rpc_types_eth::{BlockId, TransactionRequest}; - use super::*; - #[tokio::test] async fn test_get_block() { run_with_tempdir("get-block", |dir| async move { @@ -463,4 +657,105 @@ mod tests { provider.save_cache(path).unwrap(); }).await; } + + #[tokio::test] + async fn test_get_tx_by_hash_and_receipt() { + run_with_tempdir("get-tx-by-hash", |dir| async move { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().block_time_f64(0.3).spawn(); + let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + + let path = dir.join("rpc-cache-tx.txt"); + provider.load_cache(path.clone()).unwrap(); + + let req = TransactionRequest::default() + .from(anvil.addresses()[0]) + .to(Address::repeat_byte(5)) + .value(U256::ZERO) + .input(bytes!("deadbeef").into()); + + let tx_hash = + *provider.send_transaction(req).await.expect("failed to send tx").tx_hash(); + + let tx = provider.get_transaction_by_hash(tx_hash).await.unwrap(); // Received from RPC. + let tx2 = provider.get_transaction_by_hash(tx_hash).await.unwrap(); // Received from cache. + assert_eq!(tx, tx2); + + let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap(); // Received from RPC. + let receipt2 = provider.get_transaction_receipt(tx_hash).await.unwrap(); // Received from cache. + + assert_eq!(receipt, receipt2); + + provider.save_cache(path).unwrap(); + }) + .await; + } + + #[tokio::test] + async fn test_block_receipts() { + run_with_tempdir("get-block-receipts", |dir| async move { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().spawn(); + let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + + let path = dir.join("rpc-cache-block-receipts.txt"); + provider.load_cache(path.clone()).unwrap(); + + // Send txs + + let receipt = provider + .send_raw_transaction( + // Transfer 1 ETH from default EOA address to the Genesis address. + bytes!("f865808477359400825208940000000000000000000000000000000000000000018082f4f5a00505e227c1c636c76fac55795db1a40a4d24840d81b40d2fe0cc85767f6bd202a01e91b437099a8a90234ac5af3cb7ca4fb1432e133f75f9a91678eaf5f487c74b").as_ref() + ) + .await.unwrap().get_receipt().await.unwrap(); + + let block_number = receipt.block_number.unwrap(); + + let receipts = + provider.get_block_receipts(block_number.into()).await.unwrap(); // Received from RPC. + let receipts2 = + provider.get_block_receipts(block_number.into()).await.unwrap(); // Received from cache. + assert_eq!(receipts, receipts2); + + assert!(receipts.is_some_and(|r| r[0] == receipt)); + + provider.save_cache(path).unwrap(); + }) + .await + } + + #[tokio::test] + async fn test_get_code() { + run_with_tempdir("get-code", |dir| async move { + let cache = CacheLayer::new(100); + let anvil = Anvil::new().spawn(); + let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + + let path = dir.join("rpc-cache-code.txt"); + provider.load_cache(path.clone()).unwrap(); + + let bytecode = hex::decode( + // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin + "6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033" + ).unwrap(); + + + let tx = TransactionRequest::default().with_deploy_code(bytecode); + + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + let counter_addr = receipt.contract_address.unwrap(); + + let block_id = BlockId::number(receipt.block_number.unwrap()); + + let code = provider.get_code_at(counter_addr).block_id(block_id).await.unwrap(); // Received from RPC. + let code2 = provider.get_code_at(counter_addr).block_id(block_id).await.unwrap(); // Received from cache. + assert_eq!(code, code2); + + provider.save_cache(path).unwrap(); + }) + .await; + } + } From ce08a8851942269debf8bd374bfcee831894e423 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:28:33 +0530 Subject: [PATCH 39/48] fmt --- crates/provider/src/layers/cache.rs | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 27f745d04eb..cdbbada03a9 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -2,13 +2,11 @@ use crate::{ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider use alloy_eips::BlockId; use alloy_json_rpc::{RpcError, RpcParam}; use alloy_network::Network; -use alloy_rpc_types_eth::{Filter, Log}; use alloy_primitives::{ - keccak256, Address, BlockHash, StorageKey, StorageValue, TxHash, B256, U256,Bytes + keccak256, Address, BlockHash, Bytes, StorageKey, StorageValue, TxHash, B256, U256, U64, }; -use alloy_primitives::U64; use alloy_rpc_types_eth::{ - BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, + BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, Filter, Log, }; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; use lru::LruCache; @@ -132,14 +130,13 @@ macro_rules! cache_get_or_fetch { }}; } +/// macro_rules! rpc_call_with_block { ($cache:expr, $client:expr, $req:expr) => {{ let client = $client.upgrade().ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped")); let cache = $cache.clone(); ProviderCall::BoxedFuture(Box::pin(async move { - println!("Cache miss"); - let client = client?; let result = client.request($req.method(), $req.params()).map_params(|params| { @@ -147,7 +144,6 @@ macro_rules! rpc_call_with_block { }); let res = result.await?; - // Insert into cache. let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?; let hash = $req.params_hash()?; @@ -168,7 +164,6 @@ macro_rules! cache_rpc_call_with_block { if let Some(hash) = hash { if let Some(cached) = $cache.write().get(&hash) { - println!("Cache hit"); let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); return ProviderCall::BoxedFuture(Box::pin(async move { let res = result?; @@ -258,7 +253,6 @@ where })) } - /// Get the account and storage values of the specified account including the merkle proofs. /// /// This call can be used to verify that the data has not been tampered with. @@ -299,10 +293,7 @@ where }) } - fn get_transaction_count( - &self, - address: Address, - ) -> RpcWithBlock { + fn get_transaction_count(&self, address: Address) -> RpcWithBlock { let client = self.inner.weak_client(); let cache = self.cache.clone(); RpcWithBlock::new_provider(move |block_id| { @@ -312,7 +303,6 @@ where }) } - async fn get_logs(&self, filter: &Filter) -> TransportResult> { let req = RequestType::new("eth_getLogs", filter.clone()); @@ -320,13 +310,11 @@ where if let Some(hash) = params_hash { if let Ok(Some(cached)) = self.cache.get(&hash) { - println!("Cache hit"); let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); return result; } } - println!("Cache miss"); let result = self.inner.get_logs(filter).await?; let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?; @@ -441,6 +429,7 @@ where } } +/// Internal type to handle different types of requests and generating their param hashes. struct RequestType { method: &'static str, params: Params, @@ -458,7 +447,7 @@ impl RequestType { } fn params_hash(&self) -> TransportResult { - // Merge the method and params and hash them. + // Merge the method + params and hash them. let hash = serde_json::to_string(&self.params()) .map(|p| keccak256(format!("{}{}", self.method(), p).as_bytes())) .map_err(RpcError::ser_err)?; @@ -739,8 +728,6 @@ mod tests { // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin "6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033" ).unwrap(); - - let tx = TransactionRequest::default().with_deploy_code(bytecode); let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); @@ -757,5 +744,4 @@ mod tests { }) .await; } - } From ccb67c1fa1fab506c333b59ce48d0bf25789fa6e Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:34:42 +0530 Subject: [PATCH 40/48] docs --- crates/provider/src/layers/cache.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index cdbbada03a9..1dfea5b6dd6 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -54,7 +54,12 @@ where } } -/// A provider that caches responses to RPC requests. +/// The [`CacheProvider`] holds the underlying in-memory LRU cache and overrides methods from the +/// [`Provider`] trait that should attempt to fetch from cache and fallback to the RPC in case of a +/// cache miss. +/// +/// Most importantly, the [`CacheProvider`] adds `save_cache` and `load_cache` methods to the +/// provider interface to lets users save cache to the disk and load from it on method. #[derive(Debug, Clone)] pub struct CacheProvider { /// Inner provider. From 55fe2ff90217135d30eb2c338a4af14cadce8a80 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:50:57 +0530 Subject: [PATCH 41/48] docs --- crates/provider/src/layers/cache.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 1dfea5b6dd6..c82b1a69362 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -117,6 +117,11 @@ where } } +/// Attempts to fetch the response from the cache by using the hash of the request params. +/// +/// In case of a cache miss, fetches from the RPC and saves the response to the cache. +/// +/// This helps overriding [`Provider`] methods that return [`TransportResult`]. macro_rules! cache_get_or_fetch { ($self:expr, $req:expr, $fetch_fn:expr) => {{ let hash = $req.params_hash()?; @@ -135,7 +140,13 @@ macro_rules! cache_get_or_fetch { }}; } +/// Uses underlying transport client to fetch data from the RPC. +/// +/// This is specific to RPC requests that require the `block_id` parameter. /// +/// Fetches from the RPC and saves the response to the cache. +/// +/// Returns a ProviderCall::BoxedFuture macro_rules! rpc_call_with_block { ($cache:expr, $client:expr, $req:expr) => {{ let client = @@ -159,6 +170,11 @@ macro_rules! rpc_call_with_block { }}; } +/// Attempts to fetch the response from the cache by using the hash of the request params. +/// +/// Fetches from the RPC in case of a cache miss +/// +/// This helps overriding [`Provider`] methods that return `RpcWithBlock`. macro_rules! cache_rpc_call_with_block { ($cache:expr, $client:expr, $req:expr) => {{ if $req.has_block_tag() { @@ -204,7 +220,6 @@ where cache_get_or_fetch!(self, hash, self.inner.get_block_by_number(number, hydrate)) } - /// Gets a block by its [BlockHash], with full transactions or only hashes. async fn get_block_by_hash( &self, hash: BlockHash, @@ -258,9 +273,6 @@ where })) } - /// Get the account and storage values of the specified account including the merkle proofs. - /// - /// This call can be used to verify that the data has not been tampered with. fn get_proof( &self, address: Address, @@ -275,7 +287,6 @@ where }) } - /// Gets the specified storage value from [Address]. fn get_storage_at( &self, address: Address, From 93d139d16a7bf94a5c0f8430d061b661414d4583 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:54:02 +0530 Subject: [PATCH 42/48] nit --- crates/provider/src/layers/cache.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index c82b1a69362..3918b689fb1 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -59,7 +59,7 @@ where /// cache miss. /// /// Most importantly, the [`CacheProvider`] adds `save_cache` and `load_cache` methods to the -/// provider interface to lets users save cache to the disk and load from it on method. +/// provider interface to lets users save cache to the disk and load from it on demand. #[derive(Debug, Clone)] pub struct CacheProvider { /// Inner provider. From dcc448137e81da781d3f06c6acc36829881ad628 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:01:14 +0530 Subject: [PATCH 43/48] fix --- crates/provider/src/layers/cache.rs | 33 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 3918b689fb1..b1ddb3691a4 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -241,17 +241,23 @@ where ) -> ProviderCall>> { let req = RequestType::new("eth_getBlockReceipts", (block,)); - // TODO: Redirect to RPC if BlockId is a tag. - - let params_hash = req.params_hash().ok(); + let redirect = match block { + BlockId::Hash(_) => false, + BlockId::Number(BlockNumberOrTag::Number(_)) => false, + _ => true, + }; - if let Some(hash) = params_hash { - if let Ok(Some(cached)) = self.cache.get(&hash) { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); + if !redirect { + let params_hash = req.params_hash().ok(); + + if let Some(hash) = params_hash { + if let Ok(Some(cached)) = self.cache.get(&hash) { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); + return ProviderCall::BoxedFuture(Box::pin(async move { + let res = result?; + Ok(res) + })); + } } } @@ -266,8 +272,11 @@ where let result = client.request(req.method(), req.params()).await?; let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?; - let hash = req.params_hash()?; - let _ = cache.put(hash, json_str); + + if !redirect { + let hash = req.params_hash()?; + let _ = cache.put(hash, json_str); + } Ok(result) })) From 60bd432c483f046a5b28bd8d87395cc18ff5094f Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:12:09 +0530 Subject: [PATCH 44/48] clippy --- crates/provider/src/layers/cache.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index b1ddb3691a4..ca83db27648 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -241,11 +241,8 @@ where ) -> ProviderCall>> { let req = RequestType::new("eth_getBlockReceipts", (block,)); - let redirect = match block { - BlockId::Hash(_) => false, - BlockId::Number(BlockNumberOrTag::Number(_)) => false, - _ => true, - }; + let redirect = + !matches!(block, BlockId::Hash(_) | BlockId::Number(BlockNumberOrTag::Number(_))); if !redirect { let params_hash = req.params_hash().ok(); From b77279fe521d56ef7da604f1979f85da9dfbb5f7 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Sat, 5 Oct 2024 14:09:57 +0530 Subject: [PATCH 45/48] mv SharedCache --- crates/provider/src/layers/cache.rs | 144 ++++++++++++++-------------- 1 file changed, 74 insertions(+), 70 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index ca83db27648..45f48db52d4 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -23,15 +23,17 @@ use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf, /// file system by calling `save_cache`. #[derive(Debug, Clone)] pub struct CacheLayer { + /// Configuration for the cache layer. config: CacheConfig, + /// In-memory LRU cache, mapping requests to responses. + cache: SharedCache, } impl CacheLayer { /// Instantiate a new cache layer with the the maximum number of /// items to store. - #[inline] - pub const fn new(max_items: usize) -> Self { - Self { config: CacheConfig { max_items } } + pub fn new(max_items: usize) -> Self { + Self { config: CacheConfig { max_items }, cache: SharedCache::new(max_items) } } /// Returns the maximum number of items that can be stored in the cache, set at initialization. @@ -39,6 +41,11 @@ impl CacheLayer { pub const fn max_items(&self) -> usize { self.config.max_items } + + /// Returns the shared cache. + pub fn cache(&self) -> SharedCache { + self.cache.clone() + } } impl ProviderLayer for CacheLayer @@ -50,7 +57,7 @@ where type Provider = CacheProvider; fn layer(&self, inner: P) -> Self::Provider { - CacheProvider::new(inner, self.max_items()) + CacheProvider::new(inner, self.cache()) } } @@ -77,44 +84,9 @@ where N: Network, { /// Instantiate a new cache provider. - pub fn new(inner: P, max_items: usize) -> Self { - let cache = SharedCache::new(max_items); + pub fn new(inner: P, cache: SharedCache) -> Self { Self { inner, cache, _pd: PhantomData } } - - /// Saves the cache to a file specified by the path. - /// If the files does not exist, it creates one. - /// If the file exists, it overwrites it. - pub fn save_cache(&self, path: PathBuf) -> TransportResult<()> { - let cache = self.cache.read(); - let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; - - // Iterate over the cache and dump to the file. - let entries = cache - .iter() - .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() }) - .collect::>(); - serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; - Ok(()) - } - - /// Loads the cache from a file specified by the path. - /// If the file does not exist, it returns without error. - pub fn load_cache(&self, path: PathBuf) -> TransportResult<()> { - if !path.exists() { - return Ok(()); - }; - let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; - let file = BufReader::new(file); - let entries: Vec = - serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; - let mut cache = self.cache.write(); - for entry in entries { - cache.put(entry.key, entry.value); - } - - Ok(()) - } } /// Attempts to fetch the response from the cache by using the hash of the request params. @@ -184,8 +156,8 @@ macro_rules! cache_rpc_call_with_block { let hash = $req.params_hash().ok(); if let Some(hash) = hash { - if let Some(cached) = $cache.write().get(&hash) { - let result = serde_json::from_str(cached).map_err(TransportErrorKind::custom); + if let Ok(Some(cached)) = $cache.get(&hash) { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); return ProviderCall::BoxedFuture(Box::pin(async move { let res = result?; Ok(res) @@ -542,12 +514,38 @@ impl SharedCache { Ok(val) } - fn read(&self) -> parking_lot::RwLockReadGuard<'_, LruCache> { - self.inner.read() + /// Saves the cache to a file specified by the path. + /// If the files does not exist, it creates one. + /// If the file exists, it overwrites it. + pub fn save_cache(&self, path: PathBuf) -> TransportResult<()> { + let cache = self.inner.read(); + let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; + + // Iterate over the cache and dump to the file. + let entries = cache + .iter() + .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() }) + .collect::>(); + serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?; + Ok(()) } - fn write(&self) -> parking_lot::RwLockWriteGuard<'_, LruCache> { - self.inner.write() + /// Loads the cache from a file specified by the path. + /// If the file does not exist, it returns without error. + pub fn load_cache(&self, path: PathBuf) -> TransportResult<()> { + if !path.exists() { + return Ok(()); + }; + let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?; + let file = BufReader::new(file); + let entries: Vec = + serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; + let mut cache = self.inner.write(); + for entry in entries { + cache.put(entry.key, entry.value); + } + + Ok(()) } } @@ -563,12 +561,13 @@ mod tests { #[tokio::test] async fn test_get_block() { run_with_tempdir("get-block", |dir| async move { - let cache = CacheLayer::new(100); + let cache_layer = CacheLayer::new(100); + let shared_cache = cache_layer.cache(); let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().layer(cache_layer).on_http(anvil.endpoint_url()); let path = dir.join("rpc-cache-block.txt"); - provider.load_cache(path.clone()).unwrap(); + shared_cache.load_cache(path.clone()).unwrap(); let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. @@ -586,7 +585,7 @@ mod tests { provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. assert_eq!(block3, block4); - provider.save_cache(path).unwrap(); + shared_cache.save_cache(path).unwrap(); }) .await; } @@ -594,15 +593,16 @@ mod tests { #[tokio::test] async fn test_get_block_any_network() { run_with_tempdir("get-block", |dir| async move { - let cache = CacheLayer::new(100); + let cache_layer = CacheLayer::new(100); + let shared_cache = cache_layer.cache(); let anvil = Anvil::new().block_time_f64(0.3).spawn(); let provider = ProviderBuilder::new() .network::() - .layer(cache) + .layer(cache_layer) .on_http(anvil.endpoint_url()); let path = dir.join("rpc-cache-block.txt"); - provider.load_cache(path.clone()).unwrap(); + shared_cache.load_cache(path.clone()).unwrap(); let block = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from RPC. let block2 = provider.get_block(0.into(), BlockTransactionsKind::Full).await.unwrap(); // Received from cache. @@ -620,7 +620,7 @@ mod tests { provider.get_block_by_hash(latest_hash, BlockTransactionsKind::Full).await.unwrap(); // Received from cache. assert_eq!(block3, block4); - provider.save_cache(path).unwrap(); + shared_cache.save_cache(path).unwrap(); }) .await; } @@ -628,14 +628,15 @@ mod tests { #[tokio::test] async fn test_get_proof() { run_with_tempdir("get-proof", |dir| async move { - let cache = CacheLayer::new(100); + let cache_layer = CacheLayer::new(100); + let shared_cache = cache_layer.cache(); let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().layer(cache_layer).on_http(anvil.endpoint_url()); let from = anvil.addresses()[0]; let path = dir.join("rpc-cache-proof.txt"); - provider.load_cache(path.clone()).unwrap(); + shared_cache.load_cache(path.clone()).unwrap(); let calldata: Bytes = "0x6080604052348015600f57600080fd5b506101f28061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80633fb5c1cb146100465780638381f58a14610062578063d09de08a14610080575b600080fd5b610060600480360381019061005b91906100ee565b61008a565b005b61006a610094565b604051610077919061012a565b60405180910390f35b61008861009a565b005b8060008190555050565b60005481565b6000808154809291906100ac90610174565b9190505550565b600080fd5b6000819050919050565b6100cb816100b8565b81146100d657600080fd5b50565b6000813590506100e8816100c2565b92915050565b600060208284031215610104576101036100b3565b5b6000610112848285016100d9565b91505092915050565b610124816100b8565b82525050565b600060208201905061013f600083018461011b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017f826100b8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101b1576101b0610145565b5b60018201905091905056fea264697066735822122067ac0f21f648b0cacd1b7260772852ad4a0f63e2cc174168c51a6887fd5197a964736f6c634300081a0033".parse().unwrap(); @@ -665,19 +666,20 @@ mod tests { assert_eq!(proof, proof2); - provider.save_cache(path).unwrap(); + shared_cache.save_cache(path).unwrap(); }).await; } #[tokio::test] async fn test_get_tx_by_hash_and_receipt() { run_with_tempdir("get-tx-by-hash", |dir| async move { - let cache = CacheLayer::new(100); + let cache_layer = CacheLayer::new(100); + let shared_cache = cache_layer.cache(); let anvil = Anvil::new().block_time_f64(0.3).spawn(); - let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().layer(cache_layer).on_http(anvil.endpoint_url()); let path = dir.join("rpc-cache-tx.txt"); - provider.load_cache(path.clone()).unwrap(); + shared_cache.load_cache(path.clone()).unwrap(); let req = TransactionRequest::default() .from(anvil.addresses()[0]) @@ -697,7 +699,7 @@ mod tests { assert_eq!(receipt, receipt2); - provider.save_cache(path).unwrap(); + shared_cache.save_cache(path).unwrap(); }) .await; } @@ -705,12 +707,13 @@ mod tests { #[tokio::test] async fn test_block_receipts() { run_with_tempdir("get-block-receipts", |dir| async move { - let cache = CacheLayer::new(100); + let cache_layer = CacheLayer::new(100); + let shared_cache = cache_layer.cache(); let anvil = Anvil::new().spawn(); - let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().layer(cache_layer).on_http(anvil.endpoint_url()); let path = dir.join("rpc-cache-block-receipts.txt"); - provider.load_cache(path.clone()).unwrap(); + shared_cache.load_cache(path.clone()).unwrap(); // Send txs @@ -731,7 +734,7 @@ mod tests { assert!(receipts.is_some_and(|r| r[0] == receipt)); - provider.save_cache(path).unwrap(); + shared_cache.save_cache(path).unwrap(); }) .await } @@ -739,12 +742,13 @@ mod tests { #[tokio::test] async fn test_get_code() { run_with_tempdir("get-code", |dir| async move { - let cache = CacheLayer::new(100); + let cache_layer = CacheLayer::new(100); + let shared_cache = cache_layer.cache(); let anvil = Anvil::new().spawn(); - let provider = ProviderBuilder::new().layer(cache).on_http(anvil.endpoint_url()); + let provider = ProviderBuilder::new().layer(cache_layer).on_http(anvil.endpoint_url()); let path = dir.join("rpc-cache-code.txt"); - provider.load_cache(path.clone()).unwrap(); + shared_cache.load_cache(path.clone()).unwrap(); let bytecode = hex::decode( // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin @@ -762,7 +766,7 @@ mod tests { let code2 = provider.get_code_at(counter_addr).block_id(block_id).await.unwrap(); // Received from cache. assert_eq!(code, code2); - provider.save_cache(path).unwrap(); + shared_cache.save_cache(path).unwrap(); }) .await; } From 8facd6fb096067b8d57e2935adbe266f890eb323 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Sat, 5 Oct 2024 16:30:56 +0530 Subject: [PATCH 46/48] use schnellru --- Cargo.toml | 1 + crates/provider/Cargo.toml | 3 ++- crates/provider/src/layers/cache.rs | 24 +++++++++++------------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a30a5a4767..c040927dbf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ semver = "1.0" thiserror = "1.0" thiserror-no-std = "2.0.2" url = "2.5" +schnellru = "0.2.3" lru = "0.12" parking_lot = "0.12.3" derive_more = { version = "1.0.0", default-features = false } diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index 56e3532f059..f2fbe59e0d7 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -51,7 +51,8 @@ auto_impl.workspace = true dashmap = "6.0" futures-utils-wasm.workspace = true futures.workspace = true -lru = "0.12" +schnellru.workspace = true +lru.workspace = true pin-project.workspace = true reqwest = { workspace = true, optional = true } serde_json.workspace = true diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 45f48db52d4..7c815b463a8 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -9,10 +9,10 @@ use alloy_rpc_types_eth::{ BlockNumberOrTag, BlockTransactionsKind, EIP1186AccountProofResponse, Filter, Log, }; use alloy_transport::{Transport, TransportErrorKind, TransportResult}; -use lru::LruCache; use parking_lot::RwLock; +use schnellru::{ByLength, LruMap}; use serde::{Deserialize, Serialize}; -use std::{io::BufReader, marker::PhantomData, num::NonZeroUsize, path::PathBuf, sync::Arc}; +use std::{io::BufReader, marker::PhantomData, path::PathBuf, sync::Arc}; /// A provider layer that caches RPC responses and serves them on subsequent requests. /// @@ -32,13 +32,13 @@ pub struct CacheLayer { impl CacheLayer { /// Instantiate a new cache layer with the the maximum number of /// items to store. - pub fn new(max_items: usize) -> Self { + pub fn new(max_items: u32) -> Self { Self { config: CacheConfig { max_items }, cache: SharedCache::new(max_items) } } /// Returns the maximum number of items that can be stored in the cache, set at initialization. #[inline] - pub const fn max_items(&self) -> usize { + pub const fn max_items(&self) -> u32 { self.config.max_items } @@ -484,27 +484,25 @@ struct FsCacheEntry { #[derive(Debug, Clone)] pub struct CacheConfig { /// Maximum number of items to store in the cache. - pub max_items: usize, + pub max_items: u32, } /// Shareable cache. #[derive(Debug, Clone)] pub struct SharedCache { - inner: Arc>>, + inner: Arc>>, } impl SharedCache { /// Instantiate a new shared cache. - pub fn new(max_items: usize) -> Self { - let cache = Arc::new(RwLock::new(LruCache::::new( - NonZeroUsize::new(max_items).unwrap(), - ))); + pub fn new(max_items: u32) -> Self { + let cache = Arc::new(RwLock::new(LruMap::::new(ByLength::new(max_items)))); Self { inner: cache } } /// Puts a value into the cache, and returns the old value if it existed. - pub fn put(&self, key: B256, value: String) -> TransportResult> { - Ok(self.inner.write().put(key, value)) + pub fn put(&self, key: B256, value: String) -> TransportResult { + Ok(self.inner.write().insert(key, value)) } /// Gets a value from the cache, if it exists. @@ -542,7 +540,7 @@ impl SharedCache { serde_json::from_reader(file).map_err(TransportErrorKind::custom)?; let mut cache = self.inner.write(); for entry in entries { - cache.put(entry.key, entry.value); + cache.insert(entry.key, entry.value); } Ok(()) From 62cfbeb8a87900e9bc6e4b229263183c88ea6e28 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Sat, 5 Oct 2024 17:32:09 +0530 Subject: [PATCH 47/48] feat: get_derialized --- crates/provider/src/layers/cache.rs | 69 +++++++++++++---------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 7c815b463a8..580cfad6469 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -84,7 +84,7 @@ where N: Network, { /// Instantiate a new cache provider. - pub fn new(inner: P, cache: SharedCache) -> Self { + pub const fn new(inner: P, cache: SharedCache) -> Self { Self { inner, cache, _pd: PhantomData } } } @@ -97,9 +97,8 @@ where macro_rules! cache_get_or_fetch { ($self:expr, $req:expr, $fetch_fn:expr) => {{ let hash = $req.params_hash()?; - if let Some(cached) = $self.cache.get(&hash)? { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; - return Ok(Some(result)); + if let Some(cached) = $self.cache.get_deserialized(&hash)? { + return Ok(Some(cached)); } let result = $fetch_fn.await?; @@ -156,12 +155,8 @@ macro_rules! cache_rpc_call_with_block { let hash = $req.params_hash().ok(); if let Some(hash) = hash { - if let Ok(Some(cached)) = $cache.get(&hash) { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); + if let Ok(Some(cached)) = $cache.get_deserialized(&hash) { + return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) })); } } @@ -220,12 +215,8 @@ where let params_hash = req.params_hash().ok(); if let Some(hash) = params_hash { - if let Ok(Some(cached)) = self.cache.get(&hash) { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); + if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) { + return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) })); } } } @@ -303,9 +294,8 @@ where let params_hash = req.params_hash().ok(); if let Some(hash) = params_hash { - if let Ok(Some(cached)) = self.cache.get(&hash) { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return result; + if let Some(cached) = self.cache.get_deserialized(&hash)? { + return Ok(cached); } } @@ -328,12 +318,8 @@ where let params_hash = req.params_hash().ok(); if let Some(hash) = params_hash { - if let Ok(Some(cached)) = self.cache.get(&hash) { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); + if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) { + return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) })); } } let client = self.inner.weak_client(); @@ -361,12 +347,8 @@ where let params_hash = req.params_hash().ok(); if let Some(hash) = params_hash { - if let Ok(Some(cached)) = self.cache.get(&hash) { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); + if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) { + return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) })); } } @@ -396,12 +378,8 @@ where let params_hash = req.params_hash().ok(); if let Some(hash) = params_hash { - if let Ok(Some(cached)) = self.cache.get(&hash) { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom); - return ProviderCall::BoxedFuture(Box::pin(async move { - let res = result?; - Ok(res) - })); + if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) { + return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) })); } } @@ -496,8 +474,8 @@ pub struct SharedCache { impl SharedCache { /// Instantiate a new shared cache. pub fn new(max_items: u32) -> Self { - let cache = Arc::new(RwLock::new(LruMap::::new(ByLength::new(max_items)))); - Self { inner: cache } + let inner = Arc::new(RwLock::new(LruMap::::new(ByLength::new(max_items)))); + Self { inner } } /// Puts a value into the cache, and returns the old value if it existed. @@ -512,6 +490,19 @@ impl SharedCache { Ok(val) } + /// Get deserialized value from the cache. + pub fn get_deserialized(&self, key: &B256) -> TransportResult> + where + T: for<'de> Deserialize<'de>, + { + if let Some(cached) = self.get(key)? { + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; + Ok(Some(result)) + } else { + Ok(None) + } + } + /// Saves the cache to a file specified by the path. /// If the files does not exist, it creates one. /// If the file exists, it overwrites it. From 6716c93efc8e8ecefcb46719deef76f4f8764953 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Sat, 5 Oct 2024 21:32:56 +0530 Subject: [PATCH 48/48] nits --- crates/provider/src/layers/cache.rs | 43 ++++++++++++----------------- crates/provider/src/layers/mod.rs | 2 +- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/crates/provider/src/layers/cache.rs b/crates/provider/src/layers/cache.rs index 580cfad6469..08f27625361 100644 --- a/crates/provider/src/layers/cache.rs +++ b/crates/provider/src/layers/cache.rs @@ -23,8 +23,6 @@ use std::{io::BufReader, marker::PhantomData, path::PathBuf, sync::Arc}; /// file system by calling `save_cache`. #[derive(Debug, Clone)] pub struct CacheLayer { - /// Configuration for the cache layer. - config: CacheConfig, /// In-memory LRU cache, mapping requests to responses. cache: SharedCache, } @@ -33,13 +31,12 @@ impl CacheLayer { /// Instantiate a new cache layer with the the maximum number of /// items to store. pub fn new(max_items: u32) -> Self { - Self { config: CacheConfig { max_items }, cache: SharedCache::new(max_items) } + Self { cache: SharedCache::new(max_items) } } /// Returns the maximum number of items that can be stored in the cache, set at initialization. - #[inline] pub const fn max_items(&self) -> u32 { - self.config.max_items + self.cache.max_items() } /// Returns the shared cache. @@ -456,26 +453,23 @@ struct FsCacheEntry { /// Serialized response to the request from which the hash was computed. value: String, } - -/// Configuration for the cache layer. -/// For future extensibility of the configurations. -#[derive(Debug, Clone)] -pub struct CacheConfig { - /// Maximum number of items to store in the cache. - pub max_items: u32, -} - /// Shareable cache. #[derive(Debug, Clone)] pub struct SharedCache { inner: Arc>>, + max_items: u32, } impl SharedCache { /// Instantiate a new shared cache. pub fn new(max_items: u32) -> Self { - let inner = Arc::new(RwLock::new(LruMap::::new(ByLength::new(max_items)))); - Self { inner } + let inner = Arc::new(RwLock::new(LruMap::new(ByLength::new(max_items)))); + Self { inner, max_items } + } + + /// Maximum number of items that can be stored in the cache. + pub const fn max_items(&self) -> u32 { + self.max_items } /// Puts a value into the cache, and returns the old value if it existed. @@ -484,10 +478,9 @@ impl SharedCache { } /// Gets a value from the cache, if it exists. - pub fn get(&self, key: &B256) -> TransportResult> { + pub fn get(&self, key: &B256) -> Option { // Need to acquire a write guard to change the order of keys in LRU cache. - let val = self.inner.write().get(key).cloned(); - Ok(val) + self.inner.write().get(key).cloned() } /// Get deserialized value from the cache. @@ -495,19 +488,17 @@ impl SharedCache { where T: for<'de> Deserialize<'de>, { - if let Some(cached) = self.get(key)? { - let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; - Ok(Some(result)) - } else { - Ok(None) - } + let Some(cached) = self.get(key) else { return Ok(None) }; + let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?; + Ok(Some(result)) } /// Saves the cache to a file specified by the path. /// If the files does not exist, it creates one. /// If the file exists, it overwrites it. pub fn save_cache(&self, path: PathBuf) -> TransportResult<()> { - let cache = self.inner.read(); + let cloned = self.inner.clone(); + let cache = cloned.read(); let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?; // Iterate over the cache and dump to the file. diff --git a/crates/provider/src/layers/mod.rs b/crates/provider/src/layers/mod.rs index c704b6737e0..3d4d62c9225 100644 --- a/crates/provider/src/layers/mod.rs +++ b/crates/provider/src/layers/mod.rs @@ -13,4 +13,4 @@ pub use chain::ChainLayer; #[cfg(not(target_arch = "wasm32"))] mod cache; #[cfg(not(target_arch = "wasm32"))] -pub use cache::{CacheConfig, CacheLayer, CacheProvider, SharedCache}; +pub use cache::{CacheLayer, CacheProvider, SharedCache};