Skip to main content

solana_entry/
poh.rs

1//! The `Poh` module provides an object for generating a Proof of History.
2use {
3    log::*,
4    solana_hash::Hash,
5    solana_sha256_hasher::{hash, hashv},
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_until_tick: 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_until_tick: 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 hashes_per_tick_config(&self) -> Option<u64> {
56        (self.hashes_per_tick != LOW_POWER_MODE).then_some(self.hashes_per_tick)
57    }
58
59    pub fn target_poh_time(&self, target_ns_per_tick: u64) -> Instant {
60        assert!(self.hashes_per_tick > 0);
61        let offset_tick_ns = target_ns_per_tick * self.tick_number;
62        let offset_ns = target_ns_per_tick * self.num_hashes / self.hashes_per_tick;
63        self.slot_start_time + Duration::from_nanos(offset_ns + offset_tick_ns)
64    }
65
66    /// Return `true` if the caller needs to `tick()` next, i.e. if the
67    /// remaining_hashes is 1.
68    pub fn hash(&mut self, max_num_hashes: u64) -> bool {
69        let num_hashes = std::cmp::min(self.remaining_hashes_until_tick - 1, max_num_hashes);
70
71        for _ in 0..num_hashes {
72            self.hash = hash(self.hash.as_ref());
73        }
74        self.num_hashes += num_hashes;
75        self.remaining_hashes_until_tick -= num_hashes;
76
77        assert!(self.remaining_hashes_until_tick > 0);
78        self.remaining_hashes_until_tick == 1
79    }
80
81    pub fn record(&mut self, mixin: Hash) -> Option<PohEntry> {
82        if self.remaining_hashes_until_tick == 1 {
83            return None; // Caller needs to `tick()` first
84        }
85
86        self.hash = hashv(&[self.hash.as_ref(), mixin.as_ref()]);
87        let num_hashes = self.num_hashes + 1;
88        self.num_hashes = 0;
89        self.remaining_hashes_until_tick -= 1;
90
91        Some(PohEntry {
92            num_hashes,
93            hash: self.hash,
94        })
95    }
96
97    /// Returns `true` if the batches were recorded successfully and `false` if the batches
98    /// were not recorded because there were not enough hashes remaining to record all `mixins`.
99    /// If `true` is returned, the `entries` vector will be populated with the `PohEntry`s for each
100    /// batch. If `false` is returned, the `entries` vector will not be modified.
101    pub fn record_batches(&mut self, mixins: &[Hash], entries: &mut Vec<PohEntry>) -> bool {
102        let num_mixins = mixins.len() as u64;
103        debug_assert_ne!(num_mixins, 0, "mixins.len() == 0");
104
105        if self.remaining_hashes_until_tick < num_mixins + 1 {
106            return false; // Not enough hashes remaining to record all mixins
107        }
108
109        entries.clear();
110        entries.reserve(mixins.len());
111
112        // The first entry will have the current number of hashes plus one.
113        // All subsequent entries will have 1.
114        let mut num_hashes = self.num_hashes + 1;
115        entries.extend(mixins.iter().map(|mixin| {
116            self.hash = hashv(&[self.hash.as_ref(), mixin.as_ref()]);
117            let entry = PohEntry {
118                num_hashes,
119                hash: self.hash,
120            };
121
122            num_hashes = 1;
123            entry
124        }));
125
126        self.num_hashes = 0;
127        self.remaining_hashes_until_tick -= num_mixins;
128
129        true
130    }
131
132    pub fn tick(&mut self) -> Option<PohEntry> {
133        self.hash = hash(self.hash.as_ref());
134        self.num_hashes += 1;
135        self.remaining_hashes_until_tick -= 1;
136
137        // If we are in low power mode then always generate a tick.
138        // Otherwise only tick if there are no remaining hashes
139        if self.hashes_per_tick != LOW_POWER_MODE && self.remaining_hashes_until_tick != 0 {
140            return None;
141        }
142
143        let num_hashes = self.num_hashes;
144        self.remaining_hashes_until_tick = self.hashes_per_tick;
145        self.num_hashes = 0;
146        self.tick_number += 1;
147        Some(PohEntry {
148            num_hashes,
149            hash: self.hash,
150        })
151    }
152
153    pub fn remaining_hashes_in_slot(&self, ticks_per_slot: u64) -> u64 {
154        // ticks_per_slot must be a power of two so we can use a bitmask
155        debug_assert!(ticks_per_slot.is_power_of_two() && ticks_per_slot > 0);
156        ticks_per_slot
157            .saturating_sub((self.tick_number & (ticks_per_slot.wrapping_sub(1))).wrapping_add(1))
158            .wrapping_mul(self.hashes_per_tick)
159            .wrapping_add(self.remaining_hashes_until_tick)
160    }
161}
162
163pub fn compute_hash_time(hashes_sample_size: u64) -> Duration {
164    info!("Running {hashes_sample_size} hashes...");
165    let mut v = Hash::default();
166    let start = Instant::now();
167    for _ in 0..hashes_sample_size {
168        v = hash(v.as_ref());
169    }
170    start.elapsed()
171}
172
173pub fn compute_hashes_per_tick(duration: Duration, hashes_sample_size: u64) -> u64 {
174    let elapsed_ms = compute_hash_time(hashes_sample_size).as_millis() as u64;
175    duration.as_millis() as u64 * hashes_sample_size / elapsed_ms
176}
177
178#[cfg(test)]
179mod tests {
180    use {
181        crate::poh::{Poh, PohEntry},
182        assert_matches::assert_matches,
183        solana_hash::Hash,
184        solana_sha256_hasher::{hash, hashv},
185        std::time::Duration,
186    };
187
188    fn verify(initial_hash: Hash, entries: &[(PohEntry, Option<Hash>)]) -> bool {
189        let mut current_hash = initial_hash;
190
191        for (entry, mixin) in entries {
192            assert_ne!(entry.num_hashes, 0);
193
194            for _ in 1..entry.num_hashes {
195                current_hash = hash(current_hash.as_ref());
196            }
197            current_hash = match mixin {
198                Some(mixin) => hashv(&[current_hash.as_ref(), mixin.as_ref()]),
199                None => hash(current_hash.as_ref()),
200            };
201            if current_hash != entry.hash {
202                return false;
203            }
204        }
205
206        true
207    }
208
209    #[test]
210    fn test_target_poh_time() {
211        let zero = Hash::default();
212        for target_ns_per_tick in 10..12 {
213            let mut poh = Poh::new(zero, None);
214            assert_eq!(poh.target_poh_time(target_ns_per_tick), poh.slot_start_time);
215            poh.tick_number = 2;
216            assert_eq!(
217                poh.target_poh_time(target_ns_per_tick),
218                poh.slot_start_time + Duration::from_nanos(target_ns_per_tick * 2)
219            );
220            let mut poh = Poh::new(zero, Some(5));
221            assert_eq!(poh.target_poh_time(target_ns_per_tick), poh.slot_start_time);
222            poh.tick_number = 2;
223            assert_eq!(
224                poh.target_poh_time(target_ns_per_tick),
225                poh.slot_start_time + Duration::from_nanos(target_ns_per_tick * 2)
226            );
227            poh.num_hashes = 3;
228            assert_eq!(
229                poh.target_poh_time(target_ns_per_tick),
230                poh.slot_start_time
231                    + Duration::from_nanos(target_ns_per_tick * 2 + target_ns_per_tick * 3 / 5)
232            );
233        }
234    }
235
236    #[test]
237    #[should_panic(expected = "hashes_per_tick > 1")]
238    fn test_target_poh_time_hashes_per_tick() {
239        let zero = Hash::default();
240        let poh = Poh::new(zero, Some(0));
241        let target_ns_per_tick = 10;
242        poh.target_poh_time(target_ns_per_tick);
243    }
244
245    #[test]
246    fn test_poh_verify() {
247        let zero = Hash::default();
248        let one = hash(zero.as_ref());
249        let two = hash(one.as_ref());
250        let one_with_zero = hashv(&[zero.as_ref(), zero.as_ref()]);
251
252        let mut poh = Poh::new(zero, None);
253        assert!(verify(
254            zero,
255            &[
256                (poh.tick().unwrap(), None),
257                (poh.record(zero).unwrap(), Some(zero)),
258                (poh.record(zero).unwrap(), Some(zero)),
259                (poh.tick().unwrap(), None),
260            ],
261        ));
262
263        assert!(verify(
264            zero,
265            &[(
266                PohEntry {
267                    num_hashes: 1,
268                    hash: one,
269                },
270                None
271            )],
272        ));
273        assert!(verify(
274            zero,
275            &[(
276                PohEntry {
277                    num_hashes: 2,
278                    hash: two,
279                },
280                None
281            )]
282        ));
283
284        assert!(verify(
285            zero,
286            &[(
287                PohEntry {
288                    num_hashes: 1,
289                    hash: one_with_zero,
290                },
291                Some(zero)
292            )]
293        ));
294        assert!(!verify(
295            zero,
296            &[(
297                PohEntry {
298                    num_hashes: 1,
299                    hash: zero,
300                },
301                None
302            )]
303        ));
304
305        assert!(verify(
306            zero,
307            &[
308                (
309                    PohEntry {
310                        num_hashes: 1,
311                        hash: one_with_zero,
312                    },
313                    Some(zero)
314                ),
315                (
316                    PohEntry {
317                        num_hashes: 1,
318                        hash: hash(one_with_zero.as_ref()),
319                    },
320                    None
321                )
322            ]
323        ));
324    }
325
326    #[test]
327    #[should_panic]
328    fn test_poh_verify_assert() {
329        verify(
330            Hash::default(),
331            &[(
332                PohEntry {
333                    num_hashes: 0,
334                    hash: Hash::default(),
335                },
336                None,
337            )],
338        );
339    }
340
341    #[test]
342    fn test_poh_tick() {
343        let mut poh = Poh::new(Hash::default(), Some(2));
344        assert_eq!(poh.remaining_hashes_until_tick, 2);
345        assert!(poh.tick().is_none());
346        assert_eq!(poh.remaining_hashes_until_tick, 1);
347        assert_matches!(poh.tick(), Some(PohEntry { num_hashes: 2, .. }));
348        assert_eq!(poh.remaining_hashes_until_tick, 2); // Ready for the next tick
349    }
350
351    #[test]
352    fn test_poh_tick_large_batch() {
353        let mut poh = Poh::new(Hash::default(), Some(2));
354        assert_eq!(poh.remaining_hashes_until_tick, 2);
355        assert!(poh.hash(1_000_000)); // Stop hashing before the next tick
356        assert_eq!(poh.remaining_hashes_until_tick, 1);
357        assert!(poh.hash(1_000_000)); // Does nothing...
358        assert_eq!(poh.remaining_hashes_until_tick, 1);
359        assert_eq!(poh.remaining_hashes_in_slot(2), 3);
360        poh.tick();
361        assert_eq!(poh.remaining_hashes_until_tick, 2); // Ready for the next tick
362        assert_eq!(poh.remaining_hashes_in_slot(2), 2);
363    }
364
365    #[test]
366    fn test_poh_tick_too_soon() {
367        let mut poh = Poh::new(Hash::default(), Some(2));
368        assert_eq!(poh.remaining_hashes_until_tick, 2);
369        assert_eq!(poh.remaining_hashes_in_slot(2), 4);
370        assert!(poh.tick().is_none());
371    }
372
373    #[test]
374    fn test_poh_record_not_permitted_at_final_hash() {
375        let mut poh = Poh::new(Hash::default(), Some(10));
376        assert!(poh.hash(9));
377        assert_eq!(poh.remaining_hashes_until_tick, 1);
378        assert_eq!(poh.remaining_hashes_in_slot(2), 11);
379        assert!(poh.record(Hash::default()).is_none()); // <-- record() rejected to avoid exceeding hashes_per_tick
380        assert_matches!(poh.tick(), Some(PohEntry { num_hashes: 10, .. }));
381        assert_matches!(
382            poh.record(Hash::default()),
383            Some(PohEntry { num_hashes: 1, .. }) // <-- record() ok
384        );
385        assert_eq!(poh.remaining_hashes_until_tick, 9);
386        assert_eq!(poh.remaining_hashes_in_slot(2), 9);
387    }
388
389    #[test]
390    fn test_poh_record_batches() {
391        let mut poh = Poh::new(Hash::default(), Some(10));
392        assert!(!poh.hash(4));
393
394        let mut entries = Vec::with_capacity(3);
395        let dummy_hashes = [Hash::default(); 4];
396        assert!(poh.record_batches(&dummy_hashes[..3], &mut entries,));
397        assert_eq!(entries.len(), 3);
398        assert_eq!(entries[0].num_hashes, 5);
399        assert_eq!(entries[1].num_hashes, 1);
400        assert_eq!(entries[2].num_hashes, 1);
401        assert_eq!(poh.remaining_hashes_until_tick, 3);
402        assert_eq!(poh.remaining_hashes_in_slot(2), 13);
403
404        // Cannot record more than number of remaining hashes
405        assert!(!poh.record_batches(&dummy_hashes[..4], &mut entries,));
406
407        // Cannot record more than number of remaining hashes
408        assert!(!poh.record_batches(&dummy_hashes[..3], &mut entries,));
409
410        // Can record less than number of remaining hashes
411        assert!(poh.record_batches(&dummy_hashes[..2], &mut entries,));
412        assert_eq!(entries.len(), 2);
413        assert_eq!(entries[0].num_hashes, 1);
414        assert_eq!(entries[1].num_hashes, 1);
415        assert_eq!(poh.remaining_hashes_until_tick, 1);
416        assert_eq!(poh.remaining_hashes_in_slot(2), 11);
417    }
418}