Skip to main content

alloy_provider/layers/
cache.rs

1use crate::{
2    utils, ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock,
3};
4use alloy_eips::BlockId;
5use alloy_json_rpc::{RpcError, RpcSend};
6use alloy_network::Network;
7use alloy_network_primitives::TransactionResponse;
8use alloy_primitives::{
9    keccak256, Address, Bytes, StorageKey, StorageValue, TxHash, B256, U256, U64,
10};
11use alloy_rpc_types_eth::{
12    BlockNumberOrTag, EIP1186AccountProofResponse, Filter, Log, StorageValuesRequest,
13    StorageValuesResponse,
14};
15use alloy_transport::{TransportErrorKind, TransportResult};
16use lru::LruCache;
17use parking_lot::RwLock;
18use serde::{Deserialize, Serialize};
19use std::{io::BufReader, marker::PhantomData, num::NonZero, path::PathBuf, sync::Arc};
20/// A provider layer that caches RPC responses and serves them on subsequent requests.
21///
22/// The cache is an in-memory LRU with a fixed maximum item count. Block-sensitive methods are
23/// cached only for explicit block numbers or hashes; dynamic tags such as `latest` and `pending`
24/// bypass the cache. Log queries are cached only when pinned to a block hash or a fixed numeric
25/// range. It also caches block receipts, included transactions, raw transactions, and transaction
26/// receipts; other [`Provider`] methods pass through to the inner provider.
27///
28/// Persistence is opt-in. Obtain a [`SharedCache`] with [`CacheLayer::cache`] before applying the
29/// layer, retain that handle, and use [`SharedCache::load_cache`] or [`SharedCache::save_cache`].
30/// There is no automatic persistence or invalidation.
31#[derive(Debug, Clone)]
32pub struct CacheLayer {
33    /// In-memory LRU cache, mapping requests to responses.
34    cache: SharedCache,
35}
36
37impl CacheLayer {
38    /// Instantiate a new cache layer with the maximum number of
39    /// items to store.
40    pub fn new(max_items: u32) -> Self {
41        Self { cache: SharedCache::new(max_items) }
42    }
43
44    /// Returns the maximum number of items that can be stored in the cache, set at initialization.
45    pub const fn max_items(&self) -> u32 {
46        self.cache.max_items()
47    }
48
49    /// Returns the shared cache.
50    pub fn cache(&self) -> SharedCache {
51        self.cache.clone()
52    }
53}
54
55impl<P, N> ProviderLayer<P, N> for CacheLayer
56where
57    P: Provider<N>,
58    N: Network,
59{
60    type Provider = CacheProvider<P, N>;
61
62    fn layer(&self, inner: P) -> Self::Provider {
63        CacheProvider::new(inner, self.cache())
64    }
65}
66
67/// The [`CacheProvider`] holds the underlying in-memory LRU cache and overrides methods
68/// from the [`Provider`] trait. It attempts to fetch from the cache and fallbacks to
69/// the RPC in case of a cache miss.
70///
71/// Access to persistence remains on the shared [`SharedCache`] handle obtained from
72/// [`CacheLayer::cache`].
73#[derive(Debug, Clone)]
74pub struct CacheProvider<P, N> {
75    /// Inner provider.
76    inner: P,
77    /// In-memory LRU cache, mapping requests to responses.
78    cache: SharedCache,
79    /// Phantom data
80    _pd: PhantomData<N>,
81}
82
83impl<P, N> CacheProvider<P, N>
84where
85    P: Provider<N>,
86    N: Network,
87{
88    /// Instantiate a new cache provider.
89    pub const fn new(inner: P, cache: SharedCache) -> Self {
90        Self { inner, cache, _pd: PhantomData }
91    }
92}
93
94/// Uses underlying transport client to fetch data from the RPC.
95///
96/// This is specific to RPC requests that require the `block_id` parameter.
97///
98/// Fetches from the RPC and saves the response to the cache.
99///
100/// Returns a ProviderCall::BoxedFuture
101macro_rules! rpc_call_with_block {
102    ($cache:expr, $client:expr, $req:expr) => {{
103        let client =
104            $client.upgrade().ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"));
105        let cache = $cache.clone();
106        ProviderCall::BoxedFuture(Box::pin(async move {
107            let client = client?;
108
109            let result = client.request($req.method(), $req.params()).map_params(|params| {
110                ParamsWithBlock::new(params, $req.block_id.unwrap_or(BlockId::latest()))
111            });
112
113            let res = result.await?;
114            // Insert into cache only for deterministic block identifiers. Caching tag-based or
115            // requireCanonical queries can lead to stale data.
116            if $req.should_cache() {
117                let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?;
118                let hash = $req.params_hash()?;
119                let _ = cache.put(hash, json_str);
120            }
121
122            Ok(res)
123        }))
124    }};
125}
126
127/// Attempts to fetch the response from the cache by using the hash of the request params.
128///
129/// Fetches from the RPC in case of a cache miss
130///
131/// This helps overriding [`Provider`] methods that return `RpcWithBlock`.
132macro_rules! cache_rpc_call_with_block {
133    ($cache:expr, $client:expr, $req:expr) => {{
134        if !$req.should_cache() {
135            return rpc_call_with_block!($cache, $client, $req);
136        }
137
138        let hash = $req.params_hash().ok();
139
140        if let Some(hash) = hash {
141            if let Ok(Some(cached)) = $cache.get_deserialized(&hash) {
142                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
143            }
144        }
145
146        rpc_call_with_block!($cache, $client, $req)
147    }};
148}
149
150#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
151#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
152impl<P, N> Provider<N> for CacheProvider<P, N>
153where
154    P: Provider<N>,
155    N: Network,
156{
157    #[inline(always)]
158    fn root(&self) -> &RootProvider<N> {
159        self.inner.root()
160    }
161
162    fn get_block_receipts(
163        &self,
164        block: BlockId,
165    ) -> ProviderCall<(BlockId,), Option<Vec<N::ReceiptResponse>>> {
166        let req = RequestType::new("eth_getBlockReceipts", (block,)).with_block_id(block);
167
168        let should_cache = req.should_cache();
169
170        if should_cache {
171            let params_hash = req.params_hash().ok();
172
173            if let Some(hash) = params_hash {
174                if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
175                    return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
176                }
177            }
178        }
179
180        let client = self.inner.weak_client();
181        let cache = self.cache.clone();
182
183        ProviderCall::BoxedFuture(Box::pin(async move {
184            let client = client
185                .upgrade()
186                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
187
188            let result = client.request(req.method(), req.params()).await?;
189
190            if should_cache {
191                if let Some(ref receipts) = result {
192                    let json_str =
193                        serde_json::to_string(receipts).map_err(TransportErrorKind::custom)?;
194                    let hash = req.params_hash()?;
195                    let _ = cache.put(hash, json_str);
196                }
197            }
198
199            Ok(result)
200        }))
201    }
202
203    fn get_balance(&self, address: Address) -> RpcWithBlock<Address, U256> {
204        let client = self.inner.weak_client();
205        let cache = self.cache.clone();
206        RpcWithBlock::new_provider(move |block_id| {
207            let req = RequestType::new("eth_getBalance", address).with_block_id(block_id);
208            cache_rpc_call_with_block!(cache, client, req)
209        })
210    }
211
212    fn get_code_at(&self, address: Address) -> RpcWithBlock<Address, Bytes> {
213        let client = self.inner.weak_client();
214        let cache = self.cache.clone();
215        RpcWithBlock::new_provider(move |block_id| {
216            let req = RequestType::new("eth_getCode", address).with_block_id(block_id);
217            cache_rpc_call_with_block!(cache, client, req)
218        })
219    }
220
221    async fn get_logs(&self, filter: &Filter) -> TransportResult<Vec<Log>> {
222        if filter.block_option.as_block_hash().is_none() {
223            // if block options have dynamic range we can't cache them
224            let from_is_number = filter
225                .block_option
226                .get_from_block()
227                .as_ref()
228                .is_some_and(|block| block.is_number());
229            let to_is_number =
230                filter.block_option.get_to_block().as_ref().is_some_and(|block| block.is_number());
231
232            if !from_is_number || !to_is_number {
233                return self.inner.get_logs(filter).await;
234            }
235        }
236
237        let req = RequestType::new("eth_getLogs", (filter,));
238
239        let params_hash = req.params_hash().ok();
240
241        if let Some(hash) = params_hash {
242            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
243                return Ok(cached);
244            }
245        }
246
247        let result = self.inner.get_logs(filter).await?;
248
249        let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?;
250
251        let hash = req.params_hash()?;
252        let _ = self.cache.put(hash, json_str);
253
254        Ok(result)
255    }
256
257    fn get_proof(
258        &self,
259        address: Address,
260        keys: Vec<StorageKey>,
261    ) -> RpcWithBlock<(Address, Vec<StorageKey>), EIP1186AccountProofResponse> {
262        let client = self.inner.weak_client();
263        let cache = self.cache.clone();
264        RpcWithBlock::new_provider(move |block_id| {
265            let req =
266                RequestType::new("eth_getProof", (address, keys.clone())).with_block_id(block_id);
267            cache_rpc_call_with_block!(cache, client, req)
268        })
269    }
270
271    fn get_storage_at(
272        &self,
273        address: Address,
274        key: U256,
275    ) -> RpcWithBlock<(Address, U256), StorageValue> {
276        let client = self.inner.weak_client();
277        let cache = self.cache.clone();
278        RpcWithBlock::new_provider(move |block_id| {
279            let req = RequestType::new("eth_getStorageAt", (address, key)).with_block_id(block_id);
280            cache_rpc_call_with_block!(cache, client, req)
281        })
282    }
283
284    fn get_storage_values(
285        &self,
286        requests: StorageValuesRequest,
287    ) -> RpcWithBlock<(StorageValuesRequest,), StorageValuesResponse> {
288        let client = self.inner.weak_client();
289        let cache = self.cache.clone();
290        RpcWithBlock::new_provider(move |block_id| {
291            let req = RequestType::new("eth_getStorageValues", (requests.clone(),))
292                .with_block_id(block_id);
293            cache_rpc_call_with_block!(cache, client, req)
294        })
295    }
296
297    fn get_transaction_by_hash(
298        &self,
299        hash: TxHash,
300    ) -> ProviderCall<(TxHash,), Option<N::TransactionResponse>> {
301        let req = RequestType::new("eth_getTransactionByHash", (hash,));
302
303        let params_hash = req.params_hash().ok();
304
305        if let Some(hash) = params_hash {
306            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
307                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
308            }
309        }
310        let client = self.inner.weak_client();
311        let cache = self.cache.clone();
312        ProviderCall::BoxedFuture(Box::pin(async move {
313            let client = client
314                .upgrade()
315                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
316            let result: Option<N::TransactionResponse> =
317                client.request(req.method(), req.params()).await?;
318
319            if let Some(ref tx) = result {
320                // Pending transactions can transition to an included state with additional fields
321                // (e.g., block hash/number). Caching pending snapshots can hide these updates.
322                if tx.block_hash_num().is_some() {
323                    let json_str = serde_json::to_string(tx).map_err(TransportErrorKind::custom)?;
324                    let hash = req.params_hash()?;
325                    let _ = cache.put(hash, json_str);
326                }
327            }
328
329            Ok(result)
330        }))
331    }
332
333    fn get_raw_transaction_by_hash(&self, hash: TxHash) -> ProviderCall<(TxHash,), Option<Bytes>> {
334        let req = RequestType::new("eth_getRawTransactionByHash", (hash,));
335
336        let params_hash = req.params_hash().ok();
337
338        if let Some(hash) = params_hash {
339            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
340                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
341            }
342        }
343
344        let client = self.inner.weak_client();
345        let cache = self.cache.clone();
346        ProviderCall::BoxedFuture(Box::pin(async move {
347            let client = client
348                .upgrade()
349                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
350
351            let result = client.request(req.method(), req.params()).await?;
352
353            if let Some(ref tx) = result {
354                let json_str = serde_json::to_string(tx).map_err(TransportErrorKind::custom)?;
355                let hash = req.params_hash()?;
356                let _ = cache.put(hash, json_str);
357            }
358
359            Ok(result)
360        }))
361    }
362
363    fn get_transaction_receipt(
364        &self,
365        hash: TxHash,
366    ) -> ProviderCall<(TxHash,), Option<N::ReceiptResponse>> {
367        let req = RequestType::new("eth_getTransactionReceipt", (hash,));
368
369        let params_hash = req.params_hash().ok();
370
371        if let Some(hash) = params_hash {
372            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
373                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
374            }
375        }
376
377        let client = self.inner.weak_client();
378        let cache = self.cache.clone();
379        ProviderCall::BoxedFuture(Box::pin(async move {
380            let client = client
381                .upgrade()
382                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
383
384            let result = client.request(req.method(), req.params()).await?;
385
386            if let Some(ref receipt) = result {
387                let json_str =
388                    serde_json::to_string(receipt).map_err(TransportErrorKind::custom)?;
389                let hash = req.params_hash()?;
390                let _ = cache.put(hash, json_str);
391            }
392
393            Ok(result)
394        }))
395    }
396
397    fn get_transaction_count(
398        &self,
399        address: Address,
400    ) -> RpcWithBlock<Address, U64, u64, fn(U64) -> u64> {
401        let client = self.inner.weak_client();
402        let cache = self.cache.clone();
403        RpcWithBlock::new_provider(move |block_id| {
404            let req = RequestType::new("eth_getTransactionCount", address).with_block_id(block_id);
405
406            let should_cache = req.should_cache();
407
408            if should_cache {
409                let params_hash = req.params_hash().ok();
410
411                if let Some(hash) = params_hash {
412                    if let Ok(Some(cached)) = cache.get_deserialized::<U64>(&hash) {
413                        return ProviderCall::BoxedFuture(Box::pin(async move {
414                            Ok(utils::convert_u64(cached))
415                        }));
416                    }
417                }
418            }
419
420            let client = client.clone();
421            let cache = cache.clone();
422
423            ProviderCall::BoxedFuture(Box::pin(async move {
424                let client = client
425                    .upgrade()
426                    .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
427
428                let result: U64 = client
429                    .request(req.method(), req.params())
430                    .map_params(|params| ParamsWithBlock::new(params, block_id))
431                    .await?;
432
433                if should_cache {
434                    let json_str =
435                        serde_json::to_string(&result).map_err(TransportErrorKind::custom)?;
436                    let hash = req.params_hash()?;
437                    let _ = cache.put(hash, json_str);
438                }
439
440                Ok(utils::convert_u64(result))
441            }))
442        })
443    }
444}
445
446/// Internal type to handle different types of requests and generating their param hashes.
447struct RequestType<Params: RpcSend> {
448    method: &'static str,
449    params: Params,
450    block_id: Option<BlockId>,
451}
452
453impl<Params: RpcSend> RequestType<Params> {
454    const fn new(method: &'static str, params: Params) -> Self {
455        Self { method, params, block_id: None }
456    }
457
458    const fn with_block_id(mut self, block_id: BlockId) -> Self {
459        self.block_id = Some(block_id);
460        self
461    }
462
463    fn params_hash(&self) -> TransportResult<B256> {
464        // Merge the block_id + method + params and hash them.
465        // Ignoring all other BlockIds than BlockId::Hash and
466        // BlockId::Number(BlockNumberOrTag::Number(_)).
467        let hash = serde_json::to_string(&self.params())
468            .map(|p| {
469                keccak256(
470                    match self.block_id {
471                        Some(BlockId::Hash(rpc_block_hash)) => {
472                            format!("{}{}{}", rpc_block_hash, self.method(), p)
473                        }
474                        Some(BlockId::Number(BlockNumberOrTag::Number(number))) => {
475                            format!("{}{}{}", number, self.method(), p)
476                        }
477                        _ => format!("{}{}", self.method(), p),
478                    }
479                    .as_bytes(),
480                )
481            })
482            .map_err(RpcError::ser_err)?;
483
484        Ok(hash)
485    }
486
487    const fn method(&self) -> &'static str {
488        self.method
489    }
490
491    fn params(&self) -> Params {
492        self.params.clone()
493    }
494
495    /// Returns true if the request can be safely served from cache.
496    const fn should_cache(&self) -> bool {
497        if let Some(block_id) = self.block_id {
498            return match block_id {
499                BlockId::Hash(hash) => !matches!(hash.require_canonical, Some(true)),
500                BlockId::Number(BlockNumberOrTag::Number(_)) => true,
501                _ => false,
502            };
503        }
504
505        // Treat absence of BlockId as tag-based (e.g., 'latest'), which is non-deterministic
506        // and should not be cached.
507        false
508    }
509}
510
511#[derive(Debug, Serialize, Deserialize)]
512struct FsCacheEntry {
513    /// Hash of the request params
514    key: B256,
515    /// Serialized response to the request from which the hash was computed.
516    value: String,
517}
518
519/// Shareable cache.
520#[derive(Debug, Clone)]
521pub struct SharedCache {
522    inner: Arc<RwLock<LruCache<B256, String, alloy_primitives::map::FbBuildHasher<32>>>>,
523    max_items: NonZero<usize>,
524}
525
526impl SharedCache {
527    /// Instantiate a new shared cache.
528    pub fn new(max_items: u32) -> Self {
529        let max_items = NonZero::new(max_items as usize).unwrap_or(NonZero::<usize>::MIN);
530        let inner = Arc::new(RwLock::new(LruCache::with_hasher(max_items, Default::default())));
531        Self { inner, max_items }
532    }
533
534    /// Maximum number of items that can be stored in the cache.
535    pub const fn max_items(&self) -> u32 {
536        self.max_items.get() as u32
537    }
538
539    /// Puts a value into the cache and returns whether an existing value was replaced.
540    pub fn put(&self, key: B256, value: String) -> TransportResult<bool> {
541        Ok(self.inner.write().put(key, value).is_some())
542    }
543
544    /// Gets a value from the cache, if it exists.
545    pub fn get(&self, key: &B256) -> Option<String> {
546        // Need to acquire a write guard to change the order of keys in LRU cache.
547        self.inner.write().get(key).cloned()
548    }
549
550    /// Get deserialized value from the cache.
551    pub fn get_deserialized<T>(&self, key: &B256) -> TransportResult<Option<T>>
552    where
553        T: for<'de> Deserialize<'de>,
554    {
555        let Some(cached) = self.get(key) else { return Ok(None) };
556        let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?;
557        Ok(Some(result))
558    }
559
560    /// Saves the cache to a file specified by the path.
561    /// If the files does not exist, it creates one.
562    /// If the file exists, it overwrites it.
563    pub fn save_cache(&self, path: PathBuf) -> TransportResult<()> {
564        let entries: Vec<FsCacheEntry> = {
565            self.inner
566                .read()
567                .iter()
568                .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() })
569                .collect()
570        };
571        let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?;
572        serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?;
573        Ok(())
574    }
575
576    /// Loads entries from a file and merges them into the existing cache.
577    ///
578    /// Existing keys may be replaced, and entries beyond the configured capacity are evicted
579    /// according to the LRU policy. If the file does not exist, this returns without error.
580    pub fn load_cache(&self, path: PathBuf) -> TransportResult<()> {
581        if !path.exists() {
582            return Ok(());
583        };
584        let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?;
585        let file = BufReader::new(file);
586        let entries: Vec<FsCacheEntry> =
587            serde_json::from_reader(file).map_err(TransportErrorKind::custom)?;
588        let mut cache = self.inner.write();
589        for entry in entries {
590            cache.put(entry.key, entry.value);
591        }
592
593        Ok(())
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use crate::ProviderBuilder;
601    use alloy_network::TransactionBuilder;
602    use alloy_node_bindings::{utils::run_with_tempdir, Anvil};
603    use alloy_primitives::{b256, bytes, hex, utils::Unit, Bytes, FixedBytes};
604    use alloy_rpc_types_eth::{BlockId, Transaction, TransactionReceipt, TransactionRequest};
605    use alloy_transport::mock::Asserter;
606
607    #[tokio::test]
608    async fn test_get_proof() {
609        run_with_tempdir("get-proof", |dir| async move {
610            let cache_layer = CacheLayer::new(100);
611            let shared_cache = cache_layer.cache();
612            let anvil = Anvil::new().block_time_f64(0.3).spawn();
613            let provider = ProviderBuilder::new().layer(cache_layer).connect_http(anvil.endpoint_url());
614
615            let from = anvil.addresses()[0];
616            let path = dir.join("rpc-cache-proof.txt");
617
618            shared_cache.load_cache(path.clone()).unwrap();
619
620            let calldata: Bytes = "0x6080604052348015600f57600080fd5b506101f28061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80633fb5c1cb146100465780638381f58a14610062578063d09de08a14610080575b600080fd5b610060600480360381019061005b91906100ee565b61008a565b005b61006a610094565b604051610077919061012a565b60405180910390f35b61008861009a565b005b8060008190555050565b60005481565b6000808154809291906100ac90610174565b9190505550565b600080fd5b6000819050919050565b6100cb816100b8565b81146100d657600080fd5b50565b6000813590506100e8816100c2565b92915050565b600060208284031215610104576101036100b3565b5b6000610112848285016100d9565b91505092915050565b610124816100b8565b82525050565b600060208201905061013f600083018461011b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017f826100b8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101b1576101b0610145565b5b60018201905091905056fea264697066735822122067ac0f21f648b0cacd1b7260772852ad4a0f63e2cc174168c51a6887fd5197a964736f6c634300081a0033".parse().unwrap();
621
622            let tx = TransactionRequest::default()
623                .with_from(from)
624                .with_input(calldata)
625                .with_max_fee_per_gas(1_000_000_000)
626                .with_max_priority_fee_per_gas(1_000_000)
627                .with_gas_limit(1_000_000)
628                .with_nonce(0);
629
630            let tx_receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
631
632            let counter_addr = tx_receipt.contract_address.unwrap();
633
634            let keys = vec![
635                FixedBytes::with_last_byte(0),
636                FixedBytes::with_last_byte(0x1),
637                FixedBytes::with_last_byte(0x2),
638                FixedBytes::with_last_byte(0x3),
639                FixedBytes::with_last_byte(0x4),
640            ];
641
642            let proof =
643                provider.get_proof(counter_addr, keys.clone()).block_id(1.into()).await.unwrap();
644            let proof2 = provider.get_proof(counter_addr, keys).block_id(1.into()).await.unwrap();
645
646            assert_eq!(proof, proof2);
647
648            shared_cache.save_cache(path).unwrap();
649        }).await;
650    }
651
652    #[tokio::test]
653    async fn test_get_tx_by_hash_and_receipt() {
654        run_with_tempdir("get-tx-by-hash", |dir| async move {
655            let cache_layer = CacheLayer::new(100);
656            let shared_cache = cache_layer.cache();
657            let anvil = Anvil::new().block_time_f64(0.3).spawn();
658            let provider = ProviderBuilder::new()
659                .disable_recommended_fillers()
660                .layer(cache_layer)
661                .connect_http(anvil.endpoint_url());
662
663            let path = dir.join("rpc-cache-tx.txt");
664            shared_cache.load_cache(path.clone()).unwrap();
665
666            let req = TransactionRequest::default()
667                .from(anvil.addresses()[0])
668                .to(Address::repeat_byte(5))
669                .value(U256::ZERO)
670                .input(bytes!("deadbeef").into());
671
672            let tx_hash =
673                *provider.send_transaction(req).await.expect("failed to send tx").tx_hash();
674
675            let tx = provider.get_transaction_by_hash(tx_hash).await.unwrap(); // Received from RPC.
676            let tx2 = provider.get_transaction_by_hash(tx_hash).await.unwrap(); // Received from
677                                                                                // cache.
678            assert_eq!(tx, tx2);
679
680            let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap(); // Received from
681                                                                                    // RPC.
682            let receipt2 = provider.get_transaction_receipt(tx_hash).await.unwrap(); // Received from cache.
683
684            assert_eq!(receipt, receipt2);
685
686            shared_cache.save_cache(path).unwrap();
687        })
688        .await;
689    }
690
691    #[tokio::test]
692    async fn test_get_transaction_by_hash_retries_after_none() {
693        let cache_layer = CacheLayer::new(100);
694        let shared_cache = cache_layer.cache();
695        let asserter = Asserter::new();
696        let provider = ProviderBuilder::new()
697            .disable_recommended_fillers()
698            .layer(cache_layer)
699            .connect_mocked_client(asserter.clone());
700
701        let tx_hash = b256!("018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f");
702        let req = RequestType::new("eth_getTransactionByHash", (tx_hash,));
703        let cache_key = req.params_hash().unwrap();
704
705        let tx: Transaction = serde_json::from_str(
706            r#"{"hash":"0x018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f","nonce":"0x1","blockHash":"0x6e4e53d1de650d5a5ebed19b38321db369ef1dc357904284ecf4d89b8834969c","blockNumber":"0x2","transactionIndex":"0x0","from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0x5fbdb2315678afecb367f032d93f642f64180aa3","value":"0x0","gasPrice":"0x3a29f0f8","gas":"0x1c9c380","maxFeePerGas":"0xba43b7400","maxPriorityFeePerGas":"0x5f5e100","input":"0xd09de08a","r":"0xd309309a59a49021281cb6bb41d164c96eab4e50f0c1bd24c03ca336e7bc2bb7","s":"0x28a7f089143d0a1355ebeb2a1b9f0e5ad9eca4303021c1400d61bc23c9ac5319","v":"0x0","yParity":"0x0","chainId":"0x7a69","accessList":[],"type":"0x2"}"#,
707        )
708        .unwrap();
709
710        asserter.push_success(&Option::<Transaction>::None);
711        asserter.push_success(&Some(tx.clone()));
712
713        let first = provider.get_transaction_by_hash(tx_hash).await.unwrap();
714        assert_eq!(first, None);
715        assert!(shared_cache.get(&cache_key).is_none());
716
717        let second = provider.get_transaction_by_hash(tx_hash).await.unwrap();
718        assert_eq!(second, Some(tx));
719        assert!(shared_cache.get(&cache_key).is_some());
720    }
721
722    #[tokio::test]
723    async fn test_hash_canonical_block_ids_do_not_use_cache() {
724        let cache_layer = CacheLayer::new(100);
725        let shared_cache = cache_layer.cache();
726        let asserter = Asserter::new();
727        let provider = ProviderBuilder::new()
728            .disable_recommended_fillers()
729            .layer(cache_layer)
730            .connect_mocked_client(asserter.clone());
731
732        let address = Address::repeat_byte(5);
733        let block_hash = B256::repeat_byte(0x11);
734        let block_id = BlockId::hash_canonical(block_hash);
735        let req = RequestType::new("eth_getBalance", address).with_block_id(block_id);
736        let cache_key = req.params_hash().unwrap();
737
738        asserter.push_success(&U256::from(456));
739        asserter.push_failure_msg("block is not canonical");
740
741        let first = provider.get_balance(address).block_id(block_id).await.unwrap();
742        assert_eq!(first, U256::from(456));
743        assert!(shared_cache.get(&cache_key).is_none());
744
745        let second = provider.get_balance(address).block_id(block_id).await;
746        assert!(second.is_err());
747    }
748
749    #[tokio::test]
750    async fn test_get_transaction_by_hash_does_not_cache_pending() {
751        let cache_layer = CacheLayer::new(100);
752        let shared_cache = cache_layer.cache();
753        let asserter = Asserter::new();
754        let provider = ProviderBuilder::new()
755            .disable_recommended_fillers()
756            .layer(cache_layer)
757            .connect_mocked_client(asserter.clone());
758
759        let tx_hash = b256!("018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f");
760        let req = RequestType::new("eth_getTransactionByHash", (tx_hash,));
761        let cache_key = req.params_hash().unwrap();
762
763        let pending_tx: Transaction = serde_json::from_str(
764            r#"{"hash":"0x018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f","nonce":"0x1","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0x5fbdb2315678afecb367f032d93f642f64180aa3","value":"0x0","gasPrice":"0x3a29f0f8","gas":"0x1c9c380","maxFeePerGas":"0xba43b7400","maxPriorityFeePerGas":"0x5f5e100","input":"0xd09de08a","r":"0xd309309a59a49021281cb6bb41d164c96eab4e50f0c1bd24c03ca336e7bc2bb7","s":"0x28a7f089143d0a1355ebeb2a1b9f0e5ad9eca4303021c1400d61bc23c9ac5319","v":"0x0","yParity":"0x0","chainId":"0x7a69","accessList":[],"type":"0x2"}"#,
765        )
766        .unwrap();
767
768        let mined_tx: Transaction = serde_json::from_str(
769            r#"{"hash":"0x018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f","nonce":"0x1","blockHash":"0x6e4e53d1de650d5a5ebed19b38321db369ef1dc357904284ecf4d89b8834969c","blockNumber":"0x2","transactionIndex":"0x0","from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0x5fbdb2315678afecb367f032d93f642f64180aa3","value":"0x0","gasPrice":"0x3a29f0f8","gas":"0x1c9c380","maxFeePerGas":"0xba43b7400","maxPriorityFeePerGas":"0x5f5e100","input":"0xd09de08a","r":"0xd309309a59a49021281cb6bb41d164c96eab4e50f0c1bd24c03ca336e7bc2bb7","s":"0x28a7f089143d0a1355ebeb2a1b9f0e5ad9eca4303021c1400d61bc23c9ac5319","v":"0x0","yParity":"0x0","chainId":"0x7a69","accessList":[],"type":"0x2"}"#,
770        )
771        .unwrap();
772
773        asserter.push_success(&Some(pending_tx.clone()));
774        asserter.push_success(&Some(mined_tx.clone()));
775
776        let first = provider.get_transaction_by_hash(tx_hash).await.unwrap();
777        assert_eq!(first, Some(pending_tx));
778        assert!(shared_cache.get(&cache_key).is_none());
779
780        let second = provider.get_transaction_by_hash(tx_hash).await.unwrap();
781        assert_eq!(second, Some(mined_tx.clone()));
782        assert!(shared_cache.get(&cache_key).is_some());
783
784        // Third call should be served from cache.
785        let third = provider.get_transaction_by_hash(tx_hash).await.unwrap();
786        assert_eq!(third, Some(mined_tx));
787    }
788
789    #[tokio::test]
790    async fn test_get_raw_transaction_by_hash_retries_after_none() {
791        let cache_layer = CacheLayer::new(100);
792        let shared_cache = cache_layer.cache();
793        let asserter = Asserter::new();
794        let provider = ProviderBuilder::new()
795            .disable_recommended_fillers()
796            .layer(cache_layer)
797            .connect_mocked_client(asserter.clone());
798
799        let tx_hash = TxHash::with_last_byte(1);
800        let req = RequestType::new("eth_getRawTransactionByHash", (tx_hash,));
801        let cache_key = req.params_hash().unwrap();
802        let raw_tx = bytes!("deadbeef");
803
804        asserter.push_success(&Option::<Bytes>::None);
805        asserter.push_success(&Some(raw_tx.clone()));
806
807        let first = provider.get_raw_transaction_by_hash(tx_hash).await.unwrap();
808        assert_eq!(first, None);
809        assert!(shared_cache.get(&cache_key).is_none());
810
811        let second = provider.get_raw_transaction_by_hash(tx_hash).await.unwrap();
812        assert_eq!(second, Some(raw_tx));
813        assert!(shared_cache.get(&cache_key).is_some());
814    }
815
816    #[tokio::test]
817    async fn test_get_transaction_receipt_retries_after_none() {
818        let cache_layer = CacheLayer::new(100);
819        let shared_cache = cache_layer.cache();
820        let asserter = Asserter::new();
821        let provider = ProviderBuilder::new()
822            .disable_recommended_fillers()
823            .layer(cache_layer)
824            .connect_mocked_client(asserter.clone());
825
826        let tx_hash = b256!("ea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f");
827        let req = RequestType::new("eth_getTransactionReceipt", (tx_hash,));
828        let cache_key = req.params_hash().unwrap();
829
830        let receipt: TransactionReceipt = serde_json::from_str(
831            r#"{
832                "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f",
833                "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
834                "blockNumber": "0xf4240",
835                "logsBloom": "0x00000000000000000000000000000000000800000000000000000000000800000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000",
836                "gasUsed": "0x723c",
837                "root": "0x284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10",
838                "contractAddress": null,
839                "cumulativeGasUsed": "0x723c",
840                "transactionIndex": "0x0",
841                "from": "0x39fa8c5f2793459d6622857e7d9fbb4bd91766d3",
842                "to": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
843                "type": "0x0",
844                "effectiveGasPrice": "0x12bfb19e60",
845                "logs": [
846                    {
847                        "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
848                        "address": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
849                        "logIndex": "0x0",
850                        "data": "0x00000000000000000000000039fa8c5f2793459d6622857e7d9fbb4bd91766d30000000000000000000000000000000000000000000000056bc75e2d63100000",
851                        "removed": false,
852                        "topics": [
853                            "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"
854                        ],
855                        "blockNumber": "0xf4240",
856                        "transactionIndex": "0x0",
857                        "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f"
858                    }
859                ]
860            }"#,
861        )
862        .unwrap();
863
864        asserter.push_success(&Option::<TransactionReceipt>::None);
865        asserter.push_success(&Some(receipt.clone()));
866
867        let first = provider.get_transaction_receipt(tx_hash).await.unwrap();
868        assert_eq!(first, None);
869        assert!(shared_cache.get(&cache_key).is_none());
870
871        let second = provider.get_transaction_receipt(tx_hash).await.unwrap();
872        assert_eq!(second, Some(receipt));
873        assert!(shared_cache.get(&cache_key).is_some());
874    }
875
876    #[tokio::test]
877    async fn test_block_receipts() {
878        run_with_tempdir("get-block-receipts", |dir| async move {
879            let cache_layer = CacheLayer::new(100);
880            let shared_cache = cache_layer.cache();
881            let anvil = Anvil::new().spawn();
882            let provider = ProviderBuilder::new().layer(cache_layer).connect_http(anvil.endpoint_url());
883
884            let path = dir.join("rpc-cache-block-receipts.txt");
885            shared_cache.load_cache(path.clone()).unwrap();
886
887            // Send txs
888
889            let receipt = provider
890                    .send_raw_transaction(
891                        // Transfer 1 ETH from default EOA address to the Genesis address.
892                        bytes!("f865808477359400825208940000000000000000000000000000000000000000018082f4f5a00505e227c1c636c76fac55795db1a40a4d24840d81b40d2fe0cc85767f6bd202a01e91b437099a8a90234ac5af3cb7ca4fb1432e133f75f9a91678eaf5f487c74b").as_ref()
893                    )
894                    .await.unwrap().get_receipt().await.unwrap();
895
896            let block_number = receipt.block_number.unwrap();
897
898            let receipts =
899                provider.get_block_receipts(block_number.into()).await.unwrap(); // Received from RPC.
900            let receipts2 =
901                provider.get_block_receipts(block_number.into()).await.unwrap(); // Received from cache.
902            assert_eq!(receipts, receipts2);
903
904            assert!(receipts.is_some_and(|r| r[0] == receipt));
905
906            shared_cache.save_cache(path).unwrap();
907        })
908        .await
909    }
910
911    #[tokio::test]
912    async fn test_get_balance() {
913        run_with_tempdir("get-balance", |dir| async move {
914            let cache_layer = CacheLayer::new(100);
915            let cache_layer2 = cache_layer.clone();
916            let shared_cache = cache_layer.cache();
917            let anvil = Anvil::new().spawn();
918            let provider = ProviderBuilder::new()
919                .disable_recommended_fillers()
920                .layer(cache_layer)
921                .connect_http(anvil.endpoint_url());
922
923            let path = dir.join("rpc-cache-balance.txt");
924            shared_cache.load_cache(path.clone()).unwrap();
925
926            let to = Address::repeat_byte(5);
927
928            // Send a transaction to change balance
929            let req = TransactionRequest::default()
930                .from(anvil.addresses()[0])
931                .to(to)
932                .value(Unit::ETHER.wei());
933
934            let receipt = provider
935                .send_transaction(req)
936                .await
937                .expect("failed to send tx")
938                .get_receipt()
939                .await
940                .unwrap();
941            let block_number = receipt.block_number.unwrap();
942
943            // Get balance from RPC (populates cache)
944            let balance = provider.get_balance(to).block_id(block_number.into()).await.unwrap();
945            assert_eq!(balance, Unit::ETHER.wei());
946
947            // Drop anvil to ensure second call can't hit RPC
948            drop(anvil);
949
950            // Create new provider with same cache but dead endpoint
951            let provider2 = ProviderBuilder::new()
952                .disable_recommended_fillers()
953                .layer(cache_layer2)
954                .connect_http("http://localhost:1".parse().unwrap());
955
956            // This only succeeds if cache is hit
957            let balance2 = provider2.get_balance(to).block_id(block_number.into()).await.unwrap();
958            assert_eq!(balance, balance2);
959
960            shared_cache.save_cache(path).unwrap();
961        })
962        .await;
963    }
964
965    #[tokio::test]
966    async fn test_get_code() {
967        run_with_tempdir("get-code", |dir| async move {
968            let cache_layer = CacheLayer::new(100);
969            let shared_cache = cache_layer.cache();
970            let provider = ProviderBuilder::new().disable_recommended_fillers().with_gas_estimation().layer(cache_layer).connect_anvil_with_wallet();
971
972            let path = dir.join("rpc-cache-code.txt");
973            shared_cache.load_cache(path.clone()).unwrap();
974
975            let bytecode = hex::decode(
976                // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin
977                "6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033"
978            ).unwrap();
979            let tx = TransactionRequest::default().with_nonce(0).with_deploy_code(bytecode).with_chain_id(31337);
980
981            let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
982
983            let counter_addr = receipt.contract_address.unwrap();
984
985            let block_id = BlockId::number(receipt.block_number.unwrap());
986
987            let code = provider.get_code_at(counter_addr).block_id(block_id).await.unwrap(); // Received from RPC.
988            let code2 = provider.get_code_at(counter_addr).block_id(block_id).await.unwrap(); // Received from cache.
989            assert_eq!(code, code2);
990
991            shared_cache.save_cache(path).unwrap();
992        })
993        .await;
994    }
995
996    #[cfg(all(test, feature = "anvil-api"))]
997    #[tokio::test]
998    async fn test_get_storage_at_different_block_ids() {
999        use crate::ext::AnvilApi;
1000
1001        run_with_tempdir("get-code-different-block-id", |dir| async move {
1002            let cache_layer = CacheLayer::new(100);
1003            let shared_cache = cache_layer.cache();
1004            let provider = ProviderBuilder::new().disable_recommended_fillers().with_gas_estimation().layer(cache_layer).connect_anvil_with_wallet();
1005
1006            let path = dir.join("rpc-cache-code.txt");
1007            shared_cache.load_cache(path.clone()).unwrap();
1008
1009            let bytecode = hex::decode(
1010                // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin
1011                "6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033"
1012            ).unwrap();
1013
1014            let tx = TransactionRequest::default().with_nonce(0).with_deploy_code(bytecode).with_chain_id(31337);
1015            let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
1016            let counter_addr = receipt.contract_address.unwrap();
1017            let block_id = BlockId::number(receipt.block_number.unwrap());
1018
1019            let counter = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id).await.unwrap(); // Received from RPC.
1020            assert_eq!(counter, U256::ZERO);
1021            let counter_cached = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id).await.unwrap(); // Received from cache.
1022            assert_eq!(counter, counter_cached);
1023
1024            provider.anvil_mine(Some(1), None).await.unwrap();
1025
1026            // Send a tx incrementing the counter
1027            let tx2 = TransactionRequest::default().with_nonce(1).to(counter_addr).input(hex::decode("d09de08a").unwrap().into()).with_chain_id(31337);
1028            let receipt2 = provider.send_transaction(tx2).await.unwrap().get_receipt().await.unwrap();
1029            let block_id2 = BlockId::number(receipt2.block_number.unwrap());
1030
1031            let counter2 = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id2).await.unwrap(); // Received from RPC
1032            assert_eq!(counter2, U256::from(1));
1033            let counter2_cached = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id2).await.unwrap(); // Received from cache.
1034            assert_eq!(counter2, counter2_cached);
1035
1036            shared_cache.save_cache(path).unwrap();
1037        })
1038        .await;
1039    }
1040
1041    #[tokio::test]
1042    async fn test_get_transaction_count() {
1043        run_with_tempdir("get-tx-count", |dir| async move {
1044            let cache_layer = CacheLayer::new(100);
1045            // CacheLayer uses Arc internally, so cloning shares the same cache.
1046            let cache_layer2 = cache_layer.clone();
1047            let shared_cache = cache_layer.cache();
1048            let anvil = Anvil::new().spawn();
1049            let provider = ProviderBuilder::new()
1050                .disable_recommended_fillers()
1051                .layer(cache_layer)
1052                .connect_http(anvil.endpoint_url());
1053
1054            let path = dir.join("rpc-cache-tx-count.txt");
1055            shared_cache.load_cache(path.clone()).unwrap();
1056
1057            let address = anvil.addresses()[0];
1058
1059            // Send a transaction to increase the nonce
1060            let req = TransactionRequest::default()
1061                .from(address)
1062                .to(Address::repeat_byte(5))
1063                .value(U256::ZERO)
1064                .input(bytes!("deadbeef").into());
1065
1066            let receipt = provider
1067                .send_transaction(req)
1068                .await
1069                .expect("failed to send tx")
1070                .get_receipt()
1071                .await
1072                .unwrap();
1073            let block_number = receipt.block_number.unwrap();
1074
1075            // Get transaction count from RPC (populates cache)
1076            let count = provider
1077                .get_transaction_count(address)
1078                .block_id(block_number.into())
1079                .await
1080                .unwrap();
1081            assert_eq!(count, 1);
1082
1083            // Drop anvil to ensure second call can't hit RPC
1084            drop(anvil);
1085
1086            // Create new provider with same cache but dead endpoint
1087            let provider2 = ProviderBuilder::new()
1088                .disable_recommended_fillers()
1089                .layer(cache_layer2)
1090                .connect_http("http://localhost:1".parse().unwrap());
1091
1092            // This only succeeds if cache is hit
1093            let count2 = provider2
1094                .get_transaction_count(address)
1095                .block_id(block_number.into())
1096                .await
1097                .unwrap();
1098            assert_eq!(count, count2);
1099
1100            shared_cache.save_cache(path).unwrap();
1101        })
1102        .await;
1103    }
1104}