gemachain_runtime/
blockhash_queue.rs

1use serde::{Deserialize, Serialize};
2#[allow(deprecated)]
3use gemachain_sdk::sysvar::recent_blockhashes;
4use gemachain_sdk::{fee_calculator::FeeCalculator, hash::Hash, timing::timestamp};
5use std::collections::HashMap;
6
7#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, AbiExample)]
8struct HashAge {
9    fee_calculator: FeeCalculator,
10    hash_height: u64,
11    timestamp: u64,
12}
13
14/// Low memory overhead, so can be cloned for every checkpoint
15#[frozen_abi(digest = "J1fGiMHyiKEBcWE6mfm7grAEGJgYEaVLzcrNZvd37iA2")]
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AbiExample)]
17pub struct BlockhashQueue {
18    /// updated whenever an hash is registered
19    hash_height: u64,
20
21    /// last hash to be registered
22    last_hash: Option<Hash>,
23
24    ages: HashMap<Hash, HashAge>,
25
26    /// hashes older than `max_age` will be dropped from the queue
27    max_age: usize,
28}
29
30impl BlockhashQueue {
31    pub fn new(max_age: usize) -> Self {
32        Self {
33            ages: HashMap::new(),
34            hash_height: 0,
35            last_hash: None,
36            max_age,
37        }
38    }
39
40    #[allow(dead_code)]
41    pub fn hash_height(&self) -> u64 {
42        self.hash_height
43    }
44
45    pub fn last_hash(&self) -> Hash {
46        self.last_hash.expect("no hash has been set")
47    }
48
49    #[deprecated(
50        since = "1.8.0",
51        note = "Please do not use, will no longer be available in the future"
52    )]
53    pub fn get_fee_calculator(&self, hash: &Hash) -> Option<&FeeCalculator> {
54        self.ages.get(hash).map(|hash_age| &hash_age.fee_calculator)
55    }
56
57    /// Check if the age of the hash is within the max_age
58    /// return false for any hashes with an age above max_age
59    /// return None for any hashes that were not found
60    pub fn check_hash_age(&self, hash: &Hash, max_age: usize) -> Option<bool> {
61        self.ages
62            .get(hash)
63            .map(|age| self.hash_height - age.hash_height <= max_age as u64)
64    }
65
66    pub fn get_hash_age(&self, hash: &Hash) -> Option<u64> {
67        self.ages
68            .get(hash)
69            .map(|age| self.hash_height - age.hash_height)
70    }
71
72    /// check if hash is valid
73    pub fn check_hash(&self, hash: &Hash) -> bool {
74        self.ages.get(hash).is_some()
75    }
76
77    pub fn genesis_hash(&mut self, hash: &Hash, fee_calculator: &FeeCalculator) {
78        self.ages.insert(
79            *hash,
80            HashAge {
81                fee_calculator: fee_calculator.clone(),
82                hash_height: 0,
83                timestamp: timestamp(),
84            },
85        );
86
87        self.last_hash = Some(*hash);
88    }
89
90    fn check_age(hash_height: u64, max_age: usize, age: &HashAge) -> bool {
91        hash_height - age.hash_height <= max_age as u64
92    }
93
94    pub fn register_hash(&mut self, hash: &Hash, fee_calculator: &FeeCalculator) {
95        self.hash_height += 1;
96        let hash_height = self.hash_height;
97
98        // this clean up can be deferred until sigs gets larger
99        //  because we verify age.nth every place we check for validity
100        let max_age = self.max_age;
101        if self.ages.len() >= max_age {
102            self.ages
103                .retain(|_, age| Self::check_age(hash_height, max_age, age));
104        }
105        self.ages.insert(
106            *hash,
107            HashAge {
108                fee_calculator: fee_calculator.clone(),
109                hash_height,
110                timestamp: timestamp(),
111            },
112        );
113
114        self.last_hash = Some(*hash);
115    }
116
117    /// Maps a hash height to a timestamp
118    pub fn hash_height_to_timestamp(&self, hash_height: u64) -> Option<u64> {
119        for age in self.ages.values() {
120            if age.hash_height == hash_height {
121                return Some(age.timestamp);
122            }
123        }
124        None
125    }
126
127    #[deprecated(
128        since = "1.8.0",
129        note = "Please do not use, will no longer be available in the future"
130    )]
131    #[allow(deprecated)]
132    pub fn get_recent_blockhashes(&self) -> impl Iterator<Item = recent_blockhashes::IterItem> {
133        (&self.ages)
134            .iter()
135            .map(|(k, v)| recent_blockhashes::IterItem(v.hash_height, k, &v.fee_calculator))
136    }
137
138    pub(crate) fn len(&self) -> usize {
139        self.max_age
140    }
141}
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use bincode::serialize;
146    #[allow(deprecated)]
147    use gemachain_sdk::sysvar::recent_blockhashes::IterItem;
148    use gemachain_sdk::{clock::MAX_RECENT_BLOCKHASHES, hash::hash};
149
150    #[test]
151    fn test_register_hash() {
152        let last_hash = Hash::default();
153        let mut hash_queue = BlockhashQueue::new(100);
154        assert!(!hash_queue.check_hash(&last_hash));
155        hash_queue.register_hash(&last_hash, &FeeCalculator::default());
156        assert!(hash_queue.check_hash(&last_hash));
157        assert_eq!(hash_queue.hash_height(), 1);
158    }
159
160    #[test]
161    fn test_reject_old_last_hash() {
162        let mut hash_queue = BlockhashQueue::new(100);
163        let last_hash = hash(&serialize(&0).unwrap());
164        for i in 0..102 {
165            let last_hash = hash(&serialize(&i).unwrap());
166            hash_queue.register_hash(&last_hash, &FeeCalculator::default());
167        }
168        // Assert we're no longer able to use the oldest hash.
169        assert!(!hash_queue.check_hash(&last_hash));
170        assert_eq!(None, hash_queue.check_hash_age(&last_hash, 0));
171
172        // Assert we are not able to use the oldest remaining hash.
173        let last_valid_hash = hash(&serialize(&1).unwrap());
174        assert!(hash_queue.check_hash(&last_valid_hash));
175        assert_eq!(Some(false), hash_queue.check_hash_age(&last_valid_hash, 0));
176    }
177
178    /// test that when max age is 0, that a valid last_hash still passes the age check
179    #[test]
180    fn test_queue_init_blockhash() {
181        let last_hash = Hash::default();
182        let mut hash_queue = BlockhashQueue::new(100);
183        hash_queue.register_hash(&last_hash, &FeeCalculator::default());
184        assert_eq!(last_hash, hash_queue.last_hash());
185        assert_eq!(Some(true), hash_queue.check_hash_age(&last_hash, 0));
186    }
187
188    #[test]
189    fn test_get_recent_blockhashes() {
190        let mut blockhash_queue = BlockhashQueue::new(MAX_RECENT_BLOCKHASHES);
191        #[allow(deprecated)]
192        let recent_blockhashes = blockhash_queue.get_recent_blockhashes();
193        // Sanity-check an empty BlockhashQueue
194        assert_eq!(recent_blockhashes.count(), 0);
195        for i in 0..MAX_RECENT_BLOCKHASHES {
196            let hash = hash(&serialize(&i).unwrap());
197            blockhash_queue.register_hash(&hash, &FeeCalculator::default());
198        }
199        #[allow(deprecated)]
200        let recent_blockhashes = blockhash_queue.get_recent_blockhashes();
201        // Verify that the returned hashes are most recent
202        #[allow(deprecated)]
203        for IterItem(_slot, hash, _fee_calc) in recent_blockhashes {
204            assert_eq!(
205                Some(true),
206                blockhash_queue.check_hash_age(hash, MAX_RECENT_BLOCKHASHES)
207            );
208        }
209    }
210}