solana_runtime/
stake_weighted_timestamp.rs

1/// A helper for calculating a stake-weighted timestamp estimate from a set of timestamps and epoch
2/// stake.
3use solana_sdk::{
4    clock::{Slot, UnixTimestamp},
5    pubkey::Pubkey,
6};
7use std::{
8    borrow::Borrow,
9    collections::{BTreeMap, HashMap},
10    time::Duration,
11};
12
13// Obsolete limits
14const _MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 50;
15const _MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW: u32 = 80;
16
17pub(crate) const MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST: u32 = 25;
18pub(crate) const MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW_V2: u32 = 150;
19
20#[derive(Copy, Clone)]
21pub(crate) struct MaxAllowableDrift {
22    pub fast: u32, // Max allowable drift percentage faster than poh estimate
23    pub slow: u32, // Max allowable drift percentage slower than poh estimate
24}
25
26pub(crate) fn calculate_stake_weighted_timestamp<I, K, V, T>(
27    unique_timestamps: I,
28    stakes: &HashMap<Pubkey, (u64, T /*Account|VoteAccount*/)>,
29    slot: Slot,
30    slot_duration: Duration,
31    epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
32    max_allowable_drift: MaxAllowableDrift,
33    fix_estimate_into_u64: bool,
34) -> Option<UnixTimestamp>
35where
36    I: IntoIterator<Item = (K, V)>,
37    K: Borrow<Pubkey>,
38    V: Borrow<(Slot, UnixTimestamp)>,
39{
40    let mut stake_per_timestamp: BTreeMap<UnixTimestamp, u128> = BTreeMap::new();
41    let mut total_stake: u128 = 0;
42    for (vote_pubkey, slot_timestamp) in unique_timestamps {
43        let (timestamp_slot, timestamp) = slot_timestamp.borrow();
44        let offset = slot_duration.saturating_mul(slot.saturating_sub(*timestamp_slot) as u32);
45        let estimate = timestamp.saturating_add(offset.as_secs() as i64);
46        let stake = stakes
47            .get(vote_pubkey.borrow())
48            .map(|(stake, _account)| stake)
49            .unwrap_or(&0);
50        stake_per_timestamp
51            .entry(estimate)
52            .and_modify(|stake_sum| *stake_sum = stake_sum.saturating_add(*stake as u128))
53            .or_insert(*stake as u128);
54        total_stake = total_stake.saturating_add(*stake as u128);
55    }
56    if total_stake == 0 {
57        return None;
58    }
59    let mut stake_accumulator: u128 = 0;
60    let mut estimate = 0;
61    // Populate `estimate` with stake-weighted median timestamp
62    for (timestamp, stake) in stake_per_timestamp.into_iter() {
63        stake_accumulator = stake_accumulator.saturating_add(stake);
64        if stake_accumulator > total_stake / 2 {
65            estimate = timestamp;
66            break;
67        }
68    }
69    // Bound estimate by `max_allowable_drift` since the start of the epoch
70    if let Some((epoch_start_slot, epoch_start_timestamp)) = epoch_start_timestamp {
71        let poh_estimate_offset =
72            slot_duration.saturating_mul(slot.saturating_sub(epoch_start_slot) as u32);
73        let estimate_offset = Duration::from_secs(if fix_estimate_into_u64 {
74            (estimate as u64).saturating_sub(epoch_start_timestamp as u64)
75        } else {
76            estimate.saturating_sub(epoch_start_timestamp) as u64
77        });
78        let max_allowable_drift_fast =
79            poh_estimate_offset.saturating_mul(max_allowable_drift.fast) / 100;
80        let max_allowable_drift_slow =
81            poh_estimate_offset.saturating_mul(max_allowable_drift.slow) / 100;
82        if estimate_offset > poh_estimate_offset
83            && estimate_offset.saturating_sub(poh_estimate_offset) > max_allowable_drift_slow
84        {
85            // estimate offset since the start of the epoch is higher than
86            // `max_allowable_drift_slow`
87            estimate = epoch_start_timestamp
88                .saturating_add(poh_estimate_offset.as_secs() as i64)
89                .saturating_add(max_allowable_drift_slow.as_secs() as i64);
90        } else if estimate_offset < poh_estimate_offset
91            && poh_estimate_offset.saturating_sub(estimate_offset) > max_allowable_drift_fast
92        {
93            // estimate offset since the start of the epoch is lower than
94            // `max_allowable_drift_fast`
95            estimate = epoch_start_timestamp
96                .saturating_add(poh_estimate_offset.as_secs() as i64)
97                .saturating_sub(max_allowable_drift_fast.as_secs() as i64);
98        }
99    }
100    Some(estimate)
101}
102
103#[cfg(test)]
104pub mod tests {
105    use {
106        super::*,
107        solana_sdk::{account::Account, native_token::sol_to_lamports},
108    };
109
110    #[test]
111    fn test_calculate_stake_weighted_timestamp_uses_median() {
112        let recent_timestamp: UnixTimestamp = 1_578_909_061;
113        let slot = 5;
114        let slot_duration = Duration::from_millis(400);
115        let pubkey0 = solana_pubkey::new_rand();
116        let pubkey1 = solana_pubkey::new_rand();
117        let pubkey2 = solana_pubkey::new_rand();
118        let pubkey3 = solana_pubkey::new_rand();
119        let pubkey4 = solana_pubkey::new_rand();
120        let max_allowable_drift = MaxAllowableDrift { fast: 25, slow: 25 };
121
122        // Test low-staked outlier(s)
123        let stakes: HashMap<Pubkey, (u64, Account)> = [
124            (
125                pubkey0,
126                (sol_to_lamports(1.0), Account::new(1, 0, &Pubkey::default())),
127            ),
128            (
129                pubkey1,
130                (sol_to_lamports(1.0), Account::new(1, 0, &Pubkey::default())),
131            ),
132            (
133                pubkey2,
134                (
135                    sol_to_lamports(1_000_000.0),
136                    Account::new(1, 0, &Pubkey::default()),
137                ),
138            ),
139            (
140                pubkey3,
141                (
142                    sol_to_lamports(1_000_000.0),
143                    Account::new(1, 0, &Pubkey::default()),
144                ),
145            ),
146            (
147                pubkey4,
148                (
149                    sol_to_lamports(1_000_000.0),
150                    Account::new(1, 0, &Pubkey::default()),
151                ),
152            ),
153        ]
154        .iter()
155        .cloned()
156        .collect();
157
158        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
159            (pubkey0, (5, 0)),
160            (pubkey1, (5, recent_timestamp)),
161            (pubkey2, (5, recent_timestamp)),
162            (pubkey3, (5, recent_timestamp)),
163            (pubkey4, (5, recent_timestamp)),
164        ]
165        .iter()
166        .cloned()
167        .collect();
168
169        let bounded = calculate_stake_weighted_timestamp(
170            &unique_timestamps,
171            &stakes,
172            slot as Slot,
173            slot_duration,
174            None,
175            max_allowable_drift,
176            true,
177        )
178        .unwrap();
179        // With no bounding, timestamp w/ 0.00003% of the stake can shift the timestamp backward 8min
180        assert_eq!(bounded, recent_timestamp); // low-staked outlier cannot affect bounded timestamp
181
182        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
183            (pubkey0, (5, recent_timestamp)),
184            (pubkey1, (5, i64::MAX)),
185            (pubkey2, (5, recent_timestamp)),
186            (pubkey3, (5, recent_timestamp)),
187            (pubkey4, (5, recent_timestamp)),
188        ]
189        .iter()
190        .cloned()
191        .collect();
192
193        let bounded = calculate_stake_weighted_timestamp(
194            &unique_timestamps,
195            &stakes,
196            slot as Slot,
197            slot_duration,
198            None,
199            max_allowable_drift,
200            true,
201        )
202        .unwrap();
203        // With no bounding, timestamp w/ 0.00003% of the stake can shift the timestamp forward 97k years!
204        assert_eq!(bounded, recent_timestamp); // low-staked outlier cannot affect bounded timestamp
205
206        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
207            (pubkey0, (5, 0)),
208            (pubkey1, (5, i64::MAX)),
209            (pubkey2, (5, recent_timestamp)),
210            (pubkey3, (5, recent_timestamp)),
211            (pubkey4, (5, recent_timestamp)),
212        ]
213        .iter()
214        .cloned()
215        .collect();
216
217        let bounded = calculate_stake_weighted_timestamp(
218            &unique_timestamps,
219            &stakes,
220            slot as Slot,
221            slot_duration,
222            None,
223            max_allowable_drift,
224            true,
225        )
226        .unwrap();
227        assert_eq!(bounded, recent_timestamp); // multiple low-staked outliers cannot affect bounded timestamp if they don't shift the median
228
229        // Test higher-staked outlier(s)
230        let stakes: HashMap<Pubkey, (u64, Account)> = [
231            (
232                pubkey0,
233                (
234                    sol_to_lamports(1_000_000.0), // 1/3 stake
235                    Account::new(1, 0, &Pubkey::default()),
236                ),
237            ),
238            (
239                pubkey1,
240                (
241                    sol_to_lamports(1_000_000.0),
242                    Account::new(1, 0, &Pubkey::default()),
243                ),
244            ),
245            (
246                pubkey2,
247                (
248                    sol_to_lamports(1_000_000.0),
249                    Account::new(1, 0, &Pubkey::default()),
250                ),
251            ),
252        ]
253        .iter()
254        .cloned()
255        .collect();
256
257        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
258            (pubkey0, (5, 0)),
259            (pubkey1, (5, i64::MAX)),
260            (pubkey2, (5, recent_timestamp)),
261        ]
262        .iter()
263        .cloned()
264        .collect();
265
266        let bounded = calculate_stake_weighted_timestamp(
267            &unique_timestamps,
268            &stakes,
269            slot as Slot,
270            slot_duration,
271            None,
272            max_allowable_drift,
273            true,
274        )
275        .unwrap();
276        assert_eq!(bounded, recent_timestamp); // outlier(s) cannot affect bounded timestamp if they don't shift the median
277
278        let stakes: HashMap<Pubkey, (u64, Account)> = [
279            (
280                pubkey0,
281                (
282                    sol_to_lamports(1_000_001.0), // 1/3 stake
283                    Account::new(1, 0, &Pubkey::default()),
284                ),
285            ),
286            (
287                pubkey1,
288                (
289                    sol_to_lamports(1_000_000.0),
290                    Account::new(1, 0, &Pubkey::default()),
291                ),
292            ),
293        ]
294        .iter()
295        .cloned()
296        .collect();
297
298        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> =
299            [(pubkey0, (5, 0)), (pubkey1, (5, recent_timestamp))]
300                .iter()
301                .cloned()
302                .collect();
303
304        let bounded = calculate_stake_weighted_timestamp(
305            &unique_timestamps,
306            &stakes,
307            slot as Slot,
308            slot_duration,
309            None,
310            max_allowable_drift,
311            true,
312        )
313        .unwrap();
314        assert_eq!(recent_timestamp - bounded, 1578909061); // outliers > 1/2 of available stake can affect timestamp
315    }
316
317    #[test]
318    fn test_calculate_stake_weighted_timestamp_poh() {
319        let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
320        let slot = 20;
321        let slot_duration = Duration::from_millis(400);
322        let poh_offset = (slot * slot_duration).as_secs();
323        let max_allowable_drift_percentage = 25;
324        let max_allowable_drift = MaxAllowableDrift {
325            fast: max_allowable_drift_percentage,
326            slow: max_allowable_drift_percentage,
327        };
328        let acceptable_delta = (max_allowable_drift_percentage * poh_offset as u32 / 100) as i64;
329        let poh_estimate = epoch_start_timestamp + poh_offset as i64;
330        let pubkey0 = solana_pubkey::new_rand();
331        let pubkey1 = solana_pubkey::new_rand();
332        let pubkey2 = solana_pubkey::new_rand();
333
334        let stakes: HashMap<Pubkey, (u64, Account)> = [
335            (
336                pubkey0,
337                (
338                    sol_to_lamports(1_000_000.0),
339                    Account::new(1, 0, &Pubkey::default()),
340                ),
341            ),
342            (
343                pubkey1,
344                (
345                    sol_to_lamports(1_000_000.0),
346                    Account::new(1, 0, &Pubkey::default()),
347                ),
348            ),
349            (
350                pubkey2,
351                (
352                    sol_to_lamports(1_000_000.0),
353                    Account::new(1, 0, &Pubkey::default()),
354                ),
355            ),
356        ]
357        .iter()
358        .cloned()
359        .collect();
360
361        // Test when stake-weighted median is too high
362        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
363            (pubkey0, (slot as u64, poh_estimate + acceptable_delta + 1)),
364            (pubkey1, (slot as u64, poh_estimate + acceptable_delta + 1)),
365            (pubkey2, (slot as u64, poh_estimate + acceptable_delta + 1)),
366        ]
367        .iter()
368        .cloned()
369        .collect();
370
371        let bounded = calculate_stake_weighted_timestamp(
372            &unique_timestamps,
373            &stakes,
374            slot as Slot,
375            slot_duration,
376            Some((0, epoch_start_timestamp)),
377            max_allowable_drift,
378            true,
379        )
380        .unwrap();
381        assert_eq!(bounded, poh_estimate + acceptable_delta);
382
383        // Test when stake-weighted median is too low
384        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
385            (pubkey0, (slot as u64, poh_estimate - acceptable_delta - 1)),
386            (pubkey1, (slot as u64, poh_estimate - acceptable_delta - 1)),
387            (pubkey2, (slot as u64, poh_estimate - acceptable_delta - 1)),
388        ]
389        .iter()
390        .cloned()
391        .collect();
392
393        let bounded = calculate_stake_weighted_timestamp(
394            &unique_timestamps,
395            &stakes,
396            slot as Slot,
397            slot_duration,
398            Some((0, epoch_start_timestamp)),
399            max_allowable_drift,
400            true,
401        )
402        .unwrap();
403        assert_eq!(bounded, poh_estimate - acceptable_delta);
404
405        // Test stake-weighted median within bounds
406        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
407            (pubkey0, (slot as u64, poh_estimate + acceptable_delta)),
408            (pubkey1, (slot as u64, poh_estimate + acceptable_delta)),
409            (pubkey2, (slot as u64, poh_estimate + acceptable_delta)),
410        ]
411        .iter()
412        .cloned()
413        .collect();
414
415        let bounded = calculate_stake_weighted_timestamp(
416            &unique_timestamps,
417            &stakes,
418            slot as Slot,
419            slot_duration,
420            Some((0, epoch_start_timestamp)),
421            max_allowable_drift,
422            true,
423        )
424        .unwrap();
425        assert_eq!(bounded, poh_estimate + acceptable_delta);
426
427        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
428            (pubkey0, (slot as u64, poh_estimate - acceptable_delta)),
429            (pubkey1, (slot as u64, poh_estimate - acceptable_delta)),
430            (pubkey2, (slot as u64, poh_estimate - acceptable_delta)),
431        ]
432        .iter()
433        .cloned()
434        .collect();
435
436        let bounded = calculate_stake_weighted_timestamp(
437            &unique_timestamps,
438            &stakes,
439            slot as Slot,
440            slot_duration,
441            Some((0, epoch_start_timestamp)),
442            max_allowable_drift,
443            true,
444        )
445        .unwrap();
446        assert_eq!(bounded, poh_estimate - acceptable_delta);
447    }
448
449    #[test]
450    fn test_calculate_stake_weighted_timestamp_levels() {
451        let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
452        let slot = 20;
453        let slot_duration = Duration::from_millis(400);
454        let poh_offset = (slot * slot_duration).as_secs();
455        let max_allowable_drift_percentage_25 = 25;
456        let allowable_drift_25 = MaxAllowableDrift {
457            fast: max_allowable_drift_percentage_25,
458            slow: max_allowable_drift_percentage_25,
459        };
460        let max_allowable_drift_percentage_50 = 50;
461        let allowable_drift_50 = MaxAllowableDrift {
462            fast: max_allowable_drift_percentage_50,
463            slow: max_allowable_drift_percentage_50,
464        };
465        let acceptable_delta_25 =
466            (max_allowable_drift_percentage_25 * poh_offset as u32 / 100) as i64;
467        let acceptable_delta_50 =
468            (max_allowable_drift_percentage_50 * poh_offset as u32 / 100) as i64;
469        assert!(acceptable_delta_50 > acceptable_delta_25 + 1);
470        let poh_estimate = epoch_start_timestamp + poh_offset as i64;
471        let pubkey0 = solana_pubkey::new_rand();
472        let pubkey1 = solana_pubkey::new_rand();
473        let pubkey2 = solana_pubkey::new_rand();
474
475        let stakes: HashMap<Pubkey, (u64, Account)> = [
476            (
477                pubkey0,
478                (
479                    sol_to_lamports(1_000_000.0),
480                    Account::new(1, 0, &Pubkey::default()),
481                ),
482            ),
483            (
484                pubkey1,
485                (
486                    sol_to_lamports(1_000_000.0),
487                    Account::new(1, 0, &Pubkey::default()),
488                ),
489            ),
490            (
491                pubkey2,
492                (
493                    sol_to_lamports(1_000_000.0),
494                    Account::new(1, 0, &Pubkey::default()),
495                ),
496            ),
497        ]
498        .iter()
499        .cloned()
500        .collect();
501
502        // Test when stake-weighted median is above 25% deviance but below 50% deviance
503        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
504            (
505                pubkey0,
506                (slot as u64, poh_estimate + acceptable_delta_25 + 1),
507            ),
508            (
509                pubkey1,
510                (slot as u64, poh_estimate + acceptable_delta_25 + 1),
511            ),
512            (
513                pubkey2,
514                (slot as u64, poh_estimate + acceptable_delta_25 + 1),
515            ),
516        ]
517        .iter()
518        .cloned()
519        .collect();
520
521        let bounded = calculate_stake_weighted_timestamp(
522            &unique_timestamps,
523            &stakes,
524            slot as Slot,
525            slot_duration,
526            Some((0, epoch_start_timestamp)),
527            allowable_drift_25,
528            true,
529        )
530        .unwrap();
531        assert_eq!(bounded, poh_estimate + acceptable_delta_25);
532
533        let bounded = calculate_stake_weighted_timestamp(
534            &unique_timestamps,
535            &stakes,
536            slot as Slot,
537            slot_duration,
538            Some((0, epoch_start_timestamp)),
539            allowable_drift_50,
540            true,
541        )
542        .unwrap();
543        assert_eq!(bounded, poh_estimate + acceptable_delta_25 + 1);
544
545        // Test when stake-weighted median is above 50% deviance
546        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
547            (
548                pubkey0,
549                (slot as u64, poh_estimate + acceptable_delta_50 + 1),
550            ),
551            (
552                pubkey1,
553                (slot as u64, poh_estimate + acceptable_delta_50 + 1),
554            ),
555            (
556                pubkey2,
557                (slot as u64, poh_estimate + acceptable_delta_50 + 1),
558            ),
559        ]
560        .iter()
561        .cloned()
562        .collect();
563
564        let bounded = calculate_stake_weighted_timestamp(
565            &unique_timestamps,
566            &stakes,
567            slot as Slot,
568            slot_duration,
569            Some((0, epoch_start_timestamp)),
570            allowable_drift_25,
571            true,
572        )
573        .unwrap();
574        assert_eq!(bounded, poh_estimate + acceptable_delta_25);
575
576        let bounded = calculate_stake_weighted_timestamp(
577            &unique_timestamps,
578            &stakes,
579            slot as Slot,
580            slot_duration,
581            Some((0, epoch_start_timestamp)),
582            allowable_drift_50,
583            true,
584        )
585        .unwrap();
586        assert_eq!(bounded, poh_estimate + acceptable_delta_50);
587    }
588
589    #[test]
590    fn test_calculate_stake_weighted_timestamp_fast_slow() {
591        let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
592        let slot = 20;
593        let slot_duration = Duration::from_millis(400);
594        let poh_offset = (slot * slot_duration).as_secs();
595        let max_allowable_drift_percentage_25 = 25;
596        let max_allowable_drift_percentage_50 = 50;
597        let max_allowable_drift = MaxAllowableDrift {
598            fast: max_allowable_drift_percentage_25,
599            slow: max_allowable_drift_percentage_50,
600        };
601        let acceptable_delta_fast =
602            (max_allowable_drift_percentage_25 * poh_offset as u32 / 100) as i64;
603        let acceptable_delta_slow =
604            (max_allowable_drift_percentage_50 * poh_offset as u32 / 100) as i64;
605        assert!(acceptable_delta_slow > acceptable_delta_fast + 1);
606        let poh_estimate = epoch_start_timestamp + poh_offset as i64;
607        let pubkey0 = solana_pubkey::new_rand();
608        let pubkey1 = solana_pubkey::new_rand();
609        let pubkey2 = solana_pubkey::new_rand();
610
611        let stakes: HashMap<Pubkey, (u64, Account)> = [
612            (
613                pubkey0,
614                (
615                    sol_to_lamports(1_000_000.0),
616                    Account::new(1, 0, &Pubkey::default()),
617                ),
618            ),
619            (
620                pubkey1,
621                (
622                    sol_to_lamports(1_000_000.0),
623                    Account::new(1, 0, &Pubkey::default()),
624                ),
625            ),
626            (
627                pubkey2,
628                (
629                    sol_to_lamports(1_000_000.0),
630                    Account::new(1, 0, &Pubkey::default()),
631                ),
632            ),
633        ]
634        .iter()
635        .cloned()
636        .collect();
637
638        // Test when stake-weighted median is more than 25% fast
639        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
640            (
641                pubkey0,
642                (slot as u64, poh_estimate - acceptable_delta_fast - 1),
643            ),
644            (
645                pubkey1,
646                (slot as u64, poh_estimate - acceptable_delta_fast - 1),
647            ),
648            (
649                pubkey2,
650                (slot as u64, poh_estimate - acceptable_delta_fast - 1),
651            ),
652        ]
653        .iter()
654        .cloned()
655        .collect();
656
657        let bounded = calculate_stake_weighted_timestamp(
658            &unique_timestamps,
659            &stakes,
660            slot as Slot,
661            slot_duration,
662            Some((0, epoch_start_timestamp)),
663            max_allowable_drift,
664            true,
665        )
666        .unwrap();
667        assert_eq!(bounded, poh_estimate - acceptable_delta_fast);
668
669        // Test when stake-weighted median is more than 25% but less than 50% slow
670        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
671            (
672                pubkey0,
673                (slot as u64, poh_estimate + acceptable_delta_fast + 1),
674            ),
675            (
676                pubkey1,
677                (slot as u64, poh_estimate + acceptable_delta_fast + 1),
678            ),
679            (
680                pubkey2,
681                (slot as u64, poh_estimate + acceptable_delta_fast + 1),
682            ),
683        ]
684        .iter()
685        .cloned()
686        .collect();
687
688        let bounded = calculate_stake_weighted_timestamp(
689            &unique_timestamps,
690            &stakes,
691            slot as Slot,
692            slot_duration,
693            Some((0, epoch_start_timestamp)),
694            max_allowable_drift,
695            true,
696        )
697        .unwrap();
698        assert_eq!(bounded, poh_estimate + acceptable_delta_fast + 1);
699
700        // Test when stake-weighted median is more than 50% slow
701        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
702            (
703                pubkey0,
704                (slot as u64, poh_estimate + acceptable_delta_slow + 1),
705            ),
706            (
707                pubkey1,
708                (slot as u64, poh_estimate + acceptable_delta_slow + 1),
709            ),
710            (
711                pubkey2,
712                (slot as u64, poh_estimate + acceptable_delta_slow + 1),
713            ),
714        ]
715        .iter()
716        .cloned()
717        .collect();
718
719        let bounded = calculate_stake_weighted_timestamp(
720            &unique_timestamps,
721            &stakes,
722            slot as Slot,
723            slot_duration,
724            Some((0, epoch_start_timestamp)),
725            max_allowable_drift,
726            true,
727        )
728        .unwrap();
729        assert_eq!(bounded, poh_estimate + acceptable_delta_slow);
730    }
731
732    #[test]
733    fn test_calculate_stake_weighted_timestamp_early() {
734        let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
735        let slot = 20;
736        let slot_duration = Duration::from_millis(400);
737        let poh_offset = (slot * slot_duration).as_secs();
738        let max_allowable_drift_percentage = 50;
739        let max_allowable_drift = MaxAllowableDrift {
740            fast: max_allowable_drift_percentage,
741            slow: max_allowable_drift_percentage,
742        };
743        let acceptable_delta = (max_allowable_drift_percentage * poh_offset as u32 / 100) as i64;
744        let poh_estimate = epoch_start_timestamp + poh_offset as i64;
745        let pubkey0 = solana_pubkey::new_rand();
746        let pubkey1 = solana_pubkey::new_rand();
747        let pubkey2 = solana_pubkey::new_rand();
748
749        let stakes: HashMap<Pubkey, (u64, Account)> = [
750            (
751                pubkey0,
752                (
753                    sol_to_lamports(1_000_000.0),
754                    Account::new(1, 0, &Pubkey::default()),
755                ),
756            ),
757            (
758                pubkey1,
759                (
760                    sol_to_lamports(1_000_000.0),
761                    Account::new(1, 0, &Pubkey::default()),
762                ),
763            ),
764            (
765                pubkey2,
766                (
767                    sol_to_lamports(1_000_000.0),
768                    Account::new(1, 0, &Pubkey::default()),
769                ),
770            ),
771        ]
772        .iter()
773        .cloned()
774        .collect();
775
776        // Test when stake-weighted median is before epoch_start_timestamp
777        let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
778            (pubkey0, (slot as u64, poh_estimate - acceptable_delta - 20)),
779            (pubkey1, (slot as u64, poh_estimate - acceptable_delta - 20)),
780            (pubkey2, (slot as u64, poh_estimate - acceptable_delta - 20)),
781        ]
782        .iter()
783        .cloned()
784        .collect();
785
786        // Without fix, median timestamps before epoch_start_timestamp actually increase the time
787        // estimate due to incorrect casting.
788        let bounded = calculate_stake_weighted_timestamp(
789            &unique_timestamps,
790            &stakes,
791            slot as Slot,
792            slot_duration,
793            Some((0, epoch_start_timestamp)),
794            max_allowable_drift,
795            false,
796        )
797        .unwrap();
798        assert_eq!(bounded, poh_estimate + acceptable_delta);
799
800        let bounded = calculate_stake_weighted_timestamp(
801            &unique_timestamps,
802            &stakes,
803            slot as Slot,
804            slot_duration,
805            Some((0, epoch_start_timestamp)),
806            max_allowable_drift,
807            true,
808        )
809        .unwrap();
810        assert_eq!(bounded, poh_estimate - acceptable_delta);
811    }
812}