reth_evm/
sender_recovery.rs1use 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
8const SENDER_RECOVERY_CACHE_CAPACITY: usize = 1 << 17;
10
11#[derive(Clone, Debug)]
23pub struct SenderRecoveryCache {
24 cache: Arc<fixed_cache::Cache<B256, Address, FbBuildHasher<32>, SenderRecoveryCacheConfig>>,
25}
26
27impl SenderRecoveryCache {
28 pub fn new(capacity: usize) -> Self {
32 Self { cache: Arc::new(fixed_cache::Cache::new(capacity, FbBuildHasher::<32>::default())) }
33 }
34
35 #[inline]
37 pub fn get(&self, tx_hash: &B256) -> Option<Address> {
38 self.cache.get(tx_hash)
39 }
40
41 #[inline]
45 pub fn recover<T: SignedTransaction>(&self, transaction: &T) -> Result<Address, RecoveryError> {
46 self.recover_with(transaction, SignedTransaction::try_recover)
47 }
48
49 #[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 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 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 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 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}