diff --git a/core/src/rpc.rs b/core/src/rpc.rs index d4dd778386..1305fb52bf 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -2894,7 +2894,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}, @@ -2929,6 +2929,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); @@ -5118,7 +5130,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 d8d9b12ff4..172903beb9 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}, @@ -1286,18 +1288,36 @@ 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 epoch = if let Some(epoch) = parent_epoch { - epoch + let warp_timestamp = self + .feature_set + .activated_slot(&feature_set::warp_timestamp::id()); + if warp_timestamp == Some(self.slot()) { + None } else { - self.epoch() - }; - let first_slot_in_epoch = - self.epoch_schedule.get_first_slot_in_epoch(epoch); - Some((first_slot_in_epoch, self.clock().epoch_start_timestamp)) + let epoch = if let Some(epoch) = parent_epoch { + epoch + } else { + self.epoch() + }; + let first_slot_in_epoch = + self.epoch_schedule.get_first_slot_in_epoch(epoch); + Some((first_slot_in_epoch, self.clock().epoch_start_timestamp)) + } } 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) }; @@ -11058,6 +11078,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 c10a624bcc..2d6bb69766 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -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 = [ @@ -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() diff --git a/sdk/src/stake_weighted_timestamp.rs b/sdk/src/stake_weighted_timestamp.rs index 2da3822ee1..a9145fb30f 100644 --- a/sdk/src/stake_weighted_timestamp.rs +++ b/sdk/src/stake_weighted_timestamp.rs @@ -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( - unique_timestamps: &HashMap, +pub fn calculate_stake_weighted_timestamp( + unique_timestamps: I, stakes: &HashMap, slot: Slot, slot_duration: Duration, estimate_type: EstimateType, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, -) -> Option { +) -> Option +where + I: IntoIterator, + K: Borrow, + 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( } } -fn calculate_unbounded_stake_weighted_timestamp( - unique_timestamps: &HashMap, +fn calculate_unbounded_stake_weighted_timestamp( + unique_timestamps: I, stakes: &HashMap, slot: Slot, slot_duration: Duration, -) -> Option { +) -> Option +where + I: IntoIterator, + K: Borrow, + 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( } } -fn calculate_bounded_stake_weighted_timestamp( - unique_timestamps: &HashMap, +fn calculate_bounded_stake_weighted_timestamp( + unique_timestamps: I, stakes: &HashMap, slot: Slot, slot_duration: Duration, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, -) -> Option { + max_allowable_drift_percentage: u32, +) -> Option +where + I: IntoIterator, + K: Borrow, + V: Borrow<(Slot, UnixTimestamp)>, +{ let mut stake_per_timestamp: BTreeMap = 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( 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 = [ @@ -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 = [ + ( + 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); + } }