Skip to main content

reth_evm/
sender_recovery.rs

1use alloc::{sync::Arc, vec::Vec};
2use alloy_primitives::{map::FbBuildHasher, Address, B256};
3use reth_primitives_traits::{
4    transaction::{recover::recover_signers, signed::RecoveryError},
5    SignedTransaction,
6};
7
8/// Number of entries retained in the default sender recovery cache: 131,072.
9const SENDER_RECOVERY_CACHE_CAPACITY: usize = 1 << 17;
10
11/// Shared cache of recovered transaction senders.
12///
13/// This bounded, lock-free cache shares recovered senders across transaction ingress and payload
14/// execution. Repeated transactions reuse the recovered sender while still undergoing the
15/// validation required by each caller.
16///
17/// The default cache retains up to 131,072 transaction hash-to-sender entries. Cloning the cache
18/// shares these entries rather than allocating another cache.
19///
20/// Cached senders are trusted without re-checking the signature, so only senders recovered from
21/// the transaction with the keyed hash are ever inserted.
22#[derive(Clone, Debug)]
23pub struct SenderRecoveryCache {
24    cache: Arc<fixed_cache::Cache<B256, Address, FbBuildHasher<32>, SenderRecoveryCacheConfig>>,
25}
26
27impl SenderRecoveryCache {
28    /// Creates a sender recovery cache with the given capacity.
29    ///
30    /// The capacity must satisfy [`fixed_cache::Cache`] capacity requirements.
31    pub fn new(capacity: usize) -> Self {
32        Self { cache: Arc::new(fixed_cache::Cache::new(capacity, FbBuildHasher::<32>::default())) }
33    }
34
35    /// Returns a cached sender for the transaction hash.
36    #[inline]
37    pub fn get(&self, tx_hash: &B256) -> Option<Address> {
38        self.cache.get(tx_hash)
39    }
40
41    /// Returns the cached sender or recovers and caches it on a miss.
42    ///
43    /// Failed recoveries are not cached.
44    #[inline]
45    pub fn recover<T: SignedTransaction>(&self, transaction: &T) -> Result<Address, RecoveryError> {
46        self.recover_with(transaction, SignedTransaction::try_recover)
47    }
48
49    /// Returns the cached sender or uses the given recovery function on a cache miss.
50    ///
51    /// The recovery function must validate the transaction's signature. It can also prepare
52    /// transaction-specific metadata alongside sender recovery. Failed recoveries are not cached.
53    #[inline]
54    pub fn recover_with<T: SignedTransaction>(
55        &self,
56        transaction: &T,
57        recover: impl FnOnce(&T) -> Result<Address, RecoveryError>,
58    ) -> Result<Address, RecoveryError> {
59        self.cache.get_or_try_insert_with_ref(
60            transaction.tx_hash(),
61            |_| recover(transaction),
62            |hash| *hash,
63        )
64    }
65
66    /// Recovers the senders of the given transactions, in transaction order.
67    ///
68    /// Cached senders are reused. The remaining transactions are recovered together, in parallel
69    /// when the `rayon` feature of `reth-primitives-traits` is enabled, and their senders are
70    /// cached. Nothing is cached if any recovery fails.
71    pub fn recover_signers<T: SignedTransaction>(
72        &self,
73        transactions: &[T],
74    ) -> Result<Vec<Address>, RecoveryError> {
75        let mut senders = Vec::with_capacity(transactions.len());
76        let mut misses = Vec::new();
77        for (index, transaction) in transactions.iter().enumerate() {
78            match self.get(transaction.tx_hash()) {
79                Some(sender) => senders.push(sender),
80                None => {
81                    // Placeholder that is overwritten once the miss has been recovered below.
82                    senders.push(Address::ZERO);
83                    misses.push(index);
84                }
85            }
86        }
87
88        if misses.is_empty() {
89            return Ok(senders)
90        }
91
92        let recovered =
93            recover_signers(misses.iter().map(|&index| &transactions[index]).collect::<Vec<_>>())?;
94        for (index, sender) in misses.into_iter().zip(recovered) {
95            self.cache.insert(*transactions[index].tx_hash(), sender);
96            senders[index] = sender;
97        }
98
99        Ok(senders)
100    }
101}
102
103impl Default for SenderRecoveryCache {
104    fn default() -> Self {
105        Self::new(SENDER_RECOVERY_CACHE_CAPACITY)
106    }
107}
108
109struct SenderRecoveryCacheConfig;
110
111impl fixed_cache::CacheConfig for SenderRecoveryCacheConfig {
112    const STATS: bool = false;
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use alloy_consensus::TxLegacy;
119    use alloy_primitives::{Signature, U256};
120    use reth_ethereum_primitives::{Transaction, TransactionSigned};
121
122    #[test]
123    fn recover_populates_cache() {
124        let transaction = TransactionSigned::new_unhashed(
125            Transaction::Legacy(TxLegacy::default()),
126            Signature::test_signature(),
127        );
128        let cache = SenderRecoveryCache::new(4);
129        let shared_cache = cache.clone();
130
131        let sender = cache.recover(&transaction).unwrap();
132
133        assert_eq!(shared_cache.get(transaction.tx_hash()), Some(sender));
134        assert_eq!(shared_cache.recover(&transaction).unwrap(), sender);
135    }
136
137    #[test]
138    fn failed_recovery_is_not_cached() {
139        let transaction = TransactionSigned::new_unhashed(
140            Transaction::Legacy(TxLegacy::default()),
141            Signature::new(U256::ZERO, U256::ZERO, false),
142        );
143        let cache = SenderRecoveryCache::new(4);
144
145        assert!(cache.recover(&transaction).is_err());
146        assert_eq!(cache.get(transaction.tx_hash()), None);
147    }
148
149    #[test]
150    fn custom_recovery_runs_only_on_cache_miss() {
151        let transaction = TransactionSigned::new_unhashed(
152            Transaction::Legacy(TxLegacy::default()),
153            Signature::test_signature(),
154        );
155        let cache = SenderRecoveryCache::new(4);
156        let mut recovered = None;
157
158        let sender = cache
159            .recover_with(&transaction, |tx| {
160                let signer = tx.try_recover()?;
161                recovered = Some(signer);
162                Ok(signer)
163            })
164            .unwrap();
165
166        assert_eq!(recovered, Some(sender));
167        assert_eq!(cache.get(transaction.tx_hash()), Some(sender));
168        assert_eq!(
169            cache.recover_with(&transaction, |_| panic!("cache hit must skip recovery")).unwrap(),
170            sender
171        );
172    }
173
174    #[test]
175    fn failed_custom_recovery_is_retried() {
176        let transaction = TransactionSigned::new_unhashed(
177            Transaction::Legacy(TxLegacy::default()),
178            Signature::new(U256::ZERO, U256::ZERO, false),
179        );
180        let cache = SenderRecoveryCache::new(4);
181        let mut attempts = 0;
182
183        for _ in 0..2 {
184            assert!(cache
185                .recover_with(&transaction, |tx| {
186                    attempts += 1;
187                    tx.try_recover()
188                })
189                .is_err());
190            assert_eq!(cache.get(transaction.tx_hash()), None);
191        }
192        assert_eq!(attempts, 2);
193    }
194
195    /// Returns a validly signed transaction whose sender depends on the nonce.
196    fn signed_transaction(nonce: u64) -> TransactionSigned {
197        TransactionSigned::new_unhashed(
198            Transaction::Legacy(TxLegacy { nonce, ..Default::default() }),
199            Signature::test_signature(),
200        )
201    }
202
203    #[test]
204    fn recover_signers_reuses_cached_senders_and_caches_misses() {
205        let transactions: Vec<_> = (0..4).map(signed_transaction).collect();
206        let recovered: Vec<_> =
207            transactions.iter().map(|transaction| transaction.try_recover().unwrap()).collect();
208        let cache = SenderRecoveryCache::default();
209
210        // Stands in for a sender recovered by another component: it can only be returned if
211        // the hit skipped signature recovery.
212        let cached_sender = Address::repeat_byte(0xaa);
213        cache.cache.insert(*transactions[1].tx_hash(), cached_sender);
214
215        let senders = cache.recover_signers(&transactions).unwrap();
216
217        assert_eq!(senders, [recovered[0], cached_sender, recovered[2], recovered[3]]);
218        for (transaction, sender) in transactions.iter().zip(&senders) {
219            assert_eq!(cache.get(transaction.tx_hash()), Some(*sender));
220        }
221        assert!(cache.recover_signers::<TransactionSigned>(&[]).unwrap().is_empty());
222    }
223
224    #[test]
225    fn recover_signers_rejects_invalid_signature_without_caching() {
226        let valid = signed_transaction(0);
227        let invalid = TransactionSigned::new_unhashed(
228            Transaction::Legacy(TxLegacy::default()),
229            Signature::new(U256::ZERO, U256::ZERO, false),
230        );
231        let cache = SenderRecoveryCache::default();
232
233        assert!(cache.recover_signers(&[valid.clone(), invalid.clone()]).is_err());
234        assert_eq!(cache.get(valid.tx_hash()), None);
235        assert_eq!(cache.get(invalid.tx_hash()), None);
236    }
237}