2020-10-09 14:53:41 -07:00
|
|
|
/// A helper for calculating a stake-weighted timestamp estimate from a set of timestamps and epoch
|
|
|
|
/// stake.
|
|
|
|
use solana_sdk::{
|
|
|
|
clock::{Slot, UnixTimestamp},
|
|
|
|
pubkey::Pubkey,
|
|
|
|
};
|
2020-10-23 14:01:39 -07:00
|
|
|
use std::{
|
2020-12-21 11:18:19 -08:00
|
|
|
borrow::Borrow,
|
2020-10-23 14:01:39 -07:00
|
|
|
collections::{BTreeMap, HashMap},
|
|
|
|
time::Duration,
|
|
|
|
};
|
2020-10-09 14:53:41 -07:00
|
|
|
|
2021-06-21 15:14:54 -07:00
|
|
|
pub(crate) const MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 50;
|
|
|
|
pub(crate) const MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST: u32 = 25;
|
|
|
|
pub(crate) const MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW: u32 = 80;
|
2020-10-09 14:53:41 -07:00
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
#[derive(Copy, Clone)]
|
2021-06-21 15:14:54 -07:00
|
|
|
pub(crate) struct MaxAllowableDrift {
|
2021-02-09 14:49:00 -08:00
|
|
|
pub fast: u32, // Max allowable drift percentage faster than poh estimate
|
|
|
|
pub slow: u32, // Max allowable drift percentage slower than poh estimate
|
2020-10-09 14:53:41 -07:00
|
|
|
}
|
|
|
|
|
2021-06-21 15:14:54 -07:00
|
|
|
pub(crate) fn calculate_stake_weighted_timestamp<I, K, V, T>(
|
2020-12-21 11:18:19 -08:00
|
|
|
unique_timestamps: I,
|
2020-11-30 09:18:33 -08:00
|
|
|
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
|
2020-10-23 14:01:39 -07:00
|
|
|
slot: Slot,
|
|
|
|
slot_duration: Duration,
|
|
|
|
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
|
2021-02-09 14:49:00 -08:00
|
|
|
max_allowable_drift: MaxAllowableDrift,
|
|
|
|
fix_estimate_into_u64: bool,
|
2020-12-21 11:18:19 -08:00
|
|
|
) -> Option<UnixTimestamp>
|
|
|
|
where
|
|
|
|
I: IntoIterator<Item = (K, V)>,
|
|
|
|
K: Borrow<Pubkey>,
|
|
|
|
V: Borrow<(Slot, UnixTimestamp)>,
|
|
|
|
{
|
2020-10-23 14:01:39 -07:00
|
|
|
let mut stake_per_timestamp: BTreeMap<UnixTimestamp, u128> = BTreeMap::new();
|
2021-03-08 17:37:57 -08:00
|
|
|
let mut total_stake: u128 = 0;
|
2020-12-21 11:18:19 -08:00
|
|
|
for (vote_pubkey, slot_timestamp) in unique_timestamps {
|
|
|
|
let (timestamp_slot, timestamp) = slot_timestamp.borrow();
|
2021-06-21 17:11:16 -07:00
|
|
|
let offset = slot_duration.saturating_mul(slot.saturating_sub(*timestamp_slot) as u32);
|
2021-03-08 17:37:57 -08:00
|
|
|
let estimate = timestamp.saturating_add(offset.as_secs() as i64);
|
2020-10-23 14:01:39 -07:00
|
|
|
let stake = stakes
|
2020-12-21 11:18:19 -08:00
|
|
|
.get(vote_pubkey.borrow())
|
2020-10-23 14:01:39 -07:00
|
|
|
.map(|(stake, _account)| stake)
|
|
|
|
.unwrap_or(&0);
|
|
|
|
stake_per_timestamp
|
|
|
|
.entry(estimate)
|
2021-03-08 17:37:57 -08:00
|
|
|
.and_modify(|stake_sum| *stake_sum = stake_sum.saturating_add(*stake as u128))
|
2020-10-23 14:01:39 -07:00
|
|
|
.or_insert(*stake as u128);
|
2021-03-08 17:37:57 -08:00
|
|
|
total_stake = total_stake.saturating_add(*stake as u128);
|
2020-10-23 14:01:39 -07:00
|
|
|
}
|
|
|
|
if total_stake == 0 {
|
|
|
|
return None;
|
|
|
|
}
|
2021-03-08 17:37:57 -08:00
|
|
|
let mut stake_accumulator: u128 = 0;
|
2020-10-23 14:01:39 -07:00
|
|
|
let mut estimate = 0;
|
|
|
|
// Populate `estimate` with stake-weighted median timestamp
|
|
|
|
for (timestamp, stake) in stake_per_timestamp.into_iter() {
|
2021-03-08 17:37:57 -08:00
|
|
|
stake_accumulator = stake_accumulator.saturating_add(stake);
|
2020-10-23 14:01:39 -07:00
|
|
|
if stake_accumulator > total_stake / 2 {
|
|
|
|
estimate = timestamp;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-02-09 14:49:00 -08:00
|
|
|
// Bound estimate by `max_allowable_drift` since the start of the epoch
|
2020-10-23 14:01:39 -07:00
|
|
|
if let Some((epoch_start_slot, epoch_start_timestamp)) = epoch_start_timestamp {
|
2021-03-11 22:22:40 -08:00
|
|
|
let poh_estimate_offset =
|
2021-06-21 17:11:16 -07:00
|
|
|
slot_duration.saturating_mul(slot.saturating_sub(epoch_start_slot) as u32);
|
2021-02-09 14:49:00 -08:00
|
|
|
let estimate_offset = Duration::from_secs(if fix_estimate_into_u64 {
|
|
|
|
(estimate as u64).saturating_sub(epoch_start_timestamp as u64)
|
|
|
|
} else {
|
|
|
|
estimate.saturating_sub(epoch_start_timestamp) as u64
|
|
|
|
});
|
2021-03-11 22:22:40 -08:00
|
|
|
let max_allowable_drift_fast =
|
2021-06-21 17:11:16 -07:00
|
|
|
poh_estimate_offset.saturating_mul(max_allowable_drift.fast) / 100;
|
2021-03-11 22:22:40 -08:00
|
|
|
let max_allowable_drift_slow =
|
2021-06-21 17:11:16 -07:00
|
|
|
poh_estimate_offset.saturating_mul(max_allowable_drift.slow) / 100;
|
2020-10-23 14:06:57 -07:00
|
|
|
if estimate_offset > poh_estimate_offset
|
2021-06-21 17:11:16 -07:00
|
|
|
&& estimate_offset.saturating_sub(poh_estimate_offset) > max_allowable_drift_slow
|
2020-10-23 14:06:57 -07:00
|
|
|
{
|
|
|
|
// estimate offset since the start of the epoch is higher than
|
2021-02-09 14:49:00 -08:00
|
|
|
// `MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW`
|
2020-10-23 14:06:57 -07:00
|
|
|
estimate = epoch_start_timestamp
|
2021-03-08 17:37:57 -08:00
|
|
|
.saturating_add(poh_estimate_offset.as_secs() as i64)
|
|
|
|
.saturating_add(max_allowable_drift_slow.as_secs() as i64);
|
2020-10-23 14:06:57 -07:00
|
|
|
} else if estimate_offset < poh_estimate_offset
|
2021-06-21 17:11:16 -07:00
|
|
|
&& poh_estimate_offset.saturating_sub(estimate_offset) > max_allowable_drift_fast
|
2020-10-23 14:06:57 -07:00
|
|
|
{
|
|
|
|
// estimate offset since the start of the epoch is lower than
|
2021-02-09 14:49:00 -08:00
|
|
|
// `MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST`
|
2021-03-08 17:37:57 -08:00
|
|
|
estimate = epoch_start_timestamp
|
|
|
|
.saturating_add(poh_estimate_offset.as_secs() as i64)
|
|
|
|
.saturating_sub(max_allowable_drift_fast.as_secs() as i64);
|
2020-10-23 14:01:39 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(estimate)
|
|
|
|
}
|
|
|
|
|
2020-10-09 14:53:41 -07:00
|
|
|
#[cfg(test)]
|
|
|
|
pub mod tests {
|
|
|
|
use super::*;
|
2020-11-30 09:18:33 -08:00
|
|
|
use solana_sdk::{account::Account, native_token::sol_to_lamports};
|
2020-10-09 14:53:41 -07:00
|
|
|
|
|
|
|
#[test]
|
2021-02-09 14:49:00 -08:00
|
|
|
fn test_calculate_stake_weighted_timestamp_uses_median() {
|
2020-10-23 14:01:39 -07:00
|
|
|
let recent_timestamp: UnixTimestamp = 1_578_909_061;
|
|
|
|
let slot = 5;
|
|
|
|
let slot_duration = Duration::from_millis(400);
|
|
|
|
let pubkey0 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey1 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey2 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey3 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey4 = solana_sdk::pubkey::new_rand();
|
2021-02-09 14:49:00 -08:00
|
|
|
let max_allowable_drift = MaxAllowableDrift { fast: 25, slow: 25 };
|
2020-10-23 14:01:39 -07:00
|
|
|
|
|
|
|
// Test low-staked outlier(s)
|
|
|
|
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(sol_to_lamports(1.0), Account::new(1, 0, &Pubkey::default())),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(sol_to_lamports(1.0), Account::new(1, 0, &Pubkey::default())),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey3,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey4,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (5, 0)),
|
|
|
|
(pubkey1, (5, recent_timestamp)),
|
|
|
|
(pubkey2, (5, recent_timestamp)),
|
|
|
|
(pubkey3, (5, recent_timestamp)),
|
|
|
|
(pubkey4, (5, recent_timestamp)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
None,
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
2021-02-09 14:49:00 -08:00
|
|
|
// With no bounding, timestamp w/ 0.00003% of the stake can shift the timestamp backward 8min
|
2020-10-23 14:01:39 -07:00
|
|
|
assert_eq!(bounded, recent_timestamp); // low-staked outlier cannot affect bounded timestamp
|
|
|
|
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (5, recent_timestamp)),
|
|
|
|
(pubkey1, (5, i64::MAX)),
|
|
|
|
(pubkey2, (5, recent_timestamp)),
|
|
|
|
(pubkey3, (5, recent_timestamp)),
|
|
|
|
(pubkey4, (5, recent_timestamp)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
None,
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
2021-02-09 14:49:00 -08:00
|
|
|
// With no bounding, timestamp w/ 0.00003% of the stake can shift the timestamp forward 97k years!
|
2020-10-23 14:01:39 -07:00
|
|
|
assert_eq!(bounded, recent_timestamp); // low-staked outlier cannot affect bounded timestamp
|
|
|
|
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (5, 0)),
|
|
|
|
(pubkey1, (5, i64::MAX)),
|
|
|
|
(pubkey2, (5, recent_timestamp)),
|
|
|
|
(pubkey3, (5, recent_timestamp)),
|
|
|
|
(pubkey4, (5, recent_timestamp)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
None,
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, recent_timestamp); // multiple low-staked outliers cannot affect bounded timestamp if they don't shift the median
|
|
|
|
|
|
|
|
// Test higher-staked outlier(s)
|
|
|
|
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0), // 1/3 stake
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (5, 0)),
|
|
|
|
(pubkey1, (5, i64::MAX)),
|
|
|
|
(pubkey2, (5, recent_timestamp)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
None,
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, recent_timestamp); // outlier(s) cannot affect bounded timestamp if they don't shift the median
|
|
|
|
|
|
|
|
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_001.0), // 1/3 stake
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> =
|
|
|
|
[(pubkey0, (5, 0)), (pubkey1, (5, recent_timestamp))]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
None,
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(recent_timestamp - bounded, 1578909061); // outliers > 1/2 of available stake can affect timestamp
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2021-02-09 14:49:00 -08:00
|
|
|
fn test_calculate_stake_weighted_timestamp_poh() {
|
2020-10-23 14:01:39 -07:00
|
|
|
let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
|
|
|
|
let slot = 20;
|
|
|
|
let slot_duration = Duration::from_millis(400);
|
|
|
|
let poh_offset = (slot * slot_duration).as_secs();
|
2021-02-09 14:49:00 -08:00
|
|
|
let max_allowable_drift_percentage = 25;
|
|
|
|
let max_allowable_drift = MaxAllowableDrift {
|
|
|
|
fast: max_allowable_drift_percentage,
|
|
|
|
slow: max_allowable_drift_percentage,
|
|
|
|
};
|
|
|
|
let acceptable_delta = (max_allowable_drift_percentage * poh_offset as u32 / 100) as i64;
|
2020-10-23 14:01:39 -07:00
|
|
|
let poh_estimate = epoch_start_timestamp + poh_offset as i64;
|
|
|
|
let pubkey0 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey1 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey2 = solana_sdk::pubkey::new_rand();
|
|
|
|
|
|
|
|
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
// Test when stake-weighted median is too high
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (slot as u64, poh_estimate + acceptable_delta + 1)),
|
|
|
|
(pubkey1, (slot as u64, poh_estimate + acceptable_delta + 1)),
|
|
|
|
(pubkey2, (slot as u64, poh_estimate + acceptable_delta + 1)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
2020-10-23 14:06:57 -07:00
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta);
|
2020-10-23 14:01:39 -07:00
|
|
|
|
|
|
|
// Test when stake-weighted median is too low
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (slot as u64, poh_estimate - acceptable_delta - 1)),
|
|
|
|
(pubkey1, (slot as u64, poh_estimate - acceptable_delta - 1)),
|
|
|
|
(pubkey2, (slot as u64, poh_estimate - acceptable_delta - 1)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
2020-10-23 14:06:57 -07:00
|
|
|
assert_eq!(bounded, poh_estimate - acceptable_delta);
|
2020-10-23 14:01:39 -07:00
|
|
|
|
|
|
|
// Test stake-weighted median within bounds
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (slot as u64, poh_estimate + acceptable_delta)),
|
|
|
|
(pubkey1, (slot as u64, poh_estimate + acceptable_delta)),
|
|
|
|
(pubkey2, (slot as u64, poh_estimate + acceptable_delta)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta);
|
|
|
|
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (slot as u64, poh_estimate - acceptable_delta)),
|
|
|
|
(pubkey1, (slot as u64, poh_estimate - acceptable_delta)),
|
|
|
|
(pubkey2, (slot as u64, poh_estimate - acceptable_delta)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2020-10-23 14:01:39 -07:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
2021-01-11 15:27:30 -08:00
|
|
|
max_allowable_drift,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2020-10-23 14:01:39 -07:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate - acceptable_delta);
|
|
|
|
}
|
2021-01-11 15:27:30 -08:00
|
|
|
|
|
|
|
#[test]
|
2021-02-09 14:49:00 -08:00
|
|
|
fn test_calculate_stake_weighted_timestamp_levels() {
|
2021-01-11 15:27:30 -08:00
|
|
|
let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
|
|
|
|
let slot = 20;
|
|
|
|
let slot_duration = Duration::from_millis(400);
|
|
|
|
let poh_offset = (slot * slot_duration).as_secs();
|
2021-02-09 14:49:00 -08:00
|
|
|
let max_allowable_drift_percentage_25 = 25;
|
|
|
|
let allowable_drift_25 = MaxAllowableDrift {
|
|
|
|
fast: max_allowable_drift_percentage_25,
|
|
|
|
slow: max_allowable_drift_percentage_25,
|
|
|
|
};
|
|
|
|
let max_allowable_drift_percentage_50 = 50;
|
|
|
|
let allowable_drift_50 = MaxAllowableDrift {
|
|
|
|
fast: max_allowable_drift_percentage_50,
|
|
|
|
slow: max_allowable_drift_percentage_50,
|
|
|
|
};
|
|
|
|
let acceptable_delta_25 =
|
|
|
|
(max_allowable_drift_percentage_25 * poh_offset as u32 / 100) as i64;
|
|
|
|
let acceptable_delta_50 =
|
|
|
|
(max_allowable_drift_percentage_50 * poh_offset as u32 / 100) as i64;
|
2021-01-11 15:27:30 -08:00
|
|
|
assert!(acceptable_delta_50 > acceptable_delta_25 + 1);
|
|
|
|
let poh_estimate = epoch_start_timestamp + poh_offset as i64;
|
|
|
|
let pubkey0 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey1 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey2 = solana_sdk::pubkey::new_rand();
|
|
|
|
|
|
|
|
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
// Test when stake-weighted median is above 25% deviance but below 50% deviance
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_25 + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_25 + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_25 + 1),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2021-01-11 15:27:30 -08:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
allowable_drift_25,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2021-01-11 15:27:30 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta_25);
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2021-01-11 15:27:30 -08:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
allowable_drift_50,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2021-01-11 15:27:30 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta_25 + 1);
|
|
|
|
|
|
|
|
// Test when stake-weighted median is above 50% deviance
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_50 + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_50 + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_50 + 1),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2021-01-11 15:27:30 -08:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
allowable_drift_25,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2021-01-11 15:27:30 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta_25);
|
|
|
|
|
2021-02-09 14:49:00 -08:00
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
2021-01-11 15:27:30 -08:00
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
allowable_drift_50,
|
2021-02-09 14:49:00 -08:00
|
|
|
true,
|
2021-01-11 15:27:30 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta_50);
|
|
|
|
}
|
2021-02-09 14:49:00 -08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_calculate_stake_weighted_timestamp_fast_slow() {
|
|
|
|
let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
|
|
|
|
let slot = 20;
|
|
|
|
let slot_duration = Duration::from_millis(400);
|
|
|
|
let poh_offset = (slot * slot_duration).as_secs();
|
|
|
|
let max_allowable_drift_percentage_25 = 25;
|
|
|
|
let max_allowable_drift_percentage_50 = 50;
|
|
|
|
let max_allowable_drift = MaxAllowableDrift {
|
|
|
|
fast: max_allowable_drift_percentage_25,
|
|
|
|
slow: max_allowable_drift_percentage_50,
|
|
|
|
};
|
|
|
|
let acceptable_delta_fast =
|
|
|
|
(max_allowable_drift_percentage_25 * poh_offset as u32 / 100) as i64;
|
|
|
|
let acceptable_delta_slow =
|
|
|
|
(max_allowable_drift_percentage_50 * poh_offset as u32 / 100) as i64;
|
|
|
|
assert!(acceptable_delta_slow > acceptable_delta_fast + 1);
|
|
|
|
let poh_estimate = epoch_start_timestamp + poh_offset as i64;
|
|
|
|
let pubkey0 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey1 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey2 = solana_sdk::pubkey::new_rand();
|
|
|
|
|
|
|
|
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
// Test when stake-weighted median is more than 25% fast
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(slot as u64, poh_estimate - acceptable_delta_fast - 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(slot as u64, poh_estimate - acceptable_delta_fast - 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(slot as u64, poh_estimate - acceptable_delta_fast - 1),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
max_allowable_drift,
|
|
|
|
true,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate - acceptable_delta_fast);
|
|
|
|
|
|
|
|
// Test when stake-weighted median is more than 25% but less than 50% slow
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_fast + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_fast + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_fast + 1),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
max_allowable_drift,
|
|
|
|
true,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta_fast + 1);
|
|
|
|
|
|
|
|
// Test when stake-weighted median is more than 50% slow
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_slow + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_slow + 1),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(slot as u64, poh_estimate + acceptable_delta_slow + 1),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
max_allowable_drift,
|
|
|
|
true,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta_slow);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_calculate_stake_weighted_timestamp_early() {
|
|
|
|
let epoch_start_timestamp: UnixTimestamp = 1_578_909_061;
|
|
|
|
let slot = 20;
|
|
|
|
let slot_duration = Duration::from_millis(400);
|
|
|
|
let poh_offset = (slot * slot_duration).as_secs();
|
|
|
|
let max_allowable_drift_percentage = 50;
|
|
|
|
let max_allowable_drift = MaxAllowableDrift {
|
|
|
|
fast: max_allowable_drift_percentage,
|
|
|
|
slow: max_allowable_drift_percentage,
|
|
|
|
};
|
|
|
|
let acceptable_delta = (max_allowable_drift_percentage * poh_offset as u32 / 100) as i64;
|
|
|
|
let poh_estimate = epoch_start_timestamp + poh_offset as i64;
|
|
|
|
let pubkey0 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey1 = solana_sdk::pubkey::new_rand();
|
|
|
|
let pubkey2 = solana_sdk::pubkey::new_rand();
|
|
|
|
|
|
|
|
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
|
|
|
(
|
|
|
|
pubkey0,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey1,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
pubkey2,
|
|
|
|
(
|
|
|
|
sol_to_lamports(1_000_000.0),
|
|
|
|
Account::new(1, 0, &Pubkey::default()),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
// Test when stake-weighted median is before epoch_start_timestamp
|
|
|
|
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
|
|
|
|
(pubkey0, (slot as u64, poh_estimate - acceptable_delta - 20)),
|
|
|
|
(pubkey1, (slot as u64, poh_estimate - acceptable_delta - 20)),
|
|
|
|
(pubkey2, (slot as u64, poh_estimate - acceptable_delta - 20)),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.cloned()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
// Without fix, median timestamps before epoch_start_timestamp actually increase the time
|
|
|
|
// estimate due to incorrect casting.
|
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
max_allowable_drift,
|
|
|
|
false,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate + acceptable_delta);
|
|
|
|
|
|
|
|
let bounded = calculate_stake_weighted_timestamp(
|
|
|
|
&unique_timestamps,
|
|
|
|
&stakes,
|
|
|
|
slot as Slot,
|
|
|
|
slot_duration,
|
|
|
|
Some((0, epoch_start_timestamp)),
|
|
|
|
max_allowable_drift,
|
|
|
|
true,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(bounded, poh_estimate - acceptable_delta);
|
|
|
|
}
|
2020-10-09 14:53:41 -07:00
|
|
|
}
|