solana_accounts_db/
blockhash_queue.rs

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