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,
|
||||
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!();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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