From b0e6e2952743cd31c0b295c8d8e3da1e74902532 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 11 Jan 2021 16:27:30 -0700 Subject: [PATCH] Update timestamp max allowable drift to 50% of PoH (#14531) * Repurpose warp-timestamp feature for general bump * Change max_allowable_drift to 50% * Fill in PR# * Fix rpc test setup --- core/src/rpc.rs | 16 ++- runtime/src/bank.rs | 29 ++++-- sdk/src/feature_set.rs | 4 +- sdk/src/stake_weighted_timestamp.rs | 152 ++++++++++++++++++++++++++-- 4 files changed, 184 insertions(+), 17 deletions(-) diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 3d93bec094..31d60a44a3 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -2896,7 +2896,7 @@ pub mod tests { }; use solana_vote_program::{ vote_instruction, - vote_state::{Vote, VoteInit, MAX_LOCKOUT_HISTORY}, + vote_state::{BlockTimestamp, Vote, VoteInit, VoteStateVersions, MAX_LOCKOUT_HISTORY}, }; use spl_token_v2_0::{ solana_program::{program_option::COption, pubkey::Pubkey as SplTokenPubkey}, @@ -2931,6 +2931,18 @@ pub mod tests { ) -> RpcHandler { let (bank_forks, alice, leader_vote_keypair) = new_bank_forks(); let bank = bank_forks.read().unwrap().working_bank(); + + let vote_pubkey = leader_vote_keypair.pubkey(); + let mut vote_account = bank.get_account(&vote_pubkey).unwrap_or_default(); + let mut vote_state = VoteState::from(&vote_account).unwrap_or_default(); + vote_state.last_timestamp = BlockTimestamp { + slot: bank.slot(), + timestamp: bank.clock().unix_timestamp, + }; + let versioned = VoteStateVersions::new_current(vote_state); + VoteState::to(&versioned, &mut vote_account).unwrap(); + bank.store_account(&vote_pubkey, &vote_account); + let ledger_path = get_tmp_ledger_path!(); let blockstore = Blockstore::open(&ledger_path).unwrap(); let blockstore = Arc::new(blockstore); @@ -5127,7 +5139,7 @@ pub mod tests { let res = io.handle_request_sync(&req, meta.clone()); let expected = format!( r#"{{"jsonrpc":"2.0","result":{},"id":1}}"#, - base_timestamp + (5 * slot_duration).as_secs() as i64 + base_timestamp + (7 * slot_duration).as_secs() as i64 ); let expected: Response = serde_json::from_str(&expected).expect("expected response deserialization"); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index fbed32cb4b..fd87a30dfb 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -60,7 +60,9 @@ use solana_sdk::{ slot_hashes::SlotHashes, slot_history::SlotHistory, stake_weighted_timestamp::{ - calculate_stake_weighted_timestamp, EstimateType, DEPRECATED_TIMESTAMP_SLOT_RANGE, + calculate_stake_weighted_timestamp, EstimateType, + DEPRECATED_MAX_ALLOWABLE_DRIFT_PERCENTAGE, DEPRECATED_TIMESTAMP_SLOT_RANGE, + MAX_ALLOWABLE_DRIFT_PERCENTAGE, }, system_transaction, sysvar::{self}, @@ -1332,12 +1334,10 @@ impl Bank { // needed for timestamp bounding, but isn't yet corrected for the activation slot let epoch_start_timestamp = if self.slot() > timestamp_bounding_activation_slot { - let warp_testnet_timestamp = self + let warp_timestamp = self .feature_set - .activated_slot(&feature_set::warp_testnet_timestamp::id()); - if warp_testnet_timestamp == Some(self.slot()) - && self.cluster_type() == ClusterType::Testnet - { + .activated_slot(&feature_set::warp_timestamp::id()); + if warp_timestamp == Some(self.slot()) { None } else { let epoch = if let Some(epoch) = parent_epoch { @@ -1352,7 +1352,18 @@ impl Bank { } else { None }; - (EstimateType::Bounded, epoch_start_timestamp) + let max_allowable_drift = if self + .feature_set + .is_active(&feature_set::warp_timestamp::id()) + { + MAX_ALLOWABLE_DRIFT_PERCENTAGE + } else { + DEPRECATED_MAX_ALLOWABLE_DRIFT_PERCENTAGE + }; + ( + EstimateType::Bounded(max_allowable_drift), + epoch_start_timestamp, + ) } else { (EstimateType::Unbounded, None) }; @@ -11144,6 +11155,10 @@ pub(crate) mod tests { .accounts .remove(&feature_set::timestamp_bounding::id()) .unwrap(); + genesis_config + .accounts + .remove(&feature_set::warp_timestamp::id()) + .unwrap(); genesis_config.epoch_schedule = EpochSchedule::new(slots_in_epoch); let bank = Bank::new(&genesis_config); diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 83dc2e7f25..191a7dc6d8 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -110,7 +110,7 @@ pub mod try_find_program_address_syscall_enabled { solana_sdk::declare_id!("EMsMNadQNhCYDyGpYH5Tx6dGHxiUqKHk782PU5XaWfmi"); } -pub mod warp_testnet_timestamp { +pub mod warp_timestamp { solana_sdk::declare_id!("Bfqm7fGk5MBptqa2WHXWFLH7uJvq8hkJcAQPipy2bAMk"); } @@ -163,7 +163,7 @@ lazy_static! { (simple_capitalization::id(), "simple capitalization"), (bpf_loader_upgradeable_program::id(), "upgradeable bpf loader"), (try_find_program_address_syscall_enabled::id(), "add try_find_program_address syscall"), - (warp_testnet_timestamp::id(), "warp testnet timestamp to current #14210"), + (warp_timestamp::id(), "warp timestamp to current, adjust bounding to 50% #14210 & #14531"), (stake_program_v3::id(), "solana_stake_program v3"), (max_cpi_instruction_size_ipv6_mtu::id(), "Max cross-program invocation size 1280"), (limit_cpi_loader_invoke::id(), "Loader not authorized via CPI"), diff --git a/sdk/src/stake_weighted_timestamp.rs b/sdk/src/stake_weighted_timestamp.rs index 0832269dc2..a9145fb30f 100644 --- a/sdk/src/stake_weighted_timestamp.rs +++ b/sdk/src/stake_weighted_timestamp.rs @@ -12,11 +12,12 @@ use std::{ 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( @@ -33,12 +34,13 @@ where 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, @@ -88,6 +90,7 @@ fn calculate_bounded_stake_weighted_timestamp( slot: Slot, slot_duration: Duration, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, + max_allowable_drift_percentage: u32, ) -> Option where I: IntoIterator, @@ -128,7 +131,7 @@ where 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 { @@ -271,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 = [ @@ -333,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 @@ -363,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! @@ -385,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 @@ -432,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 @@ -468,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 @@ -479,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(); @@ -528,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); @@ -548,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); @@ -568,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); @@ -587,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 = [ + ( + 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 = [ + ( + 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 = [ + ( + 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); + } }