Skip to main content

solana_accounts_db/
blockhash_queue.rs

1#[allow(deprecated)]
2use solana_sysvar::recent_blockhashes;
3use {
4    serde::{Deserialize, Serialize},
5    solana_clock::MAX_RECENT_BLOCKHASHES,
6    solana_fee_calculator::FeeCalculator,
7    solana_hash::Hash,
8    solana_time_utils::timestamp,
9    std::collections::HashMap,
10};
11
12#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
13#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
14pub struct HashInfo {
15    fee_calculator: FeeCalculator,
16    hash_index: u64,
17    timestamp: u64,
18}
19
20impl HashInfo {
21    pub fn lamports_per_signature(&self) -> u64 {
22        self.fee_calculator.lamports_per_signature
23    }
24}
25
26/// Low memory overhead, so can be cloned for every checkpoint
27#[cfg_attr(
28    feature = "frozen-abi",
29    derive(AbiExample),
30    frozen_abi(digest = "DZVVXt4saSgH1CWGrzBcX2sq5yswCuRqGx1Y1ZehtWT6")
31)]
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct BlockhashQueue {
34    /// index of last hash to be registered
35    last_hash_index: u64,
36
37    /// last hash to be registered
38    last_hash: Option<Hash>,
39
40    hashes: HashMap<Hash, HashInfo, ahash::RandomState>,
41
42    /// hashes older than `max_age` will be dropped from the queue
43    max_age: usize,
44}
45
46impl Default for BlockhashQueue {
47    fn default() -> Self {
48        Self::new(MAX_RECENT_BLOCKHASHES)
49    }
50}
51
52impl BlockhashQueue {
53    pub fn new(max_age: usize) -> Self {
54        Self {
55            hashes: HashMap::default(),
56            last_hash_index: 0,
57            last_hash: None,
58            max_age,
59        }
60    }
61
62    pub fn last_hash(&self) -> Hash {
63        self.last_hash.expect("no hash has been set")
64    }
65
66    pub fn get_lamports_per_signature(&self, hash: &Hash) -> Option<u64> {
67        self.hashes
68            .get(hash)
69            .map(|hash_age| hash_age.fee_calculator.lamports_per_signature)
70    }
71
72    /// Check if the age of the hash is within the specified age
73    pub fn is_hash_valid_for_age(&self, hash: &Hash, max_age: usize) -> bool {
74        self.get_hash_info_if_valid(hash, max_age).is_some()
75    }
76
77    /// Get hash info for the specified hash if it is in the queue and its age
78    /// of the hash is within the specified age
79    pub fn get_hash_info_if_valid(&self, hash: &Hash, max_age: usize) -> Option<&HashInfo> {
80        self.hashes.get(hash).filter(|info| {
81            Self::is_hash_index_valid(self.last_hash_index, max_age, info.hash_index)
82        })
83    }
84
85    pub fn get_hash_age(&self, hash: &Hash) -> Option<u64> {
86        self.hashes
87            .get(hash)
88            .map(|info| self.last_hash_index - info.hash_index)
89    }
90
91    pub fn genesis_hash(&mut self, hash: &Hash, lamports_per_signature: u64) {
92        self.hashes.insert(
93            *hash,
94            HashInfo {
95                fee_calculator: FeeCalculator::new(lamports_per_signature),
96                hash_index: 0,
97                timestamp: timestamp(),
98            },
99        );
100
101        self.last_hash = Some(*hash);
102    }
103
104    fn is_hash_index_valid(last_hash_index: u64, max_age: usize, hash_index: u64) -> bool {
105        last_hash_index - hash_index <= max_age as u64
106    }
107
108    pub fn register_hash(&mut self, hash: &Hash, lamports_per_signature: u64) {
109        self.last_hash_index += 1;
110        if self.hashes.len() >= self.max_age {
111            self.hashes.retain(|_, info| {
112                Self::is_hash_index_valid(self.last_hash_index, self.max_age, info.hash_index)
113            });
114        }
115
116        self.hashes.insert(
117            *hash,
118            HashInfo {
119                fee_calculator: FeeCalculator::new(lamports_per_signature),
120                hash_index: self.last_hash_index,
121                timestamp: timestamp(),
122            },
123        );
124
125        self.last_hash = Some(*hash);
126    }
127
128    #[deprecated(
129        since = "1.9.0",
130        note = "Please do not use, will no longer be available in the future"
131    )]
132    #[allow(deprecated)]
133    pub fn get_recent_blockhashes(&self) -> impl Iterator<Item = recent_blockhashes::IterItem<'_>> {
134        (self.hashes).iter().map(|(k, v)| {
135            recent_blockhashes::IterItem(v.hash_index, k, v.fee_calculator.lamports_per_signature)
136        })
137    }
138
139    #[deprecated(
140        since = "2.0.0",
141        note = "Please use `solana_clock::MAX_PROCESSING_AGE`"
142    )]
143    pub fn get_max_age(&self) -> usize {
144        #[allow(deprecated)]
145        self.max_age
146    }
147}
148#[cfg(test)]
149mod tests {
150    #[allow(deprecated)]
151    use solana_sysvar::recent_blockhashes::IterItem;
152    use {
153        super::*, bincode::serialize, solana_clock::MAX_RECENT_BLOCKHASHES,
154        solana_sha256_hasher::hash,
155    };
156
157    #[test]
158    fn test_register_hash() {
159        let last_hash = Hash::default();
160        let max_age = 100;
161        let mut hash_queue = BlockhashQueue::new(max_age);
162        assert!(!hash_queue.is_hash_valid_for_age(&last_hash, max_age));
163        hash_queue.register_hash(&last_hash, 0);
164        assert!(hash_queue.is_hash_valid_for_age(&last_hash, max_age));
165        assert_eq!(hash_queue.last_hash_index, 1);
166    }
167
168    #[test]
169    fn test_reject_old_last_hash() {
170        let max_age = 100;
171        let mut hash_queue = BlockhashQueue::new(max_age);
172        let last_hash = hash(&serialize(&0).unwrap());
173        for i in 0..102 {
174            let last_hash = hash(&serialize(&i).unwrap());
175            hash_queue.register_hash(&last_hash, 0);
176        }
177        // Assert we're no longer able to use the oldest hash.
178        assert!(!hash_queue.is_hash_valid_for_age(&last_hash, max_age));
179        assert!(!hash_queue.is_hash_valid_for_age(&last_hash, 0));
180
181        // Assert we are not able to use the oldest remaining hash.
182        let last_valid_hash = hash(&serialize(&1).unwrap());
183        assert!(hash_queue.is_hash_valid_for_age(&last_valid_hash, max_age));
184        assert!(!hash_queue.is_hash_valid_for_age(&last_valid_hash, 0));
185    }
186
187    /// test that when max age is 0, that a valid last_hash still passes the age check
188    #[test]
189    fn test_queue_init_blockhash() {
190        let last_hash = Hash::default();
191        let mut hash_queue = BlockhashQueue::new(100);
192        hash_queue.register_hash(&last_hash, 0);
193        assert_eq!(last_hash, hash_queue.last_hash());
194        assert!(hash_queue.is_hash_valid_for_age(&last_hash, 0));
195    }
196
197    #[test]
198    fn test_get_recent_blockhashes() {
199        let mut blockhash_queue = BlockhashQueue::new(MAX_RECENT_BLOCKHASHES);
200        #[allow(deprecated)]
201        let recent_blockhashes = blockhash_queue.get_recent_blockhashes();
202        // Sanity-check an empty BlockhashQueue
203        assert_eq!(recent_blockhashes.count(), 0);
204        for i in 0..MAX_RECENT_BLOCKHASHES {
205            let hash = hash(&serialize(&i).unwrap());
206            blockhash_queue.register_hash(&hash, 0);
207        }
208        #[allow(deprecated)]
209        let recent_blockhashes = blockhash_queue.get_recent_blockhashes();
210        // Verify that the returned hashes are most recent
211        #[allow(deprecated)]
212        for IterItem(_slot, hash, _lamports_per_signature) in recent_blockhashes {
213            assert!(blockhash_queue.is_hash_valid_for_age(hash, MAX_RECENT_BLOCKHASHES));
214        }
215    }
216
217    #[test]
218    fn test_len() {
219        const MAX_AGE: usize = 10;
220        let mut hash_queue = BlockhashQueue::new(MAX_AGE);
221        assert_eq!(hash_queue.hashes.len(), 0);
222
223        for _ in 0..MAX_AGE {
224            hash_queue.register_hash(&Hash::new_unique(), 0);
225        }
226        assert_eq!(hash_queue.hashes.len(), MAX_AGE);
227
228        // Show that the queue actually holds one more entry than the max age.
229        // This is because the most recent hash is considered to have an age of 0,
230        // which is likely the result of an unintentional off-by-one error in the past.
231        hash_queue.register_hash(&Hash::new_unique(), 0);
232        assert_eq!(hash_queue.hashes.len(), MAX_AGE + 1);
233
234        // Ensure that no additional entries beyond `MAX_AGE + 1` are added
235        hash_queue.register_hash(&Hash::new_unique(), 0);
236        assert_eq!(hash_queue.hashes.len(), MAX_AGE + 1);
237    }
238
239    #[test]
240    fn test_get_hash_age() {
241        const MAX_AGE: usize = 10;
242        let mut hash_list: Vec<Hash> = Vec::new();
243        hash_list.resize_with(MAX_AGE + 1, Hash::new_unique);
244
245        let mut hash_queue = BlockhashQueue::new(MAX_AGE);
246        for hash in &hash_list {
247            assert!(hash_queue.get_hash_age(hash).is_none());
248        }
249
250        for hash in &hash_list {
251            hash_queue.register_hash(hash, 0);
252        }
253
254        // Note that the "age" of a hash in the queue is 0-indexed. So when processing
255        // transactions in block 50, the hash for block 49 has an age of 0 despite
256        // being one block in the past.
257        for (age, hash) in hash_list.iter().rev().enumerate() {
258            assert_eq!(hash_queue.get_hash_age(hash), Some(age as u64));
259        }
260
261        // When the oldest hash is popped, it should no longer return a hash age
262        hash_queue.register_hash(&Hash::new_unique(), 0);
263        assert!(hash_queue.get_hash_age(&hash_list[0]).is_none());
264    }
265
266    #[test]
267    fn test_is_hash_valid_for_age() {
268        const MAX_AGE: usize = 10;
269        let mut hash_list: Vec<Hash> = Vec::new();
270        hash_list.resize_with(MAX_AGE + 1, Hash::new_unique);
271
272        let mut hash_queue = BlockhashQueue::new(MAX_AGE);
273        for hash in &hash_list {
274            assert!(!hash_queue.is_hash_valid_for_age(hash, MAX_AGE));
275        }
276
277        for hash in &hash_list {
278            hash_queue.register_hash(hash, 0);
279        }
280
281        // Note that the "age" of a hash in the queue is 0-indexed. So when checking
282        // the age of a hash is within max age, the hash from 11 slots ago is considered
283        // to be within the max age of 10.
284        for hash in &hash_list {
285            assert!(hash_queue.is_hash_valid_for_age(hash, MAX_AGE));
286        }
287
288        // When max age is 0, only the most recent blockhash is still considered valid
289        assert!(hash_queue.is_hash_valid_for_age(&hash_list[MAX_AGE], 0));
290        assert!(!hash_queue.is_hash_valid_for_age(&hash_list[MAX_AGE - 1], 0));
291    }
292
293    #[test]
294    fn test_get_hash_info_if_valid() {
295        const MAX_AGE: usize = 10;
296        let mut hash_list: Vec<Hash> = Vec::new();
297        hash_list.resize_with(MAX_AGE + 1, Hash::new_unique);
298
299        let mut hash_queue = BlockhashQueue::new(MAX_AGE);
300        for hash in &hash_list {
301            assert!(hash_queue.get_hash_info_if_valid(hash, MAX_AGE).is_none());
302        }
303
304        for hash in &hash_list {
305            hash_queue.register_hash(hash, 0);
306        }
307
308        // Note that the "age" of a hash in the queue is 0-indexed. So when checking
309        // the age of a hash is within max age, the hash from 11 slots ago is considered
310        // to be within the max age of 10.
311        for hash in &hash_list {
312            assert_eq!(
313                hash_queue.get_hash_info_if_valid(hash, MAX_AGE),
314                Some(hash_queue.hashes.get(hash).unwrap())
315            );
316        }
317
318        // When max age is 0, only the most recent blockhash is still considered valid
319        let most_recent_hash = &hash_list[MAX_AGE];
320        assert_eq!(
321            hash_queue.get_hash_info_if_valid(most_recent_hash, 0),
322            Some(hash_queue.hashes.get(most_recent_hash).unwrap())
323        );
324        assert!(hash_queue
325            .get_hash_info_if_valid(&hash_list[MAX_AGE - 1], 0)
326            .is_none());
327    }
328}