clone_solana_entry/
poh.rs

1//! The `Poh` module provides an object for generating a Proof of History.
2use {
3    clone_solana_hash::Hash,
4    clone_solana_sha256_hasher::{hash, hashv},
5    log::*,
6    std::time::{Duration, Instant},
7};
8
9const LOW_POWER_MODE: u64 = u64::MAX;
10
11pub struct Poh {
12    pub hash: Hash,
13    num_hashes: u64,
14    hashes_per_tick: u64,
15    remaining_hashes: u64,
16    tick_number: u64,
17    slot_start_time: Instant,
18}
19
20#[derive(Debug)]
21pub struct PohEntry {
22    pub num_hashes: u64,
23    pub hash: Hash,
24}
25
26impl Poh {
27    pub fn new(hash: Hash, hashes_per_tick: Option<u64>) -> Self {
28        Self::new_with_slot_info(hash, hashes_per_tick, 0)
29    }
30
31    pub fn new_with_slot_info(hash: Hash, hashes_per_tick: Option<u64>, tick_number: u64) -> Self {
32        let hashes_per_tick = hashes_per_tick.unwrap_or(LOW_POWER_MODE);
33        assert!(hashes_per_tick > 1);
34        let now = Instant::now();
35        Poh {
36            hash,
37            num_hashes: 0,
38            hashes_per_tick,
39            remaining_hashes: hashes_per_tick,
40            tick_number,
41            slot_start_time: now,
42        }
43    }
44
45    pub fn reset(&mut self, hash: Hash, hashes_per_tick: Option<u64>) {
46        // retains ticks_per_slot: this cannot change without restarting the validator
47        let tick_number = 0;
48        *self = Poh::new_with_slot_info(hash, hashes_per_tick, tick_number);
49    }
50
51    pub fn hashes_per_tick(&self) -> u64 {
52        self.hashes_per_tick
53    }
54
55    pub fn target_poh_time(&self, target_ns_per_tick: u64) -> Instant {
56        assert!(self.hashes_per_tick > 0);
57        let offset_tick_ns = target_ns_per_tick * self.tick_number;
58        let offset_ns = target_ns_per_tick * self.num_hashes / self.hashes_per_tick;
59        self.slot_start_time + Duration::from_nanos(offset_ns + offset_tick_ns)
60    }
61
62    pub fn hash(&mut self, max_num_hashes: u64) -> bool {
63        let num_hashes = std::cmp::min(self.remaining_hashes - 1, max_num_hashes);
64
65        for _ in 0..num_hashes {
66            self.hash = hash(self.hash.as_ref());
67        }
68        self.num_hashes += num_hashes;
69        self.remaining_hashes -= num_hashes;
70
71        assert!(self.remaining_hashes > 0);
72        self.remaining_hashes == 1 // Return `true` if caller needs to `tick()` next
73    }
74
75    pub fn record(&mut self, mixin: Hash) -> Option<PohEntry> {
76        if self.remaining_hashes == 1 {
77            return None; // Caller needs to `tick()` first
78        }
79
80        self.hash = hashv(&[self.hash.as_ref(), mixin.as_ref()]);
81        let num_hashes = self.num_hashes + 1;
82        self.num_hashes = 0;
83        self.remaining_hashes -= 1;
84
85        Some(PohEntry {
86            num_hashes,
87            hash: self.hash,
88        })
89    }
90
91    pub fn tick(&mut self) -> Option<PohEntry> {
92        self.hash = hash(self.hash.as_ref());
93        self.num_hashes += 1;
94        self.remaining_hashes -= 1;
95
96        // If we are in low power mode then always generate a tick.
97        // Otherwise only tick if there are no remaining hashes
98        if self.hashes_per_tick != LOW_POWER_MODE && self.remaining_hashes != 0 {
99            return None;
100        }
101
102        let num_hashes = self.num_hashes;
103        self.remaining_hashes = self.hashes_per_tick;
104        self.num_hashes = 0;
105        self.tick_number += 1;
106        Some(PohEntry {
107            num_hashes,
108            hash: self.hash,
109        })
110    }
111}
112
113pub fn compute_hash_time(hashes_sample_size: u64) -> Duration {
114    info!("Running {} hashes...", hashes_sample_size);
115    let mut v = Hash::default();
116    let start = Instant::now();
117    for _ in 0..hashes_sample_size {
118        v = hash(v.as_ref());
119    }
120    start.elapsed()
121}
122
123pub fn compute_hashes_per_tick(duration: Duration, hashes_sample_size: u64) -> u64 {
124    let elapsed_ms = compute_hash_time(hashes_sample_size).as_millis() as u64;
125    duration.as_millis() as u64 * hashes_sample_size / elapsed_ms
126}
127
128#[cfg(test)]
129mod tests {
130    use {
131        crate::poh::{Poh, PohEntry},
132        assert_matches::assert_matches,
133        clone_solana_hash::Hash,
134        clone_solana_sha256_hasher::{hash, hashv},
135        std::time::Duration,
136    };
137
138    fn verify(initial_hash: Hash, entries: &[(PohEntry, Option<Hash>)]) -> bool {
139        let mut current_hash = initial_hash;
140
141        for (entry, mixin) in entries {
142            assert_ne!(entry.num_hashes, 0);
143
144            for _ in 1..entry.num_hashes {
145                current_hash = hash(current_hash.as_ref());
146            }
147            current_hash = match mixin {
148                Some(mixin) => hashv(&[current_hash.as_ref(), mixin.as_ref()]),
149                None => hash(current_hash.as_ref()),
150            };
151            if current_hash != entry.hash {
152                return false;
153            }
154        }
155
156        true
157    }
158
159    #[test]
160    fn test_target_poh_time() {
161        let zero = Hash::default();
162        for target_ns_per_tick in 10..12 {
163            let mut poh = Poh::new(zero, None);
164            assert_eq!(poh.target_poh_time(target_ns_per_tick), poh.slot_start_time);
165            poh.tick_number = 2;
166            assert_eq!(
167                poh.target_poh_time(target_ns_per_tick),
168                poh.slot_start_time + Duration::from_nanos(target_ns_per_tick * 2)
169            );
170            let mut poh = Poh::new(zero, Some(5));
171            assert_eq!(poh.target_poh_time(target_ns_per_tick), poh.slot_start_time);
172            poh.tick_number = 2;
173            assert_eq!(
174                poh.target_poh_time(target_ns_per_tick),
175                poh.slot_start_time + Duration::from_nanos(target_ns_per_tick * 2)
176            );
177            poh.num_hashes = 3;
178            assert_eq!(
179                poh.target_poh_time(target_ns_per_tick),
180                poh.slot_start_time
181                    + Duration::from_nanos(target_ns_per_tick * 2 + target_ns_per_tick * 3 / 5)
182            );
183        }
184    }
185
186    #[test]
187    #[should_panic(expected = "hashes_per_tick > 1")]
188    fn test_target_poh_time_hashes_per_tick() {
189        let zero = Hash::default();
190        let poh = Poh::new(zero, Some(0));
191        let target_ns_per_tick = 10;
192        poh.target_poh_time(target_ns_per_tick);
193    }
194
195    #[test]
196    fn test_poh_verify() {
197        let zero = Hash::default();
198        let one = hash(zero.as_ref());
199        let two = hash(one.as_ref());
200        let one_with_zero = hashv(&[zero.as_ref(), zero.as_ref()]);
201
202        let mut poh = Poh::new(zero, None);
203        assert!(verify(
204            zero,
205            &[
206                (poh.tick().unwrap(), None),
207                (poh.record(zero).unwrap(), Some(zero)),
208                (poh.record(zero).unwrap(), Some(zero)),
209                (poh.tick().unwrap(), None),
210            ],
211        ));
212
213        assert!(verify(
214            zero,
215            &[(
216                PohEntry {
217                    num_hashes: 1,
218                    hash: one,
219                },
220                None
221            )],
222        ));
223        assert!(verify(
224            zero,
225            &[(
226                PohEntry {
227                    num_hashes: 2,
228                    hash: two,
229                },
230                None
231            )]
232        ));
233
234        assert!(verify(
235            zero,
236            &[(
237                PohEntry {
238                    num_hashes: 1,
239                    hash: one_with_zero,
240                },
241                Some(zero)
242            )]
243        ));
244        assert!(!verify(
245            zero,
246            &[(
247                PohEntry {
248                    num_hashes: 1,
249                    hash: zero,
250                },
251                None
252            )]
253        ));
254
255        assert!(verify(
256            zero,
257            &[
258                (
259                    PohEntry {
260                        num_hashes: 1,
261                        hash: one_with_zero,
262                    },
263                    Some(zero)
264                ),
265                (
266                    PohEntry {
267                        num_hashes: 1,
268                        hash: hash(one_with_zero.as_ref()),
269                    },
270                    None
271                )
272            ]
273        ));
274    }
275
276    #[test]
277    #[should_panic]
278    fn test_poh_verify_assert() {
279        verify(
280            Hash::default(),
281            &[(
282                PohEntry {
283                    num_hashes: 0,
284                    hash: Hash::default(),
285                },
286                None,
287            )],
288        );
289    }
290
291    #[test]
292    fn test_poh_tick() {
293        let mut poh = Poh::new(Hash::default(), Some(2));
294        assert_eq!(poh.remaining_hashes, 2);
295        assert!(poh.tick().is_none());
296        assert_eq!(poh.remaining_hashes, 1);
297        assert_matches!(poh.tick(), Some(PohEntry { num_hashes: 2, .. }));
298        assert_eq!(poh.remaining_hashes, 2); // Ready for the next tick
299    }
300
301    #[test]
302    fn test_poh_tick_large_batch() {
303        let mut poh = Poh::new(Hash::default(), Some(2));
304        assert_eq!(poh.remaining_hashes, 2);
305        assert!(poh.hash(1_000_000)); // Stop hashing before the next tick
306        assert_eq!(poh.remaining_hashes, 1);
307        assert!(poh.hash(1_000_000)); // Does nothing...
308        assert_eq!(poh.remaining_hashes, 1);
309        poh.tick();
310        assert_eq!(poh.remaining_hashes, 2); // Ready for the next tick
311    }
312
313    #[test]
314    fn test_poh_tick_too_soon() {
315        let mut poh = Poh::new(Hash::default(), Some(2));
316        assert_eq!(poh.remaining_hashes, 2);
317        assert!(poh.tick().is_none());
318    }
319
320    #[test]
321    fn test_poh_record_not_permitted_at_final_hash() {
322        let mut poh = Poh::new(Hash::default(), Some(10));
323        assert!(poh.hash(9));
324        assert_eq!(poh.remaining_hashes, 1);
325        assert!(poh.record(Hash::default()).is_none()); // <-- record() rejected to avoid exceeding hashes_per_tick
326        assert_matches!(poh.tick(), Some(PohEntry { num_hashes: 10, .. }));
327        assert_matches!(
328            poh.record(Hash::default()),
329            Some(PohEntry { num_hashes: 1, .. }) // <-- record() ok
330        );
331        assert_eq!(poh.remaining_hashes, 9);
332    }
333}