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 custom_recovery_runs_only_on_cache_miss() {
139 let transaction = TransactionSigned::new_unhashed(
140 Transaction::Legacy(TxLegacy::default()),
141 Signature::test_signature(),
142 );
143 let cache = SenderRecoveryCache::new(4);
144 let mut recovered = None;
145
146 let sender = cache
147 .recover_with(&transaction, |tx| {
148 let signer = tx.try_recover()?;
149 recovered = Some(signer);
150 Ok(signer)
151 })
152 .unwrap();
153
154 assert_eq!(recovered, Some(sender));
155 assert_eq!(cache.get(transaction.tx_hash()), Some(sender));
156 assert_eq!(
157 cache.recover_with(&transaction, |_| panic!("cache hit must skip recovery")).unwrap(),
158 sender
159 );
160 }
161
162 #[test]
163 fn failed_custom_recovery_is_retried() {
164 let transaction = TransactionSigned::new_unhashed(
165 Transaction::Legacy(TxLegacy::default()),
166 Signature::new(U256::ZERO, U256::ZERO, false),
167 );
168 let cache = SenderRecoveryCache::new(4);
169 let mut attempts = 0;
170
171 for _ in 0..2 {
172 assert!(cache
173 .recover_with(&transaction, |tx| {
174 attempts += 1;
175 tx.try_recover()
176 })
177 .is_err());
178 assert_eq!(cache.get(transaction.tx_hash()), None);
179 }
180 assert_eq!(attempts, 2);
181 }
182
183 fn signed_transaction(nonce: u64) -> TransactionSigned {
185 TransactionSigned::new_unhashed(
186 Transaction::Legacy(TxLegacy { nonce, ..Default::default() }),
187 Signature::test_signature(),
188 )
189 }
190
191 #[test]
192 fn recover_signers_reuses_cached_senders_and_caches_misses() {
193 let transactions: Vec<_> = (0..4).map(signed_transaction).collect();
194 let recovered: Vec<_> =
195 transactions.iter().map(|transaction| transaction.try_recover().unwrap()).collect();
196 let cache = SenderRecoveryCache::default();
197
198 let cached_sender = Address::repeat_byte(0xaa);
201 cache.cache.insert(*transactions[1].tx_hash(), cached_sender);
202
203 let senders = cache.recover_signers(&transactions).unwrap();
204
205 assert_eq!(senders, [recovered[0], cached_sender, recovered[2], recovered[3]]);
206 for (transaction, sender) in transactions.iter().zip(&senders) {
207 assert_eq!(cache.get(transaction.tx_hash()), Some(*sender));
208 }
209 assert!(cache.recover_signers::<TransactionSigned>(&[]).unwrap().is_empty());
210 }
211
212 #[test]
213 fn recover_signers_rejects_invalid_signature_without_caching() {
214 let valid = signed_transaction(0);
215 let invalid = TransactionSigned::new_unhashed(
216 Transaction::Legacy(TxLegacy::default()),
217 Signature::new(U256::ZERO, U256::ZERO, false),
218 );
219 let cache = SenderRecoveryCache::default();
220
221 assert!(cache.recover_signers(&[valid.clone(), invalid.clone()]).is_err());
222 assert_eq!(cache.get(valid.tx_hash()), None);
223 assert_eq!(cache.get(invalid.tx_hash()), None);
224 }
225}