Skip to main content

reth_evm/
sender_recovery.rs

1use alloc::sync::Arc;
2use alloy_primitives::{map::FbBuildHasher, Address, B256};
3use reth_primitives_traits::{transaction::signed::RecoveryError, SignedTransaction};
4
5/// Number of entries retained in the default sender recovery cache.
6const SENDER_RECOVERY_CACHE_CAPACITY: usize = 1 << 17;
7
8/// Shared cache of recovered transaction senders.
9///
10/// Sender recovery is performed when a transaction enters the pool and again when the same
11/// transaction is received in an execution payload. Sharing this bounded, lock-free cache lets
12/// payload prewarming reuse the result produced by transaction-pool ingress.
13#[derive(Clone, Debug)]
14pub struct SenderRecoveryCache {
15    cache: Arc<fixed_cache::Cache<B256, Address, FbBuildHasher<32>, SenderRecoveryCacheConfig>>,
16}
17
18impl SenderRecoveryCache {
19    /// Creates a sender recovery cache with the given capacity.
20    ///
21    /// The capacity must satisfy [`fixed_cache::Cache`] capacity requirements.
22    pub fn new(capacity: usize) -> Self {
23        Self { cache: Arc::new(fixed_cache::Cache::new(capacity, FbBuildHasher::<32>::default())) }
24    }
25
26    /// Returns a cached sender for the transaction hash.
27    #[inline]
28    pub fn get(&self, tx_hash: &B256) -> Option<Address> {
29        self.cache.get(tx_hash)
30    }
31
32    /// Returns the cached sender or recovers and caches it on a miss.
33    ///
34    /// Failed recoveries are not cached.
35    #[inline]
36    pub fn recover<T: SignedTransaction>(&self, transaction: &T) -> Result<Address, RecoveryError> {
37        self.cache.get_or_try_insert_with_ref(
38            transaction.tx_hash(),
39            |_| transaction.try_recover(),
40            |hash| *hash,
41        )
42    }
43}
44
45impl Default for SenderRecoveryCache {
46    fn default() -> Self {
47        Self::new(SENDER_RECOVERY_CACHE_CAPACITY)
48    }
49}
50
51struct SenderRecoveryCacheConfig;
52
53impl fixed_cache::CacheConfig for SenderRecoveryCacheConfig {
54    const STATS: bool = false;
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use alloy_consensus::TxLegacy;
61    use alloy_primitives::{Signature, U256};
62    use reth_ethereum_primitives::{Transaction, TransactionSigned};
63
64    #[test]
65    fn recover_populates_cache() {
66        let transaction = TransactionSigned::new_unhashed(
67            Transaction::Legacy(TxLegacy::default()),
68            Signature::test_signature(),
69        );
70        let cache = SenderRecoveryCache::new(4);
71        let shared_cache = cache.clone();
72
73        let sender = cache.recover(&transaction).unwrap();
74
75        assert_eq!(shared_cache.get(transaction.tx_hash()), Some(sender));
76        assert_eq!(shared_cache.recover(&transaction).unwrap(), sender);
77    }
78
79    #[test]
80    fn failed_recovery_is_not_cached() {
81        let transaction = TransactionSigned::new_unhashed(
82            Transaction::Legacy(TxLegacy::default()),
83            Signature::new(U256::ZERO, U256::ZERO, false),
84        );
85        let cache = SenderRecoveryCache::new(4);
86
87        assert!(cache.recover(&transaction).is_err());
88        assert_eq!(cache.get(transaction.tx_hash()), None);
89    }
90}