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:
parent
ed95071c27
commit
b028c47d2b
|
@ -33,6 +33,7 @@ use solana_sdk::{
|
||||||
program_utils::limited_deserialize,
|
program_utils::limited_deserialize,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{Keypair, Signature, Signer},
|
signature::{Keypair, Signature, Signer},
|
||||||
|
stake_weighted_timestamp::{calculate_stake_weighted_timestamp, TIMESTAMP_SLOT_RANGE},
|
||||||
timing::timestamp,
|
timing::timestamp,
|
||||||
transaction::Transaction,
|
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_COMPLETED_SLOTS_IN_CHANNEL: usize = 100_000;
|
||||||
pub const MAX_TURBINE_PROPAGATION_IN_MS: u64 = 100;
|
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;
|
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
|
// An upper bound on maximum number of data shreds we can handle in a slot
|
||||||
// 32K shreds would allow ~320K peak TPS
|
// 32K shreds would allow ~320K peak TPS
|
||||||
|
@ -1630,7 +1630,7 @@ impl Blockstore {
|
||||||
|
|
||||||
let mut calculate_timestamp = Measure::start("calculate_timestamp");
|
let mut calculate_timestamp = Measure::start("calculate_timestamp");
|
||||||
let stake_weighted_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)?;
|
.ok_or(BlockstoreError::EmptyEpochStakes)?;
|
||||||
calculate_timestamp.stop();
|
calculate_timestamp.stop();
|
||||||
datapoint_info!(
|
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))
|
(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).
|
// Creates a new ledger with slot 0 full of ticks (and only ticks).
|
||||||
//
|
//
|
||||||
// Returns the blockhash that can be used to append entries with.
|
// Returns the blockhash that can be used to append entries with.
|
||||||
|
@ -3478,7 +3451,6 @@ pub mod tests {
|
||||||
hash::{self, hash, Hash},
|
hash::{self, hash, Hash},
|
||||||
instruction::CompiledInstruction,
|
instruction::CompiledInstruction,
|
||||||
message::Message,
|
message::Message,
|
||||||
native_token::sol_to_lamports,
|
|
||||||
packet::PACKET_DATA_SIZE,
|
packet::PACKET_DATA_SIZE,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::Signature,
|
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]
|
#[test]
|
||||||
fn test_persist_transaction_status() {
|
fn test_persist_transaction_status() {
|
||||||
let blockstore_path = get_tmp_ledger_path!();
|
let blockstore_path = get_tmp_ledger_path!();
|
||||||
|
|
|
@ -57,6 +57,7 @@ use solana_sdk::{
|
||||||
signature::{Keypair, Signature},
|
signature::{Keypair, Signature},
|
||||||
slot_hashes::SlotHashes,
|
slot_hashes::SlotHashes,
|
||||||
slot_history::SlotHistory,
|
slot_history::SlotHistory,
|
||||||
|
stake_weighted_timestamp::{calculate_stake_weighted_timestamp, TIMESTAMP_SLOT_RANGE},
|
||||||
system_transaction,
|
system_transaction,
|
||||||
sysvar::{self, Sysvar},
|
sysvar::{self, Sysvar},
|
||||||
timing::years_as_slots,
|
timing::years_as_slots,
|
||||||
|
@ -77,6 +78,7 @@ use std::{
|
||||||
atomic::{AtomicBool, AtomicU64, Ordering::Relaxed},
|
atomic::{AtomicBool, AtomicU64, Ordering::Relaxed},
|
||||||
LockResult, RwLockWriteGuard, {Arc, RwLock, RwLockReadGuard},
|
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
|
// 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
|
/// 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
|
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 {
|
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,
|
slot: self.slot,
|
||||||
unused: Self::get_unused_from_slot(self.slot, self.unused),
|
unused: Self::get_unused_from_slot(self.slot, self.unused),
|
||||||
epoch: self.epoch_schedule.get_epoch(self.slot),
|
epoch: self.epoch_schedule.get_epoch(self.slot),
|
||||||
leader_schedule_epoch: self.epoch_schedule.get_leader_schedule_epoch(self.slot),
|
leader_schedule_epoch: self.epoch_schedule.get_leader_schedule_epoch(self.slot),
|
||||||
unix_timestamp: self.unix_timestamp(),
|
unix_timestamp,
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fn update_clock(&self) {
|
|
||||||
self.update_sysvar_account(&sysvar::clock::id(), |account| {
|
self.update_sysvar_account(&sysvar::clock::id(), |account| {
|
||||||
self.clock()
|
clock.create_account(self.inherit_sysvar_account_balance(account))
|
||||||
.create_account(self.inherit_sysvar_account_balance(account))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1361,6 +1382,46 @@ impl Bank {
|
||||||
self.update_recent_blockhashes_locked(&blockhash_queue);
|
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).
|
// 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.
|
// Each validator is incentivized to process more transactions to earn more transaction fees.
|
||||||
|
@ -3951,7 +4012,8 @@ mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
accounts_index::{AccountMap, Ancestors},
|
accounts_index::{AccountMap, Ancestors},
|
||||||
genesis_utils::{
|
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,
|
process_instruction::InvokeContext,
|
||||||
status_cache::MAX_CACHE_ENTRIES,
|
status_cache::MAX_CACHE_ENTRIES,
|
||||||
|
@ -3980,7 +4042,7 @@ mod tests {
|
||||||
use solana_vote_program::vote_state::VoteStateVersions;
|
use solana_vote_program::vote_state::VoteStateVersions;
|
||||||
use solana_vote_program::{
|
use solana_vote_program::{
|
||||||
vote_instruction,
|
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};
|
use std::{result, time::Duration};
|
||||||
|
|
||||||
|
@ -3993,11 +4055,14 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bank_unix_timestamp() {
|
fn test_bank_unix_timestamp_from_genesis() {
|
||||||
let (genesis_config, _mint_keypair) = create_genesis_config(1);
|
let (genesis_config, _mint_keypair) = create_genesis_config(1);
|
||||||
let mut bank = Arc::new(Bank::new(&genesis_config));
|
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
|
let slots_per_sec = 1.0
|
||||||
/ (duration_as_s(&genesis_config.poh_config.target_tick_duration)
|
/ (duration_as_s(&genesis_config.poh_config.target_tick_duration)
|
||||||
* genesis_config.ticks_per_slot as f32);
|
* genesis_config.ticks_per_slot as f32);
|
||||||
|
@ -4006,7 +4071,7 @@ mod tests {
|
||||||
bank = Arc::new(new_from_parent(&bank));
|
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]
|
#[test]
|
||||||
|
@ -9385,6 +9450,186 @@ mod tests {
|
||||||
assert_eq!(bank.capitalization(), original_capitalization - 100);
|
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> {
|
fn setup_bank_with_removable_zero_lamport_account() -> Arc<Bank> {
|
||||||
let (genesis_config, _mint_keypair) = create_genesis_config(2000);
|
let (genesis_config, _mint_keypair) = create_genesis_config(2000);
|
||||||
let bank0 = Bank::new(&genesis_config);
|
let bank0 = Bank::new(&genesis_config);
|
||||||
|
|
|
@ -57,6 +57,10 @@ pub mod max_program_call_depth_64 {
|
||||||
solana_sdk::declare_id!("YCKSgA6XmjtkQrHBQjpyNrX6EMhJPcYcLWMVgWn36iv");
|
solana_sdk::declare_id!("YCKSgA6XmjtkQrHBQjpyNrX6EMhJPcYcLWMVgWn36iv");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod timestamp_correction {
|
||||||
|
solana_sdk::declare_id!("3zydSLUwuqqsV3wL5wBsaVgyvMox3XTHx7zLEuQf1U2Z");
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Map of feature identifiers to user-visible description
|
/// Map of feature identifiers to user-visible description
|
||||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
||||||
|
@ -72,7 +76,8 @@ lazy_static! {
|
||||||
(no_overflow_rent_distribution::id(), "no overflow rent distribution"),
|
(no_overflow_rent_distribution::id(), "no overflow rent distribution"),
|
||||||
(ristretto_mul_syscall_enabled::id(), "ristretto multiply syscall"),
|
(ristretto_mul_syscall_enabled::id(), "ristretto multiply syscall"),
|
||||||
(max_invoke_depth_4::id(), "max invoke call depth 4"),
|
(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 ***************/
|
/*************** ADD NEW FEATURES HERE ***************/
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -45,6 +45,7 @@ pub mod short_vec;
|
||||||
pub mod slot_hashes;
|
pub mod slot_hashes;
|
||||||
pub mod slot_history;
|
pub mod slot_history;
|
||||||
pub mod stake_history;
|
pub mod stake_history;
|
||||||
|
pub mod stake_weighted_timestamp;
|
||||||
pub mod system_instruction;
|
pub mod system_instruction;
|
||||||
pub mod system_program;
|
pub mod system_program;
|
||||||
pub mod sysvar;
|
pub mod sysvar;
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue