Implement timely vote credits feature (#31291)
This commit is contained in:
parent
d26e3ff22b
commit
35ec7bf804
|
@ -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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
@ -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
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue