Correct Bank timestamp drift every slot (#12737)

* Move timestamp helper to sdk

* Add Bank method for getting timestamp estimate

* Return sysvar info from Bank::clock

* Add feature-gated timestamp correction

* Rename unix_timestamp method to be more descriptive

* Review comments

* Add timestamp metric
This commit is contained in:
Tyera Eulberg 2020-10-09 15:53:41 -06:00 committed by GitHub
parent ed95071c27
commit b028c47d2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 418 additions and 152 deletions

View File

@ -33,6 +33,7 @@ use solana_sdk::{
program_utils::limited_deserialize,
pubkey::Pubkey,
signature::{Keypair, Signature, Signer},
stake_weighted_timestamp::{calculate_stake_weighted_timestamp, TIMESTAMP_SLOT_RANGE},
timing::timestamp,
transaction::Transaction,
};
@ -77,7 +78,6 @@ thread_local!(static PAR_THREAD_POOL_ALL_CPUS: RefCell<ThreadPool> = RefCell::ne
pub const MAX_COMPLETED_SLOTS_IN_CHANNEL: usize = 100_000;
pub const MAX_TURBINE_PROPAGATION_IN_MS: u64 = 100;
pub const MAX_TURBINE_DELAY_IN_TICKS: u64 = MAX_TURBINE_PROPAGATION_IN_MS / MS_PER_TICK;
const TIMESTAMP_SLOT_RANGE: usize = 16;
// An upper bound on maximum number of data shreds we can handle in a slot
// 32K shreds would allow ~320K peak TPS
@ -1630,7 +1630,7 @@ impl Blockstore {
let mut calculate_timestamp = Measure::start("calculate_timestamp");
let stake_weighted_timestamp =
calculate_stake_weighted_timestamp(unique_timestamps, stakes, slot, slot_duration)
calculate_stake_weighted_timestamp(&unique_timestamps, stakes, slot, slot_duration)
.ok_or(BlockstoreError::EmptyEpochStakes)?;
calculate_timestamp.stop();
datapoint_info!(
@ -3130,33 +3130,6 @@ fn slot_has_updates(slot_meta: &SlotMeta, slot_meta_backup: &Option<SlotMeta>) -
(slot_meta_backup.is_some() && slot_meta_backup.as_ref().unwrap().consumed != slot_meta.consumed))
}
fn calculate_stake_weighted_timestamp(
unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)>,
stakes: &HashMap<Pubkey, (u64, Account)>,
slot: Slot,
slot_duration: Duration,
) -> Option<UnixTimestamp> {
let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps
.into_iter()
.filter_map(|(vote_pubkey, (timestamp_slot, timestamp))| {
let offset = (slot - timestamp_slot) as u32 * slot_duration;
stakes.get(&vote_pubkey).map(|(stake, _account)| {
(
(timestamp as u128 + offset.as_secs() as u128) * *stake as u128,
stake,
)
})
})
.fold((0, 0), |(timestamps, stakes), (timestamp, stake)| {
(timestamps + timestamp, stakes + *stake as u128)
});
if total_stake > 0 {
Some((stake_weighted_timestamps_sum / total_stake) as i64)
} else {
None
}
}
// Creates a new ledger with slot 0 full of ticks (and only ticks).
//
// Returns the blockhash that can be used to append entries with.
@ -3478,7 +3451,6 @@ pub mod tests {
hash::{self, hash, Hash},
instruction::CompiledInstruction,
message::Message,
native_token::sol_to_lamports,
packet::PACKET_DATA_SIZE,
pubkey::Pubkey,
signature::Signature,
@ -5889,113 +5861,6 @@ pub mod tests {
}
}
#[test]
fn test_calculate_stake_weighted_timestamp() {
let recent_timestamp: UnixTimestamp = 1_578_909_061;
let slot = 5;
let slot_duration = Duration::from_millis(400);
let expected_offset = (slot * slot_duration).as_secs();
let pubkey0 = Pubkey::new_rand();
let pubkey1 = Pubkey::new_rand();
let pubkey2 = Pubkey::new_rand();
let pubkey3 = Pubkey::new_rand();
let unique_timestamps: HashMap<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_stake_weighted_timestamp(
unique_timestamps.clone(),
&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_stake_weighted_timestamp(
unique_timestamps,
&stakes,
slot as Slot,
slot_duration
),
Some(recent_timestamp + expected_offset as i64)
);
}
#[test]
fn test_persist_transaction_status() {
let blockstore_path = get_tmp_ledger_path!();

View File

@ -57,6 +57,7 @@ use solana_sdk::{
signature::{Keypair, Signature},
slot_hashes::SlotHashes,
slot_history::SlotHistory,
stake_weighted_timestamp::{calculate_stake_weighted_timestamp, TIMESTAMP_SLOT_RANGE},
system_transaction,
sysvar::{self, Sysvar},
timing::years_as_slots,
@ -77,6 +78,7 @@ use std::{
atomic::{AtomicBool, AtomicU64, Ordering::Relaxed},
LockResult, RwLockWriteGuard, {Arc, RwLock, RwLockReadGuard},
},
time::Duration,
};
// Partial SPL Token v2.0.x declarations inlined to avoid an external dependency on the spl-token crate
@ -1012,7 +1014,7 @@ impl Bank {
}
/// computed unix_timestamp at this slot height
pub fn unix_timestamp(&self) -> i64 {
pub fn unix_timestamp_from_genesis(&self) -> i64 {
self.genesis_creation_time + ((self.slot as u128 * self.ns_per_slot) / 1_000_000_000) as i64
}
@ -1035,19 +1037,38 @@ impl Bank {
}
pub fn clock(&self) -> sysvar::clock::Clock {
sysvar::clock::Clock {
sysvar::clock::Clock::from_account(
&self.get_account(&sysvar::clock::id()).unwrap_or_default(),
)
.unwrap_or_default()
}
fn update_clock(&self) {
let mut unix_timestamp = self.unix_timestamp_from_genesis();
if self
.feature_set
.is_active(&feature_set::timestamp_correction::id())
{
if let Some(timestamp_estimate) = self.get_timestamp_estimate() {
if timestamp_estimate > unix_timestamp {
datapoint_info!(
"bank-timestamp-correction",
("from_genesis", unix_timestamp, i64),
("corrected", timestamp_estimate, i64),
);
unix_timestamp = timestamp_estimate
}
}
}
let clock = sysvar::clock::Clock {
slot: self.slot,
unused: Self::get_unused_from_slot(self.slot, self.unused),
epoch: self.epoch_schedule.get_epoch(self.slot),
leader_schedule_epoch: self.epoch_schedule.get_leader_schedule_epoch(self.slot),
unix_timestamp: self.unix_timestamp(),
}
}
fn update_clock(&self) {
unix_timestamp,
};
self.update_sysvar_account(&sysvar::clock::id(), |account| {
self.clock()
.create_account(self.inherit_sysvar_account_balance(account))
clock.create_account(self.inherit_sysvar_account_balance(account))
});
}
@ -1361,6 +1382,46 @@ impl Bank {
self.update_recent_blockhashes_locked(&blockhash_queue);
}
fn get_timestamp_estimate(&self) -> Option<UnixTimestamp> {
let mut get_timestamp_estimate_time = Measure::start("get_timestamp_estimate");
let recent_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = self
.vote_accounts()
.into_iter()
.filter_map(|(pubkey, (_, account))| {
VoteState::from(&account).and_then(|state| {
let timestamp_slot = state.last_timestamp.slot;
if self.slot().checked_sub(timestamp_slot)? <= TIMESTAMP_SLOT_RANGE as u64 {
Some((
pubkey,
(state.last_timestamp.slot, state.last_timestamp.timestamp),
))
} else {
None
}
})
})
.collect();
let slot_duration = Duration::from_nanos(self.ns_per_slot as u64);
let epoch = self.epoch_schedule().get_epoch(self.slot());
let stakes = self.epoch_vote_accounts(epoch)?;
let stake_weighted_timestamp = calculate_stake_weighted_timestamp(
&recent_timestamps,
stakes,
self.slot(),
slot_duration,
);
get_timestamp_estimate_time.stop();
datapoint_info!(
"bank-timestamp",
(
"get_timestamp_estimate_us",
get_timestamp_estimate_time.as_us(),
i64
),
);
stake_weighted_timestamp
}
// Distribute collected transaction fees for this slot to collector_id (= current leader).
//
// Each validator is incentivized to process more transactions to earn more transaction fees.
@ -3951,7 +4012,8 @@ mod tests {
use crate::{
accounts_index::{AccountMap, Ancestors},
genesis_utils::{
create_genesis_config_with_leader, GenesisConfigInfo, BOOTSTRAP_VALIDATOR_LAMPORTS,
create_genesis_config_with_leader, create_genesis_config_with_vote_accounts,
GenesisConfigInfo, ValidatorVoteKeypairs, BOOTSTRAP_VALIDATOR_LAMPORTS,
},
process_instruction::InvokeContext,
status_cache::MAX_CACHE_ENTRIES,
@ -3980,7 +4042,7 @@ mod tests {
use solana_vote_program::vote_state::VoteStateVersions;
use solana_vote_program::{
vote_instruction,
vote_state::{self, Vote, VoteInit, VoteState, MAX_LOCKOUT_HISTORY},
vote_state::{self, BlockTimestamp, Vote, VoteInit, VoteState, MAX_LOCKOUT_HISTORY},
};
use std::{result, time::Duration};
@ -3993,11 +4055,14 @@ mod tests {
}
#[test]
fn test_bank_unix_timestamp() {
fn test_bank_unix_timestamp_from_genesis() {
let (genesis_config, _mint_keypair) = create_genesis_config(1);
let mut bank = Arc::new(Bank::new(&genesis_config));
assert_eq!(genesis_config.creation_time, bank.unix_timestamp());
assert_eq!(
genesis_config.creation_time,
bank.unix_timestamp_from_genesis()
);
let slots_per_sec = 1.0
/ (duration_as_s(&genesis_config.poh_config.target_tick_duration)
* genesis_config.ticks_per_slot as f32);
@ -4006,7 +4071,7 @@ mod tests {
bank = Arc::new(new_from_parent(&bank));
}
assert!(bank.unix_timestamp() - genesis_config.creation_time >= 1);
assert!(bank.unix_timestamp_from_genesis() - genesis_config.creation_time >= 1);
}
#[test]
@ -9385,6 +9450,186 @@ mod tests {
assert_eq!(bank.capitalization(), original_capitalization - 100);
}
fn update_vote_account_timestamp(timestamp: BlockTimestamp, bank: &Bank, vote_pubkey: &Pubkey) {
let mut vote_account = bank.get_account(vote_pubkey).unwrap_or_default();
let mut vote_state = VoteState::from(&vote_account).unwrap_or_default();
vote_state.last_timestamp = timestamp;
let versioned = VoteStateVersions::Current(Box::new(vote_state));
VoteState::to(&versioned, &mut vote_account).unwrap();
bank.store_account(vote_pubkey, &vote_account);
}
#[test]
fn test_get_timestamp_estimate() {
let validator_vote_keypairs0 = ValidatorVoteKeypairs::new_rand();
let validator_vote_keypairs1 = ValidatorVoteKeypairs::new_rand();
let validator_keypairs = vec![&validator_vote_keypairs0, &validator_vote_keypairs1];
let GenesisConfigInfo {
genesis_config,
mint_keypair: _,
voting_keypair: _,
} = create_genesis_config_with_vote_accounts(
1_000_000_000,
&validator_keypairs,
vec![10_000; 2],
);
let mut bank = Bank::new(&genesis_config);
assert_eq!(bank.get_timestamp_estimate(), Some(0));
let recent_timestamp: UnixTimestamp = bank.unix_timestamp_from_genesis();
update_vote_account_timestamp(
BlockTimestamp {
slot: bank.slot(),
timestamp: recent_timestamp,
},
&bank,
&validator_vote_keypairs0.vote_keypair.pubkey(),
);
let additional_secs = 2;
update_vote_account_timestamp(
BlockTimestamp {
slot: bank.slot(),
timestamp: recent_timestamp + additional_secs,
},
&bank,
&validator_vote_keypairs1.vote_keypair.pubkey(),
);
assert_eq!(
bank.get_timestamp_estimate(),
Some(recent_timestamp + additional_secs / 2)
);
for _ in 0..10 {
bank = new_from_parent(&Arc::new(bank));
}
let adjustment = (bank.ns_per_slot as u64 * bank.slot()) / 1_000_000_000;
assert_eq!(
bank.get_timestamp_estimate(),
Some(recent_timestamp + adjustment as i64 + additional_secs / 2)
);
for _ in 0..7 {
bank = new_from_parent(&Arc::new(bank));
}
assert_eq!(bank.get_timestamp_estimate(), None);
}
#[test]
fn test_timestamp_correction_feature() {
let leader_pubkey = Pubkey::new_rand();
let GenesisConfigInfo {
mut genesis_config,
voting_keypair,
..
} = create_genesis_config_with_leader(5, &leader_pubkey, 3);
genesis_config
.accounts
.remove(&feature_set::timestamp_correction::id())
.unwrap();
let bank = Bank::new(&genesis_config);
let recent_timestamp: UnixTimestamp = bank.unix_timestamp_from_genesis();
let additional_secs = 1;
update_vote_account_timestamp(
BlockTimestamp {
slot: bank.slot(),
timestamp: recent_timestamp + additional_secs,
},
&bank,
&voting_keypair.pubkey(),
);
// Bank::new_from_parent should not adjust timestamp before feature activation
let mut bank = new_from_parent(&Arc::new(bank));
let clock =
sysvar::clock::Clock::from_account(&bank.get_account(&sysvar::clock::id()).unwrap())
.unwrap();
assert_eq!(clock.unix_timestamp, bank.unix_timestamp_from_genesis());
// Request `timestamp_correction` activation
let feature = Feature {
activated_at: Some(bank.slot),
};
bank.store_account(
&feature_set::timestamp_correction::id(),
&feature.create_account(42),
);
bank.compute_active_feature_set(true);
// Now Bank::new_from_parent should adjust timestamp
let bank = Arc::new(new_from_parent(&Arc::new(bank)));
let clock =
sysvar::clock::Clock::from_account(&bank.get_account(&sysvar::clock::id()).unwrap())
.unwrap();
assert_eq!(
clock.unix_timestamp,
bank.unix_timestamp_from_genesis() + additional_secs
);
}
#[test]
fn test_update_clock_timestamp() {
let leader_pubkey = Pubkey::new_rand();
let GenesisConfigInfo {
genesis_config,
voting_keypair,
..
} = create_genesis_config_with_leader(5, &leader_pubkey, 3);
let bank = Bank::new(&genesis_config);
assert_eq!(
bank.clock().unix_timestamp,
bank.unix_timestamp_from_genesis()
);
bank.update_clock();
assert_eq!(
bank.clock().unix_timestamp,
bank.unix_timestamp_from_genesis()
);
update_vote_account_timestamp(
BlockTimestamp {
slot: bank.slot(),
timestamp: bank.unix_timestamp_from_genesis() - 1,
},
&bank,
&voting_keypair.pubkey(),
);
bank.update_clock();
assert_eq!(
bank.clock().unix_timestamp,
bank.unix_timestamp_from_genesis()
);
update_vote_account_timestamp(
BlockTimestamp {
slot: bank.slot(),
timestamp: bank.unix_timestamp_from_genesis(),
},
&bank,
&voting_keypair.pubkey(),
);
bank.update_clock();
assert_eq!(
bank.clock().unix_timestamp,
bank.unix_timestamp_from_genesis()
);
update_vote_account_timestamp(
BlockTimestamp {
slot: bank.slot(),
timestamp: bank.unix_timestamp_from_genesis() + 1,
},
&bank,
&voting_keypair.pubkey(),
);
bank.update_clock();
assert_eq!(
bank.clock().unix_timestamp,
bank.unix_timestamp_from_genesis() + 1
);
}
fn setup_bank_with_removable_zero_lamport_account() -> Arc<Bank> {
let (genesis_config, _mint_keypair) = create_genesis_config(2000);
let bank0 = Bank::new(&genesis_config);

View File

@ -57,6 +57,10 @@ pub mod max_program_call_depth_64 {
solana_sdk::declare_id!("YCKSgA6XmjtkQrHBQjpyNrX6EMhJPcYcLWMVgWn36iv");
}
pub mod timestamp_correction {
solana_sdk::declare_id!("3zydSLUwuqqsV3wL5wBsaVgyvMox3XTHx7zLEuQf1U2Z");
}
lazy_static! {
/// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
@ -72,7 +76,8 @@ lazy_static! {
(no_overflow_rent_distribution::id(), "no overflow rent distribution"),
(ristretto_mul_syscall_enabled::id(), "ristretto multiply syscall"),
(max_invoke_depth_4::id(), "max invoke call depth 4"),
(max_program_call_depth_64::id(), "max program call depth 64")
(max_program_call_depth_64::id(), "max program call depth 64"),
(timestamp_correction::id(), "correct bank timestamps"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()

View File

@ -45,6 +45,7 @@ pub mod short_vec;
pub mod slot_hashes;
pub mod slot_history;
pub mod stake_history;
pub mod stake_weighted_timestamp;
pub mod system_instruction;
pub mod system_program;
pub mod sysvar;

View File

@ -0,0 +1,150 @@
/// A helper for calculating a stake-weighted timestamp estimate from a set of timestamps and epoch
/// stake.
use solana_sdk::{
account::Account,
clock::{Slot, UnixTimestamp},
pubkey::Pubkey,
};
use std::{collections::HashMap, time::Duration};
pub const TIMESTAMP_SLOT_RANGE: usize = 16;
pub fn calculate_stake_weighted_timestamp(
unique_timestamps: &HashMap<Pubkey, (Slot, UnixTimestamp)>,
stakes: &HashMap<Pubkey, (u64, Account)>,
slot: Slot,
slot_duration: Duration,
) -> Option<UnixTimestamp> {
let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps
.iter()
.filter_map(|(vote_pubkey, (timestamp_slot, timestamp))| {
let offset = (slot - timestamp_slot) as u32 * slot_duration;
stakes.get(&vote_pubkey).map(|(stake, _account)| {
(
(*timestamp as u128 + offset.as_secs() as u128) * *stake as u128,
stake,
)
})
})
.fold((0, 0), |(timestamps, stakes), (timestamp, stake)| {
(timestamps + timestamp, stakes + *stake as u128)
});
if total_stake > 0 {
Some((stake_weighted_timestamps_sum / total_stake) as i64)
} else {
None
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use solana_sdk::native_token::sol_to_lamports;
#[test]
fn test_calculate_stake_weighted_timestamp() {
let recent_timestamp: UnixTimestamp = 1_578_909_061;
let slot = 5;
let slot_duration = Duration::from_millis(400);
let expected_offset = (slot * slot_duration).as_secs();
let pubkey0 = Pubkey::new_rand();
let pubkey1 = Pubkey::new_rand();
let pubkey2 = Pubkey::new_rand();
let pubkey3 = Pubkey::new_rand();
let unique_timestamps: HashMap<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_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_stake_weighted_timestamp(
&unique_timestamps,
&stakes,
slot as Slot,
slot_duration
),
Some(recent_timestamp + expected_offset as i64)
);
}
}