Implement timely vote credits feature (#31291)

This commit is contained in:
bji 2023-08-10 14:07:51 -07:00 committed by GitHub
parent d26e3ff22b
commit 35ec7bf804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 601 additions and 99 deletions

View File

@ -43,9 +43,7 @@ use {
}, },
solana_vote_program::{ solana_vote_program::{
authorized_voters::AuthorizedVoters, authorized_voters::AuthorizedVoters,
vote_state::{ vote_state::{BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY},
BlockTimestamp, LandedVote, Lockout, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY,
},
}, },
std::{ std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
@ -1047,7 +1045,7 @@ impl fmt::Display for CliKeyedEpochRewards {
fn show_votes_and_credits( fn show_votes_and_credits(
f: &mut fmt::Formatter, f: &mut fmt::Formatter,
votes: &[CliLockout], votes: &[CliLandedVote],
epoch_voting_history: &[CliEpochVotingHistory], epoch_voting_history: &[CliEpochVotingHistory],
) -> fmt::Result { ) -> fmt::Result {
if votes.is_empty() { if votes.is_empty() {
@ -1070,11 +1068,16 @@ fn show_votes_and_credits(
)?; )?;
for vote in votes.iter().rev() { for vote in votes.iter().rev() {
writeln!( write!(
f, f,
"- slot: {} (confirmation count: {})", "- slot: {} (confirmation count: {})",
vote.slot, vote.confirmation_count vote.slot, vote.confirmation_count
)?; )?;
if vote.latency == 0 {
writeln!(f)?;
} else {
writeln!(f, " (latency {})", vote.latency)?;
}
} }
if let Some(newest) = newest_history_entry { if let Some(newest) = newest_history_entry {
writeln!( writeln!(
@ -1555,7 +1558,7 @@ pub struct CliVoteAccount {
pub commission: u8, pub commission: u8,
pub root_slot: Option<Slot>, pub root_slot: Option<Slot>,
pub recent_timestamp: BlockTimestamp, pub recent_timestamp: BlockTimestamp,
pub votes: Vec<CliLockout>, pub votes: Vec<CliLandedVote>,
pub epoch_voting_history: Vec<CliEpochVotingHistory>, pub epoch_voting_history: Vec<CliEpochVotingHistory>,
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub use_lamports_unit: bool, pub use_lamports_unit: bool,
@ -1637,25 +1640,18 @@ pub struct CliEpochVotingHistory {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CliLockout { pub struct CliLandedVote {
pub latency: u8,
pub slot: Slot, pub slot: Slot,
pub confirmation_count: u32, pub confirmation_count: u32,
} }
impl From<&Lockout> for CliLockout { impl From<&LandedVote> for CliLandedVote {
fn from(lockout: &Lockout) -> Self { fn from(landed_vote: &LandedVote) -> Self {
Self { Self {
slot: lockout.slot(), latency: landed_vote.latency,
confirmation_count: lockout.confirmation_count(), slot: landed_vote.slot(),
} confirmation_count: landed_vote.confirmation_count(),
}
}
impl From<&LandedVote> for CliLockout {
fn from(vote: &LandedVote) -> Self {
Self {
slot: vote.slot(),
confirmation_count: vote.confirmation_count(),
} }
} }
} }

View File

@ -23,7 +23,7 @@ use {
offline::*, offline::*,
}, },
solana_cli_output::{ solana_cli_output::{
return_signers_with_config, CliEpochVotingHistory, CliLockout, CliVoteAccount, return_signers_with_config, CliEpochVotingHistory, CliLandedVote, CliVoteAccount,
ReturnSignersConfig, ReturnSignersConfig,
}, },
solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_remote_wallet::remote_wallet::RemoteWalletManager,
@ -1215,7 +1215,7 @@ pub fn process_show_vote_account(
let epoch_schedule = rpc_client.get_epoch_schedule()?; let epoch_schedule = rpc_client.get_epoch_schedule()?;
let mut votes: Vec<CliLockout> = vec![]; let mut votes: Vec<CliLandedVote> = vec![];
let mut epoch_voting_history: Vec<CliEpochVotingHistory> = vec![]; let mut epoch_voting_history: Vec<CliEpochVotingHistory> = vec![];
if !vote_state.votes.is_empty() { if !vote_state.votes.is_empty() {
for vote in &vote_state.votes { for vote in &vote_state.votes {

View File

@ -48,7 +48,7 @@ fn create_accounts() -> (Slot, SlotHashes, Vec<TransactionAccount>, Vec<AccountM
); );
for next_vote_slot in 0..num_initial_votes { for next_vote_slot in 0..num_initial_votes {
vote_state.process_next_vote_slot(next_vote_slot, 0); vote_state.process_next_vote_slot(next_vote_slot, 0, 0);
} }
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()]; let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
let versioned = VoteStateVersions::new_current(vote_state); let versioned = VoteStateVersions::new_current(vote_state);

File diff suppressed because it is too large Load Diff

View File

@ -7278,12 +7278,16 @@ pub mod tests {
.unwrap(); .unwrap();
assert_ne!(leader_info.activated_stake, 0); assert_ne!(leader_info.activated_stake, 0);
// Subtract one because the last vote always carries over to the next epoch // Subtract one because the last vote always carries over to the next epoch
let expected_credits = TEST_SLOTS_PER_EPOCH - MAX_LOCKOUT_HISTORY as u64 - 1; // Each slot earned maximum credits
let credits_per_slot =
solana_vote_program::vote_state::VOTE_CREDITS_MAXIMUM_PER_SLOT as u64;
let expected_credits =
(TEST_SLOTS_PER_EPOCH - MAX_LOCKOUT_HISTORY as u64 - 1) * credits_per_slot;
assert_eq!( assert_eq!(
leader_info.epoch_credits, leader_info.epoch_credits,
vec![ vec![
(0, expected_credits, 0), (0, expected_credits, 0),
(1, expected_credits + 1, expected_credits) // one vote in current epoch (1, expected_credits + credits_per_slot, expected_credits) // one vote in current epoch
] ]
); );

View File

@ -35,6 +35,12 @@ pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
// Offset of VoteState::prior_voters, for determining initialization status without deserialization // Offset of VoteState::prior_voters, for determining initialization status without deserialization
const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 114; const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 114;
// Number of slots of grace period for which maximum vote credits are awarded - votes landing within this number of slots of the slot that is being voted on are awarded full credits.
pub const VOTE_CREDITS_GRACE_SLOTS: u8 = 2;
// Maximum number of credits to award for a vote; this number of credits is awarded to votes on slots that land within the grace period. After that grace period, vote credits are reduced.
pub const VOTE_CREDITS_MAXIMUM_PER_SLOT: u8 = 8;
#[frozen_abi(digest = "Ch2vVEwos2EjAVqSHCyJjnN2MNX1yrpapZTGhMSCjWUH")] #[frozen_abi(digest = "Ch2vVEwos2EjAVqSHCyJjnN2MNX1yrpapZTGhMSCjWUH")]
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)]
pub struct Vote { pub struct Vote {
@ -419,7 +425,12 @@ impl VoteState {
} }
} }
pub fn process_next_vote_slot(&mut self, next_vote_slot: Slot, epoch: Epoch) { pub fn process_next_vote_slot(
&mut self,
next_vote_slot: Slot,
epoch: Epoch,
current_slot: Slot,
) {
// Ignore votes for slots earlier than we already have votes for // Ignore votes for slots earlier than we already have votes for
if self if self
.last_voted_slot() .last_voted_slot()
@ -428,18 +439,22 @@ impl VoteState {
return; return;
} }
let lockout = Lockout::new(next_vote_slot);
self.pop_expired_votes(next_vote_slot); self.pop_expired_votes(next_vote_slot);
let landed_vote = LandedVote {
latency: Self::compute_vote_latency(next_vote_slot, current_slot),
lockout: Lockout::new(next_vote_slot),
};
// Once the stack is full, pop the oldest lockout and distribute rewards // Once the stack is full, pop the oldest lockout and distribute rewards
if self.votes.len() == MAX_LOCKOUT_HISTORY { if self.votes.len() == MAX_LOCKOUT_HISTORY {
let vote = self.votes.pop_front().unwrap(); let credits = self.credits_for_vote_at_index(0);
self.root_slot = Some(vote.slot()); let landed_vote = self.votes.pop_front().unwrap();
self.root_slot = Some(landed_vote.slot());
self.increment_credits(epoch, 1); self.increment_credits(epoch, credits);
} }
self.votes.push_back(lockout.into()); self.votes.push_back(landed_vote);
self.double_lockouts(); self.double_lockouts();
} }
@ -472,6 +487,43 @@ impl VoteState {
self.epoch_credits.last().unwrap().1.saturating_add(credits); self.epoch_credits.last().unwrap().1.saturating_add(credits);
} }
// Computes the vote latency for vote on voted_for_slot where the vote itself landed in current_slot
pub fn compute_vote_latency(voted_for_slot: Slot, current_slot: Slot) -> u8 {
std::cmp::min(current_slot.saturating_sub(voted_for_slot), u8::MAX as u64) as u8
}
/// Returns the credits to award for a vote at the given lockout slot index
pub fn credits_for_vote_at_index(&self, index: usize) -> u64 {
let latency = self
.votes
.get(index)
.map_or(0, |landed_vote| landed_vote.latency);
// If latency is 0, this means that the Lockout was created and stored from a software version that did not
// store vote latencies; in this case, 1 credit is awarded
if latency == 0 {
1
} else {
match latency.checked_sub(VOTE_CREDITS_GRACE_SLOTS) {
None | Some(0) => {
// latency was <= VOTE_CREDITS_GRACE_SLOTS, so maximum credits are awarded
VOTE_CREDITS_MAXIMUM_PER_SLOT as u64
}
Some(diff) => {
// diff = latency - VOTE_CREDITS_GRACE_SLOTS, and diff > 0
// Subtract diff from VOTE_CREDITS_MAXIMUM_PER_SLOT which is the number of credits to award
match VOTE_CREDITS_MAXIMUM_PER_SLOT.checked_sub(diff) {
// If diff >= VOTE_CREDITS_MAXIMUM_PER_SLOT, 1 credit is awarded
None | Some(0) => 1,
Some(credits) => credits as u64,
}
}
}
}
}
pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> { pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> {
if position < self.votes.len() { if position < self.votes.len() {
let pos = self let pos = self

View File

@ -620,6 +620,7 @@ pub mod delay_visibility_of_program_deployment {
pub mod apply_cost_tracker_during_replay { pub mod apply_cost_tracker_during_replay {
solana_sdk::declare_id!("2ry7ygxiYURULZCrypHhveanvP5tzZ4toRwVp89oCNSj"); solana_sdk::declare_id!("2ry7ygxiYURULZCrypHhveanvP5tzZ4toRwVp89oCNSj");
} }
pub mod bpf_account_data_direct_mapping { pub mod bpf_account_data_direct_mapping {
solana_sdk::declare_id!("9gwzizfABsKUereT6phZZxbTzuAnovkgwpVVpdcSxv9h"); solana_sdk::declare_id!("9gwzizfABsKUereT6phZZxbTzuAnovkgwpVVpdcSxv9h");
} }
@ -687,6 +688,10 @@ pub mod reduce_stake_warmup_cooldown {
} }
} }
pub mod timely_vote_credits {
solana_sdk::declare_id!("2oXpeh141pPZCTCFHBsvCwG2BtaHZZAtrVhwaxSy6brS");
}
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> = [
@ -851,6 +856,7 @@ lazy_static! {
(bpf_account_data_direct_mapping::id(), "use memory regions to map account data into the rbpf vm instead of copying the data"), (bpf_account_data_direct_mapping::id(), "use memory regions to map account data into the rbpf vm instead of copying the data"),
(last_restart_slot_sysvar::id(), "enable new sysvar last_restart_slot"), (last_restart_slot_sysvar::id(), "enable new sysvar last_restart_slot"),
(reduce_stake_warmup_cooldown::id(), "reduce stake warmup cooldown from 25% to 9%"), (reduce_stake_warmup_cooldown::id(), "reduce stake warmup cooldown from 25% to 9%"),
(timely_vote_credits::id(), "use timeliness of votes in determining credits to award"),
/*************** ADD NEW FEATURES HERE ***************/ /*************** ADD NEW FEATURES HERE ***************/
] ]
.iter() .iter()