From 40e9bd12245b5dc6c0fb1b30deadc795e4bf00d7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 10 Feb 2021 02:03:43 +0000 Subject: [PATCH] Warp timestamp and extend max-allowable-drift for accommodate slow blocks (bp #15204) (#15221) * Warp timestamp and extend max-allowable-drift for accommodate slow blocks (#15204) * Remove timestamp_correction feature gating * Remove timestamp_bounding feature gating * Remove unused deprecated ledger code * Remove unused deprecated unbounded-timestamp code * Enable independent adjustment of fast/slow timestamp bounding * Update timestamp bounds to 25% fast, 80% slow; warp timestamp * Update bank hash test * Add PR number to feature Co-authored-by: Michael Vines Co-authored-by: Michael Vines (cherry picked from commit da6753b8c084cb25c540aa933ab6c9e531666850) # Conflicts: # ledger/src/blockstore.rs # runtime/src/bank.rs # sdk/src/feature_set.rs * Fix conflicts Co-authored-by: Tyera Eulberg Co-authored-by: Tyera Eulberg --- core/src/cache_block_time_service.rs | 22 +- docs/src/developing/clients/jsonrpc-api.md | 5 - ledger/src/blockstore.rs | 326 +----------- runtime/src/bank.rs | 572 ++++++++------------- runtime/src/bank_forks.rs | 4 +- sdk/src/feature_set.rs | 16 +- sdk/src/stake_weighted_timestamp.rs | 530 +++++++++++-------- 7 files changed, 537 insertions(+), 938 deletions(-) diff --git a/core/src/cache_block_time_service.rs b/core/src/cache_block_time_service.rs index 1bd03d1a77..9959b11f46 100644 --- a/core/src/cache_block_time_service.rs +++ b/core/src/cache_block_time_service.rs @@ -2,9 +2,7 @@ use crossbeam_channel::{Receiver, RecvTimeoutError, Sender}; use solana_ledger::blockstore::Blockstore; use solana_measure::measure::Measure; use solana_runtime::bank::Bank; -use solana_sdk::{feature_set, timing::slot_duration_from_slots_per_year}; use std::{ - collections::HashMap, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -60,24 +58,8 @@ impl CacheBlockTimeService { } fn cache_block_time(bank: Arc, blockstore: &Arc) { - if bank - .feature_set - .is_active(&feature_set::timestamp_correction::id()) - { - if let Err(e) = blockstore.cache_block_time(bank.slot(), bank.clock().unix_timestamp) { - error!("cache_block_time failed: slot {:?} {:?}", bank.slot(), e); - } - } else { - let slot_duration = slot_duration_from_slots_per_year(bank.slots_per_year()); - let epoch = bank.epoch_schedule().get_epoch(bank.slot()); - let stakes = HashMap::new(); - let stakes = bank.epoch_vote_accounts(epoch).unwrap_or(&stakes); - - if let Err(e) = - blockstore.cache_block_time_from_slot_entries(bank.slot(), slot_duration, stakes) - { - error!("cache_block_time failed: slot {:?} {:?}", bank.slot(), e); - } + if let Err(e) = blockstore.cache_block_time(bank.slot(), bank.clock().unix_timestamp) { + error!("cache_block_time failed: slot {:?} {:?}", bank.slot(), e); } } diff --git a/docs/src/developing/clients/jsonrpc-api.md b/docs/src/developing/clients/jsonrpc-api.md index c7ff115e44..192164b824 100644 --- a/docs/src/developing/clients/jsonrpc-api.md +++ b/docs/src/developing/clients/jsonrpc-api.md @@ -388,11 +388,6 @@ intermittently adding a timestamp to a Vote for a particular block. A requested block's time is calculated from the stake-weighted mean of the Vote timestamps in a set of recent blocks recorded on the ledger. -Nodes that are booting from snapshot or limiting ledger size (by purging old -slots) will return null timestamps for blocks below their lowest root + -`TIMESTAMP_SLOT_RANGE`. Users interested in having this historical data must -query a node that is built from genesis and retains the entire ledger. - #### Parameters: - `` - block, identified by Slot diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 44184bf73a..92040421a2 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -24,20 +24,13 @@ use rocksdb::DBRawIterator; use solana_measure::measure::Measure; use solana_metrics::{datapoint_debug, datapoint_error}; use solana_rayon_threadlimit::get_thread_count; -use solana_runtime::{ - hardened_unpack::{unpack_genesis_archive, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE}, - vote_account::ArcVoteAccount, -}; +use solana_runtime::hardened_unpack::{unpack_genesis_archive, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE}; use solana_sdk::{ clock::{Slot, UnixTimestamp, DEFAULT_TICKS_PER_SECOND, MS_PER_TICK}, genesis_config::GenesisConfig, hash::Hash, - program_utils::limited_deserialize, pubkey::Pubkey, signature::{Keypair, Signature, Signer}, - stake_weighted_timestamp::{ - calculate_stake_weighted_timestamp, EstimateType, TIMESTAMP_SLOT_RANGE, - }, timing::timestamp, transaction::Transaction, }; @@ -46,7 +39,6 @@ use solana_transaction_status::{ ConfirmedBlock, ConfirmedTransaction, ConfirmedTransactionStatusWithSignature, Rewards, TransactionStatusMeta, TransactionWithStatusMeta, }; -use solana_vote_program::vote_instruction::VoteInstruction; use std::{ cell::RefCell, cmp, @@ -59,7 +51,6 @@ use std::{ mpsc::{sync_channel, Receiver, SyncSender, TrySendError}, Arc, Mutex, RwLock, }, - time::Duration, }; use thiserror::Error; use trees::{Tree, TreeWalk}; @@ -1678,29 +1669,6 @@ impl Blockstore { self.blocktime_cf.get(slot) } - fn get_timestamp_slots(&self, slot: Slot, timestamp_sample_range: usize) -> Vec { - let root_iterator = self - .db - .iter::(IteratorMode::From(slot, IteratorDirection::Reverse)); - if !self.is_root(slot) || root_iterator.is_err() { - return vec![]; - } - let mut get_slots = Measure::start("get_slots"); - let mut timestamp_slots: Vec = root_iterator - .unwrap() - .map(|(iter_slot, _)| iter_slot) - .take(timestamp_sample_range) - .collect(); - timestamp_slots.sort(); - get_slots.stop(); - datapoint_info!( - "blockstore-get-timestamp-slots", - ("slot", slot as i64, i64), - ("get_slots_us", get_slots.as_us() as i64, i64) - ); - timestamp_slots - } - pub fn cache_block_time(&self, slot: Slot, timestamp: UnixTimestamp) -> Result<()> { if !self.is_root(slot) { return Err(BlockstoreError::SlotNotRooted); @@ -1708,55 +1676,6 @@ impl Blockstore { self.blocktime_cf.put(slot, ×tamp) } - // DEPRECATED as of feature_set::timestamp_correction - pub fn cache_block_time_from_slot_entries( - &self, - slot: Slot, - slot_duration: Duration, - stakes: &HashMap, - ) -> Result<()> { - if !self.is_root(slot) { - return Err(BlockstoreError::SlotNotRooted); - } - let mut get_unique_timestamps = Measure::start("get_unique_timestamps"); - let unique_timestamps: HashMap = self - .get_timestamp_slots(slot, TIMESTAMP_SLOT_RANGE) - .into_iter() - .flat_map(|query_slot| self.get_block_timestamps(query_slot).unwrap_or_default()) - .collect(); - get_unique_timestamps.stop(); - if unique_timestamps.is_empty() { - return Err(BlockstoreError::NoVoteTimestampsInRange); - } - - let mut calculate_timestamp = Measure::start("calculate_timestamp"); - let stake_weighted_timestamp = calculate_stake_weighted_timestamp( - &unique_timestamps, - stakes, - slot, - slot_duration, - EstimateType::Unbounded, - None, - ) - .ok_or(BlockstoreError::EmptyEpochStakes)?; - calculate_timestamp.stop(); - datapoint_info!( - "blockstore-get-block-time", - ("slot", slot as i64, i64), - ( - "get_unique_timestamps_us", - get_unique_timestamps.as_us() as i64, - i64 - ), - ( - "calculate_stake_weighted_timestamp_us", - calculate_timestamp.as_us() as i64, - i64 - ) - ); - self.cache_block_time(slot, stake_weighted_timestamp) - } - pub fn get_first_available_block(&self) -> Result { let mut root_iterator = self.rooted_slot_iterator(self.lowest_slot())?; Ok(root_iterator.next().unwrap_or_default()) @@ -2381,36 +2300,6 @@ impl Blockstore { self.rewards_cf.put_protobuf(index, &rewards) } - fn get_block_timestamps(&self, slot: Slot) -> Result> { - let slot_entries = self.get_slot_entries(slot, 0)?; - Ok(slot_entries - .iter() - .cloned() - .flat_map(|entry| entry.transactions) - .flat_map(|transaction| { - let mut timestamps: Vec<(Pubkey, (Slot, UnixTimestamp))> = Vec::new(); - for instruction in transaction.message.instructions { - let program_id = instruction.program_id(&transaction.message.account_keys); - if program_id == &solana_vote_program::id() { - if let Ok(VoteInstruction::Vote(vote)) = - limited_deserialize(&instruction.data) - { - if let Some(timestamp) = vote.timestamp { - let timestamp_slot = vote.slots.iter().max(); - if let Some(timestamp_slot) = timestamp_slot { - let vote_pubkey = transaction.message.account_keys - [instruction.accounts[0] as usize]; - timestamps.push((vote_pubkey, (*timestamp_slot, timestamp))); - } - } - } - } - } - timestamps - }) - .collect()) - } - pub fn get_recent_perf_samples(&self, num: usize) -> Result> { Ok(self .db @@ -3594,7 +3483,6 @@ fn adjust_ulimit_nofile() -> Result<()> { pub mod tests { use super::*; use crate::{ - blockstore_processor::fill_blockstore_slot_with_ticks, entry::{next_entry, next_entry_mut}, genesis_utils::{create_genesis_config, GenesisConfigInfo}, leader_schedule::{FixedSchedule, LeaderSchedule}, @@ -3608,7 +3496,6 @@ pub mod tests { use solana_sdk::{ hash::{self, hash, Hash}, instruction::CompiledInstruction, - message::Message, packet::PACKET_DATA_SIZE, pubkey::Pubkey, signature::Signature, @@ -3616,7 +3503,6 @@ pub mod tests { }; use solana_storage_proto::convert::generated; use solana_transaction_status::{InnerInstructions, Reward, Rewards}; - use solana_vote_program::{vote_instruction, vote_state::Vote}; use std::{iter::FromIterator, time::Duration}; // used for tests only @@ -5748,92 +5634,6 @@ pub mod tests { } } - #[test] - fn test_get_timestamp_slots() { - let timestamp_sample_range = 5; - let ticks_per_slot = 5; - /* - Build a blockstore with < TIMESTAMP_SLOT_RANGE roots - */ - let blockstore_path = get_tmp_ledger_path!(); - let blockstore = Blockstore::open(&blockstore_path).unwrap(); - blockstore.set_roots(&[0]).unwrap(); - let mut last_entry_hash = Hash::default(); - for slot in 0..=3 { - let parent = { - if slot == 0 { - 0 - } else { - slot - 1 - } - }; - last_entry_hash = fill_blockstore_slot_with_ticks( - &blockstore, - ticks_per_slot, - slot, - parent, - last_entry_hash, - ); - } - blockstore.set_roots(&[1, 2, 3]).unwrap(); - - assert_eq!( - blockstore.get_timestamp_slots(2, timestamp_sample_range), - vec![0, 1, 2] - ); - assert_eq!( - blockstore.get_timestamp_slots(3, timestamp_sample_range), - vec![0, 1, 2, 3] - ); - - drop(blockstore); - Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); - - /* - Build a blockstore in the ledger with gaps in rooted slot sequence - - */ - let blockstore_path = get_tmp_ledger_path!(); - let blockstore = Blockstore::open(&blockstore_path).unwrap(); - blockstore.set_roots(&[0]).unwrap(); - let desired_roots = vec![1, 2, 3, 5, 6, 8, 11]; - let mut last_entry_hash = Hash::default(); - for (i, slot) in desired_roots.iter().enumerate() { - let parent = { - if i == 0 { - 0 - } else { - desired_roots[i - 1] - } - }; - last_entry_hash = fill_blockstore_slot_with_ticks( - &blockstore, - ticks_per_slot, - *slot, - parent, - last_entry_hash, - ); - } - blockstore.set_roots(&desired_roots).unwrap(); - - assert_eq!( - blockstore.get_timestamp_slots(2, timestamp_sample_range), - vec![0, 1, 2] - ); - assert_eq!( - blockstore.get_timestamp_slots(6, timestamp_sample_range), - vec![1, 2, 3, 5, 6] - ); - assert_eq!( - blockstore.get_timestamp_slots(8, timestamp_sample_range), - vec![2, 3, 5, 6, 8] - ); - assert_eq!( - blockstore.get_timestamp_slots(11, timestamp_sample_range), - vec![3, 5, 6, 8, 11] - ); - } - #[test] fn test_get_confirmed_block() { let slot = 10; @@ -5961,130 +5761,6 @@ pub mod tests { Blockstore::destroy(&ledger_path).expect("Expected successful database destruction"); } - #[test] - fn test_get_block_timestamps() { - let vote_keypairs: Vec = (0..6).map(|_| Keypair::new()).collect(); - let base_timestamp = 1_576_183_541; - let mut expected_timestamps: Vec<(Pubkey, (Slot, UnixTimestamp))> = Vec::new(); - - // Populate slot 1 with vote transactions, some of which have timestamps - let mut vote_entries: Vec = Vec::new(); - for (i, keypair) in vote_keypairs.iter().enumerate() { - let timestamp = if i % 2 == 0 { - let unique_timestamp = base_timestamp + i as i64; - expected_timestamps.push((keypair.pubkey(), (1, unique_timestamp))); - Some(unique_timestamp) - } else { - None - }; - let vote = Vote { - slots: vec![1], - hash: Hash::default(), - timestamp, - }; - let vote_ix = vote_instruction::vote(&keypair.pubkey(), &keypair.pubkey(), vote); - let vote_msg = Message::new(&[vote_ix], Some(&keypair.pubkey())); - let vote_tx = Transaction::new(&[keypair], vote_msg, Hash::default()); - - vote_entries.push(next_entry_mut(&mut Hash::default(), 0, vec![vote_tx])); - let mut tick = create_ticks(1, 0, hash(&serialize(&i).unwrap())); - vote_entries.append(&mut tick); - } - let shreds = entries_to_test_shreds(vote_entries, 1, 0, true, 0); - let ledger_path = get_tmp_ledger_path!(); - let blockstore = Blockstore::open(&ledger_path).unwrap(); - blockstore.insert_shreds(shreds, None, false).unwrap(); - // Populate slot 2 with ticks only - fill_blockstore_slot_with_ticks(&blockstore, 6, 2, 1, Hash::default()); - blockstore.set_roots(&[0, 1, 2]).unwrap(); - - assert_eq!( - blockstore.get_block_timestamps(1).unwrap(), - expected_timestamps - ); - assert_eq!(blockstore.get_block_timestamps(2).unwrap(), vec![]); - - blockstore.set_roots(&[3, 8]).unwrap(); - let mut stakes = HashMap::new(); - let slot_duration = Duration::from_millis(400); - for slot in &[1, 2, 3, 8] { - assert!(blockstore - .cache_block_time_from_slot_entries(*slot, slot_duration, &stakes) - .is_err()); - } - - // Build epoch vote_accounts HashMap to test stake-weighted block time - for (i, keypair) in vote_keypairs.iter().enumerate() { - stakes.insert(keypair.pubkey(), (1 + i as u64, ArcVoteAccount::default())); - } - for slot in &[1, 2, 3, 8] { - blockstore - .cache_block_time_from_slot_entries(*slot, slot_duration, &stakes) - .unwrap(); - } - let block_time_slot_3 = blockstore.get_block_time(3); - - let mut total_stake = 0; - let mut expected_time: u64 = (0..6) - .map(|x| { - if x % 2 == 0 { - total_stake += 1 + x; - (base_timestamp as u64 + x) * (1 + x) - } else { - 0 - } - }) - .sum(); - expected_time /= total_stake; - assert_eq!(block_time_slot_3.unwrap().unwrap() as u64, expected_time); - assert_eq!( - blockstore.get_block_time(8).unwrap().unwrap() as u64, - expected_time + 2 // At 400ms block duration, 5 slots == 2sec - ); - } - - #[test] - fn test_get_block_time_no_timestamps() { - let vote_keypairs: Vec = (0..6).map(|_| Keypair::new()).collect(); - - // Populate slot 1 with vote transactions, none of which have timestamps - let mut vote_entries: Vec = Vec::new(); - for (i, keypair) in vote_keypairs.iter().enumerate() { - let vote = Vote { - slots: vec![1], - hash: Hash::default(), - timestamp: None, - }; - let vote_ix = vote_instruction::vote(&keypair.pubkey(), &keypair.pubkey(), vote); - let vote_msg = Message::new(&[vote_ix], Some(&keypair.pubkey())); - let vote_tx = Transaction::new(&[keypair], vote_msg, Hash::default()); - - vote_entries.push(next_entry_mut(&mut Hash::default(), 0, vec![vote_tx])); - let mut tick = create_ticks(1, 0, hash(&serialize(&i).unwrap())); - vote_entries.append(&mut tick); - } - let shreds = entries_to_test_shreds(vote_entries, 1, 0, true, 0); - let ledger_path = get_tmp_ledger_path!(); - let blockstore = Blockstore::open(&ledger_path).unwrap(); - blockstore.insert_shreds(shreds, None, false).unwrap(); - // Populate slot 2 with ticks only - fill_blockstore_slot_with_ticks(&blockstore, 6, 2, 1, Hash::default()); - blockstore.set_roots(&[0, 1, 2]).unwrap(); - - // Build epoch vote_accounts HashMap to test stake-weighted block time - let mut stakes = HashMap::new(); - for (i, keypair) in vote_keypairs.iter().enumerate() { - stakes.insert(keypair.pubkey(), (1 + i as u64, ArcVoteAccount::default())); - } - let slot_duration = Duration::from_millis(400); - for slot in &[1, 2, 3, 8] { - assert!(blockstore - .cache_block_time_from_slot_entries(*slot, slot_duration, &stakes) - .is_err()); - assert_eq!(blockstore.get_block_time(*slot).unwrap(), None); - } - } - #[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 cef06f548d..62e9e4fd4e 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -60,9 +60,8 @@ use solana_sdk::{ slot_hashes::SlotHashes, slot_history::SlotHistory, stake_weighted_timestamp::{ - calculate_stake_weighted_timestamp, EstimateType, - DEPRECATED_MAX_ALLOWABLE_DRIFT_PERCENTAGE, DEPRECATED_TIMESTAMP_SLOT_RANGE, - MAX_ALLOWABLE_DRIFT_PERCENTAGE, + calculate_stake_weighted_timestamp, MaxAllowableDrift, MAX_ALLOWABLE_DRIFT_PERCENTAGE, + MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST, MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW, }, system_transaction, sysvar::{self}, @@ -1289,90 +1288,63 @@ impl Bank { } fn update_clock(&self, parent_epoch: Option) { - let mut unix_timestamp = self.unix_timestamp_from_genesis(); - if self + let mut unix_timestamp = self.clock().unix_timestamp; + let warp_timestamp_again = self .feature_set - .is_active(&feature_set::timestamp_correction::id()) + .activated_slot(&feature_set::warp_timestamp_again::id()); + let epoch_start_timestamp = if warp_timestamp_again == Some(self.slot()) { + None + } else { + 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)) + }; + let max_allowable_drift = if self + .feature_set + .is_active(&feature_set::warp_timestamp_again::id()) { - let (estimate_type, epoch_start_timestamp) = - if let Some(timestamp_bounding_activation_slot) = self - .feature_set - .activated_slot(&feature_set::timestamp_bounding::id()) - { - // This check avoids a chicken-egg problem with epoch_start_timestamp, which is - // 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_timestamp = self - .feature_set - .activated_slot(&feature_set::warp_timestamp::id()); - if warp_timestamp == Some(self.slot()) { - None - } else { - 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 - }; - 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) - }; - - let ancestor_timestamp = self.clock().unix_timestamp; - if let Some(timestamp_estimate) = - self.get_timestamp_estimate(estimate_type, epoch_start_timestamp) - { - if timestamp_estimate > unix_timestamp { - unix_timestamp = timestamp_estimate; - if self - .feature_set - .is_active(&feature_set::timestamp_bounding::id()) - && timestamp_estimate < ancestor_timestamp - { - unix_timestamp = ancestor_timestamp; - } - } + MaxAllowableDrift { + fast: MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST, + slow: MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW, } - datapoint_info!( - "bank-timestamp-correction", - ("slot", self.slot(), i64), - ("from_genesis", self.unix_timestamp_from_genesis(), i64), - ("corrected", unix_timestamp, i64), - ("ancestor_timestamp", ancestor_timestamp, i64), - ); - } - let epoch_start_timestamp = if self - .feature_set - .is_active(&feature_set::timestamp_bounding::id()) + } else { + MaxAllowableDrift { + fast: MAX_ALLOWABLE_DRIFT_PERCENTAGE, + slow: MAX_ALLOWABLE_DRIFT_PERCENTAGE, + } + }; + + let ancestor_timestamp = self.clock().unix_timestamp; + if let Some(timestamp_estimate) = + self.get_timestamp_estimate(max_allowable_drift, epoch_start_timestamp) { + unix_timestamp = timestamp_estimate; + if timestamp_estimate < ancestor_timestamp { + unix_timestamp = ancestor_timestamp; + } + } + datapoint_info!( + "bank-timestamp-correction", + ("slot", self.slot(), i64), + ("from_genesis", self.unix_timestamp_from_genesis(), i64), + ("corrected", unix_timestamp, i64), + ("ancestor_timestamp", ancestor_timestamp, i64), + ); + let mut epoch_start_timestamp = // On epoch boundaries, update epoch_start_timestamp if parent_epoch.is_some() && parent_epoch.unwrap() != self.epoch() { unix_timestamp } else { self.clock().epoch_start_timestamp - } - } else { - Self::get_unused_from_slot(self.slot, self.unused) as i64 - }; + }; + if self.slot == 0 { + unix_timestamp = self.unix_timestamp_from_genesis(); + epoch_start_timestamp = self.unix_timestamp_from_genesis(); + } let clock = sysvar::clock::Clock { slot: self.slot, epoch_start_timestamp, @@ -1856,44 +1828,42 @@ impl Bank { fn get_timestamp_estimate( &self, - estimate_type: EstimateType, + max_allowable_drift: MaxAllowableDrift, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, ) -> 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))| { - account.vote_state().as_ref().ok().and_then(|state| { - let timestamp_slot = state.last_timestamp.slot; - if (self - .feature_set - .is_active(&feature_set::timestamp_bounding::id()) - && self.slot().checked_sub(timestamp_slot)? - <= self.epoch_schedule().slots_per_epoch) - || self.slot().checked_sub(timestamp_slot)? - <= DEPRECATED_TIMESTAMP_SLOT_RANGE as u64 - { + let slots_per_epoch = self.epoch_schedule().slots_per_epoch; + let recent_timestamps = + self.vote_accounts() + .into_iter() + .filter_map(|(pubkey, (_, account))| { + let vote_state = account.vote_state(); + let vote_state = vote_state.as_ref().ok()?; + let slot_delta = self.slot().checked_sub(vote_state.last_timestamp.slot)?; + if slot_delta <= slots_per_epoch { Some(( pubkey, - (state.last_timestamp.slot, state.last_timestamp.timestamp), + ( + vote_state.last_timestamp.slot, + vote_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, + recent_timestamps, stakes, self.slot(), slot_duration, - estimate_type, epoch_start_timestamp, + max_allowable_drift, + self.feature_set + .is_active(&feature_set::warp_timestamp_again::id()), ); get_timestamp_estimate_time.stop(); datapoint_info!( @@ -10058,19 +10028,19 @@ pub(crate) mod tests { if bank.slot == 32 { assert_eq!( bank.hash().to_string(), - "9FwpFSUvbCfzQMGXDSdvnNhNPpvHUsEJyNA9P3nqiLaJ" + "4syPxVrVFUpksTre5BB5w7qd3BxSU4WzUT6R2fjFgMJ2" ); } if bank.slot == 64 { assert_eq!( bank.hash().to_string(), - "7p6g7GmE9quceefLtPe97fr9YQeYWB562os2ttiG3Anq" + "4GKgnCxQs6AJxcqYQkxa8oF8gEp13bfRNCm2uzCceA26" ); } if bank.slot == 128 { assert_eq!( bank.hash().to_string(), - "DfeuEsVvVRUkce31sM1d4Vhhp6Si99JdVEtYddbBgLKV" + "9YwXsk2qpM7bZLnWGdtqCmDEygiu1KpEcr4zWWBTUKw6" ); break; } @@ -10875,127 +10845,6 @@ pub(crate) mod tests { 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 { - mut genesis_config, - mint_keypair: _, - voting_keypair: _, - } = create_genesis_config_with_vote_accounts( - 1_000_000_000, - &validator_keypairs, - vec![10_000; 2], - ); - genesis_config - .accounts - .remove(&feature_set::timestamp_bounding::id()) - .unwrap(); - let mut bank = Bank::new(&genesis_config); - assert_eq!( - bank.get_timestamp_estimate(EstimateType::Unbounded, None), - 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(EstimateType::Unbounded, None), - 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(EstimateType::Unbounded, None), - 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(EstimateType::Unbounded, None), - None - ); - } - - #[test] - fn test_timestamp_correction_feature() { - let leader_pubkey = solana_sdk::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(); - genesis_config - .accounts - .remove(&feature_set::timestamp_bounding::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)); - assert_eq!( - bank.clock().unix_timestamp, - bank.unix_timestamp_from_genesis() - ); - - // Request `timestamp_correction` activation - bank.store_account( - &feature_set::timestamp_correction::id(), - &feature::create_account( - &Feature { - activated_at: Some(bank.slot), - }, - 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))); - assert_eq!( - bank.clock().unix_timestamp, - bank.unix_timestamp_from_genesis() + additional_secs - ); - } - #[test] fn test_simple_capitalization_adjustment_minimum_genesis_set() { solana_logger::setup(); @@ -11104,129 +10953,6 @@ pub(crate) mod tests { ); } - #[test] - fn test_timestamp_bounding_feature() { - let leader_pubkey = solana_sdk::pubkey::new_rand(); - let GenesisConfigInfo { - mut genesis_config, - voting_keypair, - .. - } = create_genesis_config_with_leader(5, &leader_pubkey, 3); - let slots_in_epoch = 32; - genesis_config - .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); - - 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 allow unbounded timestamp before activation - let mut bank = new_from_parent(&Arc::new(bank)); - assert_eq!( - bank.clock().unix_timestamp, - bank.unix_timestamp_from_genesis() + additional_secs - ); - - // Bank::new_from_parent should not allow epoch_start_timestamp to be set before activation - bank.update_clock(Some(0)); - assert_eq!( - bank.clock().epoch_start_timestamp, - Bank::get_unused_from_slot(bank.slot(), bank.unused) as i64 - ); - - // Request `timestamp_bounding` activation - let feature = Feature { activated_at: None }; - bank.store_account( - &feature_set::timestamp_bounding::id(), - &feature::create_account(&feature, 42), - ); - for _ in 0..30 { - bank = new_from_parent(&Arc::new(bank)); - } - - // Refresh vote timestamp - 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(), - ); - - // Advance to epoch boundary to activate - bank = new_from_parent(&Arc::new(bank)); - - // Bank::new_from_parent is bounding, but should not use epoch_start_timestamp in activation slot - assert_eq!( - bank.clock().unix_timestamp, - bank.unix_timestamp_from_genesis() + additional_secs - ); - - assert_eq!( - bank.clock().epoch_start_timestamp, - bank.unix_timestamp_from_genesis() + additional_secs - ); - - // Past activation slot, bounding should use epoch_start_timestamp in activation slot - bank = new_from_parent(&Arc::new(bank)); - assert_eq!( - bank.clock().unix_timestamp, - bank.unix_timestamp_from_genesis() - ); - - for _ in 0..30 { - bank = new_from_parent(&Arc::new(bank)); - } - - // Refresh vote timestamp - let recent_timestamp: UnixTimestamp = bank.unix_timestamp_from_genesis(); - let additional_secs = 20; - update_vote_account_timestamp( - BlockTimestamp { - slot: bank.slot(), - timestamp: recent_timestamp + additional_secs, - }, - &bank, - &voting_keypair.pubkey(), - ); - - // Advance to epoch boundary - bank = new_from_parent(&Arc::new(bank)); - - // Past activation slot, bounding should use previous epoch_start_timestamp on epoch boundary slots - assert_eq!( - bank.clock().unix_timestamp, - bank.unix_timestamp_from_genesis() // Plus estimated offset + 25% - + ((slots_in_epoch as u32 * Duration::from_nanos(bank.ns_per_slot as u64)) - .as_secs() - * 25 - / 100) as i64, - ); - - assert_eq!( - bank.clock().epoch_start_timestamp, - bank.clock().unix_timestamp - ); - } - #[test] fn test_update_clock_timestamp() { let leader_pubkey = solana_sdk::pubkey::new_rand(); @@ -11236,6 +10962,9 @@ pub(crate) mod tests { .. } = create_genesis_config_with_leader(5, &leader_pubkey, 3); let mut bank = Bank::new(&genesis_config); + // Advance past slot 0, which has special handling. + bank = new_from_parent(&Arc::new(bank)); + bank = new_from_parent(&Arc::new(bank)); assert_eq!( bank.clock().unix_timestamp, bank.unix_timestamp_from_genesis() @@ -11306,6 +11035,157 @@ pub(crate) mod tests { ); } + fn poh_estimate_offset(bank: &Bank) -> Duration { + let mut epoch_start_slot = bank.epoch_schedule.get_first_slot_in_epoch(bank.epoch()); + if epoch_start_slot == bank.slot() { + epoch_start_slot = bank + .epoch_schedule + .get_first_slot_in_epoch(bank.epoch() - 1); + } + bank.slot().saturating_sub(epoch_start_slot) as u32 + * Duration::from_nanos(bank.ns_per_slot as u64) + } + + #[test] + fn test_warp_timestamp_again_feature_slow() { + fn max_allowable_delta_since_epoch(bank: &Bank, max_allowable_drift: u32) -> i64 { + let poh_estimate_offset = poh_estimate_offset(bank); + (poh_estimate_offset.as_secs() + + (poh_estimate_offset * max_allowable_drift / 100).as_secs()) as i64 + } + + let leader_pubkey = solana_sdk::pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + voting_keypair, + .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + let slots_in_epoch = 32; + genesis_config + .accounts + .remove(&feature_set::warp_timestamp_again::id()) + .unwrap(); + genesis_config.epoch_schedule = EpochSchedule::new(slots_in_epoch); + let mut bank = Bank::new(&genesis_config); + + let recent_timestamp: UnixTimestamp = bank.unix_timestamp_from_genesis(); + let additional_secs = 8; // Greater than MAX_ALLOWABLE_DRIFT_PERCENTAGE for full epoch + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: recent_timestamp + additional_secs, + }, + &bank, + &voting_keypair.pubkey(), + ); + + // additional_secs greater than MAX_ALLOWABLE_DRIFT_PERCENTAGE for an epoch + // timestamp bounded to 50% deviation + for _ in 0..31 { + bank = new_from_parent(&Arc::new(bank)); + assert_eq!( + bank.clock().unix_timestamp, + bank.clock().epoch_start_timestamp + + max_allowable_delta_since_epoch(&bank, MAX_ALLOWABLE_DRIFT_PERCENTAGE), + ); + assert_eq!(bank.clock().epoch_start_timestamp, recent_timestamp); + } + + // Request `warp_timestamp_again` activation + let feature = Feature { activated_at: None }; + bank.store_account( + &feature_set::warp_timestamp_again::id(), + &feature::create_account(&feature, 42), + ); + let previous_epoch_timestamp = bank.clock().epoch_start_timestamp; + let previous_timestamp = bank.clock().unix_timestamp; + + // Advance to epoch boundary to activate; time is warped to estimate with no bounding + bank = new_from_parent(&Arc::new(bank)); + assert_ne!(bank.clock().epoch_start_timestamp, previous_timestamp); + assert!( + bank.clock().epoch_start_timestamp + > previous_epoch_timestamp + + max_allowable_delta_since_epoch(&bank, MAX_ALLOWABLE_DRIFT_PERCENTAGE) + ); + + // Refresh vote timestamp + let recent_timestamp: UnixTimestamp = bank.clock().unix_timestamp; + let additional_secs = 8; + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: recent_timestamp + additional_secs, + }, + &bank, + &voting_keypair.pubkey(), + ); + + // additional_secs greater than MAX_ALLOWABLE_DRIFT_PERCENTAGE for 22 slots + // timestamp bounded to 80% deviation + for _ in 0..23 { + bank = new_from_parent(&Arc::new(bank)); + assert_eq!( + bank.clock().unix_timestamp, + bank.clock().epoch_start_timestamp + + max_allowable_delta_since_epoch(&bank, MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW), + ); + assert_eq!(bank.clock().epoch_start_timestamp, recent_timestamp); + } + for _ in 0..8 { + bank = new_from_parent(&Arc::new(bank)); + assert_eq!( + bank.clock().unix_timestamp, + bank.clock().epoch_start_timestamp + + poh_estimate_offset(&bank).as_secs() as i64 + + additional_secs, + ); + assert_eq!(bank.clock().epoch_start_timestamp, recent_timestamp); + } + } + + #[test] + fn test_timestamp_fast() { + fn max_allowable_delta_since_epoch(bank: &Bank, max_allowable_drift: u32) -> i64 { + let poh_estimate_offset = poh_estimate_offset(bank); + (poh_estimate_offset.as_secs() + - (poh_estimate_offset * max_allowable_drift / 100).as_secs()) as i64 + } + + let leader_pubkey = solana_sdk::pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + voting_keypair, + .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + let slots_in_epoch = 32; + genesis_config.epoch_schedule = EpochSchedule::new(slots_in_epoch); + let mut bank = Bank::new(&genesis_config); + + let recent_timestamp: UnixTimestamp = bank.unix_timestamp_from_genesis(); + let additional_secs = 5; // Greater than MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST for full epoch + update_vote_account_timestamp( + BlockTimestamp { + slot: bank.slot(), + timestamp: recent_timestamp - additional_secs, + }, + &bank, + &voting_keypair.pubkey(), + ); + + // additional_secs greater than MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST for an epoch + // timestamp bounded to 25% deviation + for _ in 0..31 { + bank = new_from_parent(&Arc::new(bank)); + assert_eq!( + bank.clock().unix_timestamp, + bank.clock().epoch_start_timestamp + + max_allowable_delta_since_epoch(&bank, MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST), + ); + assert_eq!(bank.clock().epoch_start_timestamp, recent_timestamp); + } + } + 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/bank_forks.rs b/runtime/src/bank_forks.rs index 0a5881e0ab..5c882f2dba 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -303,7 +303,6 @@ mod tests { clock::UnixTimestamp, pubkey::Pubkey, signature::{Keypair, Signer}, - stake_weighted_timestamp::DEPRECATED_TIMESTAMP_SLOT_RANGE, sysvar::epoch_schedule::EpochSchedule, }; use solana_vote_program::vote_state::BlockTimestamp; @@ -412,8 +411,7 @@ mod tests { let additional_timestamp_secs = 2; - let num_slots = slots_in_epoch + 1 // Advance past first epoch boundary - + DEPRECATED_TIMESTAMP_SLOT_RANGE as u64 + 1; // ... and past deprecated slot range + let num_slots = slots_in_epoch + 1; // Advance past first epoch boundary for slot in 1..num_slots { // Just after the epoch boundary, timestamp a vote that will shift // Clock::unix_timestamp from Bank::unix_timestamp_from_genesis() diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 2a20daaa4e..746644e4e5 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -190,10 +190,6 @@ pub mod max_program_call_depth_64 { solana_sdk::declare_id!("YCKSgA6XmjtkQrHBQjpyNrX6EMhJPcYcLWMVgWn36iv"); } -pub mod timestamp_correction { - solana_sdk::declare_id!("3zydSLUwuqqsV3wL5wBsaVgyvMox3XTHx7zLEuQf1U2Z"); -} - pub mod cumulative_rent_related_fixes { solana_sdk::declare_id!("FtjnuAtJTWwX3Kx9m24LduNEhzaGuuPfDW6e14SX2Fy5"); } @@ -210,10 +206,6 @@ pub mod pull_request_ping_pong_check { solana_sdk::declare_id!("5RzEHTnf6D7JPZCvwEzjM19kzBsyjSU3HoMfXaQmVgnZ"); } -pub mod timestamp_bounding { - solana_sdk::declare_id!("2cGj3HJYPhBrtQizd7YbBxEsifFs5qhzabyFjUAp6dBa"); -} - pub mod stake_program_v2 { solana_sdk::declare_id!("Gvd9gGJZDHGMNf1b3jkxrfBQSR5etrfTQSBNKCvLSFJN"); } @@ -286,6 +278,10 @@ pub mod matching_buffer_upgrade_authorities { solana_sdk::declare_id!("B5PSjDEJvKJEUQSL7q94N7XCEoWJCYum8XfUg7yuugUU"); } +pub mod warp_timestamp_again { + solana_sdk::declare_id!("GvDsGDkH5gyzwpDhxNixx8vtx1kwYHH13RiNAPw27zXb"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -303,19 +299,16 @@ lazy_static! { (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"), - (timestamp_correction::id(), "correct bank timestamps"), (cumulative_rent_related_fixes::id(), "rent fixes (#10206, #10468, #11342)"), (sol_log_compute_units_syscall::id(), "sol_log_compute_units syscall (#13243)"), (pubkey_log_syscall_enabled::id(), "pubkey log syscall"), (pull_request_ping_pong_check::id(), "ping-pong packet check #12794"), - (timestamp_bounding::id(), "add timestamp-correction bounding #13120"), (stake_program_v2::id(), "solana_stake_program v2"), (rewrite_stake::id(), "rewrite stake"), (filter_stake_delegation_accounts::id(), "filter stake_delegation_accounts #14062"), (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_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"), @@ -353,6 +346,7 @@ lazy_static! { (full_inflation::stakeconomy::vote::id(), "Community vote allowing Stakeconomy.com to enable full inflation"), (full_inflation::w3m::vote::id(), "Community vote allowing w3m to enable full inflation"), (full_inflation::w3m::enable::id(), "Full inflation enabled by w3m"), + (warp_timestamp_again::id(), "warp timestamp again, adjust bounding to 25% fast 80% slow #15204"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/stake_weighted_timestamp.rs b/sdk/src/stake_weighted_timestamp.rs index a9145fb30f..620583c887 100644 --- a/sdk/src/stake_weighted_timestamp.rs +++ b/sdk/src/stake_weighted_timestamp.rs @@ -10,14 +10,19 @@ use std::{ 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 -pub const DEPRECATED_MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 25; pub const MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 50; +pub const MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST: u32 = 25; +pub const MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW: u32 = 80; pub enum EstimateType { - Bounded(u32), // Value represents max allowable drift percentage - Unbounded, // Deprecated. Remove in the Solana v1.6.0 timeframe + Bounded(MaxAllowableDrift), // Value represents max allowable drift percentage + Unbounded, // Deprecated. Remove in the Solana v1.6.0 timeframe +} + +#[derive(Copy, Clone)] +pub struct MaxAllowableDrift { + pub fast: u32, // Max allowable drift percentage faster than poh estimate + pub slow: u32, // Max allowable drift percentage slower than poh estimate } pub fn calculate_stake_weighted_timestamp( @@ -25,72 +30,9 @@ pub fn calculate_stake_weighted_timestamp( stakes: &HashMap, slot: Slot, slot_duration: Duration, - estimate_type: EstimateType, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, -) -> Option -where - I: IntoIterator, - K: Borrow, - V: Borrow<(Slot, UnixTimestamp)>, -{ - match estimate_type { - 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, - stakes, - slot, - slot_duration, - ), - } -} - -fn calculate_unbounded_stake_weighted_timestamp( - unique_timestamps: I, - stakes: &HashMap, - slot: Slot, - slot_duration: Duration, -) -> Option -where - I: IntoIterator, - K: Borrow, - V: Borrow<(Slot, UnixTimestamp)>, -{ - let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps - .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.borrow()).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 - } -} - -fn calculate_bounded_stake_weighted_timestamp( - unique_timestamps: I, - stakes: &HashMap, - slot: Slot, - slot_duration: Duration, - epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, - max_allowable_drift_percentage: u32, + max_allowable_drift: MaxAllowableDrift, + fix_estimate_into_u64: bool, ) -> Option where I: IntoIterator, @@ -126,27 +68,31 @@ where break; } } - // Bound estimate by `MAX_ALLOWABLE_DRIFT_PERCENTAGE` since the start of the epoch + // Bound estimate by `max_allowable_drift` since the start of the epoch if let Some((epoch_start_slot, epoch_start_timestamp)) = epoch_start_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 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 + }); + let max_allowable_drift_fast = poh_estimate_offset * max_allowable_drift.fast / 100; + let max_allowable_drift_slow = poh_estimate_offset * max_allowable_drift.slow / 100; if estimate_offset > poh_estimate_offset - && estimate_offset - poh_estimate_offset > max_allowable_drift + && estimate_offset - poh_estimate_offset > max_allowable_drift_slow { // estimate offset since the start of the epoch is higher than - // `MAX_ALLOWABLE_DRIFT_PERCENTAGE` + // `MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW` estimate = epoch_start_timestamp + poh_estimate_offset.as_secs() as i64 - + max_allowable_drift.as_secs() as i64; + + max_allowable_drift_slow.as_secs() as i64; } else if estimate_offset < poh_estimate_offset - && poh_estimate_offset - estimate_offset > max_allowable_drift + && poh_estimate_offset - estimate_offset > max_allowable_drift_fast { // estimate offset since the start of the epoch is lower than - // `MAX_ALLOWABLE_DRIFT_PERCENTAGE` + // `MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST` estimate = epoch_start_timestamp + poh_estimate_offset.as_secs() as i64 - - max_allowable_drift.as_secs() as i64; + - max_allowable_drift_fast.as_secs() as i64; } } Some(estimate) @@ -158,114 +104,7 @@ pub mod tests { use solana_sdk::{account::Account, 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 = 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 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_unbounded_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_unbounded_stake_weighted_timestamp( - &unique_timestamps, - &stakes, - slot as Slot, - slot_duration - ), - Some(recent_timestamp + expected_offset as i64) - ); - } - - #[test] - fn test_calculate_bounded_stake_weighted_timestamp_uses_median() { + fn test_calculate_stake_weighted_timestamp_uses_median() { let recent_timestamp: UnixTimestamp = 1_578_909_061; let slot = 5; let slot_duration = Duration::from_millis(400); @@ -274,7 +113,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; + let max_allowable_drift = MaxAllowableDrift { fast: 25, slow: 25 }; // Test low-staked outlier(s) let stakes: HashMap = [ @@ -323,24 +162,17 @@ pub mod tests { .cloned() .collect(); - let unbounded = calculate_unbounded_stake_weighted_timestamp( - &unique_timestamps, - &stakes, - slot as Slot, - slot_duration, - ) - .unwrap(); - - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, None, max_allowable_drift, + true, ) .unwrap(); - assert_eq!(bounded - unbounded, 527); // timestamp w/ 0.00003% of the stake can shift the timestamp backward 8min + // With no bounding, timestamp w/ 0.00003% of the stake can shift the timestamp backward 8min assert_eq!(bounded, recent_timestamp); // low-staked outlier cannot affect bounded timestamp let unique_timestamps: HashMap = [ @@ -354,24 +186,17 @@ pub mod tests { .cloned() .collect(); - let unbounded = calculate_unbounded_stake_weighted_timestamp( - &unique_timestamps, - &stakes, - slot as Slot, - slot_duration, - ) - .unwrap(); - - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, None, max_allowable_drift, + true, ) .unwrap(); - assert_eq!(unbounded - bounded, 3074455295455); // timestamp w/ 0.00003% of the stake can shift the timestamp forward 97k years! + // With no bounding, timestamp w/ 0.00003% of the stake can shift the timestamp forward 97k years! assert_eq!(bounded, recent_timestamp); // low-staked outlier cannot affect bounded timestamp let unique_timestamps: HashMap = [ @@ -385,13 +210,14 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, None, max_allowable_drift, + true, ) .unwrap(); assert_eq!(bounded, recent_timestamp); // multiple low-staked outliers cannot affect bounded timestamp if they don't shift the median @@ -433,13 +259,14 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, None, max_allowable_drift, + true, ) .unwrap(); assert_eq!(bounded, recent_timestamp); // outlier(s) cannot affect bounded timestamp if they don't shift the median @@ -470,26 +297,31 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, None, max_allowable_drift, + true, ) .unwrap(); assert_eq!(recent_timestamp - bounded, 1578909061); // outliers > 1/2 of available stake can affect timestamp } #[test] - fn test_calculate_bounded_stake_weighted_timestamp_poh() { + fn test_calculate_stake_weighted_timestamp_poh() { 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 = 25; - let acceptable_delta = (max_allowable_drift * poh_offset as u32 / 100) as i64; + 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; let poh_estimate = epoch_start_timestamp + poh_offset as i64; let pubkey0 = solana_sdk::pubkey::new_rand(); let pubkey1 = solana_sdk::pubkey::new_rand(); @@ -532,13 +364,14 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + 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); @@ -553,13 +386,14 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + 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); @@ -574,13 +408,14 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + 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); @@ -594,28 +429,39 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + 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); } #[test] - fn test_calculate_bounded_stake_weighted_timestamp_levels() { + fn test_calculate_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; + 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; 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(); @@ -668,24 +514,26 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, Some((0, epoch_start_timestamp)), allowable_drift_25, + true, ) .unwrap(); assert_eq!(bounded, poh_estimate + acceptable_delta_25); - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, Some((0, epoch_start_timestamp)), allowable_drift_50, + true, ) .unwrap(); assert_eq!(bounded, poh_estimate + acceptable_delta_25 + 1); @@ -709,26 +557,252 @@ pub mod tests { .cloned() .collect(); - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, Some((0, epoch_start_timestamp)), allowable_drift_25, + true, ) .unwrap(); assert_eq!(bounded, poh_estimate + acceptable_delta_25); - let bounded = calculate_bounded_stake_weighted_timestamp( + let bounded = calculate_stake_weighted_timestamp( &unique_timestamps, &stakes, slot as Slot, slot_duration, Some((0, epoch_start_timestamp)), allowable_drift_50, + true, ) .unwrap(); assert_eq!(bounded, poh_estimate + acceptable_delta_50); } + + #[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 = [ + ( + 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 = [ + ( + 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 = [ + ( + 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 = [ + ( + 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 = [ + ( + 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 = [ + (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); + } }