Skip to content

Commit

Permalink
Normalize Timely Vote Credits in Steward score (#92)
Browse files Browse the repository at this point in the history
Normalizes vote credits which are now 8x higher for validators starting
in epoch 704.
  • Loading branch information
ebatsell authored Dec 7, 2024
1 parent e99f76a commit f86e28b
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 88 deletions.
7 changes: 7 additions & 0 deletions programs/steward/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@ pub const COMPUTE_SCORE_SLOT_RANGE_MIN: u64 = 100;
pub const VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH: u64 = 520;
#[cfg(not(feature = "mainnet-beta"))]
pub const VALIDATOR_HISTORY_FIRST_RELIABLE_EPOCH: u64 = 0;
pub const TVC_FEATURE_PUBKEY: &str = "tvcF6b1TRz353zKuhBjinZkKzjmihXmBAHJdjNYw1sQ";
#[cfg(feature = "mainnet-beta")]
pub const TVC_ACTIVATION_EPOCH: u64 = 703;
#[cfg(all(not(feature = "mainnet-beta"), feature = "testnet"))]
pub const TVC_ACTIVATION_EPOCH: u64 = 705;
#[cfg(all(not(feature = "mainnet-beta"), not(feature = "testnet")))]
pub const TVC_ACTIVATION_EPOCH: u64 = 0;
28 changes: 19 additions & 9 deletions programs/steward/src/score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use anchor_lang::IdlBuild;
use anchor_lang::{
prelude::event, solana_program::pubkey::Pubkey, AnchorDeserialize, AnchorSerialize, Result,
};
use validator_history::{ClusterHistory, ValidatorHistory};
use validator_history::{constants::TVC_MULTIPLIER, ClusterHistory, ValidatorHistory};

use crate::{
constants::{
Expand Down Expand Up @@ -90,6 +90,7 @@ pub fn validator_score(
cluster: &ClusterHistory,
config: &Config,
current_epoch: u16,
tvc_activation_epoch: u64,
) -> Result<ScoreComponentsV2> {
let params = &config.parameters;

Expand All @@ -107,9 +108,11 @@ pub fn validator_score(
// Epoch credits should not include current epoch because it is in progress and data would be incomplete
let epoch_credits_end = current_epoch.checked_sub(1).ok_or(ArithmeticError)?;

let epoch_credits_window = validator
.history
.epoch_credits_range(epoch_credits_start, epoch_credits_end);
let normalized_epoch_credits_window = validator.history.epoch_credits_range_normalized(
epoch_credits_start,
epoch_credits_end,
tvc_activation_epoch,
);

let total_blocks_window = cluster
.history
Expand All @@ -132,7 +135,7 @@ pub fn validator_score(

let (vote_credits_ratio, delinquency_score, delinquency_ratio, delinquency_epoch) =
calculate_epoch_credits(
&epoch_credits_window,
&normalized_epoch_credits_window,
&total_blocks_window,
epoch_credits_start,
params.scoring_delinquency_threshold_ratio,
Expand Down Expand Up @@ -271,7 +274,7 @@ pub fn calculate_epoch_credits(
// If vote credits are None, then validator was not active because we retroactively fill credits for last 64 epochs.
// If total blocks are None, then keepers missed an upload and validator should not be punished.
let credits = maybe_credits.unwrap_or(0);
let ratio = credits as f64 / *blocks as f64;
let ratio = credits as f64 / (blocks * TVC_MULTIPLIER) as f64;
if ratio < scoring_delinquency_threshold_ratio {
delinquency_score = 0.0;
delinquency_ratio = ratio;
Expand All @@ -283,8 +286,11 @@ pub fn calculate_epoch_credits(
}
}

let normalized_vote_credits_ratio =
average_vote_credits / (average_blocks * (TVC_MULTIPLIER as f64));

Ok((
average_vote_credits / average_blocks,
normalized_vote_credits_ratio,
delinquency_score,
delinquency_ratio,
delinquency_epoch,
Expand Down Expand Up @@ -471,6 +477,7 @@ pub fn instant_unstake_validator(
config: &Config,
epoch_start_slot: u64,
current_epoch: u16,
tvc_activation_epoch: u64,
) -> Result<InstantUnstakeComponentsV2> {
let params = &config.parameters;

Expand All @@ -494,7 +501,10 @@ pub fn instant_unstake_validator(
.checked_sub(epoch_start_slot)
.ok_or(StewardError::ArithmeticError)?;

let epoch_credits_latest = validator.history.epoch_credits_latest().unwrap_or(0);
let epoch_credits_latest = validator
.history
.epoch_credits_latest_normalized(current_epoch as u64, tvc_activation_epoch)
.unwrap_or(0);

/////// Component calculations ///////
let delinquency_check = calculate_instant_unstake_delinquency(
Expand Down Expand Up @@ -555,7 +565,7 @@ pub fn calculate_instant_unstake_delinquency(

if blocks_produced_rate > 0. {
Ok(
(vote_credits_rate / blocks_produced_rate)
(vote_credits_rate / (blocks_produced_rate * (TVC_MULTIPLIER as f64)))
< instant_unstake_delinquency_threshold_ratio,
)
} else {
Expand Down
13 changes: 11 additions & 2 deletions programs/steward/src/state/steward_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::fmt::Display;

use crate::{
bitmask::BitMask,
constants::{LAMPORT_BALANCE_DEFAULT, MAX_VALIDATORS, SORTED_INDEX_DEFAULT},
constants::{
LAMPORT_BALANCE_DEFAULT, MAX_VALIDATORS, SORTED_INDEX_DEFAULT, TVC_ACTIVATION_EPOCH,
},
delegation::{
decrease_stake_calculation, increase_stake_calculation, RebalanceType, UnstakeState,
},
Expand Down Expand Up @@ -688,7 +690,13 @@ impl StewardState {
return Err(StewardError::ClusterHistoryNotRecentEnough.into());
}

let score = validator_score(validator, cluster, config, current_epoch as u16)?;
let score = validator_score(
validator,
cluster,
config,
current_epoch as u16,
TVC_ACTIVATION_EPOCH,
)?;

self.scores[index] = (score.score * 1_000_000_000.) as u32;
self.yield_scores[index] = (score.yield_score * 1_000_000_000.) as u32;
Expand Down Expand Up @@ -816,6 +824,7 @@ impl StewardState {
config,
first_slot,
clock.epoch as u16,
TVC_ACTIVATION_EPOCH,
)?;

self.instant_unstake
Expand Down
1 change: 1 addition & 0 deletions programs/validator-history/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub const MAX_ALLOC_BYTES: usize = 10240;
pub const MIN_VOTE_EPOCHS: usize = 5;
pub const TVC_MULTIPLIER: u32 = 16;
40 changes: 40 additions & 0 deletions programs/validator-history/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use {
crate::{
constants::TVC_MULTIPLIER,
crds_value::{ContactInfo, LegacyContactInfo, LegacyVersion, Version2},
errors::ValidatorHistoryError,
utils::{cast_epoch, find_insert_position, get_max_epoch, get_min_epoch},
Expand Down Expand Up @@ -274,10 +275,49 @@ impl CircBuf {
field_latest!(self, epoch_credits)
}

/// Normalized epoch credits, accounting for Timely Vote Credits making the max number of credits 16x higher
/// for every epoch starting at `tvc_activation_epoch`
pub fn epoch_credits_latest_normalized(
&self,
current_epoch: u64,
tvc_activation_epoch: u64,
) -> Option<u32> {
self.epoch_credits_latest().map(|credits| {
if current_epoch < tvc_activation_epoch {
credits.saturating_mul(TVC_MULTIPLIER)
} else {
credits
}
})
}

pub fn epoch_credits_range(&self, start_epoch: u16, end_epoch: u16) -> Vec<Option<u32>> {
field_range!(self, start_epoch, end_epoch, epoch_credits, u32)
}

/// Normalized epoch credits, accounting for Timely Vote Credits making the max number of credits 8x higher
/// for every epoch starting at `tvc_activation_epoch`
pub fn epoch_credits_range_normalized(
&self,
start_epoch: u16,
end_epoch: u16,
tvc_activation_epoch: u64,
) -> Vec<Option<u32>> {
field_range!(self, start_epoch, end_epoch, epoch_credits, u32)
.into_iter()
.zip(start_epoch..=end_epoch)
.map(|(maybe_credits, epoch)| {
maybe_credits.map(|credits| {
if (epoch as u64) < tvc_activation_epoch {
credits.saturating_mul(TVC_MULTIPLIER)
} else {
credits
}
})
})
.collect()
}

pub fn superminority_latest(&self) -> Option<u8> {
// Protect against unexpected values
if let Some(value) = field_latest!(self, is_superminority) {
Expand Down
46 changes: 25 additions & 21 deletions tests/src/steward_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ use spl_stake_pool::{
},
};
use validator_history::{
self, constants::MAX_ALLOC_BYTES, CircBuf, CircBufCluster, ClusterHistory, ClusterHistoryEntry,
ValidatorHistory, ValidatorHistoryEntry,
self,
constants::{MAX_ALLOC_BYTES, TVC_MULTIPLIER},
CircBuf, CircBufCluster, ClusterHistory, ClusterHistoryEntry, ValidatorHistory,
ValidatorHistoryEntry,
};

pub struct StakePoolMetadata {
Expand Down Expand Up @@ -1779,29 +1781,27 @@ impl Default for StateMachineFixtures {
};

// Setup Sysvars: Clock, EpochSchedule

let epoch_schedule = EpochSchedule::default();

let clock = Clock {
epoch: current_epoch,
slot: epoch_schedule.get_last_slot_in_epoch(current_epoch),
..Clock::default()
};

// Setup ValidatorHistory accounts
let vote_address_1 = Pubkey::new_unique();
let vote_address_2 = Pubkey::new_unique();
let vote_address_3 = Pubkey::new_unique();
// Setup vote account addresses
let vote_account_1 = Pubkey::new_unique();
let vote_account_2 = Pubkey::new_unique();
let vote_account_3 = Pubkey::new_unique();

// First one: Good validator
let mut validator_history_1 = validator_history_default(vote_address_1, 0);
let mut validator_history_1 = validator_history_default(vote_account_1, 0);
let mut epoch_credits: Vec<(u64, u64, u64)> = vec![];

for i in 0..=20 {
epoch_credits.push((i, (i + 1) * 1000, i * 1000));
validator_history_1.history.push(ValidatorHistoryEntry {
epoch: i as u16,
epoch_credits: 1000,
epoch_credits: 1000 * TVC_MULTIPLIER,
commission: 0,
mev_commission: 0,
is_superminority: 0,
Expand All @@ -1810,18 +1810,18 @@ impl Default for StateMachineFixtures {
..ValidatorHistoryEntry::default()
});
}
let vote_account_1 =
new_vote_state_versions(vote_address_1, vote_address_1, 0, Some(epoch_credits));
let vote_account_1_state =
new_vote_state_versions(vote_account_1, vote_account_1, 0, Some(epoch_credits));

// Second one: Bad validator
let mut validator_history_2 = validator_history_default(vote_address_2, 1);
let mut validator_history_2 = validator_history_default(vote_account_2, 1);
let mut epoch_credits: Vec<(u64, u64, u64)> = vec![];
for i in 0..=20 {
epoch_credits.push((i, (i + 1) * 200, i * 200));

validator_history_2.history.push(ValidatorHistoryEntry {
epoch: i as u16,
epoch_credits: 200,
epoch_credits: 200 * TVC_MULTIPLIER,
commission: 99,
mev_commission: 10000,
is_superminority: 1,
Expand All @@ -1830,18 +1830,18 @@ impl Default for StateMachineFixtures {
..ValidatorHistoryEntry::default()
});
}
let vote_account_2 =
new_vote_state_versions(vote_address_2, vote_address_2, 99, Some(epoch_credits));
let vote_account_2_state =
new_vote_state_versions(vote_account_2, vote_account_2, 99, Some(epoch_credits));

// Third one: Good validator
let mut validator_history_3 = validator_history_default(vote_address_3, 2);
let mut validator_history_3 = validator_history_default(vote_account_3, 2);
let mut epoch_credits: Vec<(u64, u64, u64)> = vec![];
for i in 0..=20 {
epoch_credits.push((i, (i + 1) * 1000, i * 1000));

validator_history_3.history.push(ValidatorHistoryEntry {
epoch: i as u16,
epoch_credits: 1000,
epoch_credits: 1000 * TVC_MULTIPLIER,
commission: 5,
mev_commission: 500,
is_superminority: 0,
Expand All @@ -1850,8 +1850,8 @@ impl Default for StateMachineFixtures {
..ValidatorHistoryEntry::default()
});
}
let vote_account_3 =
new_vote_state_versions(vote_address_3, vote_address_3, 5, Some(epoch_credits));
let vote_account_3_state =
new_vote_state_versions(vote_account_3, vote_account_3, 5, Some(epoch_credits));

// Setup ClusterHistory
let mut cluster_history = cluster_history_default();
Expand Down Expand Up @@ -1920,7 +1920,11 @@ impl Default for StateMachineFixtures {
validator_history_2,
validator_history_3,
],
vote_accounts: vec![vote_account_1, vote_account_2, vote_account_3],
vote_accounts: vec![
vote_account_1_state,
vote_account_2_state,
vote_account_3_state,
],
cluster_history,
config,
validator_list,
Expand Down
Loading

0 comments on commit f86e28b

Please sign in to comment.