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

View File

@ -23,7 +23,7 @@ use {
offline::*,
},
solana_cli_output::{
return_signers_with_config, CliEpochVotingHistory, CliLockout, CliVoteAccount,
return_signers_with_config, CliEpochVotingHistory, CliLandedVote, CliVoteAccount,
ReturnSignersConfig,
},
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 mut votes: Vec<CliLockout> = vec![];
let mut votes: Vec<CliLandedVote> = vec![];
let mut epoch_voting_history: Vec<CliEpochVotingHistory> = vec![];
if !vote_state.votes.is_empty() {
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 {
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 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();
assert_ne!(leader_info.activated_stake, 0);
// 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!(
leader_info.epoch_credits,
vec![
(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
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")]
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)]
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
if self
.last_voted_slot()
@ -428,18 +439,22 @@ impl VoteState {
return;
}
let lockout = Lockout::new(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
if self.votes.len() == MAX_LOCKOUT_HISTORY {
let vote = self.votes.pop_front().unwrap();
self.root_slot = Some(vote.slot());
let credits = self.credits_for_vote_at_index(0);
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();
}
@ -472,6 +487,43 @@ impl VoteState {
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> {
if position < self.votes.len() {
let pos = self

View File

@ -620,6 +620,7 @@ pub mod delay_visibility_of_program_deployment {
pub mod apply_cost_tracker_during_replay {
solana_sdk::declare_id!("2ry7ygxiYURULZCrypHhveanvP5tzZ4toRwVp89oCNSj");
}
pub mod bpf_account_data_direct_mapping {
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! {
/// Map of feature identifiers to user-visible description
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"),
(last_restart_slot_sysvar::id(), "enable new sysvar last_restart_slot"),
(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 ***************/
]
.iter()