v1.4: Update timestamp max allowable drift to 50% of PoH (#14532)
* Add timestamp warp * Change max_allowable_drift to 50% * Fill in PR# * Fix rpc test setup
This commit is contained in:
@@ -130,6 +130,10 @@ pub mod abort_on_all_cpi_failures {
|
||||
solana_sdk::declare_id!("ED5D5a2hQaECHaMmKpnU48GdsfafdCjkb3pgAw5RKbb2");
|
||||
}
|
||||
|
||||
pub mod warp_timestamp {
|
||||
solana_sdk::declare_id!("Bfqm7fGk5MBptqa2WHXWFLH7uJvq8hkJcAQPipy2bAMk");
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Map of feature identifiers to user-visible description
|
||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
||||
@@ -164,6 +168,7 @@ lazy_static! {
|
||||
(limit_cpi_loader_invoke::id(), "Loader not authorized via CPI"),
|
||||
(use_loaded_program_accounts::id(), "Use loaded program accounts"),
|
||||
(abort_on_all_cpi_failures::id(), "Abort on all CPI failures"),
|
||||
(warp_timestamp::id(), "warp timestamp to current, adjust bounding to 50% #14532"),
|
||||
/*************** ADD NEW FEATURES HERE ***************/
|
||||
]
|
||||
.iter()
|
||||
|
||||
@@ -5,34 +5,42 @@ use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub const TIMESTAMP_SLOT_RANGE: usize = 32;
|
||||
pub const DEPRECATED_TIMESTAMP_SLOT_RANGE: usize = 16; // Deprecated. Remove in the Solana v1.6.0 timeframe
|
||||
const MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 25;
|
||||
pub const DEPRECATED_MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 25;
|
||||
pub const MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 50;
|
||||
|
||||
pub enum EstimateType {
|
||||
Bounded,
|
||||
Unbounded, // Deprecated. Remove in the Solana v1.6.0 timeframe
|
||||
Bounded(u32), // Value represents max allowable drift percentage
|
||||
Unbounded, // Deprecated. Remove in the Solana v1.6.0 timeframe
|
||||
}
|
||||
|
||||
pub fn calculate_stake_weighted_timestamp<T>(
|
||||
unique_timestamps: &HashMap<Pubkey, (Slot, UnixTimestamp)>,
|
||||
pub fn calculate_stake_weighted_timestamp<I, K, V, T>(
|
||||
unique_timestamps: I,
|
||||
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
|
||||
slot: Slot,
|
||||
slot_duration: Duration,
|
||||
estimate_type: EstimateType,
|
||||
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
|
||||
) -> Option<UnixTimestamp> {
|
||||
) -> Option<UnixTimestamp>
|
||||
where
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
K: Borrow<Pubkey>,
|
||||
V: Borrow<(Slot, UnixTimestamp)>,
|
||||
{
|
||||
match estimate_type {
|
||||
EstimateType::Bounded => calculate_bounded_stake_weighted_timestamp(
|
||||
EstimateType::Bounded(max_allowable_drift) => calculate_bounded_stake_weighted_timestamp(
|
||||
unique_timestamps,
|
||||
stakes,
|
||||
slot,
|
||||
slot_duration,
|
||||
epoch_start_timestamp,
|
||||
max_allowable_drift,
|
||||
),
|
||||
EstimateType::Unbounded => calculate_unbounded_stake_weighted_timestamp(
|
||||
unique_timestamps,
|
||||
@@ -43,17 +51,23 @@ pub fn calculate_stake_weighted_timestamp<T>(
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_unbounded_stake_weighted_timestamp<T>(
|
||||
unique_timestamps: &HashMap<Pubkey, (Slot, UnixTimestamp)>,
|
||||
fn calculate_unbounded_stake_weighted_timestamp<I, K, V, T>(
|
||||
unique_timestamps: I,
|
||||
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
|
||||
slot: Slot,
|
||||
slot_duration: Duration,
|
||||
) -> Option<UnixTimestamp> {
|
||||
) -> Option<UnixTimestamp>
|
||||
where
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
K: Borrow<Pubkey>,
|
||||
V: Borrow<(Slot, UnixTimestamp)>,
|
||||
{
|
||||
let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps
|
||||
.iter()
|
||||
.filter_map(|(vote_pubkey, (timestamp_slot, timestamp))| {
|
||||
.into_iter()
|
||||
.filter_map(|(vote_pubkey, slot_timestamp)| {
|
||||
let (timestamp_slot, timestamp) = slot_timestamp.borrow();
|
||||
let offset = (slot - timestamp_slot) as u32 * slot_duration;
|
||||
stakes.get(&vote_pubkey).map(|(stake, _account)| {
|
||||
stakes.get(vote_pubkey.borrow()).map(|(stake, _account)| {
|
||||
(
|
||||
(*timestamp as u128 + offset.as_secs() as u128) * *stake as u128,
|
||||
stake,
|
||||
@@ -70,20 +84,27 @@ fn calculate_unbounded_stake_weighted_timestamp<T>(
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_bounded_stake_weighted_timestamp<T>(
|
||||
unique_timestamps: &HashMap<Pubkey, (Slot, UnixTimestamp)>,
|
||||
fn calculate_bounded_stake_weighted_timestamp<I, K, V, T>(
|
||||
unique_timestamps: I,
|
||||
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
|
||||
slot: Slot,
|
||||
slot_duration: Duration,
|
||||
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
|
||||
) -> Option<UnixTimestamp> {
|
||||
max_allowable_drift_percentage: u32,
|
||||
) -> Option<UnixTimestamp>
|
||||
where
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
K: Borrow<Pubkey>,
|
||||
V: Borrow<(Slot, UnixTimestamp)>,
|
||||
{
|
||||
let mut stake_per_timestamp: BTreeMap<UnixTimestamp, u128> = BTreeMap::new();
|
||||
let mut total_stake = 0;
|
||||
for (vote_pubkey, (timestamp_slot, timestamp)) in unique_timestamps.iter() {
|
||||
for (vote_pubkey, slot_timestamp) in unique_timestamps {
|
||||
let (timestamp_slot, timestamp) = slot_timestamp.borrow();
|
||||
let offset = slot.saturating_sub(*timestamp_slot) as u32 * slot_duration;
|
||||
let estimate = timestamp + offset.as_secs() as i64;
|
||||
let stake = stakes
|
||||
.get(&vote_pubkey)
|
||||
.get(vote_pubkey.borrow())
|
||||
.map(|(stake, _account)| stake)
|
||||
.unwrap_or(&0);
|
||||
stake_per_timestamp
|
||||
@@ -110,7 +131,7 @@ fn calculate_bounded_stake_weighted_timestamp<T>(
|
||||
let poh_estimate_offset = slot.saturating_sub(epoch_start_slot) as u32 * slot_duration;
|
||||
let estimate_offset =
|
||||
Duration::from_secs(estimate.saturating_sub(epoch_start_timestamp) as u64);
|
||||
let max_allowable_drift = poh_estimate_offset * MAX_ALLOWABLE_DRIFT_PERCENTAGE / 100;
|
||||
let max_allowable_drift = poh_estimate_offset * max_allowable_drift_percentage / 100;
|
||||
if estimate_offset > poh_estimate_offset
|
||||
&& estimate_offset - poh_estimate_offset > max_allowable_drift
|
||||
{
|
||||
@@ -253,6 +274,7 @@ pub mod tests {
|
||||
let pubkey2 = solana_sdk::pubkey::new_rand();
|
||||
let pubkey3 = solana_sdk::pubkey::new_rand();
|
||||
let pubkey4 = solana_sdk::pubkey::new_rand();
|
||||
let max_allowable_drift = 25;
|
||||
|
||||
// Test low-staked outlier(s)
|
||||
let stakes: HashMap<Pubkey, (u64, Account)> = [
|
||||
@@ -315,6 +337,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
None,
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded - unbounded, 527); // timestamp w/ 0.00003% of the stake can shift the timestamp backward 8min
|
||||
@@ -345,6 +368,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
None,
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(unbounded - bounded, 3074455295455); // timestamp w/ 0.00003% of the stake can shift the timestamp forward 97k years!
|
||||
@@ -367,6 +391,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
None,
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, recent_timestamp); // multiple low-staked outliers cannot affect bounded timestamp if they don't shift the median
|
||||
@@ -414,6 +439,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
None,
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, recent_timestamp); // outlier(s) cannot affect bounded timestamp if they don't shift the median
|
||||
@@ -450,6 +476,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
None,
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recent_timestamp - bounded, 1578909061); // outliers > 1/2 of available stake can affect timestamp
|
||||
@@ -461,7 +488,8 @@ pub mod tests {
|
||||
let slot = 20;
|
||||
let slot_duration = Duration::from_millis(400);
|
||||
let poh_offset = (slot * slot_duration).as_secs();
|
||||
let acceptable_delta = (MAX_ALLOWABLE_DRIFT_PERCENTAGE * poh_offset as u32 / 100) as i64;
|
||||
let max_allowable_drift = 25;
|
||||
let acceptable_delta = (max_allowable_drift * 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();
|
||||
@@ -510,6 +538,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, poh_estimate + acceptable_delta);
|
||||
@@ -530,6 +559,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, poh_estimate - acceptable_delta);
|
||||
@@ -550,6 +580,7 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, poh_estimate + acceptable_delta);
|
||||
@@ -569,8 +600,135 @@ pub mod tests {
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
max_allowable_drift,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, poh_estimate - acceptable_delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_bounded_stake_weighted_timestamp_levels() {
|
||||
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 allowable_drift_25 = 25;
|
||||
let allowable_drift_50 = 50;
|
||||
let acceptable_delta_25 = (allowable_drift_25 * poh_offset as u32 / 100) as i64;
|
||||
let acceptable_delta_50 = (allowable_drift_50 * poh_offset as u32 / 100) as i64;
|
||||
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();
|
||||
|
||||
let bounded = calculate_bounded_stake_weighted_timestamp(
|
||||
&unique_timestamps,
|
||||
&stakes,
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
allowable_drift_25,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, poh_estimate + acceptable_delta_25);
|
||||
|
||||
let bounded = calculate_bounded_stake_weighted_timestamp(
|
||||
&unique_timestamps,
|
||||
&stakes,
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
allowable_drift_50,
|
||||
)
|
||||
.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();
|
||||
|
||||
let bounded = calculate_bounded_stake_weighted_timestamp(
|
||||
&unique_timestamps,
|
||||
&stakes,
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
allowable_drift_25,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, poh_estimate + acceptable_delta_25);
|
||||
|
||||
let bounded = calculate_bounded_stake_weighted_timestamp(
|
||||
&unique_timestamps,
|
||||
&stakes,
|
||||
slot as Slot,
|
||||
slot_duration,
|
||||
Some((0, epoch_start_timestamp)),
|
||||
allowable_drift_50,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bounded, poh_estimate + acceptable_delta_50);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user