v1.4: Update timestamp max allowable drift to 50% of PoH (#14532)

* Add timestamp warp

* Change max_allowable_drift to 50%

* Fill in PR#

* Fix rpc test setup
This commit is contained in:
Tyera Eulberg
2021-01-11 15:49:19 -07:00
committed by GitHub
parent 664e772d0f
commit 375295a605
4 changed files with 230 additions and 31 deletions

View File

@ -2894,7 +2894,7 @@ pub mod tests {
}; };
use solana_vote_program::{ use solana_vote_program::{
vote_instruction, vote_instruction,
vote_state::{Vote, VoteInit, MAX_LOCKOUT_HISTORY}, vote_state::{BlockTimestamp, Vote, VoteInit, VoteStateVersions, MAX_LOCKOUT_HISTORY},
}; };
use spl_token_v2_0::{ use spl_token_v2_0::{
solana_program::{program_option::COption, pubkey::Pubkey as SplTokenPubkey}, solana_program::{program_option::COption, pubkey::Pubkey as SplTokenPubkey},
@ -2929,6 +2929,18 @@ pub mod tests {
) -> RpcHandler { ) -> RpcHandler {
let (bank_forks, alice, leader_vote_keypair) = new_bank_forks(); let (bank_forks, alice, leader_vote_keypair) = new_bank_forks();
let bank = bank_forks.read().unwrap().working_bank(); let bank = bank_forks.read().unwrap().working_bank();
let vote_pubkey = leader_vote_keypair.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 = BlockTimestamp {
slot: bank.slot(),
timestamp: bank.clock().unix_timestamp,
};
let versioned = VoteStateVersions::new_current(vote_state);
VoteState::to(&versioned, &mut vote_account).unwrap();
bank.store_account(&vote_pubkey, &vote_account);
let ledger_path = get_tmp_ledger_path!(); let ledger_path = get_tmp_ledger_path!();
let blockstore = Blockstore::open(&ledger_path).unwrap(); let blockstore = Blockstore::open(&ledger_path).unwrap();
let blockstore = Arc::new(blockstore); let blockstore = Arc::new(blockstore);
@ -5118,7 +5130,7 @@ pub mod tests {
let res = io.handle_request_sync(&req, meta.clone()); let res = io.handle_request_sync(&req, meta.clone());
let expected = format!( let expected = format!(
r#"{{"jsonrpc":"2.0","result":{},"id":1}}"#, r#"{{"jsonrpc":"2.0","result":{},"id":1}}"#,
base_timestamp + (5 * slot_duration).as_secs() as i64 base_timestamp + (7 * slot_duration).as_secs() as i64
); );
let expected: Response = let expected: Response =
serde_json::from_str(&expected).expect("expected response deserialization"); serde_json::from_str(&expected).expect("expected response deserialization");

View File

@ -60,7 +60,9 @@ use solana_sdk::{
slot_hashes::SlotHashes, slot_hashes::SlotHashes,
slot_history::SlotHistory, slot_history::SlotHistory,
stake_weighted_timestamp::{ stake_weighted_timestamp::{
calculate_stake_weighted_timestamp, EstimateType, DEPRECATED_TIMESTAMP_SLOT_RANGE, calculate_stake_weighted_timestamp, EstimateType,
DEPRECATED_MAX_ALLOWABLE_DRIFT_PERCENTAGE, DEPRECATED_TIMESTAMP_SLOT_RANGE,
MAX_ALLOWABLE_DRIFT_PERCENTAGE,
}, },
system_transaction, system_transaction,
sysvar::{self}, sysvar::{self},
@ -1286,18 +1288,36 @@ impl Bank {
// needed for timestamp bounding, but isn't yet corrected for the activation slot // 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 epoch_start_timestamp = if self.slot() > timestamp_bounding_activation_slot
{ {
let epoch = if let Some(epoch) = parent_epoch { let warp_timestamp = self
epoch .feature_set
.activated_slot(&feature_set::warp_timestamp::id());
if warp_timestamp == Some(self.slot()) {
None
} else { } else {
self.epoch() let epoch = if let Some(epoch) = parent_epoch {
}; epoch
let first_slot_in_epoch = } else {
self.epoch_schedule.get_first_slot_in_epoch(epoch); self.epoch()
Some((first_slot_in_epoch, self.clock().epoch_start_timestamp)) };
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 { } else {
None None
}; };
(EstimateType::Bounded, epoch_start_timestamp) 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 { } else {
(EstimateType::Unbounded, None) (EstimateType::Unbounded, None)
}; };
@ -11058,6 +11078,10 @@ pub(crate) mod tests {
.accounts .accounts
.remove(&feature_set::timestamp_bounding::id()) .remove(&feature_set::timestamp_bounding::id())
.unwrap(); .unwrap();
genesis_config
.accounts
.remove(&feature_set::warp_timestamp::id())
.unwrap();
genesis_config.epoch_schedule = EpochSchedule::new(slots_in_epoch); genesis_config.epoch_schedule = EpochSchedule::new(slots_in_epoch);
let bank = Bank::new(&genesis_config); let bank = Bank::new(&genesis_config);

View File

@ -130,6 +130,10 @@ pub mod abort_on_all_cpi_failures {
solana_sdk::declare_id!("ED5D5a2hQaECHaMmKpnU48GdsfafdCjkb3pgAw5RKbb2"); solana_sdk::declare_id!("ED5D5a2hQaECHaMmKpnU48GdsfafdCjkb3pgAw5RKbb2");
} }
pub mod warp_timestamp {
solana_sdk::declare_id!("Bfqm7fGk5MBptqa2WHXWFLH7uJvq8hkJcAQPipy2bAMk");
}
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> = [
@ -164,6 +168,7 @@ lazy_static! {
(limit_cpi_loader_invoke::id(), "Loader not authorized via CPI"), (limit_cpi_loader_invoke::id(), "Loader not authorized via CPI"),
(use_loaded_program_accounts::id(), "Use loaded program accounts"), (use_loaded_program_accounts::id(), "Use loaded program accounts"),
(abort_on_all_cpi_failures::id(), "Abort on all CPI failures"), (abort_on_all_cpi_failures::id(), "Abort on all CPI failures"),
(warp_timestamp::id(), "warp timestamp to current, adjust bounding to 50% #14532"),
/*************** ADD NEW FEATURES HERE ***************/ /*************** ADD NEW FEATURES HERE ***************/
] ]
.iter() .iter()

View File

@ -5,34 +5,42 @@ use solana_sdk::{
pubkey::Pubkey, pubkey::Pubkey,
}; };
use std::{ use std::{
borrow::Borrow,
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
time::Duration, time::Duration,
}; };
pub const TIMESTAMP_SLOT_RANGE: usize = 32; 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_TIMESTAMP_SLOT_RANGE: usize = 16; // Deprecated. Remove in the Solana v1.6.0 timeframe
const MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 25; pub const DEPRECATED_MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 25;
pub const MAX_ALLOWABLE_DRIFT_PERCENTAGE: u32 = 50;
pub enum EstimateType { pub enum EstimateType {
Bounded, Bounded(u32), // Value represents max allowable drift percentage
Unbounded, // Deprecated. Remove in the Solana v1.6.0 timeframe Unbounded, // Deprecated. Remove in the Solana v1.6.0 timeframe
} }
pub fn calculate_stake_weighted_timestamp<T>( pub fn calculate_stake_weighted_timestamp<I, K, V, T>(
unique_timestamps: &HashMap<Pubkey, (Slot, UnixTimestamp)>, unique_timestamps: I,
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>, stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
slot: Slot, slot: Slot,
slot_duration: Duration, slot_duration: Duration,
estimate_type: EstimateType, estimate_type: EstimateType,
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
) -> Option<UnixTimestamp> { ) -> Option<UnixTimestamp>
where
I: IntoIterator<Item = (K, V)>,
K: Borrow<Pubkey>,
V: Borrow<(Slot, UnixTimestamp)>,
{
match estimate_type { match estimate_type {
EstimateType::Bounded => calculate_bounded_stake_weighted_timestamp( EstimateType::Bounded(max_allowable_drift) => calculate_bounded_stake_weighted_timestamp(
unique_timestamps, unique_timestamps,
stakes, stakes,
slot, slot,
slot_duration, slot_duration,
epoch_start_timestamp, epoch_start_timestamp,
max_allowable_drift,
), ),
EstimateType::Unbounded => calculate_unbounded_stake_weighted_timestamp( EstimateType::Unbounded => calculate_unbounded_stake_weighted_timestamp(
unique_timestamps, unique_timestamps,
@ -43,17 +51,23 @@ pub fn calculate_stake_weighted_timestamp<T>(
} }
} }
fn calculate_unbounded_stake_weighted_timestamp<T>( fn calculate_unbounded_stake_weighted_timestamp<I, K, V, T>(
unique_timestamps: &HashMap<Pubkey, (Slot, UnixTimestamp)>, unique_timestamps: I,
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>, stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
slot: Slot, slot: Slot,
slot_duration: Duration, slot_duration: Duration,
) -> Option<UnixTimestamp> { ) -> Option<UnixTimestamp>
where
I: IntoIterator<Item = (K, V)>,
K: Borrow<Pubkey>,
V: Borrow<(Slot, UnixTimestamp)>,
{
let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps let (stake_weighted_timestamps_sum, total_stake) = unique_timestamps
.iter() .into_iter()
.filter_map(|(vote_pubkey, (timestamp_slot, timestamp))| { .filter_map(|(vote_pubkey, slot_timestamp)| {
let (timestamp_slot, timestamp) = slot_timestamp.borrow();
let offset = (slot - timestamp_slot) as u32 * slot_duration; let offset = (slot - timestamp_slot) as u32 * slot_duration;
stakes.get(&vote_pubkey).map(|(stake, _account)| { stakes.get(vote_pubkey.borrow()).map(|(stake, _account)| {
( (
(*timestamp as u128 + offset.as_secs() as u128) * *stake as u128, (*timestamp as u128 + offset.as_secs() as u128) * *stake as u128,
stake, stake,
@ -70,20 +84,27 @@ fn calculate_unbounded_stake_weighted_timestamp<T>(
} }
} }
fn calculate_bounded_stake_weighted_timestamp<T>( fn calculate_bounded_stake_weighted_timestamp<I, K, V, T>(
unique_timestamps: &HashMap<Pubkey, (Slot, UnixTimestamp)>, unique_timestamps: I,
stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>, stakes: &HashMap<Pubkey, (u64, T /*Account|ArcVoteAccount*/)>,
slot: Slot, slot: Slot,
slot_duration: Duration, slot_duration: Duration,
epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>,
) -> Option<UnixTimestamp> { max_allowable_drift_percentage: u32,
) -> Option<UnixTimestamp>
where
I: IntoIterator<Item = (K, V)>,
K: Borrow<Pubkey>,
V: Borrow<(Slot, UnixTimestamp)>,
{
let mut stake_per_timestamp: BTreeMap<UnixTimestamp, u128> = BTreeMap::new(); let mut stake_per_timestamp: BTreeMap<UnixTimestamp, u128> = BTreeMap::new();
let mut total_stake = 0; let mut total_stake = 0;
for (vote_pubkey, (timestamp_slot, timestamp)) in unique_timestamps.iter() { for (vote_pubkey, slot_timestamp) in unique_timestamps {
let (timestamp_slot, timestamp) = slot_timestamp.borrow();
let offset = slot.saturating_sub(*timestamp_slot) as u32 * slot_duration; let offset = slot.saturating_sub(*timestamp_slot) as u32 * slot_duration;
let estimate = timestamp + offset.as_secs() as i64; let estimate = timestamp + offset.as_secs() as i64;
let stake = stakes let stake = stakes
.get(&vote_pubkey) .get(vote_pubkey.borrow())
.map(|(stake, _account)| stake) .map(|(stake, _account)| stake)
.unwrap_or(&0); .unwrap_or(&0);
stake_per_timestamp stake_per_timestamp
@ -110,7 +131,7 @@ fn calculate_bounded_stake_weighted_timestamp<T>(
let poh_estimate_offset = slot.saturating_sub(epoch_start_slot) as u32 * slot_duration; let poh_estimate_offset = slot.saturating_sub(epoch_start_slot) as u32 * slot_duration;
let estimate_offset = let estimate_offset =
Duration::from_secs(estimate.saturating_sub(epoch_start_timestamp) as u64); Duration::from_secs(estimate.saturating_sub(epoch_start_timestamp) as u64);
let max_allowable_drift = poh_estimate_offset * MAX_ALLOWABLE_DRIFT_PERCENTAGE / 100; let max_allowable_drift = poh_estimate_offset * max_allowable_drift_percentage / 100;
if estimate_offset > poh_estimate_offset if estimate_offset > poh_estimate_offset
&& estimate_offset - poh_estimate_offset > max_allowable_drift && estimate_offset - poh_estimate_offset > max_allowable_drift
{ {
@ -253,6 +274,7 @@ pub mod tests {
let pubkey2 = solana_sdk::pubkey::new_rand(); let pubkey2 = solana_sdk::pubkey::new_rand();
let pubkey3 = solana_sdk::pubkey::new_rand(); let pubkey3 = solana_sdk::pubkey::new_rand();
let pubkey4 = solana_sdk::pubkey::new_rand(); let pubkey4 = solana_sdk::pubkey::new_rand();
let max_allowable_drift = 25;
// Test low-staked outlier(s) // Test low-staked outlier(s)
let stakes: HashMap<Pubkey, (u64, Account)> = [ let stakes: HashMap<Pubkey, (u64, Account)> = [
@ -315,6 +337,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
None, None,
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(bounded - unbounded, 527); // timestamp w/ 0.00003% of the stake can shift the timestamp backward 8min assert_eq!(bounded - unbounded, 527); // timestamp w/ 0.00003% of the stake can shift the timestamp backward 8min
@ -345,6 +368,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
None, None,
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(unbounded - bounded, 3074455295455); // timestamp w/ 0.00003% of the stake can shift the timestamp forward 97k years! assert_eq!(unbounded - bounded, 3074455295455); // timestamp w/ 0.00003% of the stake can shift the timestamp forward 97k years!
@ -367,6 +391,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
None, None,
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(bounded, recent_timestamp); // multiple low-staked outliers cannot affect bounded timestamp if they don't shift the median assert_eq!(bounded, recent_timestamp); // multiple low-staked outliers cannot affect bounded timestamp if they don't shift the median
@ -414,6 +439,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
None, None,
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(bounded, recent_timestamp); // outlier(s) cannot affect bounded timestamp if they don't shift the median assert_eq!(bounded, recent_timestamp); // outlier(s) cannot affect bounded timestamp if they don't shift the median
@ -450,6 +476,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
None, None,
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(recent_timestamp - bounded, 1578909061); // outliers > 1/2 of available stake can affect timestamp assert_eq!(recent_timestamp - bounded, 1578909061); // outliers > 1/2 of available stake can affect timestamp
@ -461,7 +488,8 @@ pub mod tests {
let slot = 20; let slot = 20;
let slot_duration = Duration::from_millis(400); let slot_duration = Duration::from_millis(400);
let poh_offset = (slot * slot_duration).as_secs(); let poh_offset = (slot * slot_duration).as_secs();
let acceptable_delta = (MAX_ALLOWABLE_DRIFT_PERCENTAGE * poh_offset as u32 / 100) as i64; let max_allowable_drift = 25;
let acceptable_delta = (max_allowable_drift * poh_offset as u32 / 100) as i64;
let poh_estimate = epoch_start_timestamp + poh_offset as i64; let poh_estimate = epoch_start_timestamp + poh_offset as i64;
let pubkey0 = solana_sdk::pubkey::new_rand(); let pubkey0 = solana_sdk::pubkey::new_rand();
let pubkey1 = solana_sdk::pubkey::new_rand(); let pubkey1 = solana_sdk::pubkey::new_rand();
@ -510,6 +538,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
Some((0, epoch_start_timestamp)), Some((0, epoch_start_timestamp)),
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(bounded, poh_estimate + acceptable_delta); assert_eq!(bounded, poh_estimate + acceptable_delta);
@ -530,6 +559,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
Some((0, epoch_start_timestamp)), Some((0, epoch_start_timestamp)),
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(bounded, poh_estimate - acceptable_delta); assert_eq!(bounded, poh_estimate - acceptable_delta);
@ -550,6 +580,7 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
Some((0, epoch_start_timestamp)), Some((0, epoch_start_timestamp)),
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(bounded, poh_estimate + acceptable_delta); assert_eq!(bounded, poh_estimate + acceptable_delta);
@ -569,8 +600,135 @@ pub mod tests {
slot as Slot, slot as Slot,
slot_duration, slot_duration,
Some((0, epoch_start_timestamp)), Some((0, epoch_start_timestamp)),
max_allowable_drift,
) )
.unwrap(); .unwrap();
assert_eq!(bounded, poh_estimate - acceptable_delta); assert_eq!(bounded, poh_estimate - acceptable_delta);
} }
#[test]
fn test_calculate_bounded_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;
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();
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 above 25% deviance but below 50% deviance
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
(
pubkey0,
(slot as u64, poh_estimate + acceptable_delta_25 + 1),
),
(
pubkey1,
(slot as u64, poh_estimate + acceptable_delta_25 + 1),
),
(
pubkey2,
(slot as u64, poh_estimate + acceptable_delta_25 + 1),
),
]
.iter()
.cloned()
.collect();
let bounded = calculate_bounded_stake_weighted_timestamp(
&unique_timestamps,
&stakes,
slot as Slot,
slot_duration,
Some((0, epoch_start_timestamp)),
allowable_drift_25,
)
.unwrap();
assert_eq!(bounded, poh_estimate + acceptable_delta_25);
let bounded = calculate_bounded_stake_weighted_timestamp(
&unique_timestamps,
&stakes,
slot as Slot,
slot_duration,
Some((0, epoch_start_timestamp)),
allowable_drift_50,
)
.unwrap();
assert_eq!(bounded, poh_estimate + acceptable_delta_25 + 1);
// Test when stake-weighted median is above 50% deviance
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = [
(
pubkey0,
(slot as u64, poh_estimate + acceptable_delta_50 + 1),
),
(
pubkey1,
(slot as u64, poh_estimate + acceptable_delta_50 + 1),
),
(
pubkey2,
(slot as u64, poh_estimate + acceptable_delta_50 + 1),
),
]
.iter()
.cloned()
.collect();
let bounded = calculate_bounded_stake_weighted_timestamp(
&unique_timestamps,
&stakes,
slot as Slot,
slot_duration,
Some((0, epoch_start_timestamp)),
allowable_drift_25,
)
.unwrap();
assert_eq!(bounded, poh_estimate + acceptable_delta_25);
let bounded = calculate_bounded_stake_weighted_timestamp(
&unique_timestamps,
&stakes,
slot as Slot,
slot_duration,
Some((0, epoch_start_timestamp)),
allowable_drift_50,
)
.unwrap();
assert_eq!(bounded, poh_estimate + acceptable_delta_50);
}
} }