Skip to main content

alloy_primitives/utils/
keccak_cache.rs

1//! A minimalistic one-way set associative cache for Keccak256 values.
2//!
3//! This cache has a fixed size to allow fast access and minimize per-call overhead.
4//!
5//! With the `keccak-cache-stats` feature, hit/miss/insert/collision counts are
6//! tracked in the global [`KECCAK_CACHE_STATS`] static.
7
8use super::{hint::unlikely, keccak256_impl as keccak256};
9use crate::{B256, KECCAK256_EMPTY};
10use std::{mem::MaybeUninit, sync::OnceLock};
11
12#[cfg(feature = "keccak-cache-stats")]
13pub use stats::{KECCAK_CACHE_STATS, KeccakCacheStats};
14
15/// Maximum input length that can be cached.
16pub(super) const MAX_INPUT_LEN: usize =
17    128 - size_of::<B256>() - size_of::<u8>() - size_of::<usize>();
18
19const DEFAULT_COUNT: usize = 1 << 17; // ~131k entries * 128 bytes = 16MiB
20static CACHE: OnceLock<Cache> = OnceLock::new();
21
22type Cache = fixed_cache::Cache<Key, B256, BuildHasher, CacheConfig>;
23
24struct CacheConfig {}
25impl fixed_cache::CacheConfig for CacheConfig {
26    const STATS: bool = cfg!(feature = "keccak-cache-stats");
27    const EPOCHS: bool = false;
28}
29
30/// Initializes the process-global keccak cache with `entries` buckets.
31///
32/// Returns `true` if this call initialized the cache, and `false` if the cache was already
33/// initialized by an earlier call or by the first cached hash computation. If this is never called,
34/// the cache is initialized lazily with the default size on first use.
35///
36/// # Panics
37///
38/// Panics if `entries` is not a power of two or is less than 4.
39#[must_use]
40pub fn init_keccak_cache(entries: usize) -> bool {
41    init_cache(&CACHE, entries)
42}
43
44pub(super) fn compute(input: &[u8], imp: impl FnOnce(&[u8]) -> B256) -> B256 {
45    if unlikely(input.is_empty() | (input.len() > MAX_INPUT_LEN)) {
46        return if input.is_empty() { KECCAK256_EMPTY } else { keccak256(input) };
47    }
48
49    let cache = CACHE.get_or_init(default_cache);
50    cache.get_or_insert_with_ref(input, imp, |input| {
51        let mut data = [MaybeUninit::uninit(); MAX_INPUT_LEN];
52        unsafe {
53            std::ptr::copy_nonoverlapping(input.as_ptr(), data.as_mut_ptr().cast(), input.len())
54        };
55        Key { len: input.len() as u8, data }
56    })
57}
58
59fn init_cache(cache: &OnceLock<Cache>, entries: usize) -> bool {
60    if cache.get().is_some() {
61        return false;
62    }
63    cache.set(new_cache(entries)).is_ok()
64}
65
66fn default_cache() -> Cache {
67    new_cache(DEFAULT_COUNT)
68}
69
70fn new_cache(entries: usize) -> Cache {
71    let cache = fixed_cache::Cache::new(entries, BuildHasher::new());
72    #[cfg(feature = "keccak-cache-stats")]
73    let cache = cache.with_stats(Some(fixed_cache::Stats::new(&stats::KECCAK_CACHE_STATS)));
74    cache
75}
76
77type BuildHasher = std::hash::BuildHasherDefault<Hasher>;
78#[derive(Default)]
79struct Hasher(u64);
80
81impl std::hash::Hasher for Hasher {
82    #[inline]
83    fn finish(&self) -> u64 {
84        self.0
85    }
86
87    #[inline]
88    fn write(&mut self, bytes: &[u8]) {
89        // This is tricky because our most common inputs are medium length: 16..=88
90        // `foldhash` and `rapidhash` have a fast-path for ..16 bytes and outline the rest,
91        // but really we want the opposite, or at least the 16.. path to be inlined.
92
93        // SAFETY: `bytes.len()` is checked to be within the bounds of `MAX_INPUT_LEN` by caller.
94        unsafe { core::hint::assert_unchecked(bytes.len() <= MAX_INPUT_LEN) };
95        if bytes.len() <= 16 {
96            super::hint::cold_path();
97        }
98        self.0 = rapidhash::v3::rapidhash_v3_micro_inline::<false, false>(
99            bytes,
100            const { &rapidhash::v3::RapidSecrets::seed(0) },
101        );
102    }
103
104    // We can just skip hashing the length prefix entirely since we know it's always
105    // `<=MAX_INPUT_LEN`, and the hash is good enough.
106
107    // `write_length_prefix` calls `write_usize` by default.
108    #[inline]
109    fn write_usize(&mut self, i: usize) {
110        debug_assert!(i <= MAX_INPUT_LEN, "{i} > {MAX_INPUT_LEN}")
111    }
112
113    #[cfg(feature = "nightly")]
114    #[inline]
115    fn write_length_prefix(&mut self, len: usize) {
116        debug_assert!(len <= MAX_INPUT_LEN, "{len} > {MAX_INPUT_LEN}")
117    }
118}
119
120#[derive(Clone, Copy)]
121struct Key {
122    len: u8,
123    data: [MaybeUninit<u8>; MAX_INPUT_LEN],
124}
125
126impl PartialEq for Key {
127    #[inline]
128    fn eq(&self, other: &Self) -> bool {
129        self.get() == other.get()
130    }
131}
132impl Eq for Key {}
133
134impl std::borrow::Borrow<[u8]> for Key {
135    #[inline]
136    fn borrow(&self) -> &[u8] {
137        self.get()
138    }
139}
140
141impl std::hash::Hash for Key {
142    #[inline]
143    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
144        state.write(self.get());
145    }
146}
147
148impl Key {
149    #[inline]
150    const fn get(&self) -> &[u8] {
151        unsafe { std::slice::from_raw_parts(self.data.as_ptr().cast(), self.len as usize) }
152    }
153}
154
155#[cfg(feature = "keccak-cache-stats")]
156mod stats {
157    use super::Key;
158    use crate::B256;
159    use std::sync::atomic::{AtomicU64, Ordering};
160
161    /// Counters for the global keccak cache.
162    ///
163    /// Accessed via the [`KECCAK_CACHE_STATS`] static. All counters use relaxed atomics.
164    ///
165    /// Available only with the `keccak-cache-stats` feature.
166    #[derive(Debug, Default)]
167    pub struct KeccakCacheStats {
168        hits: AtomicU64,
169        misses: AtomicU64,
170        inserts: AtomicU64,
171        collisions: AtomicU64,
172    }
173
174    impl KeccakCacheStats {
175        const fn new() -> Self {
176            Self {
177                hits: AtomicU64::new(0),
178                misses: AtomicU64::new(0),
179                inserts: AtomicU64::new(0),
180                collisions: AtomicU64::new(0),
181            }
182        }
183
184        /// Returns the number of cache hits.
185        #[inline]
186        pub fn hits(&self) -> u64 {
187            self.hits.load(Ordering::Relaxed)
188        }
189
190        /// Returns the number of cache misses.
191        #[inline]
192        pub fn misses(&self) -> u64 {
193            self.misses.load(Ordering::Relaxed)
194        }
195
196        /// Returns the number of inserted entries.
197        ///
198        /// Includes inserts that evicted a different key on hash collision (see
199        /// [`collisions`](Self::collisions)).
200        #[inline]
201        pub fn inserts(&self) -> u64 {
202            self.inserts.load(Ordering::Relaxed)
203        }
204
205        /// Returns the number of collisions (a different key was evicted on insert).
206        #[inline]
207        pub fn collisions(&self) -> u64 {
208            self.collisions.load(Ordering::Relaxed)
209        }
210
211        /// Resets all counters to zero.
212        pub fn reset(&self) {
213            self.hits.store(0, Ordering::Relaxed);
214            self.misses.store(0, Ordering::Relaxed);
215            self.inserts.store(0, Ordering::Relaxed);
216            self.collisions.store(0, Ordering::Relaxed);
217        }
218    }
219
220    impl fixed_cache::StatsHandler<Key, B256> for &'static KeccakCacheStats {
221        #[inline]
222        fn on_hit(&self, _key: &Key, _value: &B256) {
223            self.hits.fetch_add(1, Ordering::Relaxed);
224        }
225
226        #[inline]
227        fn on_miss(&self, _key: fixed_cache::AnyRef<'_>) {
228            self.misses.fetch_add(1, Ordering::Relaxed);
229        }
230
231        #[inline]
232        fn on_insert(&self, key: &Key, _value: &B256, evicted: Option<(&Key, &B256)>) {
233            match evicted {
234                // Race: another thread inserted the same key concurrently. Same input
235                // always produces the same hash, so this is a no-op redundant write.
236                Some((old, _)) if old == key => {}
237                Some(_) => {
238                    self.inserts.fetch_add(1, Ordering::Relaxed);
239                    self.collisions.fetch_add(1, Ordering::Relaxed);
240                }
241                None => {
242                    self.inserts.fetch_add(1, Ordering::Relaxed);
243                }
244            }
245        }
246    }
247
248    /// Global counters for the keccak cache.
249    ///
250    /// Available only with the `keccak-cache-stats` feature.
251    pub static KECCAK_CACHE_STATS: KeccakCacheStats = KeccakCacheStats::new();
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn sizes() {
260        assert_eq!(size_of::<Key>(), MAX_INPUT_LEN + 1);
261        assert_eq!(size_of::<fixed_cache::Bucket<(Key, B256)>>(), 128);
262    }
263
264    #[test]
265    fn caching() {
266        let mut count: usize = 0;
267        let mut compute = |input| {
268            compute(input, |x| {
269                count += 1;
270                keccak256(x)
271            })
272        };
273
274        let input = b"Hello World!";
275        let input2 = b"Hello World! 2";
276
277        let a = compute(input);
278        let b = compute(input);
279        let c = compute(input);
280        assert_eq!(a, b);
281        assert_eq!(a, c);
282
283        let d = compute(input2);
284        let e = compute(input2);
285        assert_ne!(a, d);
286        assert_eq!(d, e);
287
288        assert_eq!(count, 2);
289    }
290}