diff --git a/core/src/leader_schedule_utils.rs b/core/src/leader_schedule_utils.rs index 43b87c7442..7cf68b9b70 100644 --- a/core/src/leader_schedule_utils.rs +++ b/core/src/leader_schedule_utils.rs @@ -11,7 +11,7 @@ fn leader_schedule(epoch_height: u64, bank: &Bank) -> LeaderSchedule { seed[0..8].copy_from_slice(&epoch_height.to_le_bytes()); let mut stakes: Vec<_> = stakes.into_iter().collect(); sort_stakes(&mut stakes); - LeaderSchedule::new(&stakes, seed, bank.slots_per_epoch()) + LeaderSchedule::new(&stakes, seed, bank.get_slots_in_epoch(epoch_height)) } fn sort_stakes(stakes: &mut Vec<(Pubkey, u64)>) { diff --git a/core/src/staking_utils.rs b/core/src/staking_utils.rs index 33732bb002..92d10547e2 100644 --- a/core/src/staking_utils.rs +++ b/core/src/staking_utils.rs @@ -176,11 +176,7 @@ mod tests { fn test_epoch_stakes_and_lockouts() { let validator = Keypair::new(); - let (mut genesis_block, mint_keypair) = GenesisBlock::new(500); - - // makes the bank generate epoch_vote_accounts right on epoch boundaries - // handy for tests - genesis_block.stakers_slot_offset = 0; + let (genesis_block, mint_keypair) = GenesisBlock::new(500); let bank = Bank::new(&genesis_block); let bank_voter = Keypair::new(); @@ -195,11 +191,17 @@ mod tests { // should show up in the active set voting_keypair_tests::new_vote_account_with_vote(&mint_keypair, &bank_voter, &bank, 499, 0); - // Build a bank in the next epoch, it will generate an epoch_vote_accounts() for epoch 1 - let epoch = 1; - let epoch_slot = epoch * bank.slots_per_epoch(); + // soonest slot that could be a new epoch is 1 + let mut slot = 1; + let mut epoch = bank.get_stakers_epoch(0); + // find the first slot in the next stakers_epoch + while bank.get_stakers_epoch(slot) == epoch { + slot += 1; + } - let bank = new_from_parent(&Arc::new(bank), epoch_slot); + epoch = bank.get_stakers_epoch(slot); + + let bank = new_from_parent(&Arc::new(bank), slot); let result: Vec<_> = epoch_stakes_and_lockouts(&bank, 0); assert_eq!(result, vec![(1, None)]); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 5b331ae716..cf378f9fcf 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -30,6 +30,83 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::time::Instant; +/// Reasons a transaction might be rejected. +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub struct EpochSchedule { + /// The maximum number of slots in each epoch. + pub slots_per_epoch: u64, + + /// A number of slots before slot_index 0. Used to calculate finalized staked nodes. + pub stakers_slot_offset: u64, + + /// basically: log2(slots_per_epoch) + pub first_normal_epoch: u64, + + /// basically: 2.pow(first_normal_epoch) + pub first_normal_slot: u64, +} + +impl EpochSchedule { + pub fn new(slots_per_epoch: u64, stakers_slot_offset: u64, warmup: bool) -> Self { + let (first_normal_epoch, first_normal_slot) = if warmup { + let next_power_of_two = slots_per_epoch.next_power_of_two(); + let log2_slots_per_epoch = next_power_of_two.trailing_zeros(); + + (u64::from(log2_slots_per_epoch), next_power_of_two - 1) + } else { + (0, 0) + }; + EpochSchedule { + slots_per_epoch, + stakers_slot_offset, + first_normal_epoch, + first_normal_slot, + } + } + + /// get the length of the given epoch (in slots) + pub fn get_slots_in_epoch(&self, epoch: u64) -> u64 { + if epoch < self.first_normal_epoch { + 2u64.pow(epoch as u32) + } else { + self.slots_per_epoch + } + } + + /// get the epoch for which the given slot should save off + /// information about stakers + pub fn get_stakers_epoch(&self, slot: u64) -> u64 { + if slot < self.first_normal_slot { + // until we get to normal slots, behave as if stakers_slot_offset == slots_per_epoch + + self.get_epoch_and_slot_index(slot).0 + 1 + } else { + self.first_normal_epoch + + (slot - self.first_normal_slot + self.stakers_slot_offset) / self.slots_per_epoch + } + } + + /// get epoch and offset into the epoch for the given slot + pub fn get_epoch_and_slot_index(&self, slot: u64) -> (u64, u64) { + if slot < self.first_normal_slot { + let epoch = if slot < 2 { + slot as u32 + } else { + (slot + 2).next_power_of_two().trailing_zeros() - 1 + }; + + let epoch_len = 2u64.pow(epoch); + + (u64::from(epoch), slot - (epoch_len - 1)) + } else { + ( + self.first_normal_epoch + ((slot - self.first_normal_slot) / self.slots_per_epoch), + (slot - self.first_normal_slot) % self.slots_per_epoch, + ) + } + } +} + /// Reasons a transaction might be rejected. #[derive(Debug, PartialEq, Eq, Clone)] pub enum BankError { @@ -78,8 +155,12 @@ type BankStatusCache = StatusCache; /// Manager for the state of all accounts and programs after processing its entries. #[derive(Default)] pub struct Bank { + /// where all the Accounts are stored accounts: Option>, + /// Bank accounts fork id + accounts_id: u64, + /// A cache of signature statuses status_cache: RwLock, @@ -98,27 +179,21 @@ pub struct Bank { /// Bank tick height tick_height: AtomicUsize, // TODO: Use AtomicU64 if/when available - /// Bank fork (i.e. slot, i.e. block) - slot: u64, - /// The number of ticks in each slot. ticks_per_slot: u64, - /// The number of slots in each epoch. - slots_per_epoch: u64, - - /// A number of slots before slot_index 0. Used to calculate finalized staked nodes. - stakers_slot_offset: u64, + /// Bank fork (i.e. slot, i.e. block) + slot: u64, /// The pubkey to send transactions fees to. collector_id: Pubkey, + /// initialized from genesis + epoch_schedule: EpochSchedule, + /// staked nodes on epoch boundaries, saved off when a bank.slot() is at /// a leader schedule boundary epoch_vote_accounts: HashMap>, - - /// Bank accounts fork id - accounts_id: u64, } impl Default for HashQueue { @@ -141,7 +216,7 @@ impl Bank { // genesis needs stakes for all epochs up to the epoch implied by // slot = 0 and genesis configuration let vote_accounts: HashMap<_, _> = bank.vote_accounts().collect(); - for i in 0..=bank.epoch_from_stakers_slot_offset() { + for i in 0..=bank.get_stakers_epoch(bank.slot) { bank.epoch_vote_accounts.insert(i, vote_accounts.clone()); } @@ -159,8 +234,7 @@ impl Bank { bank.tick_height .store(parent.tick_height.load(Ordering::SeqCst), Ordering::SeqCst); bank.ticks_per_slot = parent.ticks_per_slot; - bank.slots_per_epoch = parent.slots_per_epoch; - bank.stakers_slot_offset = parent.stakers_slot_offset; + bank.epoch_schedule = parent.epoch_schedule; bank.slot = slot; bank.parent = RwLock::new(Some(parent.clone())); @@ -176,7 +250,7 @@ impl Bank { bank.epoch_vote_accounts = { let mut epoch_vote_accounts = parent.epoch_vote_accounts.clone(); - let epoch = bank.epoch_from_stakers_slot_offset(); + let epoch = bank.get_stakers_epoch(bank.slot); // update epoch_vote_states cache // if my parent didn't populate for this epoch, we've // crossed a boundary @@ -284,8 +358,12 @@ impl Bank { .genesis_hash(&genesis_block.hash()); self.ticks_per_slot = genesis_block.ticks_per_slot; - self.slots_per_epoch = genesis_block.slots_per_epoch; - self.stakers_slot_offset = genesis_block.stakers_slot_offset; + + self.epoch_schedule = EpochSchedule::new( + genesis_block.slots_per_epoch, + genesis_block.stakers_slot_offset, + genesis_block.epoch_warmup, + ); } pub fn add_native_program(&self, name: &str, program_id: &Pubkey) { @@ -754,12 +832,6 @@ impl Bank { extend_and_hash(&self.parent_hash, &serialize(&accounts_delta_hash).unwrap()) } - /// Return the number of slots in advance of an epoch that a leader scheduler - /// should be generated. - pub fn stakers_slot_offset(&self) -> u64 { - self.stakers_slot_offset - } - /// Return the number of ticks per slot pub fn ticks_per_slot(&self) -> u64 { self.ticks_per_slot @@ -774,15 +846,15 @@ impl Bank { self.tick_height.load(Ordering::SeqCst) as u64 } - /// Return the number of slots per epoch for this epoch - pub fn slots_per_epoch(&self) -> u64 { - self.slots_per_epoch + /// Return the number of slots per epoch for the given epoch + pub fn get_slots_in_epoch(&self, epoch: u64) -> u64 { + self.epoch_schedule.get_slots_in_epoch(epoch) } /// returns the epoch for which this bank's stakers_slot_offset and slot would /// need to cache stakers - fn epoch_from_stakers_slot_offset(&self) -> u64 { - (self.slot + self.stakers_slot_offset) / self.slots_per_epoch + pub fn get_stakers_epoch(&self, slot: u64) -> u64 { + self.epoch_schedule.get_stakers_epoch(slot) } /// current vote accounts for this bank @@ -801,7 +873,7 @@ impl Bank { /// ( slot/slots_per_epoch, slot % slots_per_epoch ) /// pub fn get_epoch_and_slot_index(&self, slot: u64) -> (u64, u64) { - (slot / self.slots_per_epoch(), slot % self.slots_per_epoch()) + self.epoch_schedule.get_epoch_and_slot_index(slot) } } @@ -1436,14 +1508,13 @@ mod tests { let leader_lamports = 3; let (mut genesis_block, _) = GenesisBlock::new_with_leader(5, leader_id, leader_lamports); - // set this up weird, forces: - // 1. genesis bank to cover epochs 0, 1, *and* 2 - // 2. child banks to cover epochs in their future - // + // set this up weird, forces future generation, odd mod(), etc. + // this says: "stakes for slot X should be generated at slot index 3 in slot X-2... const SLOTS_PER_EPOCH: u64 = 8; const STAKERS_SLOT_OFFSET: u64 = 21; genesis_block.slots_per_epoch = SLOTS_PER_EPOCH; genesis_block.stakers_slot_offset = STAKERS_SLOT_OFFSET; + genesis_block.epoch_warmup = false; // allows me to do the normal division stuff below let parent = Arc::new(Bank::new(&genesis_block)); @@ -1525,21 +1596,66 @@ mod tests { } #[test] - fn test_bank_get_epoch_and_slot_offset() { - let (mut genesis_block, _) = GenesisBlock::new(500); - - // set this up weird, forces: - // 1. genesis bank to cover epochs 0, 1, *and* 2 - // 2. child banks to cover epochs in their future - // - const SLOTS_PER_EPOCH: u64 = 8; - genesis_block.slots_per_epoch = SLOTS_PER_EPOCH; + fn test_bank_get_slots_in_epoch() { + let (genesis_block, _) = GenesisBlock::new(500); let bank = Bank::new(&genesis_block); - assert_eq!(bank.get_epoch_and_slot_index(0), (0, 0)); - assert_eq!(bank.get_epoch_and_slot_index(SLOTS_PER_EPOCH), (1, 0)); - assert_eq!(bank.get_epoch_and_slot_index(SLOTS_PER_EPOCH + 1), (1, 1)); + assert_eq!(bank.get_slots_in_epoch(0), 1); + assert_eq!(bank.get_slots_in_epoch(2), 4); + assert_eq!(bank.get_slots_in_epoch(5000), genesis_block.slots_per_epoch); + } + + #[test] + fn test_epoch_schedule() { + // one week of slots at 8 ticks/slot, 10 ticks/sec is + // (1 * 7 * 24 * 4500u64).next_power_of_two(); + + // test values between 1 and 16, should cover a good mix + for slots_per_epoch in 1..=16 { + let epoch_schedule = EpochSchedule::new(slots_per_epoch, slots_per_epoch / 2, true); + + let mut last_stakers = 0; + let mut last_epoch = 0; + let mut last_slots_in_epoch = 1; + for slot in 0..(2 * slots_per_epoch) { + // verify that stakers_epoch is continuous over the warmup + // and into the first normal epoch + + let stakers = epoch_schedule.get_stakers_epoch(slot); + if stakers != last_stakers { + assert_eq!(stakers, last_stakers + 1); + last_stakers = stakers; + } + + let (epoch, offset) = epoch_schedule.get_epoch_and_slot_index(slot); + + // verify that epoch increases continuously + if epoch != last_epoch { + assert_eq!(epoch, last_epoch + 1); + last_epoch = epoch; + + // verify that slots in an epoch double continuously + // until they reach slots_per_epoch + + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); + if slots_in_epoch != last_slots_in_epoch { + if slots_in_epoch != slots_per_epoch { + assert_eq!(slots_in_epoch, last_slots_in_epoch * 2); + } + } + last_slots_in_epoch = slots_in_epoch; + } + // verify that the slot offset is less than slots_in_epoch + assert!(offset < last_slots_in_epoch); + } + + // assert that these changed ;) + assert!(last_stakers != 0); // t + assert!(last_epoch != 0); + // assert that we got to "normal" mode + assert!(last_slots_in_epoch == slots_per_epoch); + } } } diff --git a/sdk/src/genesis_block.rs b/sdk/src/genesis_block.rs index e180ed1e02..54594bb3f4 100644 --- a/sdk/src/genesis_block.rs +++ b/sdk/src/genesis_block.rs @@ -23,6 +23,7 @@ pub struct GenesisBlock { pub ticks_per_slot: u64, pub slots_per_epoch: u64, pub stakers_slot_offset: u64, + pub epoch_warmup: bool, } impl GenesisBlock { @@ -51,6 +52,7 @@ impl GenesisBlock { ticks_per_slot: DEFAULT_TICKS_PER_SLOT, slots_per_epoch: DEFAULT_SLOTS_PER_EPOCH, stakers_slot_offset: DEFAULT_SLOTS_PER_EPOCH, + epoch_warmup: true, }, mint_keypair, )