Skip to main content

ethrex_common/
seeded_hash.rs

1//! A seeded hasher for maps whose keys a block can choose.
2//!
3//! rustc-hash 2.x is unseeded and hashes a key of 17 to 32 bytes as
4//! `multiply_mix(SEED2 ^ word(bytes[len - 16..len - 8]), ..) ^ len`, where `SEED2` is a
5//! public constant and `multiply_mix(0, _) == 0`. Keys carrying `SEED2` in that word all hash
6//! to `len` and share one hashbrown bucket, so storage slots, touched addresses or calldata
7//! crafted that way turn every lookup into a scan, at flat opcode gas. [`SeededState`] hashes
8//! with foldhash instead, seeded per process and per map, with one twist: a 32- or 20-byte
9//! key (`H256`, `U256`, `Address`) is fed to foldhash as words rather than as a byte string.
10//! foldhash's byte path spends four folded multiplies on 32 bytes where its integer path
11//! spends two, which shows on storage-heavy code; the word path resists the same attacks,
12//! since every multiply still mixes in a seed the key does not know.
13//!
14//! A keccak output is no safer as a key than the preimage behind it. Whoever picks the
15//! preimage (a CREATE2 salt, the trailing bytes of deployed code) can grind it until the hash
16//! lands in a chosen bucket, at about one keccak per bucket of the table for each key. What
17//! keeps such a map on FxHash safe is the gas a block pays for each key it adds or looks up.
18//! Code hashes are seeded: a contract deployed once can be called again for 100 gas, and every
19//! call looks its code up by hash. Created and self-destructed accounts stay on FxHash, since
20//! each entry costs the block a CREATE or a SELFDESTRUCT.
21//!
22//! A seeded map iterates in a different order in every process and every map. Whatever carries
23//! that order out of the process (a list another program replays, a report, the offender an
24//! error names) has to be sorted, as the prover's host and the guest sort a block's account
25//! updates before applying them to the state trie.
26//!
27//! Inside a zkVM guest foldhash finds no clock and no address randomization, so its seeds
28//! follow from the guest program and its input. Crafting keys against a guest then takes
29//! replaying it up to the map under attack, which is harder than against FxHash but possible.
30
31use std::hash::{BuildHasher, Hasher};
32
33/// A `std::collections::HashMap` hashed with [`SeededState`].
34pub type SeededHashMap<K, V> = std::collections::HashMap<K, V, SeededState>;
35/// A `std::collections::HashSet` hashed with [`SeededState`].
36pub type SeededHashSet<K> = std::collections::HashSet<K, SeededState>;
37
38/// A foldhash `RandomState`, seeded per process and per map, whose hasher takes fixed-size
39/// keys through foldhash's integer path. See the module docs.
40#[derive(Clone, Debug, Default)]
41pub struct SeededState(foldhash::fast::RandomState);
42
43impl BuildHasher for SeededState {
44    type Hasher = SeededHasher;
45
46    #[inline]
47    fn build_hasher(&self) -> SeededHasher {
48        SeededHasher(self.0.build_hasher())
49    }
50}
51
52/// The hasher built by [`SeededState`].
53pub struct SeededHasher(foldhash::fast::FoldHasher<'static>);
54
55impl Hasher for SeededHasher {
56    /// `H256` and `Address` hash as one 32- or 20-byte write, and `U256` as a length prefix
57    /// plus one 32-byte write. Those go through `write_u128`, which folds 16 bytes per
58    /// multiply; every other length takes foldhash's byte path.
59    #[inline]
60    fn write(&mut self, bytes: &[u8]) {
61        if let Some((lo, rest)) = bytes.split_first_chunk::<16>() {
62            if let Ok(hi) = <[u8; 16]>::try_from(rest) {
63                self.0.write_u128(u128::from_ne_bytes(*lo));
64                self.0.write_u128(u128::from_ne_bytes(hi));
65                return;
66            }
67            if let Ok(hi) = <[u8; 4]>::try_from(rest) {
68                self.0.write_u128(u128::from_ne_bytes(*lo));
69                self.0.write_u32(u32::from_ne_bytes(hi));
70                return;
71            }
72        }
73        self.0.write(bytes);
74    }
75
76    #[inline]
77    fn write_u8(&mut self, i: u8) {
78        self.0.write_u8(i);
79    }
80
81    #[inline]
82    fn write_u16(&mut self, i: u16) {
83        self.0.write_u16(i);
84    }
85
86    #[inline]
87    fn write_u32(&mut self, i: u32) {
88        self.0.write_u32(i);
89    }
90
91    #[inline]
92    fn write_u64(&mut self, i: u64) {
93        self.0.write_u64(i);
94    }
95
96    #[inline]
97    fn write_u128(&mut self, i: u128) {
98        self.0.write_u128(i);
99    }
100
101    #[inline]
102    fn write_usize(&mut self, i: usize) {
103        self.0.write_usize(i);
104    }
105
106    #[inline]
107    fn finish(&self) -> u64 {
108        self.0.finish()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::{Address, H256, U256};
116    use std::hash::Hash;
117
118    /// Keys per test, hashed into as many buckets.
119    const KEYS: u64 = 4096;
120    /// Hashing 4096 keys uniformly into 4096 buckets fills the fullest one to about 6; a
121    /// hasher that ignores the part of the key that varies puts them all in one.
122    const MAX_BUCKET_LOAD: usize = 16;
123    /// The word FxHash 2.x XORs into a 17- to 32-byte key right before a multiply, which
124    /// makes keys carrying it hash alike there. Kept as a hostile key shape; nothing here
125    /// relies on rustc-hash still behaving that way.
126    const ZEROING_WORD: u64 = 0x13198a2e03707344;
127
128    /// The load of the fullest bucket once `keys` are spread over `KEYS` buckets by the low
129    /// bits of their hash, as hashbrown picks a key's first bucket.
130    fn fullest_bucket<K: Hash>(keys: impl Iterator<Item = K>) -> usize {
131        let state = SeededState::default();
132        let mut loads = vec![0usize; KEYS as usize];
133        for key in keys {
134            let bucket = state.hash_one(key) % KEYS;
135            if let Some(load) = loads.get_mut(bucket as usize) {
136                *load += 1;
137            }
138        }
139        loads.into_iter().max().unwrap_or_default()
140    }
141
142    /// An address whose bytes `at..at + 8` hold `word` and whose bytes 4..12 hold the zeroing
143    /// word, so only one region of the key varies.
144    fn address_with(at: usize, word: u64) -> Address {
145        let mut bytes = [0u8; 20];
146        for (byte, value) in bytes.iter_mut().skip(4).zip(ZEROING_WORD.to_le_bytes()) {
147            *byte = value;
148        }
149        for (byte, value) in bytes.iter_mut().skip(at).zip(word.to_le_bytes()) {
150            *byte = value;
151        }
152        Address::from(bytes)
153    }
154
155    #[test]
156    fn spreads_h256_keys_that_differ_in_either_half() {
157        let low = (0..KEYS).map(|i| H256::from(U256([i, 0, ZEROING_WORD, 0]).to_little_endian()));
158        assert!(fullest_bucket(low) <= MAX_BUCKET_LOAD);
159        let high = (0..KEYS).map(|i| H256::from(U256([0, 0, ZEROING_WORD, i]).to_little_endian()));
160        assert!(fullest_bucket(high) <= MAX_BUCKET_LOAD);
161    }
162
163    #[test]
164    fn spreads_u256_keys_that_differ_in_either_half() {
165        let low = (0..KEYS).map(|i| U256([i, 0, ZEROING_WORD, 0]));
166        assert!(fullest_bucket(low) <= MAX_BUCKET_LOAD);
167        let high = (0..KEYS).map(|i| U256([0, 0, ZEROING_WORD, i]));
168        assert!(fullest_bucket(high) <= MAX_BUCKET_LOAD);
169    }
170
171    #[test]
172    fn spreads_addresses_that_differ_in_either_part() {
173        // Bytes 12..20 straddle the 16-byte word and the 4-byte tail; 16..20 is the tail alone.
174        assert!(fullest_bucket((0..KEYS).map(|i| address_with(12, i))) <= MAX_BUCKET_LOAD);
175        assert!(fullest_bucket((0..KEYS).map(|i| address_with(16, i))) <= MAX_BUCKET_LOAD);
176    }
177
178    #[test]
179    fn spreads_byte_strings_on_both_paths() {
180        // 32-byte calldata takes the word path; 40 bytes takes foldhash's byte path.
181        let short = (0..KEYS).map(|i| U256([i, 0, ZEROING_WORD, 0]).to_little_endian().to_vec());
182        assert!(fullest_bucket(short) <= MAX_BUCKET_LOAD);
183        let long = (0..KEYS).map(|i| {
184            let mut bytes = vec![0u8; 40];
185            for (byte, value) in bytes.iter_mut().skip(32).zip(i.to_le_bytes()) {
186                *byte = value;
187            }
188            bytes
189        });
190        assert!(fullest_bucket(long) <= MAX_BUCKET_LOAD);
191    }
192
193    #[test]
194    fn two_maps_hash_the_same_key_apart() {
195        // Each map draws its own seed, so a key crafted against one map's buckets says
196        // nothing about another's.
197        let key = H256::repeat_byte(0x5a);
198        assert_ne!(
199            SeededState::default().hash_one(key),
200            SeededState::default().hash_one(key)
201        );
202    }
203
204    #[test]
205    fn equal_keys_hash_equal_on_both_paths() {
206        let state = SeededState::default();
207        let key = H256::from_low_u64_be(7);
208        assert_eq!(state.hash_one(key), state.hash_one(key));
209        let bytes = vec![1u8; 40];
210        assert_eq!(state.hash_one(&bytes), state.hash_one(&bytes));
211    }
212}