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