diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index efa87c3a5f..c73ac9eb0e 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -33,6 +33,7 @@ use solana_sdk::{ program_utils::limited_deserialize, pubkey::Pubkey, signature::{Keypair, Signature, Signer}, + stake_weighted_timestamp::{calculate_stake_weighted_timestamp, TIMESTAMP_SLOT_RANGE}, timing::timestamp, transaction::Transaction, }; @@ -77,7 +78,6 @@ thread_local!(static PAR_THREAD_POOL_ALL_CPUS: RefCell = RefCell::ne pub const MAX_COMPLETED_SLOTS_IN_CHANNEL: usize = 100_000; pub const MAX_TURBINE_PROPAGATION_IN_MS: u64 = 100; pub const MAX_TURBINE_DELAY_IN_TICKS: u64 = MAX_TURBINE_PROPAGATION_IN_MS / MS_PER_TICK; -const TIMESTAMP_SLOT_RANGE: usize = 16; // An upper bound on maximum number of data shreds we can handle in a slot // 32K shreds would allow ~320K peak TPS @@ -1630,7 +1630,7 @@ impl Blockstore { let mut calculate_timestamp = Measure::start("calculate_timestamp"); let stake_weighted_timestamp = - calculate_stake_weighted_timestamp(unique_timestamps, stakes, slot, slot_duration) + calculate_stake_weighted_timestamp(&unique_timestamps, stakes, slot, slot_duration) .ok_or(BlockstoreError::EmptyEpochStakes)?; calculate_timestamp.stop(); datapoint_info!( @@ -3130,33 +3130,6 @@ fn slot_has_updates(slot_meta: &SlotMeta, slot_meta_backup: &Option) - (slot_meta_backup.is_some() && slot_meta_backup.as_ref().unwrap().consumed != slot_meta.consumed)) } -fn calculate_stake_weighted_timestamp( - unique_timestamps: HashMap, - stakes: &HashMap, - slot: Slot, - slot_duration: Duration, -) -> Option { - let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps - .into_iter() - .filter_map(|(vote_pubkey, (timestamp_slot, timestamp))| { - let offset = (slot - timestamp_slot) as u32 * slot_duration; - stakes.get(&vote_pubkey).map(|(stake, _account)| { - ( - (timestamp as u128 + offset.as_secs() as u128) * *stake as u128, - stake, - ) - }) - }) - .fold((0, 0), |(timestamps, stakes), (timestamp, stake)| { - (timestamps + timestamp, stakes + *stake as u128) - }); - if total_stake > 0 { - Some((stake_weighted_timestamps_sum / total_stake) as i64) - } else { - None - } -} - // Creates a new ledger with slot 0 full of ticks (and only ticks). // // Returns the blockhash that can be used to append entries with. @@ -3478,7 +3451,6 @@ pub mod tests { hash::{self, hash, Hash}, instruction::CompiledInstruction, message::Message, - native_token::sol_to_lamports, packet::PACKET_DATA_SIZE, pubkey::Pubkey, signature::Signature, @@ -5889,113 +5861,6 @@ pub mod tests { } } - #[test] - fn test_calculate_stake_weighted_timestamp() { - let recent_timestamp: UnixTimestamp = 1_578_909_061; - let slot = 5; - let slot_duration = Duration::from_millis(400); - let expected_offset = (slot * slot_duration).as_secs(); - let pubkey0 = Pubkey::new_rand(); - let pubkey1 = Pubkey::new_rand(); - let pubkey2 = Pubkey::new_rand(); - let pubkey3 = Pubkey::new_rand(); - let unique_timestamps: HashMap = [ - (pubkey0, (0, recent_timestamp)), - (pubkey1, (0, recent_timestamp)), - (pubkey2, (0, recent_timestamp)), - (pubkey3, (0, recent_timestamp)), - ] - .iter() - .cloned() - .collect(); - - let stakes: HashMap = [ - ( - pubkey0, - ( - sol_to_lamports(4_500_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ( - pubkey1, - ( - sol_to_lamports(4_500_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ( - pubkey2, - ( - sol_to_lamports(4_500_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ( - pubkey3, - ( - sol_to_lamports(4_500_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ] - .iter() - .cloned() - .collect(); - assert_eq!( - calculate_stake_weighted_timestamp( - unique_timestamps.clone(), - &stakes, - slot as Slot, - slot_duration - ), - Some(recent_timestamp + expected_offset as i64) - ); - - let stakes: HashMap = [ - ( - pubkey0, - ( - sol_to_lamports(15_000_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ( - pubkey1, - ( - sol_to_lamports(1_000_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ( - pubkey2, - ( - sol_to_lamports(1_000_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ( - pubkey3, - ( - sol_to_lamports(1_000_000_000.0), - Account::new(1, 0, &Pubkey::default()), - ), - ), - ] - .iter() - .cloned() - .collect(); - assert_eq!( - calculate_stake_weighted_timestamp( - unique_timestamps, - &stakes, - slot as Slot, - slot_duration - ), - Some(recent_timestamp + expected_offset as i64) - ); - } - #[test] fn test_persist_transaction_status() { let blockstore_path = get_tmp_ledger_path!(); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 91dddfd1a4..f882ef713a 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -57,6 +57,7 @@ use solana_sdk::{ signature::{Keypair, Signature}, slot_hashes::SlotHashes, slot_history::SlotHistory, + stake_weighted_timestamp::{calculate_stake_weighted_timestamp, TIMESTAMP_SLOT_RANGE}, system_transaction, sysvar::{self, Sysvar}, timing::years_as_slots, @@ -77,6 +78,7 @@ use std::{ atomic::{AtomicBool, AtomicU64, Ordering::Relaxed}, LockResult, RwLockWriteGuard, {Arc, RwLock, RwLockReadGuard}, }, + time::Duration, }; // Partial SPL Token v2.0.x declarations inlined to avoid an external dependency on the spl-token crate @@ -1012,7 +1014,7 @@ impl Bank { } /// computed unix_timestamp at this slot height - pub fn unix_timestamp(&self) -> i64 { + pub fn unix_timestamp_from_genesis(&self) -> i64 { self.genesis_creation_time + ((self.slot as u128 * self.ns_per_slot) / 1_000_000_000) as i64 } @@ -1035,19 +1037,38 @@ impl Bank { } pub fn clock(&self) -> sysvar::clock::Clock { - sysvar::clock::Clock { + sysvar::clock::Clock::from_account( + &self.get_account(&sysvar::clock::id()).unwrap_or_default(), + ) + .unwrap_or_default() + } + + fn update_clock(&self) { + let mut unix_timestamp = self.unix_timestamp_from_genesis(); + if self + .feature_set + .is_active(&feature_set::timestamp_correction::id()) + { + if let Some(timestamp_estimate) = self.get_timestamp_estimate() { + if timestamp_estimate > unix_timestamp { + datapoint_info!( + "bank-timestamp-correction", + ("from_genesis", unix_timestamp, i64), + ("corrected", timestamp_estimate, i64), + ); + unix_timestamp = timestamp_estimate + } + } + } + let clock = sysvar::clock::Clock { slot: self.slot, unused: Self::get_unused_from_slot(self.slot, self.unused), epoch: self.epoch_schedule.get_epoch(self.slot), leader_schedule_epoch: self.epoch_schedule.get_leader_schedule_epoch(self.slot), - unix_timestamp: self.unix_timestamp(), - } - } - - fn update_clock(&self) { + unix_timestamp, + }; self.update_sysvar_account(&sysvar::clock::id(), |account| { - self.clock() - .create_account(self.inherit_sysvar_account_balance(account)) + clock.create_account(self.inherit_sysvar_account_balance(account)) }); } @@ -1361,6 +1382,46 @@ impl Bank { self.update_recent_blockhashes_locked(&blockhash_queue); } + fn get_timestamp_estimate(&self) -> Option { + let mut get_timestamp_estimate_time = Measure::start("get_timestamp_estimate"); + let recent_timestamps: HashMap = self + .vote_accounts() + .into_iter() + .filter_map(|(pubkey, (_, account))| { + VoteState::from(&account).and_then(|state| { + let timestamp_slot = state.last_timestamp.slot; + if self.slot().checked_sub(timestamp_slot)? <= TIMESTAMP_SLOT_RANGE as u64 { + Some(( + pubkey, + (state.last_timestamp.slot, state.last_timestamp.timestamp), + )) + } else { + None + } + }) + }) + .collect(); + let slot_duration = Duration::from_nanos(self.ns_per_slot as u64); + let epoch = self.epoch_schedule().get_epoch(self.slot()); + let stakes = self.epoch_vote_accounts(epoch)?; + let stake_weighted_timestamp = calculate_stake_weighted_timestamp( + &recent_timestamps, + stakes, + self.slot(), + slot_duration, + ); + get_timestamp_estimate_time.stop(); + datapoint_info!( + "bank-timestamp", + ( + "get_timestamp_estimate_us", + get_timestamp_estimate_time.as_us(), + i64 + ), + ); + stake_weighted_timestamp + } + // Distribute collected transaction fees for this slot to collector_id (= current leader). // // Each validator is incentivized to process more transactions to earn more transaction fees. @@ -3951,7 +4012,8 @@ mod tests { use crate::{ accounts_index::{AccountMap, Ancestors}, genesis_utils::{ - create_genesis_config_with_leader, GenesisConfigInfo, BOOTSTRAP_VALIDATOR_LAMPORTS, + create_genesis_config_with_leader, create_genesis_config_with_vote_accounts, + GenesisConfigInfo, ValidatorVoteKeypairs, BOOTSTRAP_VALIDATOR_LAMPORTS, }, process_instruction::InvokeContext, status_cache::MAX_CACHE_ENTRIES, @@ -3980,7 +4042,7 @@ mod tests { use solana_vote_program::vote_state::VoteStateVersions; use solana_vote_program::{ vote_instruction, - vote_state::{self, Vote, VoteInit, VoteState, MAX_LOCKOUT_HISTORY}, + vote_state::{self, BlockTimestamp, Vote, VoteInit, VoteState, MAX_LOCKOUT_HISTORY}, }; use std::{result, time::Duration}; @@ -3993,11 +4055,14 @@ mod tests { } #[test] - fn test_bank_unix_timestamp() { + fn test_bank_unix_timestamp_from_genesis() { let (genesis_config, _mint_keypair) = create_genesis_config(1); let mut bank = Arc::new(Bank::new(&genesis_config)); - assert_eq!(genesis_config.creation_time, bank.unix_timestamp()); + assert_eq!( + genesis_config.creation_time, + bank.unix_timestamp_from_genesis() + ); let slots_per_sec = 1.0 / (duration_as_s(&genesis_config.poh_config.target_tick_duration) * genesis_config.ticks_per_slot as f32); @@ -4006,7 +4071,7 @@ mod tests { bank = Arc::new(new_from_parent(&bank)); } - assert!(bank.unix_timestamp() - genesis_config.creation_time >= 1); + assert!(bank.unix_timestamp_from_genesis() - genesis_config.creation_time >= 1); } #[test] @@ -9385,6 +9450,186 @@ mod tests { assert_eq!(bank.capitalization(), original_capitalization - 100); } + fn update_vote_account_timestamp(timestamp: BlockTimestamp, bank: &Bank, vote_pubkey: &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 = timestamp; + let versioned = VoteStateVersions::Current(Box::new(vote_state)); + VoteState::to(&versioned, &mut vote_account).unwrap(); + bank.store_account(vote_pubkey, &vote_account); + } + + #[test] + fn test_get_timestamp_estimate() { + let validator_vote_keypairs0 = ValidatorVoteKeypairs::new_rand(); + let validator_vote_keypairs1 = ValidatorVoteKeypairs::new_rand(); + let validator_keypairs = vec![&validator_vote_keypairs0, &validator_vote_keypairs1]; + let GenesisConfigInfo { + genesis_config, + mint_keypair: _, + voting_keypair: _, + } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![10_000; 2], + ); + let mut bank = Bank::new(&genesis_config); + assert_eq!(bank.get_timestamp_estimate(), Some(0)); + + let recent_timestamp: UnixTimestamp = bank.unix_timestamp_from_genesis(); + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: recent_timestamp, + }, + &bank, + &validator_vote_keypairs0.vote_keypair.pubkey(), + ); + let additional_secs = 2; + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: recent_timestamp + additional_secs, + }, + &bank, + &validator_vote_keypairs1.vote_keypair.pubkey(), + ); + assert_eq!( + bank.get_timestamp_estimate(), + Some(recent_timestamp + additional_secs / 2) + ); + + for _ in 0..10 { + bank = new_from_parent(&Arc::new(bank)); + } + let adjustment = (bank.ns_per_slot as u64 * bank.slot()) / 1_000_000_000; + assert_eq!( + bank.get_timestamp_estimate(), + Some(recent_timestamp + adjustment as i64 + additional_secs / 2) + ); + + for _ in 0..7 { + bank = new_from_parent(&Arc::new(bank)); + } + assert_eq!(bank.get_timestamp_estimate(), None); + } + + #[test] + fn test_timestamp_correction_feature() { + let leader_pubkey = Pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + voting_keypair, + .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + genesis_config + .accounts + .remove(&feature_set::timestamp_correction::id()) + .unwrap(); + let bank = Bank::new(&genesis_config); + + let recent_timestamp: UnixTimestamp = bank.unix_timestamp_from_genesis(); + let additional_secs = 1; + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: recent_timestamp + additional_secs, + }, + &bank, + &voting_keypair.pubkey(), + ); + + // Bank::new_from_parent should not adjust timestamp before feature activation + let mut bank = new_from_parent(&Arc::new(bank)); + let clock = + sysvar::clock::Clock::from_account(&bank.get_account(&sysvar::clock::id()).unwrap()) + .unwrap(); + assert_eq!(clock.unix_timestamp, bank.unix_timestamp_from_genesis()); + + // Request `timestamp_correction` activation + let feature = Feature { + activated_at: Some(bank.slot), + }; + bank.store_account( + &feature_set::timestamp_correction::id(), + &feature.create_account(42), + ); + bank.compute_active_feature_set(true); + + // Now Bank::new_from_parent should adjust timestamp + let bank = Arc::new(new_from_parent(&Arc::new(bank))); + let clock = + sysvar::clock::Clock::from_account(&bank.get_account(&sysvar::clock::id()).unwrap()) + .unwrap(); + assert_eq!( + clock.unix_timestamp, + bank.unix_timestamp_from_genesis() + additional_secs + ); + } + + #[test] + fn test_update_clock_timestamp() { + let leader_pubkey = Pubkey::new_rand(); + let GenesisConfigInfo { + genesis_config, + voting_keypair, + .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + let bank = Bank::new(&genesis_config); + assert_eq!( + bank.clock().unix_timestamp, + bank.unix_timestamp_from_genesis() + ); + + bank.update_clock(); + assert_eq!( + bank.clock().unix_timestamp, + bank.unix_timestamp_from_genesis() + ); + + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: bank.unix_timestamp_from_genesis() - 1, + }, + &bank, + &voting_keypair.pubkey(), + ); + bank.update_clock(); + assert_eq!( + bank.clock().unix_timestamp, + bank.unix_timestamp_from_genesis() + ); + + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: bank.unix_timestamp_from_genesis(), + }, + &bank, + &voting_keypair.pubkey(), + ); + bank.update_clock(); + assert_eq!( + bank.clock().unix_timestamp, + bank.unix_timestamp_from_genesis() + ); + + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: bank.unix_timestamp_from_genesis() + 1, + }, + &bank, + &voting_keypair.pubkey(), + ); + bank.update_clock(); + assert_eq!( + bank.clock().unix_timestamp, + bank.unix_timestamp_from_genesis() + 1 + ); + } + fn setup_bank_with_removable_zero_lamport_account() -> Arc { let (genesis_config, _mint_keypair) = create_genesis_config(2000); let bank0 = Bank::new(&genesis_config); diff --git a/runtime/src/feature_set.rs b/runtime/src/feature_set.rs index e7feb5d909..09743dab9d 100644 --- a/runtime/src/feature_set.rs +++ b/runtime/src/feature_set.rs @@ -57,6 +57,10 @@ pub mod max_program_call_depth_64 { solana_sdk::declare_id!("YCKSgA6XmjtkQrHBQjpyNrX6EMhJPcYcLWMVgWn36iv"); } +pub mod timestamp_correction { + solana_sdk::declare_id!("3zydSLUwuqqsV3wL5wBsaVgyvMox3XTHx7zLEuQf1U2Z"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -72,7 +76,8 @@ lazy_static! { (no_overflow_rent_distribution::id(), "no overflow rent distribution"), (ristretto_mul_syscall_enabled::id(), "ristretto multiply syscall"), (max_invoke_depth_4::id(), "max invoke call depth 4"), - (max_program_call_depth_64::id(), "max program call depth 64") + (max_program_call_depth_64::id(), "max program call depth 64"), + (timestamp_correction::id(), "correct bank timestamps"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 6dc21378f3..c69001323e 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -45,6 +45,7 @@ pub mod short_vec; pub mod slot_hashes; pub mod slot_history; pub mod stake_history; +pub mod stake_weighted_timestamp; pub mod system_instruction; pub mod system_program; pub mod sysvar; diff --git a/sdk/src/stake_weighted_timestamp.rs b/sdk/src/stake_weighted_timestamp.rs new file mode 100644 index 0000000000..bf4e62aa73 --- /dev/null +++ b/sdk/src/stake_weighted_timestamp.rs @@ -0,0 +1,150 @@ +/// A helper for calculating a stake-weighted timestamp estimate from a set of timestamps and epoch +/// stake. +use solana_sdk::{ + account::Account, + clock::{Slot, UnixTimestamp}, + pubkey::Pubkey, +}; +use std::{collections::HashMap, time::Duration}; + +pub const TIMESTAMP_SLOT_RANGE: usize = 16; + +pub fn calculate_stake_weighted_timestamp( + unique_timestamps: &HashMap, + stakes: &HashMap, + slot: Slot, + slot_duration: Duration, +) -> Option { + let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps + .iter() + .filter_map(|(vote_pubkey, (timestamp_slot, timestamp))| { + let offset = (slot - timestamp_slot) as u32 * slot_duration; + stakes.get(&vote_pubkey).map(|(stake, _account)| { + ( + (*timestamp as u128 + offset.as_secs() as u128) * *stake as u128, + stake, + ) + }) + }) + .fold((0, 0), |(timestamps, stakes), (timestamp, stake)| { + (timestamps + timestamp, stakes + *stake as u128) + }); + if total_stake > 0 { + Some((stake_weighted_timestamps_sum / total_stake) as i64) + } else { + None + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use solana_sdk::native_token::sol_to_lamports; + + #[test] + fn test_calculate_stake_weighted_timestamp() { + let recent_timestamp: UnixTimestamp = 1_578_909_061; + let slot = 5; + let slot_duration = Duration::from_millis(400); + let expected_offset = (slot * slot_duration).as_secs(); + let pubkey0 = Pubkey::new_rand(); + let pubkey1 = Pubkey::new_rand(); + let pubkey2 = Pubkey::new_rand(); + let pubkey3 = Pubkey::new_rand(); + let unique_timestamps: HashMap = [ + (pubkey0, (0, recent_timestamp)), + (pubkey1, (0, recent_timestamp)), + (pubkey2, (0, recent_timestamp)), + (pubkey3, (0, recent_timestamp)), + ] + .iter() + .cloned() + .collect(); + + let stakes: HashMap = [ + ( + pubkey0, + ( + sol_to_lamports(4_500_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ( + pubkey1, + ( + sol_to_lamports(4_500_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ( + pubkey2, + ( + sol_to_lamports(4_500_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ( + pubkey3, + ( + sol_to_lamports(4_500_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ] + .iter() + .cloned() + .collect(); + assert_eq!( + calculate_stake_weighted_timestamp( + &unique_timestamps, + &stakes, + slot as Slot, + slot_duration + ), + Some(recent_timestamp + expected_offset as i64) + ); + + let stakes: HashMap = [ + ( + pubkey0, + ( + sol_to_lamports(15_000_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ( + pubkey1, + ( + sol_to_lamports(1_000_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ( + pubkey2, + ( + sol_to_lamports(1_000_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ( + pubkey3, + ( + sol_to_lamports(1_000_000_000.0), + Account::new(1, 0, &Pubkey::default()), + ), + ), + ] + .iter() + .cloned() + .collect(); + assert_eq!( + calculate_stake_weighted_timestamp( + &unique_timestamps, + &stakes, + slot as Slot, + slot_duration + ), + Some(recent_timestamp + expected_offset as i64) + ); + } +}