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 <mvines@gmail.com>

Co-authored-by: Michael Vines <mvines@gmail.com>
This commit is contained in:
Tyera Eulberg 2021-02-09 15:49:00 -07:00 committed by GitHub
parent 2758588ddd
commit da6753b8c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 516 additions and 928 deletions

View File

@ -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<Bank>, blockstore: &Arc<Blockstore>) {
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);
}
}

View File

@ -387,11 +387,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:
- `<u64>` - block, identified by Slot

View File

@ -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};
@ -1682,29 +1673,6 @@ impl Blockstore {
self.blocktime_cf.get(slot)
}
fn get_timestamp_slots(&self, slot: Slot, timestamp_sample_range: usize) -> Vec<Slot> {
let root_iterator = self
.db
.iter::<cf::Root>(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<Slot> = root_iterator
.unwrap()
.map(|(iter_slot, _)| iter_slot)
.take(timestamp_sample_range)
.collect();
timestamp_slots.sort_unstable();
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);
@ -1712,55 +1680,6 @@ impl Blockstore {
self.blocktime_cf.put(slot, &timestamp)
}
// DEPRECATED as of feature_set::timestamp_correction
pub fn cache_block_time_from_slot_entries(
&self,
slot: Slot,
slot_duration: Duration,
stakes: &HashMap<Pubkey, (u64, ArcVoteAccount)>,
) -> Result<()> {
if !self.is_root(slot) {
return Err(BlockstoreError::SlotNotRooted);
}
let mut get_unique_timestamps = Measure::start("get_unique_timestamps");
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = 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<Slot> {
let mut root_iterator = self.rooted_slot_iterator(self.lowest_slot())?;
Ok(root_iterator.next().unwrap_or_default())
@ -2385,36 +2304,6 @@ impl Blockstore {
self.rewards_cf.put_protobuf(index, &rewards)
}
fn get_block_timestamps(&self, slot: Slot) -> Result<Vec<(Pubkey, (Slot, UnixTimestamp))>> {
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<Vec<(Slot, PerfSample)>> {
Ok(self
.db
@ -3595,7 +3484,6 @@ fn adjust_ulimit_nofile(enforce_ulimit_nofile: bool) -> 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},
@ -3609,7 +3497,6 @@ pub mod tests {
use solana_sdk::{
hash::{self, hash, Hash},
instruction::CompiledInstruction,
message::Message,
packet::PACKET_DATA_SIZE,
pubkey::Pubkey,
signature::Signature,
@ -3617,7 +3504,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::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;
@ -5963,130 +5763,6 @@ pub mod tests {
Blockstore::destroy(&ledger_path).expect("Expected successful database destruction");
}
#[test]
fn test_get_block_timestamps() {
let vote_keypairs: Vec<Keypair> = (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<Entry> = 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<Keypair> = (0..6).map(|_| Keypair::new()).collect();
// Populate slot 1 with vote transactions, none of which have timestamps
let mut vote_entries: Vec<Entry> = 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!();

View File

@ -61,9 +61,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},
@ -1355,89 +1354,59 @@ impl Bank {
}
fn update_clock(&self, parent_epoch: Option<Epoch>) {
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())
{
unix_timestamp = self.clock().unix_timestamp;
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)
{
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 mut 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();
@ -1925,13 +1894,10 @@ impl Bank {
fn get_timestamp_estimate(
&self,
estimate_type: EstimateType,
max_allowable_drift: MaxAllowableDrift,
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
) -> Option<UnixTimestamp> {
let mut get_timestamp_estimate_time = Measure::start("get_timestamp_estimate");
let timestamp_bounding_enabled = self
.feature_set
.is_active(&feature_set::timestamp_bounding::id());
let slots_per_epoch = self.epoch_schedule().slots_per_epoch;
let recent_timestamps =
self.vote_accounts()
@ -1940,9 +1906,7 @@ impl Bank {
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 (timestamp_bounding_enabled && slot_delta <= slots_per_epoch)
|| slot_delta <= DEPRECATED_TIMESTAMP_SLOT_RANGE as u64
{
if slot_delta <= slots_per_epoch {
Some((
pubkey,
(
@ -1962,8 +1926,10 @@ impl Bank {
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!(
@ -10216,19 +10182,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;
}
@ -11134,127 +11100,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();
@ -11365,129 +11210,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();
@ -11570,6 +11292,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);
}
}
#[test]
fn test_program_is_native_loader() {
let (genesis_config, mint_keypair) = create_genesis_config(50000);

View File

@ -327,7 +327,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;
@ -436,8 +435,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()

View File

@ -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");
}
@ -238,10 +230,6 @@ pub mod try_find_program_address_syscall_enabled {
solana_sdk::declare_id!("EMsMNadQNhCYDyGpYH5Tx6dGHxiUqKHk782PU5XaWfmi");
}
pub mod warp_timestamp {
solana_sdk::declare_id!("Bfqm7fGk5MBptqa2WHXWFLH7uJvq8hkJcAQPipy2bAMk");
}
pub mod stake_program_v3 {
solana_sdk::declare_id!("Ego6nTu7WsBcZBvVqJQKp6Yku2N3mrfG8oYCfaLZkAeK");
}
@ -290,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<Pubkey, &'static str> = [
@ -307,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"),
@ -358,6 +347,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()

View File

@ -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<I, K, V, T>(
@ -25,72 +30,9 @@ pub fn calculate_stake_weighted_timestamp<I, K, V, T>(
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
slot: Slot,
slot_duration: Duration,
estimate_type: EstimateType,
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
) -> Option<UnixTimestamp>
where
I: IntoIterator<Item = (K, V)>,
K: Borrow<Pubkey>,
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<I, K, V, T>(
unique_timestamps: I,
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
slot: Slot,
slot_duration: Duration,
) -> Option<UnixTimestamp>
where
I: IntoIterator<Item = (K, V)>,
K: Borrow<Pubkey>,
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<I, K, V, T>(
unique_timestamps: I,
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
slot: Slot,
slot_duration: Duration,
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
max_allowable_drift_percentage: u32,
max_allowable_drift: MaxAllowableDrift,
fix_estimate_into_u64: bool,
) -> Option<UnixTimestamp>
where
I: IntoIterator<Item = (K, V)>,
@ -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<Pubkey, (Slot, UnixTimestamp)> = [
(pubkey0, (0, recent_timestamp)),
(pubkey1, (0, recent_timestamp)),
(pubkey2, (0, recent_timestamp)),
(pubkey3, (0, recent_timestamp)),
]
.iter()
.cloned()
.collect();
let stakes: HashMap<Pubkey, (u64, Account)> = [
(
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<Pubkey, (u64, Account)> = [
(
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<Pubkey, (u64, Account)> = [
@ -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<Pubkey, (Slot, UnixTimestamp)> = [
@ -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<Pubkey, (Slot, UnixTimestamp)> = [
@ -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<Pubkey, (u64, Account)> = [
(
pubkey0,
(
sol_to_lamports(1_000_000.0),
Account::new(1, 0, &Pubkey::default()),
),
),
(
pubkey1,
(
sol_to_lamports(1_000_000.0),
Account::new(1, 0, &Pubkey::default()),
),
),
(
pubkey2,
(
sol_to_lamports(1_000_000.0),
Account::new(1, 0, &Pubkey::default()),
),
),
]
.iter()
.cloned()
.collect();
// Test when stake-weighted median is more than 25% fast
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
(
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<Pubkey, (Slot, UnixTimestamp)> = [
(
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<Pubkey, (Slot, UnixTimestamp)> = [
(
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<Pubkey, (u64, Account)> = [
(
pubkey0,
(
sol_to_lamports(1_000_000.0),
Account::new(1, 0, &Pubkey::default()),
),
),
(
pubkey1,
(
sol_to_lamports(1_000_000.0),
Account::new(1, 0, &Pubkey::default()),
),
),
(
pubkey2,
(
sol_to_lamports(1_000_000.0),
Account::new(1, 0, &Pubkey::default()),
),
),
]
.iter()
.cloned()
.collect();
// Test when stake-weighted median is before epoch_start_timestamp
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
(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);
}
}