From 5b40292359d395c55cc08b3c17097a1e6c3e7518 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 8 May 2023 23:29:50 +0300 Subject: [PATCH 001/131] add `vote_success_rate` and `block_production_rate` to validator structure in validator list --- program/src/state.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/program/src/state.rs b/program/src/state.rs index 4392d4313..a1a792861 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -347,6 +347,13 @@ pub struct Validator { /// Controls if a validator is allowed to have new stake deposits. /// When removing a validator, this flag should be set to `false`. pub active: bool, + + /// Ratio of successful votes to total votes. + /// This indicates how well the validator is voting for blocks produced by other validators. + pub vote_success_rate: u8, + + /// How many blocks, on average, the validator is producing per minute. + pub block_production_rate: u8, } /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS @@ -468,7 +475,7 @@ impl Validator { /// Get stake account address that should be merged into another right after creation. /// This function should be used to create temporary stake accounts /// tied to the epoch that should be merged into another account and destroyed - /// after a transaction. So that each epoch would have a diferent + /// after a transaction. So that each epoch would have a different /// generation of stake accounts. This is done for security purpose pub fn find_temporary_stake_account_address( &self, @@ -485,7 +492,7 @@ impl Validator { impl Sealed for Validator {} impl Pack for Validator { - const LEN: usize = 89; + const LEN: usize = 91; fn pack_into_slice(&self, data: &mut [u8]) { let mut data = data; BorshSerialize::serialize(&self, &mut data).unwrap(); @@ -506,6 +513,8 @@ impl Default for Validator { effective_stake_balance: Lamports(0), active: true, vote_account_address: Pubkey::default(), + vote_success_rate: 0, + block_production_rate: 0, } } } @@ -1854,7 +1863,7 @@ mod test_lido { let mut buffer: Vec = vec![0; ValidatorList::required_bytes(accounts.header.max_entries)]; let mut slice = &mut buffer[..]; - // seriaslize empty list to buffer, which serializes a header and lenght + // serialize empty list to buffer, which serializes a header and length BorshSerialize::serialize(&accounts, &mut slice).unwrap(); // deserialize to BigVec but with a different account type @@ -1899,6 +1908,8 @@ mod test_lido { unstake_accounts_balance: Lamports(3333), effective_stake_balance: Lamports(3465468), active: false, + vote_success_rate: 13, + block_production_rate: 37, }; accounts.entries.push(elem.clone()); From 780691f18e1eed037ac3de288a394fb4b02b232c Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 9 May 2023 11:10:14 +0300 Subject: [PATCH 002/131] filter out validators in `process_stake_deposit` --- program/src/processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 037c7b117..78aad5cc2 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -246,7 +246,7 @@ pub fn process_stake_deposit( // the same StakeDeposit transaction, only one of them succeeds. let minimum_stake_validator = validators .iter() - .filter(|&v| v.active) + .filter(|&v| v.active && v.vote_success_rate > 0 && v.block_production_rate > 0) .min_by_key(|v| v.effective_stake_balance) .ok_or(LidoError::NoActiveValidators)?; let minimum_stake_pubkey = *minimum_stake_validator.pubkey(); From 6037594327de7bd84cb8aea8043f661333f9af0b Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 10 May 2023 23:52:27 +0300 Subject: [PATCH 003/131] when maintaining, `try_update_block_production_rate` --- cli/maintainer/src/daemon.rs | 7 ++++++ cli/maintainer/src/maintenance.rs | 37 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index 2dd0f5cda..4fa423474 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -43,6 +43,9 @@ struct MaintenanceMetrics { /// Number of times we performed `UpdateExchangeRate`. transactions_update_exchange_rate: u64, + /// Number of times we performed `UpdateBlockProductionRate`. + transactions_update_block_production_rate: u64, + /// Number of times we performed `UpdateStakeAccountBalance`. transactions_update_stake_account_balance: u64, @@ -125,6 +128,9 @@ impl MaintenanceMetrics { MaintenanceOutput::UpdateExchangeRate => { self.transactions_update_exchange_rate += 1; } + MaintenanceOutput::UpdateBlockProductionRate { .. } => { + self.transactions_update_block_production_rate += 1; + } MaintenanceOutput::UpdateStakeAccountBalance { .. } => { self.transactions_update_stake_account_balance += 1; } @@ -294,6 +300,7 @@ impl<'a, 'b> Daemon<'a, 'b> { errors: 0, transactions_stake_deposit: 0, transactions_update_exchange_rate: 0, + transactions_update_block_production_rate: 0, transactions_update_stake_account_balance: 0, transactions_merge_stake: 0, transactions_unstake_from_inactive_validator: 0, diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 5ee83d183..357dc0bf9 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -64,6 +64,13 @@ pub enum MaintenanceOutput { UpdateExchangeRate, + UpdateBlockProductionRate { + // The vote account of the validator we want to update. + #[serde(serialize_with = "serialize_b58")] + validator_vote_account: Pubkey, + block_production_rate: u8, + }, + UpdateStakeAccountBalance { /// The vote account of the validator that we want to update. #[serde(serialize_with = "serialize_b58")] @@ -177,6 +184,14 @@ impl fmt::Display for MaintenanceOutput { unstake_withdrawn_to_reserve )?; } + MaintenanceOutput::UpdateBlockProductionRate { + validator_vote_account, + block_production_rate, + } => { + writeln!(f, "Updated block production rate.")?; + writeln!(f, " Validator vote account: {}", validator_vote_account)?; + writeln!(f, " New block production rate: {}", block_production_rate)?; + } MaintenanceOutput::MergeStake { validator_vote_account, from_stake, @@ -890,6 +905,27 @@ impl SolidoState { Some(MaintenanceInstruction::new(instruction, task)) } + /// Tell the program the newest block production rate. + pub fn try_update_block_production_rate(&self) -> Option { + for validator in self.validators.entries.iter() { + let task = MaintenanceOutput::UpdateBlockProductionRate { + validator_vote_account: *validator.pubkey(), + block_production_rate: validator.block_production_rate, + }; + + let instruction = lido::instruction::update_block_production_rate( + &self.solido_program_id, + &lido::instruction::UpdateBlockProductionRateAccountsMeta { + lido: self.solido_address, + validator_list: self.solido.validator_list, + }, + ); + return Some(MaintenanceInstruction::new(instruction, task)); + } + + None + } + /// Check if any validator's balance is outdated, and if so, update it. /// /// Merging stakes generates inactive stake that could be withdrawn with this transaction, @@ -1498,6 +1534,7 @@ pub fn try_perform_maintenance( // as possible. .or_else(|| state.try_merge_on_all_stakes()) .or_else(|| state.try_update_exchange_rate()) + .or_else(|| state.try_update_block_production_rate()) .or_else(|| state.try_unstake_from_inactive_validator()) // Collecting validator fees goes after updating the exchange rate, // because it may be rejected if the exchange rate is outdated. From c69e8161a493288630987faf1926e04391c96db2 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 11 May 2023 11:02:00 +0300 Subject: [PATCH 004/131] react to UBPR in the program --- program/src/instruction.rs | 33 +++++++++++++++++++++++++++++++-- program/src/processor.rs | 13 +++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 7e9903eef..258693a56 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -152,6 +152,9 @@ pub enum LidoInstruction { /// This can be called by anybody. UpdateExchangeRateV2, + /// Update the block production rate of a validator. + UpdateBlockProductionRate, + /// Withdraw a given amount of stSOL. /// /// Caller provides some `amount` of StLamports that are to be burned in @@ -438,7 +441,7 @@ accounts_struct! { // should be set to the same value as `stake_account_end`. pub stake_account_merge_into { is_signer: false, - // Is writable due to merge (stake_program::intruction::merge) of stake_account_end + // Is writable due to merge (stake_program::instruction::merge) of stake_account_end // into stake_account_merge_into under the condition that they are not equal is_writable: true, }, @@ -540,7 +543,7 @@ accounts_struct! { const sysvar_clock = sysvar::clock::id(), // Required to call cross-program. const system_program = system_program::id(), - // Required to call `stake_program::intruction::split`. + // Required to call `stake_program::instruction::split`. const stake_program = stake_program::program::id(), } } @@ -599,6 +602,32 @@ pub fn update_exchange_rate( } } +accounts_struct! { + UpdateBlockProductionRateAccountsMeta, UpdateBlockProductionRateAccountsInfo { + pub lido { + is_signer: false, + is_writable: true, + }, + pub validator_list { + is_signer: false, + is_writable: false, + }, + + const sysvar_clock = sysvar::clock::id(), + } +} + +pub fn update_block_production_rate( + program_id: &Pubkey, + accounts: &UpdateBlockProductionRateAccountsMeta, +) -> Instruction { + Instruction { + program_id: *program_id, + accounts: accounts.to_vec(), + data: LidoInstruction::UpdateBlockProductionRate.to_vec(), + } +} + // Changes the Fee spec // The new Fee structure is passed by argument and the recipients are passed here accounts_struct! { diff --git a/program/src/processor.rs b/program/src/processor.rs index 78aad5cc2..6e27d0ccd 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -614,6 +614,16 @@ pub fn process_update_exchange_rate( lido.save(accounts.lido) } +pub fn process_update_block_production_rate( + program_id: &Pubkey, + raw_accounts: &[AccountInfo], +) -> ProgramResult { + let accounts = UpdateExchangeRateAccountsInfoV2::try_from_slice(raw_accounts)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + + lido.save(accounts.lido) +} + #[derive(PartialEq, Clone, Copy)] pub enum StakeType { Stake, @@ -1176,6 +1186,9 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::UpdateStakeAccountBalance { validator_index } => { process_update_stake_account_balance(program_id, validator_index, accounts) } + LidoInstruction::UpdateBlockProductionRate => { + process_update_block_production_rate(program_id, accounts) + } LidoInstruction::WithdrawV2 { amount, validator_index, From 887cc7013b9fa1a845073a0fdcdae57ed7074201 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 11 May 2023 11:18:42 +0300 Subject: [PATCH 005/131] zero out BPR from the daemon --- cli/maintainer/src/maintenance.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 357dc0bf9..03042f861 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -907,19 +907,25 @@ impl SolidoState { /// Tell the program the newest block production rate. pub fn try_update_block_production_rate(&self) -> Option { - for validator in self.validators.entries.iter() { - let task = MaintenanceOutput::UpdateBlockProductionRate { - validator_vote_account: *validator.pubkey(), - block_production_rate: validator.block_production_rate, - }; + for (validator_index, validator) in self.validators.entries.iter().enumerate() { + if false { + continue; + } let instruction = lido::instruction::update_block_production_rate( &self.solido_program_id, &lido::instruction::UpdateBlockProductionRateAccountsMeta { lido: self.solido_address, + validator_vote_account: *validator.pubkey(), validator_list: self.solido.validator_list, }, + u32::try_from(validator_index).expect("Too many validators"), + 0, ); + let task = MaintenanceOutput::UpdateBlockProductionRate { + validator_vote_account: *validator.pubkey(), + block_production_rate: 0, + }; return Some(MaintenanceInstruction::new(instruction, task)); } From 7c1d64cf80f0365865d1047c0fca576c002043ac Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 11 May 2023 11:19:13 +0300 Subject: [PATCH 006/131] update BPR on the side of the program --- program/src/instruction.rs | 18 ++++++++++++++++-- program/src/processor.rs | 30 ++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 258693a56..8c573dac8 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -153,7 +153,11 @@ pub enum LidoInstruction { UpdateExchangeRateV2, /// Update the block production rate of a validator. - UpdateBlockProductionRate, + UpdateBlockProductionRate { + // Index of a validator in validator list + validator_index: u32, + block_production_rate: u8, + }, /// Withdraw a given amount of stSOL. /// @@ -612,6 +616,10 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_vote_account { + is_signer: false, + is_writable: false, + }, const sysvar_clock = sysvar::clock::id(), } @@ -620,11 +628,17 @@ accounts_struct! { pub fn update_block_production_rate( program_id: &Pubkey, accounts: &UpdateBlockProductionRateAccountsMeta, + validator_index: u32, + block_production_rate: u8, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::UpdateBlockProductionRate.to_vec(), + data: LidoInstruction::UpdateBlockProductionRate { + validator_index, + block_production_rate, + } + .to_vec(), } } diff --git a/program/src/processor.rs b/program/src/processor.rs index 6e27d0ccd..edbdccd58 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -9,8 +9,8 @@ use crate::{ error::LidoError, instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, - StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, - UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, + StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateBlockProductionRateAccountsInfo, + UpdateExchangeRateAccountsInfoV2, UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, }, logic::{ burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, @@ -617,10 +617,22 @@ pub fn process_update_exchange_rate( pub fn process_update_block_production_rate( program_id: &Pubkey, raw_accounts: &[AccountInfo], + validator_index: u32, + block_production_rate: u8, ) -> ProgramResult { - let accounts = UpdateExchangeRateAccountsInfoV2::try_from_slice(raw_accounts)?; + let accounts = UpdateBlockProductionRateAccountsInfo::try_from_slice(raw_accounts)?; let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + + let validator = validators.get_mut(validator_index, accounts.validator_vote_account.key)?; + validator.block_production_rate = block_production_rate; + lido.save(accounts.lido) } @@ -1186,9 +1198,15 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::UpdateStakeAccountBalance { validator_index } => { process_update_stake_account_balance(program_id, validator_index, accounts) } - LidoInstruction::UpdateBlockProductionRate => { - process_update_block_production_rate(program_id, accounts) - } + LidoInstruction::UpdateBlockProductionRate { + validator_index, + block_production_rate, + } => process_update_block_production_rate( + program_id, + accounts, + validator_index, + block_production_rate, + ), LidoInstruction::WithdrawV2 { amount, validator_index, From d2a056d53e4a0d13bceadc5331e4cd2f7a63f9b0 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 12 May 2023 09:37:30 +0300 Subject: [PATCH 007/131] factors for performance metrics --- program/src/instruction.rs | 2 ++ program/src/processor.rs | 4 ++- program/src/state.rs | 61 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 8c573dac8..2cd4d2054 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -155,7 +155,9 @@ pub enum LidoInstruction { /// Update the block production rate of a validator. UpdateBlockProductionRate { // Index of a validator in validator list + #[allow(dead_code)] validator_index: u32, + #[allow(dead_code)] block_production_rate: u8, }, diff --git a/program/src/processor.rs b/program/src/processor.rs index edbdccd58..33efaf9b6 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -28,7 +28,7 @@ use crate::{ stake_account::{deserialize_stake_account, StakeAccount}, state::{ AccountType, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, - MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, + MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, Factors, }, token::{Lamports, Rational, StLamports}, MAXIMUM_UNSTAKE_ACCOUNTS, MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, @@ -148,6 +148,7 @@ pub fn process_initialize( developer_account: *accounts.developer_account.key, }, metrics: Metrics::new(), + factors: Factors::default(), validator_list: *accounts.validator_list.key, maintainer_list: *accounts.maintainer_list.key, max_commission_percentage, @@ -1144,6 +1145,7 @@ pub fn processor_migrate_to_v2( mint_authority_bump_seed: lido_v1.mint_authority_bump_seed, stake_authority_bump_seed: lido_v1.stake_authority_bump_seed, metrics: lido_v1.metrics, + factors: Factors::default(), }; // Confirm that the fee recipients are actually stSOL accounts. diff --git a/program/src/state.rs b/program/src/state.rs index a1a792861..35dcb711d 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -513,8 +513,8 @@ impl Default for Validator { effective_stake_balance: Lamports(0), active: true, vote_account_address: Pubkey::default(), - vote_success_rate: 0, - block_production_rate: 0, + vote_success_rate: std::u8::MAX, + block_production_rate: std::u8::MAX, } } } @@ -683,6 +683,58 @@ impl ExchangeRate { } } +/// Coefficients that determine how much each factor contributes to the +/// performance score. +/// +/// Each number is a numerator of a fraction, the denominator is its type's max. +/// +#[repr(C)] +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize)] +pub struct Factors { + /// The coefficient with which a validator's stake is considered + /// in the performance scoring. + pub stake: u32, + + /// The coefficient with which a validator's commission fee is considered + /// in the performance scoring. + pub commission: u32, + + /// The coefficient with which `vote_success_rate` is considered + /// in the performance scoring. + pub vote_success_rate: u32, + + /// The coefficient with which `block_production_rate` is considered + /// in the performance scoring. + pub block_production_rate: u32, +} + +impl Default for Factors { + fn default() -> Self { + Self { + stake: std::u32::MAX, + commission: std::u32::MAX, + vote_success_rate: std::u32::MAX, + block_production_rate: std::u32::MAX, + } + } +} + +impl Factors { + pub fn new( + stake: u32, + commission: u32, + vote_success_rate: u32, + block_production_rate: u32, + ) -> Self { + Self { + stake, + commission, + vote_success_rate, + block_production_rate, + } + } +} + #[repr(C)] #[derive( Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize, @@ -723,6 +775,10 @@ pub struct Lido { /// these metrics. pub metrics: Metrics, + /// Coefficients to linearly combine the performance metrics + /// of validators into a single score. + pub factors: Factors, + /// Validator list account #[serde(serialize_with = "serialize_b58")] pub validator_list: Pubkey, @@ -1501,6 +1557,7 @@ mod test_lido { developer_account: Pubkey::new_unique(), }, metrics: Metrics::new(), + factors: Factors::default(), validator_list: Pubkey::new_unique(), maintainer_list: Pubkey::new_unique(), max_commission_percentage: 5, From b66bfad0451cfd67255fd7e16e89af0df439621b Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 12 May 2023 09:40:11 +0300 Subject: [PATCH 008/131] Lido::LEN -> 434 --- program/src/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/program/src/state.rs b/program/src/state.rs index 35dcb711d..8b2fa04d7 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -802,7 +802,7 @@ impl Lido { /// Size of a serialized `Lido` struct excluding validators and maintainers. /// /// To update this, run the tests and replace the value here with the test output. - pub const LEN: usize = 418; + pub const LEN: usize = 434; pub fn deserialize_lido(program_id: &Pubkey, lido: &AccountInfo) -> Result { check_account_owner(lido, program_id)?; @@ -840,7 +840,7 @@ impl Lido { LidoV1::LEN, Lido::LEN, // it's enough to check only bytes for `a list of size 1` to be zero, - // otherwize the list won't be deserializable + // otherwise the list won't be deserializable ValidatorList::required_bytes(1), MaintainerList::required_bytes(1), ] From ccc41f8c801e77fbacfb24fdf0b9d8482b4dcdb3 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 16 May 2023 22:39:30 +0300 Subject: [PATCH 009/131] introduce performance thresholds, factor in there max commission --- cli/maintainer/src/commands_solido.rs | 6 +- cli/maintainer/src/config.rs | 2 +- cli/maintainer/src/maintenance.rs | 2 +- program/src/process_management.rs | 8 +- program/src/processor.rs | 10 +-- program/src/state.rs | 73 ++++++++----------- .../tests/tests/max_commission_percentage.rs | 2 +- 7 files changed, 43 insertions(+), 60 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 6d30fc76b..1984aa0b5 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -524,7 +524,7 @@ impl fmt::Display for ShowSolidoOutput { writeln!( f, "Max validation commission: {}%", - self.solido.max_commission_percentage + self.solido.thresholds.max_commission, )?; writeln!(f, "\nMetrics:")?; @@ -1048,7 +1048,7 @@ pub fn command_deactivate_validator_if_commission_exceeds_max( .ok() .ok_or_else(|| CliError::new("Validator account data too small"))?; - if !validator.active || commission <= solido.max_commission_percentage { + if !validator.active || commission <= solido.thresholds.max_commission { continue; } @@ -1076,7 +1076,7 @@ pub fn command_deactivate_validator_if_commission_exceeds_max( Ok(DeactivateValidatorIfCommissionExceedsMaxOutput { entries: violations, - max_commission_percentage: solido.max_commission_percentage, + max_commission_percentage: solido.thresholds.max_commission, }) } diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index bf4b8fb2d..1b1c1d97f 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -481,7 +481,7 @@ cli_opt_struct! { #[clap(long, value_name = "address")] solido_address: Pubkey, - /// Max percent of rewards a validator can recieve (validation commission), in range [0, 100] + /// Max percent of rewards a validator can receive (validation commission), in range [0, 100] #[clap(long, value_name = "fee")] max_commission_percentage: u8, diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 03042f861..9583054f6 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -766,7 +766,7 @@ impl SolidoState { // We are only interested in validators that violate commission limit if let Some(state) = vote_state { - if state.commission <= self.solido.max_commission_percentage { + if state.commission <= self.solido.thresholds.max_commission { continue; } } else { diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 8b8ff3f33..6c050ddd2 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -57,7 +57,7 @@ pub fn process_add_validator(program_id: &Pubkey, accounts_raw: &[AccountInfo]) // satisfy the commission limit. let _partial_vote_state = PartialVoteState::deserialize( accounts.validator_vote_account, - lido.max_commission_percentage, + lido.thresholds.max_commission, )?; let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); @@ -165,7 +165,7 @@ pub fn process_deactivate_validator_if_commission_exceeds_max( let data = accounts.validator_vote_account_to_deactivate.data.borrow(); let commission = get_vote_account_commission(&data)?; - if commission <= lido.max_commission_percentage { + if commission <= lido.thresholds.max_commission { return Ok(()); } } else { @@ -215,7 +215,7 @@ pub fn process_remove_maintainer( Ok(()) } -/// Sets max validation commission for Lido. If validators exeed the threshold +/// Sets max validation commission for Lido. If validators exceed the threshold /// they will be deactivated by DeactivateValidatorIfCommissionExceedsMax pub fn process_set_max_commission_percentage( program_id: &Pubkey, @@ -231,7 +231,7 @@ pub fn process_set_max_commission_percentage( lido.check_manager(accounts.manager)?; - lido.max_commission_percentage = max_commission_percentage; + lido.thresholds.max_commission = max_commission_percentage; lido.save(accounts.lido) } diff --git a/program/src/processor.rs b/program/src/processor.rs index 33efaf9b6..976f9d699 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -28,7 +28,7 @@ use crate::{ stake_account::{deserialize_stake_account, StakeAccount}, state::{ AccountType, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, - MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, Factors, + MaintainerList, RewardDistribution, StakeDeposit, Thresholds, Validator, ValidatorList, }, token::{Lamports, Rational, StLamports}, MAXIMUM_UNSTAKE_ACCOUNTS, MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, @@ -148,10 +148,9 @@ pub fn process_initialize( developer_account: *accounts.developer_account.key, }, metrics: Metrics::new(), - factors: Factors::default(), + thresholds: Thresholds::default(), validator_list: *accounts.validator_list.key, maintainer_list: *accounts.maintainer_list.key, - max_commission_percentage, }; // Confirm that the fee recipients are actually stSOL accounts. @@ -247,7 +246,7 @@ pub fn process_stake_deposit( // the same StakeDeposit transaction, only one of them succeeds. let minimum_stake_validator = validators .iter() - .filter(|&v| v.active && v.vote_success_rate > 0 && v.block_production_rate > 0) + .filter(|&v| v.active) .min_by_key(|v| v.effective_stake_balance) .ok_or(LidoError::NoActiveValidators)?; let minimum_stake_pubkey = *minimum_stake_validator.pubkey(); @@ -1131,7 +1130,6 @@ pub fn processor_migrate_to_v2( account_type: AccountType::Lido, validator_list: *accounts.validator_list.key, maintainer_list: *accounts.maintainer_list.key, - max_commission_percentage, fee_recipients: FeeRecipients { treasury_account: lido_v1.fee_recipients.treasury_account, developer_account: *accounts.developer_account.key, @@ -1145,7 +1143,7 @@ pub fn processor_migrate_to_v2( mint_authority_bump_seed: lido_v1.mint_authority_bump_seed, stake_authority_bump_seed: lido_v1.stake_authority_bump_seed, metrics: lido_v1.metrics, - factors: Factors::default(), + thresholds: Thresholds::default(), }; // Confirm that the fee recipients are actually stSOL accounts. diff --git a/program/src/state.rs b/program/src/state.rs index 8b2fa04d7..7e36aa124 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -683,54 +683,43 @@ impl ExchangeRate { } } -/// Coefficients that determine how much each factor contributes to the -/// performance score. -/// -/// Each number is a numerator of a fraction, the denominator is its type's max. +/// Each field is an optimum for a metric. +/// If a validator has a value for a metric that does not meet the threshold, +/// then the validator gets deactivated. /// #[repr(C)] #[derive(Clone, Debug, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize)] -pub struct Factors { - /// The coefficient with which a validator's stake is considered - /// in the performance scoring. - pub stake: u32, - - /// The coefficient with which a validator's commission fee is considered - /// in the performance scoring. - pub commission: u32, - - /// The coefficient with which `vote_success_rate` is considered - /// in the performance scoring. - pub vote_success_rate: u32, - - /// The coefficient with which `block_production_rate` is considered - /// in the performance scoring. - pub block_production_rate: u32, +pub struct Thresholds { + /// If a validator has the commission higher than this, then it gets deactivated. + pub max_commission: u8, + + /// If a validator has `vote_success_rate` lower than this, then it gets deactivated. + pub min_vote_success_rate: u8, + + /// If a validator has `block_production_rate` lower than this, then it gets deactivated. + pub min_block_production_rate: u8, } -impl Default for Factors { +impl Default for Thresholds { fn default() -> Self { Self { - stake: std::u32::MAX, - commission: std::u32::MAX, - vote_success_rate: std::u32::MAX, - block_production_rate: std::u32::MAX, + max_commission: 100, + min_vote_success_rate: 0, + min_block_production_rate: 0, } } } -impl Factors { +impl Thresholds { pub fn new( - stake: u32, - commission: u32, - vote_success_rate: u32, - block_production_rate: u32, + max_commission: u8, + min_vote_success_rate: u8, + min_block_production_rate: u8, ) -> Self { Self { - stake, - commission, - vote_success_rate, - block_production_rate, + max_commission, + min_vote_success_rate, + min_block_production_rate, } } } @@ -775,9 +764,9 @@ pub struct Lido { /// these metrics. pub metrics: Metrics, - /// Coefficients to linearly combine the performance metrics - /// of validators into a single score. - pub factors: Factors, + /// Metrics of validator's performance such that if a validator's metrics + /// do not meet the threshold, then the validator gets deactivated. + pub thresholds: Thresholds, /// Validator list account #[serde(serialize_with = "serialize_b58")] @@ -791,9 +780,6 @@ pub struct Lido { /// In the future we plan to make maintenance operations callable by anybody. #[serde(serialize_with = "serialize_b58")] pub maintainer_list: Pubkey, - - /// Maximum validation commission percentage in [0, 100] - pub max_commission_percentage: u8, } impl Lido { @@ -802,7 +788,7 @@ impl Lido { /// Size of a serialized `Lido` struct excluding validators and maintainers. /// /// To update this, run the tests and replace the value here with the test output. - pub const LEN: usize = 434; + pub const LEN: usize = 420; pub fn deserialize_lido(program_id: &Pubkey, lido: &AccountInfo) -> Result { check_account_owner(lido, program_id)?; @@ -830,7 +816,7 @@ impl Lido { get_instance_packed_len(&lido_instance).unwrap() } - /// Get maximum number of bytes over all Solido owned accounts, including previuos + /// Get maximum number of bytes over all Solido owned accounts, including previous /// versions, that should be checked to be zero to initialize Solido instance /// /// This is also done to avoid account confusion that could cause an old, @@ -1557,10 +1543,9 @@ mod test_lido { developer_account: Pubkey::new_unique(), }, metrics: Metrics::new(), - factors: Factors::default(), + thresholds: Thresholds::new(5, 0, 0), validator_list: Pubkey::new_unique(), maintainer_list: Pubkey::new_unique(), - max_commission_percentage: 5, }; let mut data = Vec::new(); BorshSerialize::serialize(&lido, &mut data).unwrap(); diff --git a/program/tests/tests/max_commission_percentage.rs b/program/tests/tests/max_commission_percentage.rs index 8436b29e3..fddd3d1fd 100644 --- a/program/tests/tests/max_commission_percentage.rs +++ b/program/tests/tests/max_commission_percentage.rs @@ -18,7 +18,7 @@ async fn test_set_max_commission_percentage() { let solido = context.get_solido().await.lido; assert_eq!( - solido.max_commission_percentage, + solido.thresholds.max_commission, context.max_commission_percentage + 1 ); From 390e46334b801b3d091a9a07d53006be56be2759 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 17 May 2023 10:20:35 +0300 Subject: [PATCH 010/131] zip validators with their infos in `show-solido` --- cli/maintainer/src/commands_solido.rs | 73 +++++++++++++-------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 1984aa0b5..b07886d13 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -435,18 +435,13 @@ pub struct ShowSolidoOutput { #[serde(serialize_with = "serialize_b58")] pub mint_authority: Pubkey, - /// Identity account address for all validators in the same order as `solido.validators`. - pub validator_identities: Vec, + /// Validator structure as the program sees it, along with the validator's + /// identity account address, their info, and their commission percentage. + pub validators: Vec<(Validator, Pubkey, ValidatorInfo, u8)>, + pub validators_max: u32, - /// Contains validator info in the same order as `solido.validators`. - pub validator_infos: Vec, - - /// Contains validator fees in the same order as `solido.validators`. - pub validator_commission_percentages: Vec, - - pub validators: AccountList, - - pub maintainers: AccountList, + pub maintainers: Vec, + pub maintainers_max: u32, pub reserve_account_balance: Lamports, } @@ -582,16 +577,9 @@ impl fmt::Display for ShowSolidoOutput { f, "Validators: {} in use out of {} that the instance can support", self.validators.len(), - self.validators.header.max_entries + self.validators_max, )?; - for (((pe, identity), info), commission) in self - .validators - .entries - .iter() - .zip(&self.validator_identities) - .zip(&self.validator_infos) - .zip(&self.validator_commission_percentages) - { + for (v, identity, info, commission) in self.validators.iter() { writeln!( f, "\n - \ @@ -609,25 +597,26 @@ impl fmt::Display for ShowSolidoOutput { Some(username) => &username[..], None => "not set", }, - pe.pubkey(), + v.pubkey(), identity, commission, - pe.active, - pe.stake_accounts_balance, - pe.effective_stake_balance, - pe.unstake_accounts_balance, + v.active, + v.block_production_rate, + v.stake_accounts_balance, + v.effective_stake_balance, + v.unstake_accounts_balance, )?; writeln!(f, " Stake accounts (seed, address):")?; - if pe.stake_seeds.begin == pe.stake_seeds.end { + if v.stake_seeds.begin == v.stake_seeds.end { writeln!(f, " This validator has no stake accounts.")?; }; - for seed in &pe.stake_seeds { + for seed in &v.stake_seeds { writeln!( f, " - {}: {}", seed, - pe.find_stake_account_address( + v.find_stake_account_address( &self.solido_program_id, &self.solido_address, seed, @@ -638,15 +627,15 @@ impl fmt::Display for ShowSolidoOutput { } writeln!(f, " Unstake accounts (seed, address):")?; - if pe.unstake_seeds.begin == pe.unstake_seeds.end { + if v.unstake_seeds.begin == v.unstake_seeds.end { writeln!(f, " This validator has no unstake accounts.")?; }; - for seed in &pe.unstake_seeds { + for seed in &v.unstake_seeds { writeln!( f, " - {}: {}", seed, - pe.find_stake_account_address( + v.find_stake_account_address( &self.solido_program_id, &self.solido_address, seed, @@ -661,9 +650,9 @@ impl fmt::Display for ShowSolidoOutput { f, "Maintainers: {} in use out of {} that the instance can support\n", self.maintainers.len(), - self.maintainers.header.max_entries + self.maintainers_max, )?; - for e in &self.maintainers.entries { + for e in &self.maintainers { writeln!(f, " - {}", e.pubkey())?; } Ok(()) @@ -687,14 +676,18 @@ pub fn command_show_solido( let validators = config .client .get_account_list::(&lido.validator_list)?; + let validators_max = validators.header.max_entries; + let validators = validators.entries; let maintainers = config .client .get_account_list::(&lido.maintainer_list)?; + let maintainers_max = maintainers.header.max_entries; + let maintainers = maintainers.entries; let mut validator_identities = Vec::new(); let mut validator_infos = Vec::new(); let mut validator_commission_percentages = Vec::new(); - for validator in validators.entries.iter() { + for validator in validators.iter() { let vote_state = config.client.get_vote_account(validator.pubkey())?; validator_identities.push(vote_state.node_pubkey); let info = config.client.get_validator_info(&vote_state.node_pubkey)?; @@ -705,19 +698,25 @@ pub fn command_show_solido( .ok_or_else(|| CliError::new("Validator account data too small"))?; validator_commission_percentages.push(commission); } + let validators = validators + .into_iter() + .zip(validator_identities.into_iter()) + .zip(validator_infos.into_iter()) + .zip(validator_commission_percentages.into_iter()) + .map(|(((v, identity), info), commission)| (v, identity, info, commission)) + .collect(); Ok(ShowSolidoOutput { solido_program_id: *opts.solido_program_id(), solido_address: *opts.solido_address(), solido: lido, - validator_identities, - validator_infos, - validator_commission_percentages, reserve_account, stake_authority, mint_authority, validators, + validators_max, maintainers, + maintainers_max, reserve_account_balance: Lamports(reserve_account_balance), }) } From 3bb8cc57e6d8117dc94c4cc8b89c16e2c4a34a32 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 17 May 2023 10:22:07 +0300 Subject: [PATCH 011/131] workspace: run solido by default --- cli/maintainer/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/maintainer/Cargo.toml b/cli/maintainer/Cargo.toml index 793dc2e2e..fb0834cb1 100644 --- a/cli/maintainer/Cargo.toml +++ b/cli/maintainer/Cargo.toml @@ -4,6 +4,7 @@ description = "Solido Command-line Utility" license = "GPL-3.0" edition = "2018" name = "solido-cli" +default-run = "solido" version = "1.3.6" [dependencies] From 77157763fab472e45438543613f519bd77015370 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 17 May 2023 12:14:33 +0300 Subject: [PATCH 012/131] `deactivate_if_commission_exceeds_max` -> `deactivate_if_violates` --- cli/maintainer/src/commands_solido.rs | 1 - program/src/instruction.rs | 15 +++++++------- program/src/process_management.rs | 12 +++++------ program/src/processor.rs | 29 ++++++++------------------- testlib/src/solido_context.rs | 2 +- 5 files changed, 22 insertions(+), 37 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index b07886d13..83c7184b3 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -601,7 +601,6 @@ impl fmt::Display for ShowSolidoOutput { identity, commission, v.active, - v.block_production_rate, v.stake_accounts_balance, v.effective_stake_balance, v.unstake_accounts_balance, diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 2cd4d2054..9618ed1b7 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -107,15 +107,15 @@ pub enum LidoInstruction { /// or if vote account is closed, then deactivate it /// /// Requires no permission - DeactivateValidatorIfCommissionExceedsMax { + DeactivateIfViolates { // Index of a validator in validator list #[allow(dead_code)] // but it's not validator_index: u32, }, /// Set max_commission_percentage to control validator's fees. - /// If validators exeed the threshold they will be deactivated by - /// DeactivateValidatorIfCommissionExceedsMax. + /// If validators exceed the threshold they will be deactivated by + /// DeactivateIfViolates. /// /// Requires the manager to sign. SetMaxValidationCommission { @@ -983,8 +983,8 @@ pub fn update_stake_account_balance( } accounts_struct! { - DeactivateValidatorIfCommissionExceedsMaxMeta, - DeactivateValidatorIfCommissionExceedsMaxInfo { + DeactivateIfViolatesMeta, + DeactivateIfViolatesInfo { pub lido { is_signer: false, is_writable: false, @@ -1002,14 +1002,13 @@ accounts_struct! { pub fn deactivate_validator_if_commission_exceeds_max( program_id: &Pubkey, - accounts: &DeactivateValidatorIfCommissionExceedsMaxMeta, + accounts: &DeactivateIfViolatesMeta, validator_index: u32, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::DeactivateValidatorIfCommissionExceedsMax { validator_index } - .to_vec(), + data: LidoInstruction::DeactivateIfViolates { validator_index }.to_vec(), } } diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 6c050ddd2..f9f846b48 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -14,7 +14,7 @@ use crate::{ error::LidoError, instruction::{ AddMaintainerInfoV2, AddValidatorInfoV2, ChangeRewardDistributionInfo, - DeactivateValidatorIfCommissionExceedsMaxInfo, DeactivateValidatorInfoV2, MergeStakeInfoV2, + DeactivateIfViolatesInfo, DeactivateValidatorInfoV2, MergeStakeInfoV2, RemoveMaintainerInfoV2, RemoveValidatorInfoV2, SetMaxValidationCommissionInfo, }, state::{ListEntry, Maintainer, RewardDistribution, Validator}, @@ -132,17 +132,17 @@ pub fn process_deactivate_validator( Ok(()) } -/// Mark validator inactive if it's commission is bigger then max -/// allowed or if it's vote account is closed. It is permissionless. +/// Mark a validator inactive if any of their performance metrics exceeds the +/// allowed range of values. /// /// This prevents new funds from being staked with this validator, and enables -/// removing the validator once no stake is delegated to it any more. -pub fn process_deactivate_validator_if_commission_exceeds_max( +/// removing the validator once no stake is delegated to it anymore. +pub fn process_deactivate_if_violates( program_id: &Pubkey, validator_index: u32, accounts_raw: &[AccountInfo], ) -> ProgramResult { - let accounts = DeactivateValidatorIfCommissionExceedsMaxInfo::try_from_slice(accounts_raw)?; + let accounts = DeactivateIfViolatesInfo::try_from_slice(accounts_raw)?; let lido = Lido::deserialize_lido(program_id, accounts.lido)?; let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); diff --git a/program/src/processor.rs b/program/src/processor.rs index 976f9d699..871319d5f 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -21,7 +21,7 @@ use crate::{ metrics::Metrics, process_management::{ process_add_maintainer, process_add_validator, process_change_reward_distribution, - process_deactivate_validator, process_deactivate_validator_if_commission_exceeds_max, + process_deactivate_validator, process_deactivate_if_violates, process_merge_stake, process_remove_maintainer, process_remove_validator, process_set_max_commission_percentage, }, @@ -615,25 +615,12 @@ pub fn process_update_exchange_rate( } pub fn process_update_block_production_rate( - program_id: &Pubkey, - raw_accounts: &[AccountInfo], - validator_index: u32, - block_production_rate: u8, + _program_id: &Pubkey, + _raw_accounts: &[AccountInfo], + _validator_index: u32, + _block_production_rate: u8, ) -> ProgramResult { - let accounts = UpdateBlockProductionRateAccountsInfo::try_from_slice(raw_accounts)?; - let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - - let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); - let mut validators = lido.deserialize_account_list_info::( - program_id, - accounts.validator_list, - validator_list_data, - )?; - - let validator = validators.get_mut(validator_index, accounts.validator_vote_account.key)?; - validator.block_production_rate = block_production_rate; - - lido.save(accounts.lido) + unimplemented!("no no") } #[derive(PartialEq, Clone, Copy)] @@ -1228,8 +1215,8 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::MergeStakeV2 { validator_index } => { process_merge_stake(program_id, validator_index, accounts) } - LidoInstruction::DeactivateValidatorIfCommissionExceedsMax { validator_index } => { - process_deactivate_validator_if_commission_exceeds_max( + LidoInstruction::DeactivateIfViolates { validator_index } => { + process_deactivate_if_violates( program_id, validator_index, accounts, diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index ee53ca12c..a05deb5a3 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1387,7 +1387,7 @@ impl Context { &[ lido::instruction::deactivate_validator_if_commission_exceeds_max( &id(), - &lido::instruction::DeactivateValidatorIfCommissionExceedsMaxMeta { + &lido::instruction::DeactivateIfViolatesMeta { lido: self.solido.pubkey(), validator_vote_account_to_deactivate: vote_account, validator_list: self.validator_list.pubkey(), From f1a0f0931c9dc446d3fceffb64b97372df428a7c Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 17 May 2023 12:15:02 +0300 Subject: [PATCH 013/131] `Validator::LEN` -> 89 --- program/src/state.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/program/src/state.rs b/program/src/state.rs index 7e36aa124..b2e58c87e 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -347,13 +347,6 @@ pub struct Validator { /// Controls if a validator is allowed to have new stake deposits. /// When removing a validator, this flag should be set to `false`. pub active: bool, - - /// Ratio of successful votes to total votes. - /// This indicates how well the validator is voting for blocks produced by other validators. - pub vote_success_rate: u8, - - /// How many blocks, on average, the validator is producing per minute. - pub block_production_rate: u8, } /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS @@ -492,7 +485,7 @@ impl Validator { impl Sealed for Validator {} impl Pack for Validator { - const LEN: usize = 91; + const LEN: usize = 89; fn pack_into_slice(&self, data: &mut [u8]) { let mut data = data; BorshSerialize::serialize(&self, &mut data).unwrap(); @@ -513,8 +506,6 @@ impl Default for Validator { effective_stake_balance: Lamports(0), active: true, vote_account_address: Pubkey::default(), - vote_success_rate: std::u8::MAX, - block_production_rate: std::u8::MAX, } } } @@ -1950,8 +1941,6 @@ mod test_lido { unstake_accounts_balance: Lamports(3333), effective_stake_balance: Lamports(3465468), active: false, - vote_success_rate: 13, - block_production_rate: 37, }; accounts.entries.push(elem.clone()); From 2b288ff2d21ee088bbce601841d0a2b6f8f31b0e Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 17 May 2023 12:28:37 +0300 Subject: [PATCH 014/131] cli: `DeactivateValidatorIfCommissionExceedsMax` -> `DeactivateIfViolates` --- cli/maintainer/src/commands_solido.rs | 2 +- cli/maintainer/src/daemon.rs | 2 +- cli/maintainer/src/maintenance.rs | 8 ++++---- program/src/processor.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 83c7184b3..dff3694ba 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -1052,7 +1052,7 @@ pub fn command_deactivate_validator_if_commission_exceeds_max( let instruction = lido::instruction::deactivate_validator_if_commission_exceeds_max( opts.solido_program_id(), - &lido::instruction::DeactivateValidatorIfCommissionExceedsMaxMeta { + &lido::instruction::DeactivateIfViolatesMeta { lido: *opts.solido_address(), validator_vote_account_to_deactivate: *validator.pubkey(), validator_list: solido.validator_list, diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index 4fa423474..c3078fbab 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -139,7 +139,7 @@ impl MaintenanceMetrics { self.transactions_unstake_from_inactive_validator += 1 } MaintenanceOutput::RemoveValidator { .. } => self.transactions_remove_validator += 1, - MaintenanceOutput::DeactivateValidatorIfCommissionExceedsMax { .. } => { + MaintenanceOutput::DeactivateIfViolates { .. } => { self.transactions_deactivate_validator_if_commission_exceeds_max += 1 } MaintenanceOutput::UnstakeFromActiveValidator { .. } => { diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 9583054f6..b1bbfbbb1 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -104,7 +104,7 @@ pub enum MaintenanceOutput { #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, }, - DeactivateValidatorIfCommissionExceedsMax { + DeactivateIfViolates { #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, }, @@ -224,7 +224,7 @@ impl fmt::Display for MaintenanceOutput { writeln!(f, "Remove validator")?; writeln!(f, " Validator vote account: {}", validator_vote_account)?; } - MaintenanceOutput::DeactivateValidatorIfCommissionExceedsMax { + MaintenanceOutput::DeactivateIfViolates { validator_vote_account, } => { writeln!(f, "Check max commission violation.")?; @@ -773,13 +773,13 @@ impl SolidoState { // Vote account is closed } - let task = MaintenanceOutput::DeactivateValidatorIfCommissionExceedsMax { + let task = MaintenanceOutput::DeactivateIfViolates { validator_vote_account: *validator.pubkey(), }; let instruction = lido::instruction::deactivate_validator_if_commission_exceeds_max( &self.solido_program_id, - &lido::instruction::DeactivateValidatorIfCommissionExceedsMaxMeta { + &lido::instruction::DeactivateIfViolatesMeta { lido: self.solido_address, validator_vote_account_to_deactivate: *validator.pubkey(), validator_list: self.solido.validator_list, diff --git a/program/src/processor.rs b/program/src/processor.rs index 871319d5f..c3ce6ca58 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -9,7 +9,7 @@ use crate::{ error::LidoError, instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, - StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateBlockProductionRateAccountsInfo, + StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, }, logic::{ From e9d8fbaf8a67b9d3f05cfff3aad59a07f56b1231 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 17 May 2023 18:04:33 +0300 Subject: [PATCH 015/131] consistent naming for `deactivate_if_volates` --- cli/maintainer/src/commands_solido.rs | 21 +++++++-------- cli/maintainer/src/config.rs | 26 +++++++++---------- cli/maintainer/src/daemon.rs | 24 ++++++++++------- cli/maintainer/src/main.rs | 23 ++++++++-------- cli/maintainer/src/maintenance.rs | 6 ++--- program/src/instruction.rs | 2 +- program/src/process_management.rs | 2 +- .../tests/tests/max_commission_percentage.rs | 6 ++--- scripts/test_solido.py | 4 +-- testlib/src/solido_context.rs | 4 +-- 10 files changed, 61 insertions(+), 57 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index dff3694ba..12ea4327a 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -37,9 +37,8 @@ use crate::{ use crate::{ config::{ AddRemoveMaintainerOpts, AddValidatorOpts, CreateSolidoOpts, CreateV2AccountsOpts, - DeactivateValidatorIfCommissionExceedsMaxOpts, DeactivateValidatorOpts, DepositOpts, - MigrateStateToV2Opts, SetMaxValidationCommissionOpts, ShowSolidoAuthoritiesOpts, - ShowSolidoOpts, WithdrawOpts, + DeactivateIfViolatesOpts, DeactivateValidatorOpts, DepositOpts, MigrateStateToV2Opts, + SetMaxValidationCommissionOpts, ShowSolidoAuthoritiesOpts, ShowSolidoOpts, WithdrawOpts, }, get_signer_from_path, }; @@ -994,7 +993,7 @@ pub fn command_withdraw( } #[derive(Serialize)] -pub struct DeactivateValidatorIfCommissionExceedsMaxOutput { +pub struct DeactivateIfViolatesOutput { // List of validators that exceeded max commission entries: Vec, max_commission_percentage: u8, @@ -1007,7 +1006,7 @@ struct ValidatorViolationInfo { pub commission: u8, } -impl fmt::Display for DeactivateValidatorIfCommissionExceedsMaxOutput { +impl fmt::Display for DeactivateIfViolatesOutput { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( f, @@ -1027,10 +1026,10 @@ impl fmt::Display for DeactivateValidatorIfCommissionExceedsMaxOutput { } /// CLI entry point to punish validator for commission violation. -pub fn command_deactivate_validator_if_commission_exceeds_max( +pub fn command_deactivate_if_violates( config: &mut SnapshotConfig, - opts: &DeactivateValidatorIfCommissionExceedsMaxOpts, -) -> solido_cli_common::Result { + opts: &DeactivateIfViolatesOpts, +) -> solido_cli_common::Result { let solido = config.client.get_solido(opts.solido_address())?; let validators = config @@ -1050,7 +1049,7 @@ pub fn command_deactivate_validator_if_commission_exceeds_max( continue; } - let instruction = lido::instruction::deactivate_validator_if_commission_exceeds_max( + let instruction = lido::instruction::deactivate_if_violates( opts.solido_program_id(), &lido::instruction::DeactivateIfViolatesMeta { lido: *opts.solido_address(), @@ -1068,11 +1067,11 @@ pub fn command_deactivate_validator_if_commission_exceeds_max( let signers: Vec<&dyn Signer> = vec![]; // Due to the fact that Solana has a limit on number of instructions in a transaction - // this can fall if there would be alot of misbehaved validators each + // this can fall if there would be a lot of misbehaved validators each // exceeding `max_commission_percentage`. But it is a very improbable scenario. config.sign_and_send_transaction(&instructions, &signers)?; - Ok(DeactivateValidatorIfCommissionExceedsMaxOutput { + Ok(DeactivateIfViolatesOutput { entries: violations, max_commission_percentage: solido.thresholds.max_commission, }) diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index 1b1c1d97f..a394b2701 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -388,6 +388,18 @@ cli_opt_struct! { } } +cli_opt_struct! { + DeactivateIfViolatesOpts { + /// Address of the Solido program. + #[clap(long, value_name = "address")] + solido_program_id: Pubkey, + + /// Account that stores the data for this Solido instance. + #[clap(long, value_name = "address")] + solido_address: Pubkey, + } +} + cli_opt_struct! { AddRemoveMaintainerOpts { /// Address of the Solido program. @@ -459,18 +471,6 @@ cli_opt_struct! { } } -cli_opt_struct! { - DeactivateValidatorIfCommissionExceedsMaxOpts { - /// Address of the Solido program. - #[clap(long, value_name = "address")] - solido_program_id: Pubkey, - - /// Account that stores the data for this Solido instance. - #[clap(long, value_name = "address")] - solido_address: Pubkey, - } -} - cli_opt_struct! { SetMaxValidationCommissionOpts { /// Address of the Solido program. @@ -531,7 +531,7 @@ impl CreateMultisigOpts { } cli_opt_struct! { -ProposeUpgradeOpts { + ProposeUpgradeOpts { /// The multisig account whose owners should vote for this proposal. #[clap(long, value_name = "address")] multisig_address: Pubkey, diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index c3078fbab..53a39711f 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -61,8 +61,8 @@ struct MaintenanceMetrics { /// Number of times we performed `RemoveValidator`. transactions_remove_validator: u64, - /// Number of times we performed `DeactivateValidatorIfCommissionExceedsMax`. - transactions_deactivate_validator_if_commission_exceeds_max: u64, + /// Number of times we performed `DeactivateIfViolates`. + transactions_deactivate_if_violates: u64, /// Number of times we performed `Unstake` on an active validator for balancing purposes. transactions_unstake_from_active_validator: u64, @@ -108,10 +108,10 @@ impl MaintenanceMetrics { .with_label("operation", "RemoveValidator".to_string()), Metric::new(self.transactions_unstake_from_active_validator) .with_label("operation", "UnstakeFromActiveValidator".to_string()), - Metric::new(self.transactions_deactivate_validator_if_commission_exceeds_max) + Metric::new(self.transactions_deactivate_if_violates) .with_label( "operation", - "DeactivateValidatorIfCommissionExceedsMax".to_string(), + "DeactivateIfViolates".to_string(), ), ], }, @@ -134,16 +134,20 @@ impl MaintenanceMetrics { MaintenanceOutput::UpdateStakeAccountBalance { .. } => { self.transactions_update_stake_account_balance += 1; } - MaintenanceOutput::MergeStake { .. } => self.transactions_merge_stake += 1, + MaintenanceOutput::MergeStake { .. } => { + self.transactions_merge_stake += 1; + } MaintenanceOutput::UnstakeFromInactiveValidator { .. } => { - self.transactions_unstake_from_inactive_validator += 1 + self.transactions_unstake_from_inactive_validator += 1; + } + MaintenanceOutput::RemoveValidator { .. } => { + self.transactions_remove_validator += 1; } - MaintenanceOutput::RemoveValidator { .. } => self.transactions_remove_validator += 1, MaintenanceOutput::DeactivateIfViolates { .. } => { - self.transactions_deactivate_validator_if_commission_exceeds_max += 1 + self.transactions_deactivate_if_violates += 1; } MaintenanceOutput::UnstakeFromActiveValidator { .. } => { - self.transactions_unstake_from_active_validator += 1 + self.transactions_unstake_from_active_validator += 1; } } } @@ -305,7 +309,7 @@ impl<'a, 'b> Daemon<'a, 'b> { transactions_merge_stake: 0, transactions_unstake_from_inactive_validator: 0, transactions_remove_validator: 0, - transactions_deactivate_validator_if_commission_exceeds_max: 0, + transactions_deactivate_if_violates: 0, transactions_unstake_from_active_validator: 0, }; Daemon { diff --git a/cli/maintainer/src/main.rs b/cli/maintainer/src/main.rs index 8fb8b4692..abf15e1d2 100644 --- a/cli/maintainer/src/main.rs +++ b/cli/maintainer/src/main.rs @@ -20,10 +20,10 @@ use solido_cli_common::snapshot::{Config, OutputMode, SnapshotClient}; use crate::commands_multisig::MultisigOpts; use crate::commands_solido::{ command_add_maintainer, command_add_validator, command_create_solido, - command_create_v2_accounts, command_deactivate_validator, - command_deactivate_validator_if_commission_exceeds_max, command_deposit, - command_migrate_state_to_v2, command_remove_maintainer, command_set_max_commission_percentage, - command_show_solido, command_show_solido_authorities, command_withdraw, + command_create_v2_accounts, command_deactivate_if_violates, command_deactivate_validator, + command_deposit, command_migrate_state_to_v2, command_remove_maintainer, + command_set_max_commission_percentage, command_show_solido, command_show_solido_authorities, + command_withdraw, }; use crate::config::*; @@ -164,8 +164,10 @@ REWARDS DeactivateValidator(DeactivateValidatorOpts), /// Deactivates a validator and initiates the removal process if - /// validator exceeds maximum validation commission. Requires no permission. - DeactivateValidatorIfCommissionExceedsMax(DeactivateValidatorIfCommissionExceedsMaxOpts), + /// validator exceeds maximum validation commission or any other performance metric + /// as per the thresholds. + /// Requires no permission. + DeactivateIfViolates(DeactivateIfViolatesOpts), /// Adds a maintainer to the Solido instance. AddMaintainer(AddRemoveMaintainerOpts), @@ -314,10 +316,9 @@ fn main() { let output = result.ok_or_abort_with("Failed to deactivate validator."); print_output(output_mode, &output); } - SubCommand::DeactivateValidatorIfCommissionExceedsMax(cmd_opts) => { - let result = config.with_snapshot(|config| { - command_deactivate_validator_if_commission_exceeds_max(config, &cmd_opts) - }); + SubCommand::DeactivateIfViolates(cmd_opts) => { + let result = + config.with_snapshot(|config| command_deactivate_if_violates(config, &cmd_opts)); let output = result.ok_or_abort_with("Failed to check max commission violation."); print_output(output_mode, &output); } @@ -384,7 +385,7 @@ fn merge_with_config_and_environment( SubCommand::DeactivateValidator(opts) => { opts.merge_with_config_and_environment(config_file) } - SubCommand::DeactivateValidatorIfCommissionExceedsMax(opts) => { + SubCommand::DeactivateIfViolates(opts) => { opts.merge_with_config_and_environment(config_file) } SubCommand::AddMaintainer(opts) | SubCommand::RemoveMaintainer(opts) => { diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index b1bbfbbb1..be4838153 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -750,7 +750,7 @@ impl SolidoState { /// If there is a validator which exceeded commission limit or it's vote account is closed, /// try to deactivate it. - pub fn try_deactivate_validator_if_commission_exceeds_max( + pub fn try_deactivate_if_violates( &self, ) -> Option { for (validator_index, (validator, vote_state)) in self @@ -777,7 +777,7 @@ impl SolidoState { validator_vote_account: *validator.pubkey(), }; - let instruction = lido::instruction::deactivate_validator_if_commission_exceeds_max( + let instruction = lido::instruction::deactivate_if_violates( &self.solido_program_id, &lido::instruction::DeactivateIfViolatesMeta { lido: self.solido_address, @@ -1546,7 +1546,7 @@ pub fn try_perform_maintenance( // because it may be rejected if the exchange rate is outdated. // Same for updating the validator balance. .or_else(|| state.try_update_stake_account_balance()) - .or_else(|| state.try_deactivate_validator_if_commission_exceeds_max()) + .or_else(|| state.try_deactivate_if_violates()) .or_else(|| state.try_stake_deposit()) .or_else(|| state.try_unstake_from_active_validators()) .or_else(|| state.try_remove_validator()); diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 9618ed1b7..b4ab012d2 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -1000,7 +1000,7 @@ accounts_struct! { } } -pub fn deactivate_validator_if_commission_exceeds_max( +pub fn deactivate_if_violates( program_id: &Pubkey, accounts: &DeactivateIfViolatesMeta, validator_index: u32, diff --git a/program/src/process_management.rs b/program/src/process_management.rs index f9f846b48..9cabec141 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -216,7 +216,7 @@ pub fn process_remove_maintainer( } /// Sets max validation commission for Lido. If validators exceed the threshold -/// they will be deactivated by DeactivateValidatorIfCommissionExceedsMax +/// they will be deactivated by DeactivateIfViolates pub fn process_set_max_commission_percentage( program_id: &Pubkey, max_commission_percentage: u8, diff --git a/program/tests/tests/max_commission_percentage.rs b/program/tests/tests/max_commission_percentage.rs index fddd3d1fd..ad5de45d9 100644 --- a/program/tests/tests/max_commission_percentage.rs +++ b/program/tests/tests/max_commission_percentage.rs @@ -22,7 +22,7 @@ async fn test_set_max_commission_percentage() { context.max_commission_percentage + 1 ); - let result = context.try_deactivate_validator_if_commission_exceeds_max(*validator.pubkey()); + let result = context.try_deactivate_if_violates(*validator.pubkey()); assert_eq!(result.await.is_ok(), true); // check validator is not deactivated @@ -39,7 +39,7 @@ async fn test_set_max_commission_percentage() { let result = context.try_set_max_commission_percentage(context.max_commission_percentage - 1); assert_eq!(result.await.is_ok(), true); - let result = context.try_deactivate_validator_if_commission_exceeds_max(*validator.pubkey()); + let result = context.try_deactivate_if_violates(*validator.pubkey()); assert_eq!(result.await.is_ok(), true); // check validator is deactivated @@ -67,7 +67,7 @@ async fn test_close_vote_account() { let result = context.try_close_vote_account(&vote_account, &withdraw_authority); assert_eq!(result.await.is_ok(), true); - let result = context.try_deactivate_validator_if_commission_exceeds_max(*validator.pubkey()); + let result = context.try_deactivate_if_violates(*validator.pubkey()); assert_eq!(result.await.is_ok(), true); let validator = &context.get_solido().await.validators.entries[0]; diff --git a/scripts/test_solido.py b/scripts/test_solido.py index 0bfa1da0c..34af3b26a 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -737,7 +737,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida result = perform_maintenance() # check validator_1 is deactivated expected_result = { - 'DeactivateValidatorIfCommissionExceedsMax': { + 'DeactivateIfViolates': { 'validator_vote_account': validator_1.vote_account.pubkey } } @@ -799,7 +799,7 @@ def set_max_validation_commission(fee: int) -> Any: maintainance_result = perform_maintenance() expected_result = { - 'DeactivateValidatorIfCommissionExceedsMax': { + 'DeactivateIfViolates': { 'validator_vote_account': validator_2.vote_account.pubkey } } diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index a05deb5a3..c976ac631 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1376,7 +1376,7 @@ impl Context { .await } - pub async fn try_deactivate_validator_if_commission_exceeds_max( + pub async fn try_deactivate_if_violates( &mut self, vote_account: Pubkey, ) -> transport::Result<()> { @@ -1385,7 +1385,7 @@ impl Context { send_transaction( &mut self.context, &[ - lido::instruction::deactivate_validator_if_commission_exceeds_max( + lido::instruction::deactivate_if_violates( &id(), &lido::instruction::DeactivateIfViolatesMeta { lido: self.solido.pubkey(), From 7cf32bb54f37bca0b8fedc88813e117c33375298 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 18 May 2023 08:57:12 +0300 Subject: [PATCH 016/131] fmt --- cli/maintainer/src/daemon.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index 53a39711f..191f1d7af 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -109,10 +109,7 @@ impl MaintenanceMetrics { Metric::new(self.transactions_unstake_from_active_validator) .with_label("operation", "UnstakeFromActiveValidator".to_string()), Metric::new(self.transactions_deactivate_if_violates) - .with_label( - "operation", - "DeactivateIfViolates".to_string(), - ), + .with_label("operation", "DeactivateIfViolates".to_string()), ], }, )?; From 43235627f185a1ba0af75cb6b87ddc4c38dd1c69 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 18 May 2023 09:07:56 +0300 Subject: [PATCH 017/131] add a separate data account for the perflist --- program/src/instruction.rs | 4 ++++ testlib/src/solido_context.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index b4ab012d2..d0159f89c 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -262,6 +262,10 @@ accounts_struct! { is_signer: false, is_writable: true, }, + pub validator_perf_list { + is_signer: false, + is_writable: true, + }, pub maintainer_list { is_signer: false, is_writable: true, diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index c976ac631..ba2db9f85 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -85,6 +85,7 @@ pub struct Context { pub maintainer: Option, pub validator: Option, pub validator_list: Keypair, + pub validator_perf_list: Keypair, pub maintainer_list: Keypair, pub treasury_st_sol_account: Pubkey, @@ -195,6 +196,7 @@ impl Context { let manager = deterministic_keypair.new_keypair(); let solido = deterministic_keypair.new_keypair(); let validator_list = deterministic_keypair.new_keypair(); + let validator_perf_list = deterministic_keypair.new_keypair(); let maintainer_list = deterministic_keypair.new_keypair(); let reward_distribution = RewardDistribution { @@ -231,6 +233,7 @@ impl Context { manager, solido, validator_list, + validator_perf_list, maintainer_list, st_sol_mint: Pubkey::default(), maintainer: None, @@ -311,6 +314,7 @@ impl Context { developer_account: result.developer_st_sol_account, reserve_account: result.reserve_address, validator_list: result.validator_list.pubkey(), + validator_perf_list: result.validator_perf_list.pubkey(), maintainer_list: result.maintainer_list.pubkey(), }, ), From ffdf886b50002faf623806dba4eac78ffded7c0c Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 18 May 2023 22:27:42 +0300 Subject: [PATCH 018/131] rework max-commission through thresholds --- program/src/instruction.rs | 10 +++---- program/src/processor.rs | 25 +++++++--------- .../tests/tests/max_commission_percentage.rs | 6 ++-- testlib/src/solido_context.rs | 30 +++++++++---------- 4 files changed, 32 insertions(+), 39 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index d0159f89c..d73a0a037 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -17,7 +17,7 @@ use solana_program::{ use crate::{ accounts_struct, accounts_struct_meta, error::LidoError, - state::RewardDistribution, + state::{RewardDistribution, Thresholds}, token::{Lamports, StLamports}, }; @@ -28,11 +28,11 @@ pub enum LidoInstruction { #[allow(dead_code)] // but it's not reward_distribution: RewardDistribution, #[allow(dead_code)] // but it's not + thresholds: Thresholds, + #[allow(dead_code)] // but it's not max_validators: u32, #[allow(dead_code)] // but it's not max_maintainers: u32, - #[allow(dead_code)] // but it's not - max_commission_percentage: u8, }, /// Deposit a given amount of SOL. @@ -279,16 +279,16 @@ accounts_struct! { pub fn initialize( program_id: &Pubkey, reward_distribution: RewardDistribution, + thresholds: Thresholds, max_validators: u32, max_maintainers: u32, - max_commission_percentage: u8, accounts: &InitializeAccountsMeta, ) -> Instruction { let data = LidoInstruction::Initialize { reward_distribution, + thresholds, max_validators, max_maintainers, - max_commission_percentage, }; Instruction { program_id: *program_id, diff --git a/program/src/processor.rs b/program/src/processor.rs index c3ce6ca58..14e09d0df 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -9,8 +9,8 @@ use crate::{ error::LidoError, instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, - StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, - UpdateExchangeRateAccountsInfoV2, UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, + StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, + UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, }, logic::{ burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, @@ -21,9 +21,8 @@ use crate::{ metrics::Metrics, process_management::{ process_add_maintainer, process_add_validator, process_change_reward_distribution, - process_deactivate_validator, process_deactivate_if_violates, - process_merge_stake, process_remove_maintainer, process_remove_validator, - process_set_max_commission_percentage, + process_deactivate_if_violates, process_deactivate_validator, process_merge_stake, + process_remove_maintainer, process_remove_validator, process_set_max_commission_percentage, }, stake_account::{deserialize_stake_account, StakeAccount}, state::{ @@ -58,9 +57,9 @@ use { pub fn process_initialize( program_id: &Pubkey, reward_distribution: RewardDistribution, + thresholds: Thresholds, max_validators: u32, max_maintainers: u32, - max_commission_percentage: u8, accounts_raw: &[AccountInfo], ) -> ProgramResult { let accounts = InitializeAccountsInfo::try_from_slice(accounts_raw)?; @@ -111,7 +110,7 @@ pub fn process_initialize( ); if &reserve_account_pda != accounts.reserve_account.key { msg!( - "Resrve account {} is incorrect, should be {}", + "Reserve account {} is incorrect, should be {}", accounts.reserve_account.key, reserve_account_pda ); @@ -128,7 +127,7 @@ pub fn process_initialize( // Check if the token has no minted tokens and right mint authority. check_mint(rent, accounts.st_sol_mint, &mint_authority)?; - if max_commission_percentage > 100 { + if thresholds.max_commission > 100 { return Err(LidoError::ValidationCommissionOutOfBounds.into()); } @@ -1147,15 +1146,15 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P match instruction { LidoInstruction::Initialize { reward_distribution, + thresholds, max_validators, max_maintainers, - max_commission_percentage, } => process_initialize( program_id, reward_distribution, + thresholds, max_validators, max_maintainers, - max_commission_percentage, accounts, ), LidoInstruction::Deposit { amount } => process_deposit(program_id, amount, accounts), @@ -1216,11 +1215,7 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P process_merge_stake(program_id, validator_index, accounts) } LidoInstruction::DeactivateIfViolates { validator_index } => { - process_deactivate_if_violates( - program_id, - validator_index, - accounts, - ) + process_deactivate_if_violates(program_id, validator_index, accounts) } LidoInstruction::SetMaxValidationCommission { max_commission_percentage, diff --git a/program/tests/tests/max_commission_percentage.rs b/program/tests/tests/max_commission_percentage.rs index ad5de45d9..8540fd0f9 100644 --- a/program/tests/tests/max_commission_percentage.rs +++ b/program/tests/tests/max_commission_percentage.rs @@ -13,13 +13,13 @@ async fn test_set_max_commission_percentage() { let validator = &context.get_solido().await.validators.entries[0]; // increase max_commission_percentage - let result = context.try_set_max_commission_percentage(context.max_commission_percentage + 1); + let result = context.try_set_max_commission_percentage(context.thresholds.max_commission + 1); assert_eq!(result.await.is_ok(), true); let solido = context.get_solido().await.lido; assert_eq!( solido.thresholds.max_commission, - context.max_commission_percentage + 1 + context.thresholds.max_commission + 1 ); let result = context.try_deactivate_if_violates(*validator.pubkey()); @@ -36,7 +36,7 @@ async fn test_set_max_commission_percentage() { ); // decrease max_commission_percentage - let result = context.try_set_max_commission_percentage(context.max_commission_percentage - 1); + let result = context.try_set_max_commission_percentage(context.thresholds.max_commission - 1); assert_eq!(result.await.is_ok(), true); let result = context.try_deactivate_if_violates(*validator.pubkey()); diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index ba2db9f85..2b2f3eaa1 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -34,7 +34,7 @@ use lido::{error::LidoError, instruction, RESERVE_ACCOUNT, STAKE_AUTHORITY}; use lido::{ state::{ AccountList, FeeRecipients, Lido, ListEntry, Maintainer, RewardDistribution, StakeDeposit, - Validator, + Thresholds, Validator, }, MINT_AUTHORITY, }; @@ -96,7 +96,7 @@ pub struct Context { pub stake_authority: Pubkey, pub mint_authority: Pubkey, - pub max_commission_percentage: u8, + pub thresholds: Thresholds, } pub struct ValidatorAccounts { @@ -245,7 +245,7 @@ impl Context { stake_authority, mint_authority, deterministic_keypair, - max_commission_percentage: 5, + thresholds: Thresholds::new(5, 0, 0), }; result.st_sol_mint = result.create_mint(result.mint_authority).await; @@ -303,9 +303,9 @@ impl Context { instruction::initialize( &id(), result.reward_distribution.clone(), + result.thresholds.clone(), max_validators, max_maintainers, - result.max_commission_percentage, &instruction::InitializeAccountsMeta { lido: result.solido.pubkey(), manager: result.manager.pubkey(), @@ -730,7 +730,7 @@ impl Context { .create_vote_account( &node_account, withdraw_authority.pubkey(), - self.max_commission_percentage, + self.thresholds.max_commission, ) .await; @@ -1388,17 +1388,15 @@ impl Context { let validator_index = solido.validators.position(&vote_account).unwrap(); send_transaction( &mut self.context, - &[ - lido::instruction::deactivate_if_violates( - &id(), - &lido::instruction::DeactivateIfViolatesMeta { - lido: self.solido.pubkey(), - validator_vote_account_to_deactivate: vote_account, - validator_list: self.validator_list.pubkey(), - }, - validator_index, - ), - ], + &[lido::instruction::deactivate_if_violates( + &id(), + &lido::instruction::DeactivateIfViolatesMeta { + lido: self.solido.pubkey(), + validator_vote_account_to_deactivate: vote_account, + validator_list: self.validator_list.pubkey(), + }, + validator_index, + )], vec![], ) .await From 911b4cb2f1693f667e95964c616ea65c162222c4 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 18 May 2023 22:30:01 +0300 Subject: [PATCH 019/131] new data account for perf --- program/src/processor.rs | 1 + testlib/src/solido_context.rs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/program/src/processor.rs b/program/src/processor.rs index 14e09d0df..ba251ce8c 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -67,6 +67,7 @@ pub fn process_initialize( check_rent_exempt(rent, accounts.lido, "Solido account")?; check_rent_exempt(rent, accounts.reserve_account, "Reserve account")?; check_rent_exempt(rent, accounts.validator_list, "Validator list account")?; + check_rent_exempt(rent, accounts.validator_perf_list, "Perf list account")?; check_rent_exempt(rent, accounts.maintainer_list, "Maintainer list account")?; check_account_owner(accounts.lido, program_id)?; diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 2b2f3eaa1..6adc65c80 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -266,6 +266,8 @@ impl Context { let rent_reserve = rent.minimum_balance(0); let validator_list_size = AccountList::::required_bytes(max_validators); + let validator_perf_list_size = AccountList::::required_bytes(max_validators); + let rent_validator_perf_list = rent.minimum_balance(validator_perf_list_size); let rent_validator_list = rent.minimum_balance(validator_list_size); let maintainer_list_size = AccountList::::required_bytes(max_maintainers); @@ -293,6 +295,13 @@ impl Context { validator_list_size as u64, &id(), ), + system_instruction::create_account( + &payer, + &result.validator_perf_list.pubkey(), + rent_validator_perf_list, + validator_perf_list_size as u64, + &id(), + ), system_instruction::create_account( &payer, &result.maintainer_list.pubkey(), @@ -322,6 +331,7 @@ impl Context { vec![ &result.solido, &result.validator_list, + &result.validator_perf_list, &result.maintainer_list, ], ) From 8b2e3e524eea93335502d0eab5d290c5731b9a92 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 18 May 2023 22:53:04 +0300 Subject: [PATCH 020/131] separate structure for `ValidatorPerf` --- program/src/processor.rs | 7 ++++ program/src/state.rs | 62 +++++++++++++++++++++++++++++++++++ testlib/src/solido_context.rs | 4 +-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index ba251ce8c..67c139aaf 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -28,6 +28,7 @@ use crate::{ state::{ AccountType, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, MaintainerList, RewardDistribution, StakeDeposit, Thresholds, Validator, ValidatorList, + ValidatorPerfList, }, token::{Lamports, Rational, StLamports}, MAXIMUM_UNSTAKE_ACCOUNTS, MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, @@ -72,6 +73,7 @@ pub fn process_initialize( check_account_owner(accounts.lido, program_id)?; check_account_owner(accounts.validator_list, program_id)?; + check_account_owner(accounts.validator_perf_list, program_id)?; check_account_owner(accounts.maintainer_list, program_id)?; check_account_data(accounts.lido, Lido::LEN, AccountType::Lido)?; @@ -80,6 +82,11 @@ pub fn process_initialize( ValidatorList::required_bytes(max_validators), AccountType::Validator, )?; + check_account_data( + accounts.validator_perf_list, + ValidatorPerfList::required_bytes(max_validators), + AccountType::Validator, + )?; check_account_data( accounts.maintainer_list, MaintainerList::required_bytes(max_maintainers), diff --git a/program/src/state.rs b/program/src/state.rs index b2e58c87e..f7f081444 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -48,6 +48,7 @@ pub enum AccountType { Uninitialized, Lido, Validator, + ValidatorPerf, Maintainer, } @@ -74,6 +75,7 @@ pub struct AccountList { } pub type ValidatorList = AccountList; +pub type ValidatorPerfList = AccountList; pub type MaintainerList = AccountList; /// Helper type to deserialize just the start of AccountList @@ -349,6 +351,26 @@ pub struct Validator { pub active: bool, } +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] +pub struct ValidatorPerf { + /// The associated validator's vote account address. + /// It might not be present in the validator list. + /// Do not reorder this field, it should be first in the struct + #[serde(serialize_with = "serialize_b58")] + #[serde(rename = "pubkey")] + pub validator_vote_account_address: Pubkey, + + /// The number of slots the validator has produced in the last epoch. + pub block_production_rate: u64, +} + /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS /// THERE'S AN EXTREMELY GOOD REASON. /// @@ -525,6 +547,46 @@ impl ListEntry for Validator { } } +impl ValidatorPerf {} + +impl Sealed for ValidatorPerf {} + +impl Pack for ValidatorPerf { + const LEN: usize = 8; + fn pack_into_slice(&self, data: &mut [u8]) { + let mut data = data; + BorshSerialize::serialize(&self, &mut data).unwrap(); + } + fn unpack_from_slice(src: &[u8]) -> Result { + let unpacked = Self::try_from_slice(src)?; + Ok(unpacked) + } +} + +impl Default for ValidatorPerf { + fn default() -> Self { + ValidatorPerf { + validator_vote_account_address: Pubkey::default(), + block_production_rate: 0, + } + } +} + +impl ListEntry for ValidatorPerf { + const TYPE: AccountType = AccountType::ValidatorPerf; + + fn new(validator_vote_account_address: Pubkey) -> Self { + Self { + validator_vote_account_address, + ..Default::default() + } + } + + fn pubkey(&self) -> &Pubkey { + &self.validator_vote_account_address + } +} + impl Sealed for Maintainer {} impl Pack for Maintainer { diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 6adc65c80..ce174caa0 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -34,7 +34,7 @@ use lido::{error::LidoError, instruction, RESERVE_ACCOUNT, STAKE_AUTHORITY}; use lido::{ state::{ AccountList, FeeRecipients, Lido, ListEntry, Maintainer, RewardDistribution, StakeDeposit, - Thresholds, Validator, + Thresholds, Validator, ValidatorPerf, }, MINT_AUTHORITY, }; @@ -266,7 +266,7 @@ impl Context { let rent_reserve = rent.minimum_balance(0); let validator_list_size = AccountList::::required_bytes(max_validators); - let validator_perf_list_size = AccountList::::required_bytes(max_validators); + let validator_perf_list_size = AccountList::::required_bytes(max_validators); let rent_validator_perf_list = rent.minimum_balance(validator_perf_list_size); let rent_validator_list = rent.minimum_balance(validator_list_size); From dc0169c5a8c1dbb67d82604ae49b064eb277425b Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 19 May 2023 11:00:12 +0300 Subject: [PATCH 021/131] =?UTF-8?q?`tests/max=5Fcommission=5Fpercentage`?= =?UTF-8?q?=20=E2=86=92=20`tests/validators=5Fcuration`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- program/tests/tests/mod.rs | 2 +- .../{max_commission_percentage.rs => validators_curation.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename program/tests/tests/{max_commission_percentage.rs => validators_curation.rs} (100%) diff --git a/program/tests/tests/mod.rs b/program/tests/tests/mod.rs index de8ca8f6c..d698cd54a 100644 --- a/program/tests/tests/mod.rs +++ b/program/tests/tests/mod.rs @@ -8,7 +8,7 @@ pub mod change_reward_distribution; pub mod deposit; pub mod limits; pub mod maintainers; -pub mod max_commission_percentage; +pub mod validators_curation; pub mod merge_stake; pub mod solana_assumptions; pub mod stake_deposit; diff --git a/program/tests/tests/max_commission_percentage.rs b/program/tests/tests/validators_curation.rs similarity index 100% rename from program/tests/tests/max_commission_percentage.rs rename to program/tests/tests/validators_curation.rs From 8d9c0ef1911accddb0859d8bcf2c94fa6807d126 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 19 May 2023 11:03:25 +0300 Subject: [PATCH 022/131] =?UTF-8?q?`test=5Fset=5Fmax=5Fcommission=5Fpercen?= =?UTF-8?q?tage`=20=E2=86=92=20`test=5Fcurate=5Fby=5Fmax=5Fcommission=5Fpe?= =?UTF-8?q?rcentage`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- program/tests/tests/validators_curation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 8540fd0f9..674d71f06 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -8,7 +8,7 @@ use testlib::assert_solido_error; use testlib::solido_context::Context; #[tokio::test] -async fn test_set_max_commission_percentage() { +async fn test_curate_by_max_commission_percentage() { let mut context = Context::new_with_maintainer_and_validator().await; let validator = &context.get_solido().await.validators.entries[0]; From e5f2d3a959d2f41401cd7eca42d01c94cef1a2ba Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 19 May 2023 21:35:10 +0300 Subject: [PATCH 023/131] generalize `set_max_commission` to new `change_thresholds` --- program/src/instruction.rs | 10 ++++------ program/src/process_management.rs | 18 +++++++++--------- program/src/processor.rs | 8 ++++---- testlib/src/solido_context.rs | 17 +++++++++++++---- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index d73a0a037..13f57adc6 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -1029,14 +1029,12 @@ accounts_struct! { } } -pub fn set_max_commission_percentage( +pub fn change_thresholds( program_id: &Pubkey, - accounts: &SetMaxValidationCommissionMeta, - max_commission_percentage: u8, + accounts: &ChangeThresholdsMeta, + new_thresholds: Thresholds, ) -> Instruction { - let data = LidoInstruction::SetMaxValidationCommission { - max_commission_percentage, - }; + let data = LidoInstruction::ChangeThresholds { new_thresholds }; Instruction { program_id: *program_id, accounts: accounts.to_vec(), diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 9cabec141..77b2e59a9 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -8,14 +8,14 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, use crate::logic::check_rent_exempt; use crate::processor::StakeType; -use crate::state::Lido; +use crate::state::{Lido, Thresholds}; use crate::vote_state::PartialVoteState; use crate::{ error::LidoError, instruction::{ AddMaintainerInfoV2, AddValidatorInfoV2, ChangeRewardDistributionInfo, DeactivateIfViolatesInfo, DeactivateValidatorInfoV2, MergeStakeInfoV2, - RemoveMaintainerInfoV2, RemoveValidatorInfoV2, SetMaxValidationCommissionInfo, + RemoveMaintainerInfoV2, RemoveValidatorInfoV2, ChangeThresholdsInfo, }, state::{ListEntry, Maintainer, RewardDistribution, Validator}, vote_state::get_vote_account_commission, @@ -215,23 +215,23 @@ pub fn process_remove_maintainer( Ok(()) } -/// Sets max validation commission for Lido. If validators exceed the threshold -/// they will be deactivated by DeactivateIfViolates -pub fn process_set_max_commission_percentage( +/// Set the new curation thresholds. If validators exceed those threshold, +/// they will be deactivated by `DeactivateIfViolates`. +pub fn process_change_thresholds( program_id: &Pubkey, - max_commission_percentage: u8, + new_thresholds: Thresholds, accounts_raw: &[AccountInfo], ) -> ProgramResult { - if max_commission_percentage > 100 { + if new_thresholds.max_commission > 100 { return Err(LidoError::ValidationCommissionOutOfBounds.into()); } - let accounts = SetMaxValidationCommissionInfo::try_from_slice(accounts_raw)?; + let accounts = ChangeThresholdsInfo::try_from_slice(accounts_raw)?; let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; lido.check_manager(accounts.manager)?; - lido.thresholds.max_commission = max_commission_percentage; + lido.thresholds = new_thresholds; lido.save(accounts.lido) } diff --git a/program/src/processor.rs b/program/src/processor.rs index 67c139aaf..67e8d83e2 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -22,7 +22,7 @@ use crate::{ process_management::{ process_add_maintainer, process_add_validator, process_change_reward_distribution, process_deactivate_if_violates, process_deactivate_validator, process_merge_stake, - process_remove_maintainer, process_remove_validator, process_set_max_commission_percentage, + process_remove_maintainer, process_remove_validator, process_change_thresholds, }, stake_account::{deserialize_stake_account, StakeAccount}, state::{ @@ -1225,9 +1225,9 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::DeactivateIfViolates { validator_index } => { process_deactivate_if_violates(program_id, validator_index, accounts) } - LidoInstruction::SetMaxValidationCommission { - max_commission_percentage, - } => process_set_max_commission_percentage(program_id, max_commission_percentage, accounts), + LidoInstruction::ChangeThresholds { new_thresholds } => { + process_change_thresholds(program_id, new_thresholds, accounts) + } LidoInstruction::MigrateStateToV2 { reward_distribution, max_validators, diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index ce174caa0..a9a2323ff 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1374,16 +1374,25 @@ impl Context { VoteState::deserialize(&vote_acc.data) } - pub async fn try_set_max_commission_percentage(&mut self, fee: u8) -> transport::Result<()> { + pub async fn try_set_max_commission_percentage( + &mut self, + max_commission: u8, + ) -> transport::Result<()> { + let solido = self.get_solido().await; + let current_thresholds = solido.lido.thresholds; + send_transaction( &mut self.context, - &[lido::instruction::set_max_commission_percentage( + &[lido::instruction::change_thresholds( &id(), - &lido::instruction::SetMaxValidationCommissionMeta { + &lido::instruction::ChangeThresholdsMeta { lido: self.solido.pubkey(), manager: self.manager.pubkey(), }, - fee, + Thresholds { + max_commission, + ..current_thresholds + }, )], vec![&self.manager], ) From 50ae378e5b8c9470031e188824fabe8f9b1fef14 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 19 May 2023 22:53:03 +0300 Subject: [PATCH 024/131] propagate `set_max_commission` -> `change_thresholds` to CLI --- cli/maintainer/src/commands_multisig.rs | 40 ++++++++++-------- cli/maintainer/src/commands_solido.rs | 54 +++++++++++++++++++------ cli/maintainer/src/config.rs | 32 ++++++++++++--- cli/maintainer/src/main.rs | 20 +++++---- program/src/instruction.rs | 12 +++--- 5 files changed, 106 insertions(+), 52 deletions(-) diff --git a/cli/maintainer/src/commands_multisig.rs b/cli/maintainer/src/commands_multisig.rs index 53a9c59f1..ab70bb6f9 100644 --- a/cli/maintainer/src/commands_multisig.rs +++ b/cli/maintainer/src/commands_multisig.rs @@ -26,10 +26,10 @@ use solana_sdk::sysvar; use lido::{ instruction::{ AddMaintainerMetaV2, AddValidatorMetaV2, ChangeRewardDistributionMeta, - DeactivateValidatorMetaV2, LidoInstruction, MigrateStateToV2Meta, RemoveMaintainerMetaV2, - SetMaxValidationCommissionMeta, + ChangeThresholdsMeta, DeactivateValidatorMetaV2, LidoInstruction, MigrateStateToV2Meta, + RemoveMaintainerMetaV2, }, - state::{FeeRecipients, Lido, RewardDistribution}, + state::{FeeRecipients, Lido, RewardDistribution, Thresholds}, util::{serialize_b58, serialize_b58_slice}, }; use solido_cli_common::error::Abort; @@ -472,11 +472,11 @@ enum SolidoInstruction { fee_recipients: FeeRecipients, }, - SetMaxValidationCommission { + ChangeThresholds { #[serde(serialize_with = "serialize_b58")] solido_instance: Pubkey, - max_commission_percentage: u8, + thresholds: Thresholds, #[serde(serialize_with = "serialize_b58")] manager: Pubkey, @@ -668,18 +668,28 @@ impl fmt::Display for ShowTransactionOutput { print_changed_reward_distribution(f, current_solido, reward_distribution)?; print_changed_recipients(f, current_solido, fee_recipients)?; } - SolidoInstruction::SetMaxValidationCommission { + SolidoInstruction::ChangeThresholds { solido_instance, - max_commission_percentage, + thresholds, manager, } => { - writeln!(f, "It sets the maximum validation commission")?; + writeln!(f, "It sets the curation thresholds")?; writeln!(f, " Solido instance: {}", solido_instance)?; writeln!(f, " Manager: {}", manager)?; writeln!( f, - " Max validation commission: {}%", - max_commission_percentage + " Max commission for validators: {}%", + thresholds.max_commission, + )?; + writeln!( + f, + " Min vote success rate: {}%", + thresholds.min_vote_success_rate, + )?; + writeln!( + f, + " Min block production rate: {}%", + thresholds.min_block_production_rate, )?; } SolidoInstruction::MigrateStateToV2 { @@ -1065,13 +1075,11 @@ fn try_parse_solido_instruction( maintainer_index, }) } - LidoInstruction::SetMaxValidationCommission { - max_commission_percentage, - } => { - let accounts = SetMaxValidationCommissionMeta::try_from_slice(&instr.accounts)?; - ParsedInstruction::SolidoInstruction(SolidoInstruction::SetMaxValidationCommission { + LidoInstruction::ChangeThresholds { new_thresholds } => { + let accounts = ChangeThresholdsMeta::try_from_slice(&instr.accounts)?; + ParsedInstruction::SolidoInstruction(SolidoInstruction::ChangeThresholds { solido_instance: accounts.lido, - max_commission_percentage, + thresholds: new_thresholds, manager: accounts.manager, }) } diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 12ea4327a..9e5e849dc 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -16,7 +16,10 @@ use lido::{ find_authority_program_address, metrics::LamportsHistogram, processor::StakeType, - state::{AccountList, Lido, ListEntry, Maintainer, RewardDistribution, Validator}, + state::{ + AccountList, Lido, ListEntry, Maintainer, RewardDistribution, Thresholds, Validator, + ValidatorPerf, + }, token::{Lamports, StLamports}, util::serialize_b58, vote_state::get_vote_account_commission, @@ -36,9 +39,9 @@ use crate::{ }; use crate::{ config::{ - AddRemoveMaintainerOpts, AddValidatorOpts, CreateSolidoOpts, CreateV2AccountsOpts, - DeactivateIfViolatesOpts, DeactivateValidatorOpts, DepositOpts, MigrateStateToV2Opts, - SetMaxValidationCommissionOpts, ShowSolidoAuthoritiesOpts, ShowSolidoOpts, WithdrawOpts, + AddRemoveMaintainerOpts, AddValidatorOpts, ChangeThresholdsOpts, CreateSolidoOpts, + CreateV2AccountsOpts, DeactivateIfViolatesOpts, DeactivateValidatorOpts, DepositOpts, + MigrateStateToV2Opts, ShowSolidoAuthoritiesOpts, ShowSolidoOpts, WithdrawOpts, }, get_signer_from_path, }; @@ -135,6 +138,7 @@ pub fn command_create_solido( ) -> solido_cli_common::Result { let lido_signer = from_key_path_or_random(opts.solido_key_path())?; let validator_list_signer = from_key_path_or_random(opts.validator_list_key_path())?; + let validator_perf_list_signer = from_key_path_or_random(opts.validator_perf_list_key_path())?; let maintainer_list_signer = from_key_path_or_random(opts.maintainer_list_key_path())?; let (reserve_account, _) = lido::find_authority_program_address( @@ -162,6 +166,12 @@ pub fn command_create_solido( .client .get_minimum_balance_for_rent_exemption(validator_list_size)?; + let validator_perf_list_size = + AccountList::::required_bytes(*opts.max_validators()); + let validator_perf_list_account_balance = config + .client + .get_minimum_balance_for_rent_exemption(validator_perf_list_size)?; + let maintainer_list_size = AccountList::::required_bytes(*opts.max_maintainers()); let maintainer_list_account_balance = config .client @@ -236,6 +246,15 @@ pub fn command_create_solido( opts.solido_program_id(), )); + // Create the account that holds the validator perf list itself. + instructions.push(system_instruction::create_account( + &config.signer.pubkey(), + &validator_perf_list_signer.pubkey(), + validator_perf_list_account_balance.0, + validator_perf_list_size as u64, + opts.solido_program_id(), + )); + // Create the account that holds the maintainer list itself. instructions.push(system_instruction::create_account( &config.signer.pubkey(), @@ -252,17 +271,22 @@ pub fn command_create_solido( developer_fee: *opts.developer_fee_share(), st_sol_appreciation: *opts.st_sol_appreciation_share(), }, + Thresholds { + max_commission: *opts.max_commission(), + min_vote_success_rate: *opts.min_vote_success_rate(), + min_block_production_rate: *opts.min_block_production_rate(), + }, *opts.max_validators(), *opts.max_maintainers(), - *opts.max_commission_percentage(), &lido::instruction::InitializeAccountsMeta { lido: lido_signer.pubkey(), - st_sol_mint: st_sol_mint_pubkey, manager, + st_sol_mint: st_sol_mint_pubkey, treasury_account: treasury_keypair.pubkey(), developer_account: developer_keypair.pubkey(), reserve_account, validator_list: validator_list_signer.pubkey(), + validator_perf_list: validator_perf_list_signer.pubkey(), maintainer_list: maintainer_list_signer.pubkey(), }, )); @@ -1025,7 +1049,7 @@ impl fmt::Display for DeactivateIfViolatesOutput { } } -/// CLI entry point to punish validator for commission violation. +/// CLI entry point to curate out the validators that violate the thresholds pub fn command_deactivate_if_violates( config: &mut SnapshotConfig, opts: &DeactivateIfViolatesOpts, @@ -1077,21 +1101,25 @@ pub fn command_deactivate_if_violates( }) } -/// CLI entry point to set max validation commission -pub fn command_set_max_commission_percentage( +/// CLI entry point to change the thresholds of curating out the validators +pub fn command_change_thresholds( config: &mut SnapshotConfig, - opts: &SetMaxValidationCommissionOpts, + opts: &ChangeThresholdsOpts, ) -> solido_cli_common::Result { let (multisig_address, _) = get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); - let instruction = lido::instruction::set_max_commission_percentage( + let instruction = lido::instruction::change_thresholds( opts.solido_program_id(), - &lido::instruction::SetMaxValidationCommissionMeta { + &lido::instruction::ChangeThresholdsMeta { lido: *opts.solido_address(), manager: multisig_address, }, - *opts.max_commission_percentage(), + Thresholds { + max_commission: *opts.max_commission(), + min_vote_success_rate: *opts.min_vote_success_rate(), + min_block_production_rate: *opts.min_block_production_rate(), + }, ); propose_instruction( config, diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index a394b2701..15446d21b 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -254,7 +254,15 @@ cli_opt_struct! { /// The maximum validator fee a validator can have to be accepted by protocol. #[clap(long, value_name = "int")] - max_commission_percentage: u8, + max_commission: u8, + + /// The minimum vote success rate a validator must have to not be deactivated. + #[clap(long, value_name = "int")] + min_vote_success_rate: u8, + + /// The minimum block production rate a validator must have to not be deactivated. + #[clap(long, value_name = "int")] + min_block_production_rate: u8, // See also the docs section of `create-solido` in main.rs for a description // of the fee shares. @@ -293,12 +301,16 @@ cli_opt_struct! { #[clap(long)] validator_list_key_path: PathBuf => PathBuf::default(), + /// Optional argument for the validator performance list address, + /// if not passed, a random one will be created. + #[clap(long)] + validator_perf_list_key_path: PathBuf => PathBuf::default(), + /// Optional argument for the maintainer list address, if not passed a random one /// will be created. #[clap(long)] maintainer_list_key_path: PathBuf => PathBuf::default(), - /// Used to compute Solido's manager. Multisig instance. #[clap(long, value_name = "address")] multisig_address: Pubkey, @@ -472,7 +484,7 @@ cli_opt_struct! { } cli_opt_struct! { - SetMaxValidationCommissionOpts { + ChangeThresholdsOpts { /// Address of the Solido program. #[clap(long, value_name = "address")] solido_program_id: Pubkey, @@ -481,9 +493,17 @@ cli_opt_struct! { #[clap(long, value_name = "address")] solido_address: Pubkey, - /// Max percent of rewards a validator can receive (validation commission), in range [0, 100] - #[clap(long, value_name = "fee")] - max_commission_percentage: u8, + /// Max percent of rewards a validator can receive (validation commission), in range `[0, 100]`. + #[clap(long, value_name = "percentage")] + max_commission: u8, + + /// Min vote success rate that a validator must uphold. + #[clap(long, value_name = "rate")] + min_vote_success_rate: u8, + + /// Min block production rate that a validator must uphold. + #[clap(long, value_name = "rate")] + min_block_production_rate: u8, /// Multisig instance. #[clap(long, value_name = "address")] diff --git a/cli/maintainer/src/main.rs b/cli/maintainer/src/main.rs index abf15e1d2..fdc531965 100644 --- a/cli/maintainer/src/main.rs +++ b/cli/maintainer/src/main.rs @@ -19,10 +19,10 @@ use solido_cli_common::snapshot::{Config, OutputMode, SnapshotClient}; use crate::commands_multisig::MultisigOpts; use crate::commands_solido::{ - command_add_maintainer, command_add_validator, command_create_solido, - command_create_v2_accounts, command_deactivate_if_violates, command_deactivate_validator, - command_deposit, command_migrate_state_to_v2, command_remove_maintainer, - command_set_max_commission_percentage, command_show_solido, command_show_solido_authorities, + command_add_maintainer, command_add_validator, command_change_thresholds, + command_create_solido, command_create_v2_accounts, command_deactivate_if_violates, + command_deactivate_validator, command_deposit, command_migrate_state_to_v2, + command_remove_maintainer, command_show_solido, command_show_solido_authorities, command_withdraw, }; use crate::config::*; @@ -212,7 +212,7 @@ REWARDS /// a maintainer. /// /// Requires the manager to sign. - SetMaxValidationCommission(SetMaxValidationCommissionOpts), + ChangeThresholds(ChangeThresholdsOpts), /// Update Solido state to V2 MigrateStateToV2(MigrateStateToV2Opts), @@ -355,9 +355,9 @@ fn main() { let output = result.ok_or_abort_with("Failed to withdraw."); print_output(output_mode, &output); } - SubCommand::SetMaxValidationCommission(cmd_opts) => { - let result = config - .with_snapshot(|config| command_set_max_commission_percentage(config, &cmd_opts)); + SubCommand::ChangeThresholds(cmd_opts) => { + let result = + config.with_snapshot(|config| command_change_thresholds(config, &cmd_opts)); let output = result.ok_or_abort_with("Failed to set max validation commission."); print_output(output_mode, &output); } @@ -398,9 +398,7 @@ fn merge_with_config_and_environment( SubCommand::PerformMaintenance(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::Multisig(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::RunMaintainer(opts) => opts.merge_with_config_and_environment(config_file), - SubCommand::SetMaxValidationCommission(opts) => { - opts.merge_with_config_and_environment(config_file) - } + SubCommand::ChangeThresholds(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::MigrateStateToV2(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::CreateV2Accounts(opts) => opts.merge_with_config_and_environment(config_file), } diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 13f57adc6..5badc83e5 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -113,14 +113,14 @@ pub enum LidoInstruction { validator_index: u32, }, - /// Set max_commission_percentage to control validator's fees. - /// If validators exceed the threshold they will be deactivated by - /// DeactivateIfViolates. + /// Set the curation thresholds to control validator's desired performance. + /// If validators fall below the threshold they will be deactivated by + /// `DeactivateIfViolates`. /// /// Requires the manager to sign. - SetMaxValidationCommission { + ChangeThresholds { #[allow(dead_code)] // but it's not - max_commission_percentage: u8, // percent in [0, 100] + new_thresholds: Thresholds, }, /// Move deposits from the reserve into a stake account and delegate it to a member validator. @@ -1017,7 +1017,7 @@ pub fn deactivate_if_violates( } accounts_struct! { - SetMaxValidationCommissionMeta, SetMaxValidationCommissionInfo { + ChangeThresholdsMeta, ChangeThresholdsInfo { pub lido { is_signer: false, is_writable: true, From 5a7a284d27a3068af528e9f55d2cdbd714f20526 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 22 May 2023 11:02:03 +0300 Subject: [PATCH 025/131] =?UTF-8?q?curation=20thresholds=20=E2=86=92=20cur?= =?UTF-8?q?ation=20criteria?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/maintainer/src/commands_multisig.rs | 31 +++++++++++----------- cli/maintainer/src/commands_solido.rs | 22 +++++++-------- cli/maintainer/src/config.rs | 2 +- cli/maintainer/src/main.rs | 18 ++++++------- cli/maintainer/src/maintenance.rs | 6 ++--- program/src/instruction.rs | 22 +++++++-------- program/src/process_management.rs | 22 +++++++-------- program/src/processor.rs | 27 ++++++++++--------- program/src/state.rs | 12 ++++----- program/tests/tests/mod.rs | 2 +- program/tests/tests/validators_curation.rs | 8 +++--- testlib/src/solido_context.rs | 22 +++++++-------- 12 files changed, 95 insertions(+), 99 deletions(-) diff --git a/cli/maintainer/src/commands_multisig.rs b/cli/maintainer/src/commands_multisig.rs index ab70bb6f9..80bf3db0b 100644 --- a/cli/maintainer/src/commands_multisig.rs +++ b/cli/maintainer/src/commands_multisig.rs @@ -25,11 +25,10 @@ use solana_sdk::sysvar; use lido::{ instruction::{ - AddMaintainerMetaV2, AddValidatorMetaV2, ChangeRewardDistributionMeta, - ChangeThresholdsMeta, DeactivateValidatorMetaV2, LidoInstruction, MigrateStateToV2Meta, - RemoveMaintainerMetaV2, + AddMaintainerMetaV2, AddValidatorMetaV2, ChangeCriteriaMeta, ChangeRewardDistributionMeta, + DeactivateValidatorMetaV2, LidoInstruction, MigrateStateToV2Meta, RemoveMaintainerMetaV2, }, - state::{FeeRecipients, Lido, RewardDistribution, Thresholds}, + state::{Criteria, FeeRecipients, Lido, RewardDistribution}, util::{serialize_b58, serialize_b58_slice}, }; use solido_cli_common::error::Abort; @@ -472,11 +471,11 @@ enum SolidoInstruction { fee_recipients: FeeRecipients, }, - ChangeThresholds { + ChangeCriteria { #[serde(serialize_with = "serialize_b58")] solido_instance: Pubkey, - thresholds: Thresholds, + criteria: Criteria, #[serde(serialize_with = "serialize_b58")] manager: Pubkey, @@ -668,28 +667,28 @@ impl fmt::Display for ShowTransactionOutput { print_changed_reward_distribution(f, current_solido, reward_distribution)?; print_changed_recipients(f, current_solido, fee_recipients)?; } - SolidoInstruction::ChangeThresholds { + SolidoInstruction::ChangeCriteria { solido_instance, - thresholds, + criteria, manager, } => { - writeln!(f, "It sets the curation thresholds")?; + writeln!(f, "It sets the curation criteria")?; writeln!(f, " Solido instance: {}", solido_instance)?; writeln!(f, " Manager: {}", manager)?; writeln!( f, " Max commission for validators: {}%", - thresholds.max_commission, + criteria.max_commission, )?; writeln!( f, " Min vote success rate: {}%", - thresholds.min_vote_success_rate, + criteria.min_vote_success_rate, )?; writeln!( f, " Min block production rate: {}%", - thresholds.min_block_production_rate, + criteria.min_block_production_rate, )?; } SolidoInstruction::MigrateStateToV2 { @@ -1075,11 +1074,11 @@ fn try_parse_solido_instruction( maintainer_index, }) } - LidoInstruction::ChangeThresholds { new_thresholds } => { - let accounts = ChangeThresholdsMeta::try_from_slice(&instr.accounts)?; - ParsedInstruction::SolidoInstruction(SolidoInstruction::ChangeThresholds { + LidoInstruction::ChangeCriteria { new_criteria } => { + let accounts = ChangeCriteriaMeta::try_from_slice(&instr.accounts)?; + ParsedInstruction::SolidoInstruction(SolidoInstruction::ChangeCriteria { solido_instance: accounts.lido, - thresholds: new_thresholds, + criteria: new_criteria, manager: accounts.manager, }) } diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 9e5e849dc..033fb4310 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -17,7 +17,7 @@ use lido::{ metrics::LamportsHistogram, processor::StakeType, state::{ - AccountList, Lido, ListEntry, Maintainer, RewardDistribution, Thresholds, Validator, + AccountList, Criteria, Lido, ListEntry, Maintainer, RewardDistribution, Validator, ValidatorPerf, }, token::{Lamports, StLamports}, @@ -39,7 +39,7 @@ use crate::{ }; use crate::{ config::{ - AddRemoveMaintainerOpts, AddValidatorOpts, ChangeThresholdsOpts, CreateSolidoOpts, + AddRemoveMaintainerOpts, AddValidatorOpts, ChangeCriteriaOpts, CreateSolidoOpts, CreateV2AccountsOpts, DeactivateIfViolatesOpts, DeactivateValidatorOpts, DepositOpts, MigrateStateToV2Opts, ShowSolidoAuthoritiesOpts, ShowSolidoOpts, WithdrawOpts, }, @@ -271,7 +271,7 @@ pub fn command_create_solido( developer_fee: *opts.developer_fee_share(), st_sol_appreciation: *opts.st_sol_appreciation_share(), }, - Thresholds { + Criteria { max_commission: *opts.max_commission(), min_vote_success_rate: *opts.min_vote_success_rate(), min_block_production_rate: *opts.min_block_production_rate(), @@ -542,7 +542,7 @@ impl fmt::Display for ShowSolidoOutput { writeln!( f, "Max validation commission: {}%", - self.solido.thresholds.max_commission, + self.solido.criteria.max_commission, )?; writeln!(f, "\nMetrics:")?; @@ -1069,7 +1069,7 @@ pub fn command_deactivate_if_violates( .ok() .ok_or_else(|| CliError::new("Validator account data too small"))?; - if !validator.active || commission <= solido.thresholds.max_commission { + if !validator.active || commission <= solido.criteria.max_commission { continue; } @@ -1097,25 +1097,25 @@ pub fn command_deactivate_if_violates( Ok(DeactivateIfViolatesOutput { entries: violations, - max_commission_percentage: solido.thresholds.max_commission, + max_commission_percentage: solido.criteria.max_commission, }) } /// CLI entry point to change the thresholds of curating out the validators -pub fn command_change_thresholds( +pub fn command_change_criteria( config: &mut SnapshotConfig, - opts: &ChangeThresholdsOpts, + opts: &ChangeCriteriaOpts, ) -> solido_cli_common::Result { let (multisig_address, _) = get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); - let instruction = lido::instruction::change_thresholds( + let instruction = lido::instruction::change_criteria( opts.solido_program_id(), - &lido::instruction::ChangeThresholdsMeta { + &lido::instruction::ChangeCriteriaMeta { lido: *opts.solido_address(), manager: multisig_address, }, - Thresholds { + Criteria { max_commission: *opts.max_commission(), min_vote_success_rate: *opts.min_vote_success_rate(), min_block_production_rate: *opts.min_block_production_rate(), diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index 15446d21b..8b1518977 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -484,7 +484,7 @@ cli_opt_struct! { } cli_opt_struct! { - ChangeThresholdsOpts { + ChangeCriteriaOpts { /// Address of the Solido program. #[clap(long, value_name = "address")] solido_program_id: Pubkey, diff --git a/cli/maintainer/src/main.rs b/cli/maintainer/src/main.rs index fdc531965..74b4e273b 100644 --- a/cli/maintainer/src/main.rs +++ b/cli/maintainer/src/main.rs @@ -19,11 +19,10 @@ use solido_cli_common::snapshot::{Config, OutputMode, SnapshotClient}; use crate::commands_multisig::MultisigOpts; use crate::commands_solido::{ - command_add_maintainer, command_add_validator, command_change_thresholds, - command_create_solido, command_create_v2_accounts, command_deactivate_if_violates, - command_deactivate_validator, command_deposit, command_migrate_state_to_v2, - command_remove_maintainer, command_show_solido, command_show_solido_authorities, - command_withdraw, + command_add_maintainer, command_add_validator, command_change_criteria, command_create_solido, + command_create_v2_accounts, command_deactivate_if_violates, command_deactivate_validator, + command_deposit, command_migrate_state_to_v2, command_remove_maintainer, command_show_solido, + command_show_solido_authorities, command_withdraw, }; use crate::config::*; @@ -212,7 +211,7 @@ REWARDS /// a maintainer. /// /// Requires the manager to sign. - ChangeThresholds(ChangeThresholdsOpts), + ChangeCriteria(ChangeCriteriaOpts), /// Update Solido state to V2 MigrateStateToV2(MigrateStateToV2Opts), @@ -355,9 +354,8 @@ fn main() { let output = result.ok_or_abort_with("Failed to withdraw."); print_output(output_mode, &output); } - SubCommand::ChangeThresholds(cmd_opts) => { - let result = - config.with_snapshot(|config| command_change_thresholds(config, &cmd_opts)); + SubCommand::ChangeCriteria(cmd_opts) => { + let result = config.with_snapshot(|config| command_change_criteria(config, &cmd_opts)); let output = result.ok_or_abort_with("Failed to set max validation commission."); print_output(output_mode, &output); } @@ -398,7 +396,7 @@ fn merge_with_config_and_environment( SubCommand::PerformMaintenance(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::Multisig(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::RunMaintainer(opts) => opts.merge_with_config_and_environment(config_file), - SubCommand::ChangeThresholds(opts) => opts.merge_with_config_and_environment(config_file), + SubCommand::ChangeCriteria(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::MigrateStateToV2(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::CreateV2Accounts(opts) => opts.merge_with_config_and_environment(config_file), } diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index be4838153..b6a863714 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -750,9 +750,7 @@ impl SolidoState { /// If there is a validator which exceeded commission limit or it's vote account is closed, /// try to deactivate it. - pub fn try_deactivate_if_violates( - &self, - ) -> Option { + pub fn try_deactivate_if_violates(&self) -> Option { for (validator_index, (validator, vote_state)) in self .validators .entries @@ -766,7 +764,7 @@ impl SolidoState { // We are only interested in validators that violate commission limit if let Some(state) = vote_state { - if state.commission <= self.solido.thresholds.max_commission { + if state.commission <= self.solido.criteria.max_commission { continue; } } else { diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 5badc83e5..1c5bb4409 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -17,7 +17,7 @@ use solana_program::{ use crate::{ accounts_struct, accounts_struct_meta, error::LidoError, - state::{RewardDistribution, Thresholds}, + state::{Criteria, RewardDistribution}, token::{Lamports, StLamports}, }; @@ -28,7 +28,7 @@ pub enum LidoInstruction { #[allow(dead_code)] // but it's not reward_distribution: RewardDistribution, #[allow(dead_code)] // but it's not - thresholds: Thresholds, + criteria: Criteria, #[allow(dead_code)] // but it's not max_validators: u32, #[allow(dead_code)] // but it's not @@ -118,9 +118,9 @@ pub enum LidoInstruction { /// `DeactivateIfViolates`. /// /// Requires the manager to sign. - ChangeThresholds { + ChangeCriteria { #[allow(dead_code)] // but it's not - new_thresholds: Thresholds, + new_criteria: Criteria, }, /// Move deposits from the reserve into a stake account and delegate it to a member validator. @@ -279,14 +279,14 @@ accounts_struct! { pub fn initialize( program_id: &Pubkey, reward_distribution: RewardDistribution, - thresholds: Thresholds, + criteria: Criteria, max_validators: u32, max_maintainers: u32, accounts: &InitializeAccountsMeta, ) -> Instruction { let data = LidoInstruction::Initialize { reward_distribution, - thresholds, + criteria, max_validators, max_maintainers, }; @@ -1017,7 +1017,7 @@ pub fn deactivate_if_violates( } accounts_struct! { - ChangeThresholdsMeta, ChangeThresholdsInfo { + ChangeCriteriaMeta, ChangeCriteriaInfo { pub lido { is_signer: false, is_writable: true, @@ -1029,12 +1029,12 @@ accounts_struct! { } } -pub fn change_thresholds( +pub fn change_criteria( program_id: &Pubkey, - accounts: &ChangeThresholdsMeta, - new_thresholds: Thresholds, + accounts: &ChangeCriteriaMeta, + new_criteria: Criteria, ) -> Instruction { - let data = LidoInstruction::ChangeThresholds { new_thresholds }; + let data = LidoInstruction::ChangeCriteria { new_criteria }; Instruction { program_id: *program_id, accounts: accounts.to_vec(), diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 77b2e59a9..04050f27d 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -8,14 +8,14 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, use crate::logic::check_rent_exempt; use crate::processor::StakeType; -use crate::state::{Lido, Thresholds}; +use crate::state::{Criteria, Lido}; use crate::vote_state::PartialVoteState; use crate::{ error::LidoError, instruction::{ - AddMaintainerInfoV2, AddValidatorInfoV2, ChangeRewardDistributionInfo, + AddMaintainerInfoV2, AddValidatorInfoV2, ChangeCriteriaInfo, ChangeRewardDistributionInfo, DeactivateIfViolatesInfo, DeactivateValidatorInfoV2, MergeStakeInfoV2, - RemoveMaintainerInfoV2, RemoveValidatorInfoV2, ChangeThresholdsInfo, + RemoveMaintainerInfoV2, RemoveValidatorInfoV2, }, state::{ListEntry, Maintainer, RewardDistribution, Validator}, vote_state::get_vote_account_commission, @@ -57,7 +57,7 @@ pub fn process_add_validator(program_id: &Pubkey, accounts_raw: &[AccountInfo]) // satisfy the commission limit. let _partial_vote_state = PartialVoteState::deserialize( accounts.validator_vote_account, - lido.thresholds.max_commission, + lido.criteria.max_commission, )?; let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); @@ -165,7 +165,7 @@ pub fn process_deactivate_if_violates( let data = accounts.validator_vote_account_to_deactivate.data.borrow(); let commission = get_vote_account_commission(&data)?; - if commission <= lido.thresholds.max_commission { + if commission <= lido.criteria.max_commission { return Ok(()); } } else { @@ -215,23 +215,23 @@ pub fn process_remove_maintainer( Ok(()) } -/// Set the new curation thresholds. If validators exceed those threshold, +/// Set the new curation criteria. If validators exceed those thresholds, /// they will be deactivated by `DeactivateIfViolates`. -pub fn process_change_thresholds( +pub fn process_change_criteria( program_id: &Pubkey, - new_thresholds: Thresholds, + new_criteria: Criteria, accounts_raw: &[AccountInfo], ) -> ProgramResult { - if new_thresholds.max_commission > 100 { + if new_criteria.max_commission > 100 { return Err(LidoError::ValidationCommissionOutOfBounds.into()); } - let accounts = ChangeThresholdsInfo::try_from_slice(accounts_raw)?; + let accounts = ChangeCriteriaInfo::try_from_slice(accounts_raw)?; let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; lido.check_manager(accounts.manager)?; - lido.thresholds = new_thresholds; + lido.criteria = new_criteria; lido.save(accounts.lido) } diff --git a/program/src/processor.rs b/program/src/processor.rs index 67e8d83e2..479b565e6 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -20,14 +20,15 @@ use crate::{ }, metrics::Metrics, process_management::{ - process_add_maintainer, process_add_validator, process_change_reward_distribution, - process_deactivate_if_violates, process_deactivate_validator, process_merge_stake, - process_remove_maintainer, process_remove_validator, process_change_thresholds, + process_add_maintainer, process_add_validator, process_change_criteria, + process_change_reward_distribution, process_deactivate_if_violates, + process_deactivate_validator, process_merge_stake, process_remove_maintainer, + process_remove_validator, }, stake_account::{deserialize_stake_account, StakeAccount}, state::{ - AccountType, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, - MaintainerList, RewardDistribution, StakeDeposit, Thresholds, Validator, ValidatorList, + AccountType, Criteria, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, + MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, ValidatorPerfList, }, token::{Lamports, Rational, StLamports}, @@ -58,7 +59,7 @@ use { pub fn process_initialize( program_id: &Pubkey, reward_distribution: RewardDistribution, - thresholds: Thresholds, + criteria: Criteria, max_validators: u32, max_maintainers: u32, accounts_raw: &[AccountInfo], @@ -135,7 +136,7 @@ pub fn process_initialize( // Check if the token has no minted tokens and right mint authority. check_mint(rent, accounts.st_sol_mint, &mint_authority)?; - if thresholds.max_commission > 100 { + if criteria.max_commission > 100 { return Err(LidoError::ValidationCommissionOutOfBounds.into()); } @@ -155,7 +156,7 @@ pub fn process_initialize( developer_account: *accounts.developer_account.key, }, metrics: Metrics::new(), - thresholds: Thresholds::default(), + criteria: Criteria::default(), validator_list: *accounts.validator_list.key, maintainer_list: *accounts.maintainer_list.key, }; @@ -1137,7 +1138,7 @@ pub fn processor_migrate_to_v2( mint_authority_bump_seed: lido_v1.mint_authority_bump_seed, stake_authority_bump_seed: lido_v1.stake_authority_bump_seed, metrics: lido_v1.metrics, - thresholds: Thresholds::default(), + criteria: Criteria::default(), }; // Confirm that the fee recipients are actually stSOL accounts. @@ -1154,13 +1155,13 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P match instruction { LidoInstruction::Initialize { reward_distribution, - thresholds, + criteria, max_validators, max_maintainers, } => process_initialize( program_id, reward_distribution, - thresholds, + criteria, max_validators, max_maintainers, accounts, @@ -1225,8 +1226,8 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::DeactivateIfViolates { validator_index } => { process_deactivate_if_violates(program_id, validator_index, accounts) } - LidoInstruction::ChangeThresholds { new_thresholds } => { - process_change_thresholds(program_id, new_thresholds, accounts) + LidoInstruction::ChangeCriteria { new_criteria } => { + process_change_criteria(program_id, new_criteria, accounts) } LidoInstruction::MigrateStateToV2 { reward_distribution, diff --git a/program/src/state.rs b/program/src/state.rs index f7f081444..e521dae66 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -742,7 +742,7 @@ impl ExchangeRate { /// #[repr(C)] #[derive(Clone, Debug, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize)] -pub struct Thresholds { +pub struct Criteria { /// If a validator has the commission higher than this, then it gets deactivated. pub max_commission: u8, @@ -753,7 +753,7 @@ pub struct Thresholds { pub min_block_production_rate: u8, } -impl Default for Thresholds { +impl Default for Criteria { fn default() -> Self { Self { max_commission: 100, @@ -763,7 +763,7 @@ impl Default for Thresholds { } } -impl Thresholds { +impl Criteria { pub fn new( max_commission: u8, min_vote_success_rate: u8, @@ -818,8 +818,8 @@ pub struct Lido { pub metrics: Metrics, /// Metrics of validator's performance such that if a validator's metrics - /// do not meet the threshold, then the validator gets deactivated. - pub thresholds: Thresholds, + /// do not meet that criteria, then the validator gets deactivated. + pub criteria: Criteria, /// Validator list account #[serde(serialize_with = "serialize_b58")] @@ -1596,7 +1596,7 @@ mod test_lido { developer_account: Pubkey::new_unique(), }, metrics: Metrics::new(), - thresholds: Thresholds::new(5, 0, 0), + criteria: Criteria::new(5, 0, 0), validator_list: Pubkey::new_unique(), maintainer_list: Pubkey::new_unique(), }; diff --git a/program/tests/tests/mod.rs b/program/tests/tests/mod.rs index d698cd54a..e39c6b4e6 100644 --- a/program/tests/tests/mod.rs +++ b/program/tests/tests/mod.rs @@ -8,11 +8,11 @@ pub mod change_reward_distribution; pub mod deposit; pub mod limits; pub mod maintainers; -pub mod validators_curation; pub mod merge_stake; pub mod solana_assumptions; pub mod stake_deposit; pub mod unstake; pub mod update_exchange_rate; pub mod update_stake_account_balance; +pub mod validators_curation; pub mod withdrawals; diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 674d71f06..3e23d685c 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -13,13 +13,13 @@ async fn test_curate_by_max_commission_percentage() { let validator = &context.get_solido().await.validators.entries[0]; // increase max_commission_percentage - let result = context.try_set_max_commission_percentage(context.thresholds.max_commission + 1); + let result = context.try_set_max_commission_percentage(context.criteria.max_commission + 1); assert_eq!(result.await.is_ok(), true); let solido = context.get_solido().await.lido; assert_eq!( - solido.thresholds.max_commission, - context.thresholds.max_commission + 1 + solido.criteria.max_commission, + context.criteria.max_commission + 1 ); let result = context.try_deactivate_if_violates(*validator.pubkey()); @@ -36,7 +36,7 @@ async fn test_curate_by_max_commission_percentage() { ); // decrease max_commission_percentage - let result = context.try_set_max_commission_percentage(context.thresholds.max_commission - 1); + let result = context.try_set_max_commission_percentage(context.criteria.max_commission - 1); assert_eq!(result.await.is_ok(), true); let result = context.try_deactivate_if_violates(*validator.pubkey()); diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index a9a2323ff..52f62cd16 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -33,8 +33,8 @@ use lido::token::{Lamports, StLamports}; use lido::{error::LidoError, instruction, RESERVE_ACCOUNT, STAKE_AUTHORITY}; use lido::{ state::{ - AccountList, FeeRecipients, Lido, ListEntry, Maintainer, RewardDistribution, StakeDeposit, - Thresholds, Validator, ValidatorPerf, + AccountList, Criteria, FeeRecipients, Lido, ListEntry, Maintainer, RewardDistribution, + StakeDeposit, Validator, ValidatorPerf, }, MINT_AUTHORITY, }; @@ -96,7 +96,7 @@ pub struct Context { pub stake_authority: Pubkey, pub mint_authority: Pubkey, - pub thresholds: Thresholds, + pub criteria: Criteria, } pub struct ValidatorAccounts { @@ -245,7 +245,7 @@ impl Context { stake_authority, mint_authority, deterministic_keypair, - thresholds: Thresholds::new(5, 0, 0), + criteria: Criteria::new(5, 0, 0), }; result.st_sol_mint = result.create_mint(result.mint_authority).await; @@ -312,7 +312,7 @@ impl Context { instruction::initialize( &id(), result.reward_distribution.clone(), - result.thresholds.clone(), + result.criteria.clone(), max_validators, max_maintainers, &instruction::InitializeAccountsMeta { @@ -740,7 +740,7 @@ impl Context { .create_vote_account( &node_account, withdraw_authority.pubkey(), - self.thresholds.max_commission, + self.criteria.max_commission, ) .await; @@ -1379,19 +1379,19 @@ impl Context { max_commission: u8, ) -> transport::Result<()> { let solido = self.get_solido().await; - let current_thresholds = solido.lido.thresholds; + let current_criteria = solido.lido.criteria; send_transaction( &mut self.context, - &[lido::instruction::change_thresholds( + &[lido::instruction::change_criteria( &id(), - &lido::instruction::ChangeThresholdsMeta { + &lido::instruction::ChangeCriteriaMeta { lido: self.solido.pubkey(), manager: self.manager.pubkey(), }, - Thresholds { + Criteria { max_commission, - ..current_thresholds + ..current_criteria }, )], vec![&self.manager], From 03dcb48d239b7d112908dbf8fe41a07a1e9ed7c9 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 23 May 2023 18:59:23 +0300 Subject: [PATCH 026/131] late than never --- program/src/processor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 479b565e6..796cb3b16 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1051,7 +1051,7 @@ pub fn process_withdraw( } /// Migrate Solido state to version 2 -pub fn processor_migrate_to_v2( +pub fn process_migrate_to_v2( program_id: &Pubkey, reward_distribution: RewardDistribution, max_validators: u32, @@ -1234,7 +1234,7 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P max_validators, max_maintainers, max_commission_percentage, - } => processor_migrate_to_v2( + } => process_migrate_to_v2( program_id, reward_distribution, max_validators, From 0923ba47b0453d7bc2acd1f182924fdbf471c7fa Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 23 May 2023 21:23:16 +0300 Subject: [PATCH 027/131] perflist address in accounts-meta --- cli/maintainer/src/commands_solido.rs | 10 +++++++++- cli/maintainer/src/config.rs | 4 ++++ cli/maintainer/src/maintenance.rs | 2 ++ program/src/instruction.rs | 12 ++++++++++++ program/src/processor.rs | 14 +++++++++++--- program/src/state.rs | 5 +++++ 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 033fb4310..7fbb98681 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -76,6 +76,10 @@ pub struct CreateSolidoOutput { #[serde(serialize_with = "serialize_b58")] pub validator_list_address: Pubkey, + /// Data account that holds list of validators + #[serde(serialize_with = "serialize_b58")] + pub validator_perf_list_address: Pubkey, + /// Data account that holds list of maintainers #[serde(serialize_with = "serialize_b58")] pub maintainer_list_address: Pubkey, @@ -293,10 +297,11 @@ pub fn command_create_solido( config.sign_and_send_transaction( &instructions[..], - &[ + &vec![ config.signer, &*lido_signer, &*validator_list_signer, + &*validator_perf_list_signer, &*maintainer_list_signer, ], )?; @@ -310,6 +315,7 @@ pub fn command_create_solido( treasury_account: treasury_keypair.pubkey(), developer_account: developer_keypair.pubkey(), validator_list_address: validator_list_signer.pubkey(), + validator_perf_list_address: validator_perf_list_signer.pubkey(), maintainer_list_address: maintainer_list_signer.pubkey(), }; Ok(result) @@ -1079,6 +1085,7 @@ pub fn command_deactivate_if_violates( lido: *opts.solido_address(), validator_vote_account_to_deactivate: *validator.pubkey(), validator_list: solido.validator_list, + validator_perf_list: solido.validator_perf_list, }, u32::try_from(validator_index).expect("Too many validators"), ); @@ -1248,6 +1255,7 @@ pub fn command_migrate_state_to_v2( lido: *opts.solido_address(), manager: multisig_address, validator_list: *opts.validator_list_address(), + validator_perf_list: *opts.validator_perf_list_address(), maintainer_list: *opts.maintainer_list_address(), developer_account: *opts.developer_fee_address(), }, diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index 8b1518977..7d4509eb3 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -881,6 +881,10 @@ cli_opt_struct! { #[clap(long)] validator_list_address: Pubkey, + /// Validator list data address + #[clap(long)] + validator_perf_list_address: Pubkey, + /// Maintainer list data address #[clap(long)] maintainer_list_address: Pubkey, diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index b6a863714..eba871b21 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -781,6 +781,7 @@ impl SolidoState { lido: self.solido_address, validator_vote_account_to_deactivate: *validator.pubkey(), validator_list: self.solido.validator_list, + validator_perf_list: self.solido.validator_perf_list, }, u32::try_from(validator_index).expect("Too many validators"), ); @@ -916,6 +917,7 @@ impl SolidoState { lido: self.solido_address, validator_vote_account: *validator.pubkey(), validator_list: self.solido.validator_list, + validator_perf_list: self.solido.validator_perf_list, }, u32::try_from(validator_index).expect("Too many validators"), 0, diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 1c5bb4409..a99584ec0 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -622,6 +622,10 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_perf_list { + is_signer: false, + is_writable: true, + }, pub validator_vote_account { is_signer: false, is_writable: false, @@ -1001,6 +1005,10 @@ accounts_struct! { is_signer: false, is_writable: true, }, + pub validator_perf_list { + is_signer: false, + is_writable: false, + }, } } @@ -1057,6 +1065,10 @@ accounts_struct! { is_signer: false, is_writable: true, }, + pub validator_perf_list { + is_signer: false, + is_writable: false, + }, pub maintainer_list { is_signer: false, is_writable: true, diff --git a/program/src/processor.rs b/program/src/processor.rs index 796cb3b16..9f556c261 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -158,6 +158,7 @@ pub fn process_initialize( metrics: Metrics::new(), criteria: Criteria::default(), validator_list: *accounts.validator_list.key, + validator_perf_list: *accounts.validator_perf_list.key, maintainer_list: *accounts.maintainer_list.key, }; @@ -624,9 +625,9 @@ pub fn process_update_exchange_rate( pub fn process_update_block_production_rate( _program_id: &Pubkey, - _raw_accounts: &[AccountInfo], _validator_index: u32, _block_production_rate: u8, + _raw_accounts: &[AccountInfo], ) -> ProgramResult { unimplemented!("no no") } @@ -1069,9 +1070,11 @@ pub fn process_migrate_to_v2( let rent = &Rent::get()?; check_rent_exempt(rent, accounts.validator_list, "Validator list account")?; + check_rent_exempt(rent, accounts.validator_perf_list, "Perf list account")?; check_rent_exempt(rent, accounts.maintainer_list, "Maintainer list account")?; check_account_owner(accounts.validator_list, program_id)?; + check_account_owner(accounts.validator_perf_list, program_id)?; check_account_owner(accounts.maintainer_list, program_id)?; check_account_data( @@ -1079,6 +1082,11 @@ pub fn process_migrate_to_v2( ValidatorList::required_bytes(max_validators), AccountType::Validator, )?; + check_account_data( + accounts.validator_perf_list, + ValidatorPerfList::required_bytes(max_validators), + AccountType::ValidatorPerf, + )?; check_account_data( accounts.maintainer_list, MaintainerList::required_bytes(max_maintainers), @@ -1124,13 +1132,13 @@ pub fn process_migrate_to_v2( lido_version: Lido::VERSION, account_type: AccountType::Lido, validator_list: *accounts.validator_list.key, + validator_perf_list: *accounts.validator_perf_list.key, maintainer_list: *accounts.maintainer_list.key, fee_recipients: FeeRecipients { treasury_account: lido_v1.fee_recipients.treasury_account, developer_account: *accounts.developer_account.key, }, reward_distribution, - manager: lido_v1.manager, st_sol_mint: lido_v1.st_sol_mint, exchange_rate: lido_v1.exchange_rate, @@ -1198,9 +1206,9 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P block_production_rate, } => process_update_block_production_rate( program_id, - accounts, validator_index, block_production_rate, + accounts, ), LidoInstruction::WithdrawV2 { amount, diff --git a/program/src/state.rs b/program/src/state.rs index e521dae66..876054849 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -825,6 +825,10 @@ pub struct Lido { #[serde(serialize_with = "serialize_b58")] pub validator_list: Pubkey, + /// Validator performance readings account + #[serde(serialize_with = "serialize_b58")] + pub validator_perf_list: Pubkey, + /// Maintainer list account /// /// Maintainers are granted low security risk privileges. Maintainers are @@ -1598,6 +1602,7 @@ mod test_lido { metrics: Metrics::new(), criteria: Criteria::new(5, 0, 0), validator_list: Pubkey::new_unique(), + validator_perf_list: Pubkey::new_unique(), maintainer_list: Pubkey::new_unique(), }; let mut data = Vec::new(); From 9cb83870572fedda11c5ca85c5697ab905abefcd Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 10:38:18 +0300 Subject: [PATCH 028/131] attention is all you really need --- program/src/processor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 9f556c261..05c316cd8 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -86,7 +86,7 @@ pub fn process_initialize( check_account_data( accounts.validator_perf_list, ValidatorPerfList::required_bytes(max_validators), - AccountType::Validator, + AccountType::ValidatorPerf, )?; check_account_data( accounts.maintainer_list, @@ -110,6 +110,9 @@ pub fn process_initialize( let mut validators = ValidatorList::new_default(0); validators.header.max_entries = max_validators; + let mut validator_perf = ValidatorPerfList::new_default(0); + validator_perf.header.max_entries = max_validators; + let mut maintainers = MaintainerList::new_default(0); maintainers.header.max_entries = max_maintainers; @@ -167,6 +170,7 @@ pub fn process_initialize( lido.check_is_st_sol_account(accounts.developer_account)?; validators.save(accounts.validator_list)?; + validator_perf.save(accounts.validator_perf_list)?; maintainers.save(accounts.maintainer_list)?; lido.save(accounts.lido) } From fd8f6fd318bee11cb8604dbc2f3b5814bab57b00 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 10:56:08 +0300 Subject: [PATCH 029/131] =?UTF-8?q?`Lido::LEN`=20=E2=86=92=20452?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- program/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/state.rs b/program/src/state.rs index 876054849..e47bbc1cd 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -845,7 +845,7 @@ impl Lido { /// Size of a serialized `Lido` struct excluding validators and maintainers. /// /// To update this, run the tests and replace the value here with the test output. - pub const LEN: usize = 420; + pub const LEN: usize = 452; pub fn deserialize_lido(program_id: &Pubkey, lido: &AccountInfo) -> Result { check_account_owner(lido, program_id)?; From 9c52e398740413dc96ed6be35cf622106453a29f Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 10:58:30 +0300 Subject: [PATCH 030/131] read out validator performance list in testlib --- testlib/src/solido_context.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 52f62cd16..050482646 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1278,6 +1278,10 @@ impl Context { .get_account_list::(lido.validator_list) .await .unwrap_or_else(|| AccountList::::new_default(0)); + let _validator_performances = self + .get_account_list::(lido.validator_perf_list) + .await + .unwrap_or_else(|| AccountList::::new_default(0)); let maintainers = self .get_account_list::(lido.maintainer_list) .await From 2e6b102734ea4eb8e6ddae2c21f3f2ac6f42a142 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:45:44 +0300 Subject: [PATCH 031/131] attention is, indeed, all you really need --- program/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/state.rs b/program/src/state.rs index e47bbc1cd..6dca12430 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -552,7 +552,7 @@ impl ValidatorPerf {} impl Sealed for ValidatorPerf {} impl Pack for ValidatorPerf { - const LEN: usize = 8; + const LEN: usize = 40; fn pack_into_slice(&self, data: &mut [u8]) { let mut data = data; BorshSerialize::serialize(&self, &mut data).unwrap(); From 61f8198d8e12c496a602a7ba573b97c98c0b1bd9 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:46:53 +0300 Subject: [PATCH 032/131] accounts in `update_block_production_rate` are now taken from the accounts --- program/src/instruction.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index a99584ec0..f89fab826 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -637,15 +637,13 @@ accounts_struct! { pub fn update_block_production_rate( program_id: &Pubkey, - accounts: &UpdateBlockProductionRateAccountsMeta, - validator_index: u32, block_production_rate: u8, + accounts: &UpdateBlockProductionRateAccountsMeta, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), data: LidoInstruction::UpdateBlockProductionRate { - validator_index, block_production_rate, } .to_vec(), From a407793fad860b3a4243f4844ef7b6544ce93f66 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:47:16 +0300 Subject: [PATCH 033/131] add `update_vote_success_rate` instruction --- program/src/instruction.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index f89fab826..0f7b021e2 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -650,6 +650,18 @@ pub fn update_block_production_rate( } } +pub fn update_vote_success_rate( + program_id: &Pubkey, + vote_success_rate: u8, + accounts: &UpdateVoteSuccessRateAccountsMeta, +) -> Instruction { + Instruction { + program_id: *program_id, + accounts: accounts.to_vec(), + data: LidoInstruction::UpdateVoteSuccessRate { vote_success_rate }.to_vec(), + } +} + // Changes the Fee spec // The new Fee structure is passed by argument and the recipients are passed here accounts_struct! { From f86371fef353e52576595b6f48bb1c2faf67335b Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:48:46 +0300 Subject: [PATCH 034/131] maintainer now writes zeroes to the correct data account --- cli/maintainer/src/maintenance.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index eba871b21..d90c87c1c 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -904,23 +904,22 @@ impl SolidoState { Some(MaintenanceInstruction::new(instruction, task)) } - /// Tell the program the newest block production rate. + /// Tell the program the newest block production rates. pub fn try_update_block_production_rate(&self) -> Option { - for (validator_index, validator) in self.validators.entries.iter().enumerate() { + for validator in self.validators.entries.iter() { if false { continue; } let instruction = lido::instruction::update_block_production_rate( &self.solido_program_id, + 0, &lido::instruction::UpdateBlockProductionRateAccountsMeta { lido: self.solido_address, - validator_vote_account: *validator.pubkey(), + validator_vote_account_to_update: *validator.pubkey(), validator_list: self.solido.validator_list, validator_perf_list: self.solido.validator_perf_list, }, - u32::try_from(validator_index).expect("Too many validators"), - 0, ); let task = MaintenanceOutput::UpdateBlockProductionRate { validator_vote_account: *validator.pubkey(), From b091a28dfdbc6855caf91ad9a65a83e46faad7ab Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:49:26 +0300 Subject: [PATCH 035/131] off-chain data args --- program/src/instruction.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 0f7b021e2..072288906 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -618,6 +618,10 @@ accounts_struct! { is_signer: false, is_writable: true, }, + pub validator_vote_account_to_update { + is_signer: false, + is_writable: false, + }, pub validator_list { is_signer: false, is_writable: false, @@ -626,10 +630,29 @@ accounts_struct! { is_signer: false, is_writable: true, }, - pub validator_vote_account { + + const sysvar_clock = sysvar::clock::id(), + } +} + +accounts_struct! { + UpdateVoteSuccessRateAccountsMeta, UpdateVoteSuccessRateAccountsInfo { + pub lido { + is_signer: false, + is_writable: true, + }, + pub validator_vote_account_to_update { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: false, + }, + pub validator_perf_list { + is_signer: false, + is_writable: true, + }, const sysvar_clock = sysvar::clock::id(), } From 5917357622c76ef114cb10c4a5ae0396bc7882e7 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:50:39 +0300 Subject: [PATCH 036/131] validator list and theirs perfs are now joined by primary key --- program/src/instruction.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 072288906..575c0fa36 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -154,13 +154,16 @@ pub enum LidoInstruction { /// Update the block production rate of a validator. UpdateBlockProductionRate { - // Index of a validator in validator list - #[allow(dead_code)] - validator_index: u32, #[allow(dead_code)] block_production_rate: u8, }, + /// Update the vote success rate of a validator. + UpdateVoteSuccessRate { + #[allow(dead_code)] + vote_success_rate: u8, + }, + /// Withdraw a given amount of stSOL. /// /// Caller provides some `amount` of StLamports that are to be burned in From f4d9efe7508b48e79ee8e6c3ff4e711a803e81d1 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:52:39 +0300 Subject: [PATCH 037/131] testlib now knows about perfz --- testlib/src/solido_context.rs | 57 ++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 050482646..4c7b02d57 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -184,6 +184,7 @@ pub async fn send_transaction( pub struct SolidoWithLists { pub lido: Lido, pub validators: AccountList, + pub validator_perfs: AccountList, pub maintainers: AccountList, } @@ -1197,6 +1198,42 @@ impl Context { .expect("Failed to withdraw inactive stake."); } + /// Update the validator's block production rate. + pub async fn try_update_validator_block_production_rate( + &mut self, + validator_vote_account: Pubkey, + new_block_production_rate: u8, + ) -> transport::Result<()> { + send_transaction( + &mut self.context, + &[instruction::update_block_production_rate( + &id(), + new_block_production_rate, + &instruction::UpdateBlockProductionRateAccountsMeta { + lido: self.solido.pubkey(), + validator_vote_account_to_update: validator_vote_account, + validator_list: self.validator_list.pubkey(), + validator_perf_list: self.validator_perf_list.pubkey(), + }, + )], + vec![], + ) + .await + } + + pub async fn update_validator_block_production_rate( + &mut self, + validator_vote_account: Pubkey, + new_block_production_rate: u8, + ) { + self.try_update_validator_block_production_rate( + validator_vote_account, + new_block_production_rate, + ) + .await + .expect("Validator performance metrics could always be updated"); + } + pub async fn try_get_account(&mut self, address: Pubkey) -> Option { self.context .banks_client @@ -1278,7 +1315,7 @@ impl Context { .get_account_list::(lido.validator_list) .await .unwrap_or_else(|| AccountList::::new_default(0)); - let _validator_performances = self + let validator_perfs = self .get_account_list::(lido.validator_perf_list) .await .unwrap_or_else(|| AccountList::::new_default(0)); @@ -1290,6 +1327,7 @@ impl Context { SolidoWithLists { lido, validators, + validator_perfs, maintainers, } } @@ -1403,6 +1441,22 @@ impl Context { .await } + pub async fn try_change_criteria(&mut self, new_criteria: &Criteria) -> transport::Result<()> { + send_transaction( + &mut self.context, + &[lido::instruction::change_criteria( + &id(), + &lido::instruction::ChangeCriteriaMeta { + lido: self.solido.pubkey(), + manager: self.manager.pubkey(), + }, + new_criteria.clone(), + )], + vec![&self.manager], + ) + .await + } + pub async fn try_deactivate_if_violates( &mut self, vote_account: Pubkey, @@ -1417,6 +1471,7 @@ impl Context { lido: self.solido.pubkey(), validator_vote_account_to_deactivate: vote_account, validator_list: self.validator_list.pubkey(), + validator_perf_list: self.validator_perf_list.pubkey(), }, validator_index, )], From caae3b22e481796882eec526ca855adae3807291 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:54:48 +0300 Subject: [PATCH 038/131] block production rate updates now get propagated to the data account --- program/src/processor.rs | 65 ++++++++++++++++++++++++++++++++-------- program/src/state.rs | 1 + 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 05c316cd8..bec761677 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -9,8 +9,8 @@ use crate::{ error::LidoError, instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, - StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, - UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, + StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateBlockProductionRateAccountsInfo, + UpdateExchangeRateAccountsInfoV2, UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, }, logic::{ burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, @@ -28,7 +28,7 @@ use crate::{ stake_account::{deserialize_stake_account, StakeAccount}, state::{ AccountType, Criteria, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, - MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, + MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, ValidatorPerf, ValidatorPerfList, }, token::{Lamports, Rational, StLamports}, @@ -628,12 +628,56 @@ pub fn process_update_exchange_rate( } pub fn process_update_block_production_rate( + program_id: &Pubkey, + block_production_rate: u8, + raw_accounts: &[AccountInfo], +) -> ProgramResult { + let accounts = UpdateBlockProductionRateAccountsInfo::try_from_slice(raw_accounts)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + + let block_production_rate = block_production_rate as u64; + let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; + + let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); + let mut validator_perfs = lido.deserialize_account_list_info::( + program_id, + accounts.validator_perf_list, + validator_perf_list_data, + )?; + + // Find the existing perf record for the validator: + let perf_index = validator_perfs + .iter() + .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); + // If there is no existing perf record, create a new one: + let perf_index = match perf_index { + None => { + validator_perfs.push(ValidatorPerf { + validator_vote_account_address, + block_production_rate, + })?; + (validator_perfs.len() as usize) - 1 + } + Some(index) => index, + }; + let perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; + + perf.block_production_rate = block_production_rate; + msg!( + "Updated block production rate for validator {} to {}.", + validator_vote_account_address, + block_production_rate + ); + + Ok(()) +} + +pub fn process_update_vote_success_rate( _program_id: &Pubkey, - _validator_index: u32, _block_production_rate: u8, _raw_accounts: &[AccountInfo], ) -> ProgramResult { - unimplemented!("no no") + unimplemented!("not yet") } #[derive(PartialEq, Clone, Copy)] @@ -1206,14 +1250,11 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P process_update_stake_account_balance(program_id, validator_index, accounts) } LidoInstruction::UpdateBlockProductionRate { - validator_index, block_production_rate, - } => process_update_block_production_rate( - program_id, - validator_index, - block_production_rate, - accounts, - ), + } => process_update_block_production_rate(program_id, block_production_rate, accounts), + LidoInstruction::UpdateVoteSuccessRate { vote_success_rate } => { + process_update_vote_success_rate(program_id, vote_success_rate, accounts) + } LidoInstruction::WithdrawV2 { amount, validator_index, diff --git a/program/src/state.rs b/program/src/state.rs index 6dca12430..09c201e8f 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -1256,6 +1256,7 @@ impl Lido { let solido_list_address = match T::TYPE { AccountType::Validator => self.validator_list, AccountType::Maintainer => self.maintainer_list, + AccountType::ValidatorPerf => self.validator_perf_list, _ => { msg!( "Invalid account type {:?} when deserializing account list", From 8c0285d37d6de4bf251d7290f252f8df318c24e8 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:56:09 +0300 Subject: [PATCH 039/131] `deactivate_if_violates` now knows about the block production rate --- program/src/process_management.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 04050f27d..9cbb567e5 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -8,7 +8,7 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, use crate::logic::check_rent_exempt; use crate::processor::StakeType; -use crate::state::{Criteria, Lido}; +use crate::state::{Criteria, Lido, ValidatorPerf}; use crate::vote_state::PartialVoteState; use crate::{ error::LidoError, @@ -145,6 +145,13 @@ pub fn process_deactivate_if_violates( let accounts = DeactivateIfViolatesInfo::try_from_slice(accounts_raw)?; let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); + let validator_perfs = lido.deserialize_account_list_info::( + program_id, + accounts.validator_perf_list, + validator_perf_list_data, + )?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); let mut validators = lido.deserialize_account_list_info::( program_id, @@ -161,11 +168,24 @@ pub fn process_deactivate_if_violates( return Ok(()); } + let validator_perf = validator_perfs.iter().find(|perf| { + &perf.validator_vote_account_address == accounts.validator_vote_account_to_deactivate.key + }); + if accounts.validator_vote_account_to_deactivate.owner == &solana_program::vote::program::id() { let data = accounts.validator_vote_account_to_deactivate.data.borrow(); let commission = get_vote_account_commission(&data)?; - if commission <= lido.criteria.max_commission { + let does_violate = match validator_perf { + Some(validator_perf) => { + validator_perf.block_production_rate + < (lido.criteria.min_block_production_rate as u64) + } + None => false, + }; + + if commission <= lido.criteria.max_commission && !does_violate { + // Does not violate. return Ok(()); } } else { From 90c954709d279875df29dbd1d76d17ef7af79593 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 24 May 2023 23:56:37 +0300 Subject: [PATCH 040/131] `test_curate_by_min_block_production_rate` --- program/tests/tests/validators_curation.rs | 35 +++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 3e23d685c..76580a5bf 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -1,5 +1,5 @@ use lido::error::LidoError; -use lido::state::ListEntry; +use lido::state::{ListEntry, Criteria}; use solana_program_test::tokio; use solana_sdk::signature::Keypair; @@ -47,6 +47,39 @@ async fn test_curate_by_max_commission_percentage() { assert_eq!(validator.active, false); } +#[tokio::test] +async fn test_curate_by_min_block_production_rate() { + // Given a Solido context and an active validator: + let mut context = Context::new_with_maintainer_and_validator().await; + let validator = &context.get_solido().await.validators.entries[0]; + assert!(validator.active); + + // When Solido imposes a minimum block production rate: + let result = context + .try_change_criteria(&Criteria { + min_block_production_rate: 99, + ..context.criteria + }) + .await; + assert!(result.is_ok()); + + // And when the validator's block production rate is observed: + let result = context + .try_update_validator_block_production_rate(*validator.pubkey(), 98) + .await; + assert!(result.is_ok()); + + // And when the validator's block production rate is below the minimum: + let result = context + .try_deactivate_if_violates(*validator.pubkey()) + .await; + assert!(result.is_ok()); + + // Then the validators with a lower block production rate are deactivated: + let validator = &context.get_solido().await.validators.entries[0]; + assert!(!validator.active); +} + #[tokio::test] async fn test_close_vote_account() { let mut context = Context::new_with_maintainer_and_validator().await; From 20c4674ae6b1d723d95ed4b3c76c2a88112f6eb4 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 08:29:26 +0300 Subject: [PATCH 041/131] default perfz to very large values --- program/src/processor.rs | 1 + program/src/state.rs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index bec761677..5900de4b6 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -655,6 +655,7 @@ pub fn process_update_block_production_rate( validator_perfs.push(ValidatorPerf { validator_vote_account_address, block_production_rate, + ..Default::default() })?; (validator_perfs.len() as usize) - 1 } diff --git a/program/src/state.rs b/program/src/state.rs index 09c201e8f..5491a12b1 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -369,6 +369,9 @@ pub struct ValidatorPerf { /// The number of slots the validator has produced in the last epoch. pub block_production_rate: u64, + + /// Ratio of successful votes to total votes. + pub vote_success_rate: u64, } /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS @@ -567,7 +570,8 @@ impl Default for ValidatorPerf { fn default() -> Self { ValidatorPerf { validator_vote_account_address: Pubkey::default(), - block_production_rate: 0, + block_production_rate: u64::MAX as _, + vote_success_rate: u64::MAX as _, } } } From 88ca70316f5cd5061875e1fa74dc8d4ca9d49937 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 08:31:31 +0300 Subject: [PATCH 042/131] =?UTF-8?q?`ValidatorPerf::LEN`=20=E2=86=92=2048?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- program/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/state.rs b/program/src/state.rs index 5491a12b1..be22b9f6c 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -555,7 +555,7 @@ impl ValidatorPerf {} impl Sealed for ValidatorPerf {} impl Pack for ValidatorPerf { - const LEN: usize = 40; + const LEN: usize = 48; fn pack_into_slice(&self, data: &mut [u8]) { let mut data = data; BorshSerialize::serialize(&self, &mut data).unwrap(); From 99c17e479266b7f651951ff56df6de78f16380f0 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 08:32:18 +0300 Subject: [PATCH 043/131] flesh out `process_update_vote_success_rate` --- program/src/processor.rs | 49 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 5900de4b6..7e7102d13 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -10,7 +10,8 @@ use crate::{ instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateBlockProductionRateAccountsInfo, - UpdateExchangeRateAccountsInfoV2, UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, + UpdateExchangeRateAccountsInfoV2, UpdateStakeAccountBalanceInfo, + UpdateVoteSuccessRateAccountsInfo, WithdrawAccountsInfoV2, }, logic::{ burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, @@ -674,11 +675,49 @@ pub fn process_update_block_production_rate( } pub fn process_update_vote_success_rate( - _program_id: &Pubkey, - _block_production_rate: u8, - _raw_accounts: &[AccountInfo], + program_id: &Pubkey, + vote_success_rate: u8, + raw_accounts: &[AccountInfo], ) -> ProgramResult { - unimplemented!("not yet") + let accounts = UpdateVoteSuccessRateAccountsInfo::try_from_slice(raw_accounts)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + + let vote_success_rate = vote_success_rate as u64; + let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; + + let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); + let mut validator_perfs = lido.deserialize_account_list_info::( + program_id, + accounts.validator_perf_list, + validator_perf_list_data, + )?; + + // Find the existing perf record for the validator: + let perf_index = validator_perfs + .iter() + .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); + // If there is no existing perf record, create a new one: + let perf_index = match perf_index { + None => { + validator_perfs.push(ValidatorPerf { + validator_vote_account_address, + vote_success_rate, + ..Default::default() + })?; + (validator_perfs.len() as usize) - 1 + } + Some(index) => index, + }; + let perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; + + perf.vote_success_rate = vote_success_rate; + msg!( + "Updated vote success rate for validator {} to {}.", + validator_vote_account_address, + vote_success_rate + ); + + Ok(()) } #[derive(PartialEq, Clone, Copy)] From 3ebe5c8772397a41937a59c86a7ac48d051ca50f Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 08:37:15 +0300 Subject: [PATCH 044/131] count in vote success rate in the performance assessment --- program/src/process_management.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 9cbb567e5..a92f1695d 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -176,15 +176,17 @@ pub fn process_deactivate_if_violates( let data = accounts.validator_vote_account_to_deactivate.data.borrow(); let commission = get_vote_account_commission(&data)?; - let does_violate = match validator_perf { + let does_perform_well = match validator_perf { Some(validator_perf) => { validator_perf.block_production_rate - < (lido.criteria.min_block_production_rate as u64) + >= (lido.criteria.min_block_production_rate as u64) + && validator_perf.vote_success_rate + >= (lido.criteria.min_vote_success_rate as u64) } - None => false, + None => true, }; - if commission <= lido.criteria.max_commission && !does_violate { + if commission <= lido.criteria.max_commission && does_perform_well { // Does not violate. return Ok(()); } From 4083c36a04ca5996e8fdd6ba50c05a791b7e3bae Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 08:39:02 +0300 Subject: [PATCH 045/131] test out curation by vote success rate --- program/tests/tests/validators_curation.rs | 37 ++++++++++++++++++++-- testlib/src/solido_context.rs | 35 +++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 76580a5bf..5d0e17cba 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -1,5 +1,5 @@ use lido::error::LidoError; -use lido::state::{ListEntry, Criteria}; +use lido::state::{Criteria, ListEntry}; use solana_program_test::tokio; use solana_sdk::signature::Keypair; @@ -63,7 +63,7 @@ async fn test_curate_by_min_block_production_rate() { .await; assert!(result.is_ok()); - // And when the validator's block production rate is observed: + // And when the validator's block production rate for the epoch is observed: let result = context .try_update_validator_block_production_rate(*validator.pubkey(), 98) .await; @@ -80,6 +80,39 @@ async fn test_curate_by_min_block_production_rate() { assert!(!validator.active); } +#[tokio::test] +async fn test_curate_by_min_vote_success_rate() { + // Given a Solido context and an active validator: + let mut context = Context::new_with_maintainer_and_validator().await; + let validator = &context.get_solido().await.validators.entries[0]; + assert!(validator.active); + + // When Solido imposes a minimum vote success rate: + let result = context + .try_change_criteria(&Criteria { + min_vote_success_rate: 99, + ..context.criteria + }) + .await; + assert!(result.is_ok()); + + // And when the validator's vote success rate for the epoch is observed: + let result = context + .try_update_validator_vote_success_rate(*validator.pubkey(), 98) + .await; + assert!(result.is_ok()); + + // And when the validator's vote success rate is below the minimum: + let result = context + .try_deactivate_if_violates(*validator.pubkey()) + .await; + assert!(result.is_ok()); + + // Then the validators with a lower vote success rate are deactivated: + let validator = &context.get_solido().await.validators.entries[0]; + assert!(!validator.active); +} + #[tokio::test] async fn test_close_vote_account() { let mut context = Context::new_with_maintainer_and_validator().await; diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 4c7b02d57..a0cb1ae31 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1198,7 +1198,7 @@ impl Context { .expect("Failed to withdraw inactive stake."); } - /// Update the validator's block production rate. + /// Update the perf account for the given validator with the given reading. pub async fn try_update_validator_block_production_rate( &mut self, validator_vote_account: Pubkey, @@ -1234,6 +1234,39 @@ impl Context { .expect("Validator performance metrics could always be updated"); } + /// Update the perf account for the given validator with the given reading. + pub async fn try_update_validator_vote_success_rate( + &mut self, + validator_vote_account: Pubkey, + new_vote_success_rate: u8, + ) -> transport::Result<()> { + send_transaction( + &mut self.context, + &[instruction::update_vote_success_rate( + &id(), + new_vote_success_rate, + &instruction::UpdateVoteSuccessRateAccountsMeta { + lido: self.solido.pubkey(), + validator_vote_account_to_update: validator_vote_account, + validator_list: self.validator_list.pubkey(), + validator_perf_list: self.validator_perf_list.pubkey(), + }, + )], + vec![], + ) + .await + } + + pub async fn update_validator_vote_success_rate( + &mut self, + validator_vote_account: Pubkey, + new_vote_success_rate: u8, + ) { + self.try_update_validator_vote_success_rate(validator_vote_account, new_vote_success_rate) + .await + .expect("Validator performance metrics could always be updated"); + } + pub async fn try_get_account(&mut self, address: Pubkey) -> Option { self.context .banks_client From bd2ef39b1158a77ee505ed642faae79082a464ff Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 09:07:42 +0300 Subject: [PATCH 046/131] =?UTF-8?q?`Lido::LEN`=20=E2=86=92=20453?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- program/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/state.rs b/program/src/state.rs index be22b9f6c..7b0a45b0e 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -849,7 +849,7 @@ impl Lido { /// Size of a serialized `Lido` struct excluding validators and maintainers. /// /// To update this, run the tests and replace the value here with the test output. - pub const LEN: usize = 452; + pub const LEN: usize = 453; pub fn deserialize_lido(program_id: &Pubkey, lido: &AccountInfo) -> Result { check_account_owner(lido, program_id)?; From 1c72dfdc40aecbfdf412c8875295ee0a598449a9 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 09:09:44 +0300 Subject: [PATCH 047/131] add uptimes to performance metrics --- program/src/instruction.rs | 41 +++++++++++++++++++++++++++++++ program/src/processor.rs | 49 ++++++++++++++++++++++++++++++++++++++ program/src/state.rs | 11 ++++++++- 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 575c0fa36..74b4967ef 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -164,6 +164,12 @@ pub enum LidoInstruction { vote_success_rate: u8, }, + /// Update the uptime of a validator. + UpdateUptime { + #[allow(dead_code)] + uptime: u8, + }, + /// Withdraw a given amount of stSOL. /// /// Caller provides some `amount` of StLamports that are to be burned in @@ -661,6 +667,29 @@ accounts_struct! { } } +accounts_struct! { + UpdateUptimeAccountsMeta, UpdateUptimeAccountsInfo { + pub lido { + is_signer: false, + is_writable: true, + }, + pub validator_vote_account_to_update { + is_signer: false, + is_writable: false, + }, + pub validator_list { + is_signer: false, + is_writable: false, + }, + pub validator_perf_list { + is_signer: false, + is_writable: true, + }, + + const sysvar_clock = sysvar::clock::id(), + } +} + pub fn update_block_production_rate( program_id: &Pubkey, block_production_rate: u8, @@ -688,6 +717,18 @@ pub fn update_vote_success_rate( } } +pub fn update_uptime( + program_id: &Pubkey, + uptime: u8, + accounts: &UpdateUptimeAccountsMeta, +) -> Instruction { + Instruction { + program_id: *program_id, + accounts: accounts.to_vec(), + data: LidoInstruction::UpdateUptime { uptime }.to_vec(), + } +} + // Changes the Fee spec // The new Fee structure is passed by argument and the recipients are passed here accounts_struct! { diff --git a/program/src/processor.rs b/program/src/processor.rs index 7e7102d13..ca31f4a02 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -720,6 +720,52 @@ pub fn process_update_vote_success_rate( Ok(()) } +pub fn process_update_uptime( + program_id: &Pubkey, + uptime: u8, + raw_accounts: &[AccountInfo], +) -> ProgramResult { + let accounts = UpdateVoteSuccessRateAccountsInfo::try_from_slice(raw_accounts)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + + let uptime = uptime as u64; + let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; + + let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); + let mut validator_perfs = lido.deserialize_account_list_info::( + program_id, + accounts.validator_perf_list, + validator_perf_list_data, + )?; + + // Find the existing perf record for the validator: + let perf_index = validator_perfs + .iter() + .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); + // If there is no existing perf record, create a new one: + let perf_index = match perf_index { + None => { + validator_perfs.push(ValidatorPerf { + validator_vote_account_address, + uptime, + ..Default::default() + })?; + (validator_perfs.len() as usize) - 1 + } + Some(index) => index, + }; + let perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; + + perf.uptime = uptime; + msg!( + "Updated uptime for validator {} to {}.", + validator_vote_account_address, + uptime + ); + + Ok(()) +} + #[derive(PartialEq, Clone, Copy)] pub enum StakeType { Stake, @@ -1295,6 +1341,9 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::UpdateVoteSuccessRate { vote_success_rate } => { process_update_vote_success_rate(program_id, vote_success_rate, accounts) } + LidoInstruction::UpdateUptime { uptime } => { + process_update_uptime(program_id, uptime, accounts) + } LidoInstruction::WithdrawV2 { amount, validator_index, diff --git a/program/src/state.rs b/program/src/state.rs index 7b0a45b0e..f381fae89 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -372,6 +372,9 @@ pub struct ValidatorPerf { /// Ratio of successful votes to total votes. pub vote_success_rate: u64, + + /// Ratio of how long the validator has been available to the total time in the epoch. + pub uptime: u64, } /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS @@ -555,7 +558,7 @@ impl ValidatorPerf {} impl Sealed for ValidatorPerf {} impl Pack for ValidatorPerf { - const LEN: usize = 48; + const LEN: usize = 56; fn pack_into_slice(&self, data: &mut [u8]) { let mut data = data; BorshSerialize::serialize(&self, &mut data).unwrap(); @@ -572,6 +575,7 @@ impl Default for ValidatorPerf { validator_vote_account_address: Pubkey::default(), block_production_rate: u64::MAX as _, vote_success_rate: u64::MAX as _, + uptime: u64::MAX as _, } } } @@ -755,6 +759,9 @@ pub struct Criteria { /// If a validator has `block_production_rate` lower than this, then it gets deactivated. pub min_block_production_rate: u8, + + /// If a validator has the uptime lower than this, then it gets deactivated. + pub min_uptime: u8, } impl Default for Criteria { @@ -763,6 +770,7 @@ impl Default for Criteria { max_commission: 100, min_vote_success_rate: 0, min_block_production_rate: 0, + min_uptime: 0, } } } @@ -777,6 +785,7 @@ impl Criteria { max_commission, min_vote_success_rate, min_block_production_rate, + min_uptime: 0, } } } From ed19c568e2992935b5c9318989f9ea28a0b0c5a2 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 09:13:32 +0300 Subject: [PATCH 048/131] `test_curate_by_min_uptime` --- program/src/process_management.rs | 1 + program/tests/tests/validators_curation.rs | 33 ++++++++++++++++++++++ testlib/src/solido_context.rs | 33 ++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/program/src/process_management.rs b/program/src/process_management.rs index a92f1695d..5dea9aff8 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -182,6 +182,7 @@ pub fn process_deactivate_if_violates( >= (lido.criteria.min_block_production_rate as u64) && validator_perf.vote_success_rate >= (lido.criteria.min_vote_success_rate as u64) + && validator_perf.uptime >= (lido.criteria.min_uptime as u64) } None => true, }; diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 5d0e17cba..7e61c3b29 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -113,6 +113,39 @@ async fn test_curate_by_min_vote_success_rate() { assert!(!validator.active); } +#[tokio::test] +async fn test_curate_by_min_uptime() { + // Given a Solido context and an active validator: + let mut context = Context::new_with_maintainer_and_validator().await; + let validator = &context.get_solido().await.validators.entries[0]; + assert!(validator.active); + + // When Solido imposes a minimum uptime: + let result = context + .try_change_criteria(&Criteria { + min_uptime: 99, + ..context.criteria + }) + .await; + assert!(result.is_ok()); + + // And when the validator's uptime for the epoch is observed: + let result = context + .try_update_validator_uptime(*validator.pubkey(), 98) + .await; + assert!(result.is_ok()); + + // And when the validator's vote success rate is below the minimum: + let result = context + .try_deactivate_if_violates(*validator.pubkey()) + .await; + assert!(result.is_ok()); + + // Then the validators with a lower vote success rate are deactivated: + let validator = &context.get_solido().await.validators.entries[0]; + assert!(!validator.active); +} + #[tokio::test] async fn test_close_vote_account() { let mut context = Context::new_with_maintainer_and_validator().await; diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index a0cb1ae31..9f891b02b 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1267,6 +1267,39 @@ impl Context { .expect("Validator performance metrics could always be updated"); } + /// Update the perf account for the given validator with the given reading. + pub async fn try_update_validator_uptime( + &mut self, + validator_vote_account: Pubkey, + new_uptime: u8, + ) -> transport::Result<()> { + send_transaction( + &mut self.context, + &[instruction::update_uptime( + &id(), + new_uptime, + &instruction::UpdateUptimeAccountsMeta { + lido: self.solido.pubkey(), + validator_vote_account_to_update: validator_vote_account, + validator_list: self.validator_list.pubkey(), + validator_perf_list: self.validator_perf_list.pubkey(), + }, + )], + vec![], + ) + .await + } + + pub async fn update_validator_uptime( + &mut self, + validator_vote_account: Pubkey, + new_vote_success_rate: u8, + ) { + self.try_update_validator_vote_success_rate(validator_vote_account, new_vote_success_rate) + .await + .expect("Validator performance metrics could always be updated"); + } + pub async fn try_get_account(&mut self, address: Pubkey) -> Option { self.context .banks_client From feb143ea3e698888fde914c0fdc9be413d2495a5 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 09:15:56 +0300 Subject: [PATCH 049/131] propagate min-uptime to cli opts --- cli/maintainer/src/commands_solido.rs | 3 ++- cli/maintainer/src/config.rs | 4 ++++ program/src/state.rs | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 7fbb98681..c7faacdf9 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -277,8 +277,9 @@ pub fn command_create_solido( }, Criteria { max_commission: *opts.max_commission(), - min_vote_success_rate: *opts.min_vote_success_rate(), min_block_production_rate: *opts.min_block_production_rate(), + min_vote_success_rate: *opts.min_vote_success_rate(), + min_uptime: *opts.min_uptime(), }, *opts.max_validators(), *opts.max_maintainers(), diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index 7d4509eb3..175c2eca0 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -264,6 +264,10 @@ cli_opt_struct! { #[clap(long, value_name = "int")] min_block_production_rate: u8, + /// The minimum block production rate a validator must have to not be deactivated. + #[clap(long, value_name = "int")] + min_uptime: u8, + // See also the docs section of `create-solido` in main.rs for a description // of the fee shares. /// Treasury fee share of the rewards. diff --git a/program/src/state.rs b/program/src/state.rs index f381fae89..e615ef60e 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -754,12 +754,12 @@ pub struct Criteria { /// If a validator has the commission higher than this, then it gets deactivated. pub max_commission: u8, - /// If a validator has `vote_success_rate` lower than this, then it gets deactivated. - pub min_vote_success_rate: u8, - /// If a validator has `block_production_rate` lower than this, then it gets deactivated. pub min_block_production_rate: u8, + /// If a validator has `vote_success_rate` lower than this, then it gets deactivated. + pub min_vote_success_rate: u8, + /// If a validator has the uptime lower than this, then it gets deactivated. pub min_uptime: u8, } From 76c4670933fd3e15bada32ecc4affe6e73f475ae Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 09:19:13 +0300 Subject: [PATCH 050/131] pass min-uptime from opts further --- cli/maintainer/src/commands_solido.rs | 3 ++- cli/maintainer/src/config.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index c7faacdf9..236200c42 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -1125,8 +1125,9 @@ pub fn command_change_criteria( }, Criteria { max_commission: *opts.max_commission(), - min_vote_success_rate: *opts.min_vote_success_rate(), min_block_production_rate: *opts.min_block_production_rate(), + min_vote_success_rate: *opts.min_vote_success_rate(), + min_uptime: *opts.min_uptime(), }, ); propose_instruction( diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index 175c2eca0..07f684062 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -501,13 +501,17 @@ cli_opt_struct! { #[clap(long, value_name = "percentage")] max_commission: u8, + /// Min block production rate that a validator must uphold. + #[clap(long, value_name = "rate")] + min_block_production_rate: u8, + /// Min vote success rate that a validator must uphold. #[clap(long, value_name = "rate")] min_vote_success_rate: u8, - /// Min block production rate that a validator must uphold. + /// Min uptime that a validator must maintain. #[clap(long, value_name = "rate")] - min_block_production_rate: u8, + min_uptime: u8, /// Multisig instance. #[clap(long, value_name = "address")] From 4cac738c891d181470c471e1980006766cda9a36 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 10:31:38 +0300 Subject: [PATCH 051/131] show-solido shows curation criteria --- cli/maintainer/src/commands_solido.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 236200c42..c7fad4c92 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -537,20 +537,36 @@ impl fmt::Display for ShowSolidoOutput { writeln!(f, "\nFee recipients:")?; writeln!( f, - "Treasury SPL token account: {}", + " Treasury SPL token account: {}", self.solido.fee_recipients.treasury_account )?; writeln!( f, - "Developer fee SPL token account: {}", + " Developer fee SPL token account: {}", self.solido.fee_recipients.developer_account )?; + writeln!(f, "\nValidator curation criteria:")?; writeln!( f, - "Max validation commission: {}%", + " Max validation commission: {}%", self.solido.criteria.max_commission, )?; + writeln!( + f, + " Min block production rate: {}/epoch", + self.solido.criteria.min_block_production_rate, + )?; + writeln!( + f, + " Min vote success rate: {}/epoch", + self.solido.criteria.min_vote_success_rate, + )?; + writeln!( + f, + " Min uptime: {}s/epoch", + self.solido.criteria.min_uptime, + )?; writeln!(f, "\nMetrics:")?; writeln!( From afc13f162ac0fd792eb89df4b362cc03e89ed49f Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 10:35:59 +0300 Subject: [PATCH 052/131] move --- cli/maintainer/src/commands_solido.rs | 44 +++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index c7fad4c92..01804f059 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -546,28 +546,6 @@ impl fmt::Display for ShowSolidoOutput { self.solido.fee_recipients.developer_account )?; - writeln!(f, "\nValidator curation criteria:")?; - writeln!( - f, - " Max validation commission: {}%", - self.solido.criteria.max_commission, - )?; - writeln!( - f, - " Min block production rate: {}/epoch", - self.solido.criteria.min_block_production_rate, - )?; - writeln!( - f, - " Min vote success rate: {}/epoch", - self.solido.criteria.min_vote_success_rate, - )?; - writeln!( - f, - " Min uptime: {}s/epoch", - self.solido.criteria.min_uptime, - )?; - writeln!(f, "\nMetrics:")?; writeln!( f, @@ -618,6 +596,28 @@ impl fmt::Display for ShowSolidoOutput { )?; } + writeln!(f, "\nValidator curation criteria:")?; + writeln!( + f, + " Max validation commission: {}%", + self.solido.criteria.max_commission, + )?; + writeln!( + f, + " Min block production rate: {}/epoch", + self.solido.criteria.min_block_production_rate, + )?; + writeln!( + f, + " Min vote success rate: {}/epoch", + self.solido.criteria.min_vote_success_rate, + )?; + writeln!( + f, + " Min uptime: {}s/epoch", + self.solido.criteria.min_uptime, + )?; + writeln!(f, "\nValidator list {}", self.solido.validator_list)?; writeln!( f, From 7b4cdd139826255d9a892ac7ab8f797027c8676a Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 25 May 2023 11:02:22 +0300 Subject: [PATCH 053/131] heed the given criteria while initializing --- program/src/processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index ca31f4a02..f2a570e70 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -160,7 +160,7 @@ pub fn process_initialize( developer_account: *accounts.developer_account.key, }, metrics: Metrics::new(), - criteria: Criteria::default(), + criteria, validator_list: *accounts.validator_list.key, validator_perf_list: *accounts.validator_perf_list.key, maintainer_list: *accounts.maintainer_list.key, From cc9fa9e022342d48032a2715adff3bd63b5cb697 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 31 May 2023 21:35:11 +0300 Subject: [PATCH 054/131] `test_block_production_rate_updates` --- program/tests/tests/validators_curation.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 7e61c3b29..909cd1720 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -146,6 +146,11 @@ async fn test_curate_by_min_uptime() { assert!(!validator.active); } +#[tokio::test] +async fn test_block_production_rate_updates() { + assert!(false); +} + #[tokio::test] async fn test_close_vote_account() { let mut context = Context::new_with_maintainer_and_validator().await; From 6ad014251f92c719cba46477622b56bf7339d256 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 1 Jun 2023 13:37:13 +0300 Subject: [PATCH 055/131] stub `test_update_vote_success_rate` and `test_update_uptime` --- program/tests/tests/validators_curation.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 909cd1720..9c9958fcf 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -147,7 +147,17 @@ async fn test_curate_by_min_uptime() { } #[tokio::test] -async fn test_block_production_rate_updates() { +async fn test_update_block_production_rate() { + assert!(false); +} + +#[tokio::test] +async fn test_update_vote_success_rate() { + assert!(false); +} + +#[tokio::test] +async fn test_update_uptime() { assert!(false); } From ea64168c58f824af4bb80647b160a69b8f7c0b92 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 1 Jun 2023 15:41:25 +0300 Subject: [PATCH 056/131] bpf tests for perf writein --- program/tests/tests/validators_curation.rs | 63 ++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 9c9958fcf..bb1d02687 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -148,17 +148,74 @@ async fn test_curate_by_min_uptime() { #[tokio::test] async fn test_update_block_production_rate() { - assert!(false); + // Given a Solido context and an active validator: + let mut context = Context::new_with_maintainer_and_validator().await; + let validator = &context.get_solido().await.validators.entries[0]; + assert!(validator.active); + + // When an epoch passes, and the validator's block production rate is observed: + let result = context + .try_update_validator_block_production_rate(*validator.pubkey(), 98) + .await; + assert!(result.is_ok()); + + // Then the validator's block production rate is updated: + let solido = &context.get_solido().await; + let perf = &solido + .validator_perfs + .entries + .iter() + .find(|x| x.validator_vote_account_address == *validator.pubkey()) + .unwrap(); + assert!(perf.block_production_rate == 98); } #[tokio::test] async fn test_update_vote_success_rate() { - assert!(false); + // Given a Solido context and an active validator: + let mut context = Context::new_with_maintainer_and_validator().await; + let validator = &context.get_solido().await.validators.entries[0]; + assert!(validator.active); + + // When an epoch passes, and the validator's vote success rate is observed: + let result = context + .try_update_validator_vote_success_rate(*validator.pubkey(), 98) + .await; + assert!(result.is_ok()); + + // Then the validator's vote success rate is updated: + let solido = &context.get_solido().await; + let perf = &solido + .validator_perfs + .entries + .iter() + .find(|x| x.validator_vote_account_address == *validator.pubkey()) + .unwrap(); + assert!(perf.vote_success_rate == 98); } #[tokio::test] async fn test_update_uptime() { - assert!(false); + // Given a Solido context and an active validator: + let mut context = Context::new_with_maintainer_and_validator().await; + let validator = &context.get_solido().await.validators.entries[0]; + assert!(validator.active); + + // When an epoch passes, and the validator's uptime is observed: + let result = context + .try_update_validator_uptime(*validator.pubkey(), 98) + .await; + assert!(result.is_ok()); + + // Then the validator's uptime is updated: + let solido = &context.get_solido().await; + let perf = &solido + .validator_perfs + .entries + .iter() + .find(|x| x.validator_vote_account_address == *validator.pubkey()) + .unwrap(); + assert!(perf.uptime == 98); } #[tokio::test] From 7c92bf01509a24ee222f59659908040dba9de033 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 7 Jun 2023 10:55:24 +0300 Subject: [PATCH 057/131] =?UTF-8?q?ValidatorPerf::LEN=20=E2=86=92=2064?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- program/src/state.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/program/src/state.rs b/program/src/state.rs index e615ef60e..196aa3f75 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -367,6 +367,10 @@ pub struct ValidatorPerf { #[serde(rename = "pubkey")] pub validator_vote_account_address: Pubkey, + /// The epoch in which the off-chain part + /// of the validator's performance was computed. + pub computed_in_epoch: Epoch, + /// The number of slots the validator has produced in the last epoch. pub block_production_rate: u64, @@ -558,7 +562,7 @@ impl ValidatorPerf {} impl Sealed for ValidatorPerf {} impl Pack for ValidatorPerf { - const LEN: usize = 56; + const LEN: usize = 64; fn pack_into_slice(&self, data: &mut [u8]) { let mut data = data; BorshSerialize::serialize(&self, &mut data).unwrap(); @@ -573,6 +577,7 @@ impl Default for ValidatorPerf { fn default() -> Self { ValidatorPerf { validator_vote_account_address: Pubkey::default(), + computed_in_epoch: Epoch::default(), block_production_rate: u64::MAX as _, vote_success_rate: u64::MAX as _, uptime: u64::MAX as _, From 748851426ae933a86e4edbc1c684a7022a4f7f0b Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 7 Jun 2023 10:56:20 +0300 Subject: [PATCH 058/131] meld off-chain validator-perf instructions into one --- program/src/instruction.rs | 94 ++------------ program/src/processor.rs | 142 +++++---------------- program/tests/tests/validators_curation.rs | 19 ++- 3 files changed, 58 insertions(+), 197 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 74b4967ef..4a6e6fc22 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -152,20 +152,12 @@ pub enum LidoInstruction { /// This can be called by anybody. UpdateExchangeRateV2, - /// Update the block production rate of a validator. - UpdateBlockProductionRate { + /// Update the off-chain performance metrics for a validator. + UpdateValidatorPerf { #[allow(dead_code)] block_production_rate: u8, - }, - - /// Update the vote success rate of a validator. - UpdateVoteSuccessRate { #[allow(dead_code)] vote_success_rate: u8, - }, - - /// Update the uptime of a validator. - UpdateUptime { #[allow(dead_code)] uptime: u8, }, @@ -622,53 +614,7 @@ pub fn update_exchange_rate( } accounts_struct! { - UpdateBlockProductionRateAccountsMeta, UpdateBlockProductionRateAccountsInfo { - pub lido { - is_signer: false, - is_writable: true, - }, - pub validator_vote_account_to_update { - is_signer: false, - is_writable: false, - }, - pub validator_list { - is_signer: false, - is_writable: false, - }, - pub validator_perf_list { - is_signer: false, - is_writable: true, - }, - - const sysvar_clock = sysvar::clock::id(), - } -} - -accounts_struct! { - UpdateVoteSuccessRateAccountsMeta, UpdateVoteSuccessRateAccountsInfo { - pub lido { - is_signer: false, - is_writable: true, - }, - pub validator_vote_account_to_update { - is_signer: false, - is_writable: false, - }, - pub validator_list { - is_signer: false, - is_writable: false, - }, - pub validator_perf_list { - is_signer: false, - is_writable: true, - }, - - const sysvar_clock = sysvar::clock::id(), - } -} - -accounts_struct! { - UpdateUptimeAccountsMeta, UpdateUptimeAccountsInfo { + UpdateValidatorPerfAccountsMeta, UpdateValidatorPerfAccountsInfo { pub lido { is_signer: false, is_writable: true, @@ -690,45 +636,25 @@ accounts_struct! { } } -pub fn update_block_production_rate( +pub fn update_validator_perf( program_id: &Pubkey, block_production_rate: u8, - accounts: &UpdateBlockProductionRateAccountsMeta, + vote_success_rate: u8, + uptime: u8, + accounts: &UpdateValidatorPerfAccountsMeta, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::UpdateBlockProductionRate { + data: LidoInstruction::UpdateValidatorPerf { block_production_rate, + vote_success_rate, + uptime, } .to_vec(), } } -pub fn update_vote_success_rate( - program_id: &Pubkey, - vote_success_rate: u8, - accounts: &UpdateVoteSuccessRateAccountsMeta, -) -> Instruction { - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: LidoInstruction::UpdateVoteSuccessRate { vote_success_rate }.to_vec(), - } -} - -pub fn update_uptime( - program_id: &Pubkey, - uptime: u8, - accounts: &UpdateUptimeAccountsMeta, -) -> Instruction { - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: LidoInstruction::UpdateUptime { uptime }.to_vec(), - } -} - // Changes the Fee spec // The new Fee structure is passed by argument and the recipients are passed here accounts_struct! { diff --git a/program/src/processor.rs b/program/src/processor.rs index f2a570e70..f421ecb28 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -9,9 +9,8 @@ use crate::{ error::LidoError, instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, - StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateBlockProductionRateAccountsInfo, - UpdateExchangeRateAccountsInfoV2, UpdateStakeAccountBalanceInfo, - UpdateVoteSuccessRateAccountsInfo, WithdrawAccountsInfoV2, + StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, + UpdateStakeAccountBalanceInfo, UpdateValidatorPerfAccountsInfo, WithdrawAccountsInfoV2, }, logic::{ burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, @@ -628,15 +627,19 @@ pub fn process_update_exchange_rate( lido.save(accounts.lido) } -pub fn process_update_block_production_rate( +pub fn process_update_validator_perf( program_id: &Pubkey, block_production_rate: u8, + vote_success_rate: u8, + uptime: u8, raw_accounts: &[AccountInfo], ) -> ProgramResult { - let accounts = UpdateBlockProductionRateAccountsInfo::try_from_slice(raw_accounts)?; + let accounts = UpdateValidatorPerfAccountsInfo::try_from_slice(raw_accounts)?; let lido = Lido::deserialize_lido(program_id, accounts.lido)?; let block_production_rate = block_production_rate as u64; + let vote_success_rate = vote_success_rate as u64; + let uptime = uptime as u64; let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); @@ -662,106 +665,29 @@ pub fn process_update_block_production_rate( } Some(index) => index, }; - let perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; - - perf.block_production_rate = block_production_rate; - msg!( - "Updated block production rate for validator {} to {}.", - validator_vote_account_address, - block_production_rate - ); - - Ok(()) -} - -pub fn process_update_vote_success_rate( - program_id: &Pubkey, - vote_success_rate: u8, - raw_accounts: &[AccountInfo], -) -> ProgramResult { - let accounts = UpdateVoteSuccessRateAccountsInfo::try_from_slice(raw_accounts)?; - let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - - let vote_success_rate = vote_success_rate as u64; - let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; - - let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); - let mut validator_perfs = lido.deserialize_account_list_info::( - program_id, - accounts.validator_perf_list, - validator_perf_list_data, - )?; + let mut perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; - // Find the existing perf record for the validator: - let perf_index = validator_perfs - .iter() - .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); - // If there is no existing perf record, create a new one: - let perf_index = match perf_index { - None => { - validator_perfs.push(ValidatorPerf { - validator_vote_account_address, - vote_success_rate, - ..Default::default() - })?; - (validator_perfs.len() as usize) - 1 - } - Some(index) => index, - }; - let perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; + // Update could happen at most once per epoch: + let clock = Clock::get()?; + if perf.computed_in_epoch >= clock.epoch { + msg!( + "The block production rate was already updated in epoch {}.", + perf.computed_in_epoch + ); + msg!("It can only be done once per epoch, so we are going to abort this transaction."); + return Err(LidoError::InstructionIsDeprecated.into()); + } + perf.block_production_rate = block_production_rate; perf.vote_success_rate = vote_success_rate; - msg!( - "Updated vote success rate for validator {} to {}.", - validator_vote_account_address, - vote_success_rate - ); - - Ok(()) -} - -pub fn process_update_uptime( - program_id: &Pubkey, - uptime: u8, - raw_accounts: &[AccountInfo], -) -> ProgramResult { - let accounts = UpdateVoteSuccessRateAccountsInfo::try_from_slice(raw_accounts)?; - let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - - let uptime = uptime as u64; - let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; - - let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); - let mut validator_perfs = lido.deserialize_account_list_info::( - program_id, - accounts.validator_perf_list, - validator_perf_list_data, - )?; - - // Find the existing perf record for the validator: - let perf_index = validator_perfs - .iter() - .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); - // If there is no existing perf record, create a new one: - let perf_index = match perf_index { - None => { - validator_perfs.push(ValidatorPerf { - validator_vote_account_address, - uptime, - ..Default::default() - })?; - (validator_perfs.len() as usize) - 1 - } - Some(index) => index, - }; - let perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; - perf.uptime = uptime; + msg!("For validator {}, updated:", validator_vote_account_address); msg!( - "Updated uptime for validator {} to {}.", - validator_vote_account_address, - uptime + "block production rate to {},", + validator_vote_account_address ); + msg!("vote success rate to {},", vote_success_rate); + msg!("uptime to {}.", uptime); Ok(()) } @@ -1335,15 +1261,17 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::UpdateStakeAccountBalance { validator_index } => { process_update_stake_account_balance(program_id, validator_index, accounts) } - LidoInstruction::UpdateBlockProductionRate { + LidoInstruction::UpdateValidatorPerf { block_production_rate, - } => process_update_block_production_rate(program_id, block_production_rate, accounts), - LidoInstruction::UpdateVoteSuccessRate { vote_success_rate } => { - process_update_vote_success_rate(program_id, vote_success_rate, accounts) - } - LidoInstruction::UpdateUptime { uptime } => { - process_update_uptime(program_id, uptime, accounts) - } + vote_success_rate, + uptime, + } => process_update_validator_perf( + program_id, + block_production_rate, + vote_success_rate, + uptime, + accounts, + ), LidoInstruction::WithdrawV2 { amount, validator_index, diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index bb1d02687..719fc21f8 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -10,6 +10,7 @@ use testlib::solido_context::Context; #[tokio::test] async fn test_curate_by_max_commission_percentage() { let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; // increase max_commission_percentage @@ -51,6 +52,7 @@ async fn test_curate_by_max_commission_percentage() { async fn test_curate_by_min_block_production_rate() { // Given a Solido context and an active validator: let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; assert!(validator.active); @@ -65,7 +67,7 @@ async fn test_curate_by_min_block_production_rate() { // And when the validator's block production rate for the epoch is observed: let result = context - .try_update_validator_block_production_rate(*validator.pubkey(), 98) + .try_update_validator_perf(*validator.pubkey(), 98, 0, 0) .await; assert!(result.is_ok()); @@ -84,6 +86,7 @@ async fn test_curate_by_min_block_production_rate() { async fn test_curate_by_min_vote_success_rate() { // Given a Solido context and an active validator: let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; assert!(validator.active); @@ -98,7 +101,7 @@ async fn test_curate_by_min_vote_success_rate() { // And when the validator's vote success rate for the epoch is observed: let result = context - .try_update_validator_vote_success_rate(*validator.pubkey(), 98) + .try_update_validator_perf(*validator.pubkey(), 0, 98, 0) .await; assert!(result.is_ok()); @@ -117,6 +120,7 @@ async fn test_curate_by_min_vote_success_rate() { async fn test_curate_by_min_uptime() { // Given a Solido context and an active validator: let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; assert!(validator.active); @@ -131,7 +135,7 @@ async fn test_curate_by_min_uptime() { // And when the validator's uptime for the epoch is observed: let result = context - .try_update_validator_uptime(*validator.pubkey(), 98) + .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) .await; assert!(result.is_ok()); @@ -150,12 +154,13 @@ async fn test_curate_by_min_uptime() { async fn test_update_block_production_rate() { // Given a Solido context and an active validator: let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; assert!(validator.active); // When an epoch passes, and the validator's block production rate is observed: let result = context - .try_update_validator_block_production_rate(*validator.pubkey(), 98) + .try_update_validator_perf(*validator.pubkey(), 98, 0, 0) .await; assert!(result.is_ok()); @@ -174,12 +179,13 @@ async fn test_update_block_production_rate() { async fn test_update_vote_success_rate() { // Given a Solido context and an active validator: let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; assert!(validator.active); // When an epoch passes, and the validator's vote success rate is observed: let result = context - .try_update_validator_vote_success_rate(*validator.pubkey(), 98) + .try_update_validator_perf(*validator.pubkey(), 0, 98, 0) .await; assert!(result.is_ok()); @@ -198,12 +204,13 @@ async fn test_update_vote_success_rate() { async fn test_update_uptime() { // Given a Solido context and an active validator: let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; assert!(validator.active); // When an epoch passes, and the validator's uptime is observed: let result = context - .try_update_validator_uptime(*validator.pubkey(), 98) + .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) .await; assert!(result.is_ok()); From 4afb69c0d70e3df373eaab08608d3670b271be6d Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 7 Jun 2023 11:24:30 +0300 Subject: [PATCH 059/131] new error and a test for it --- program/src/error.rs | 3 ++ program/src/processor.rs | 3 +- program/tests/tests/validators_curation.rs | 35 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/program/src/error.rs b/program/src/error.rs index 523b777c7..d60ee5338 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -200,6 +200,9 @@ pub enum LidoError { /// The reserve account address is wrong IncorrectReserveAddress = 58, + + /// Performance data was already updated for the epoch + ValidatorPerfAlreadyUpdatedForEpoch = 59, } // Just reuse the generated Debug impl for Display. It shows the variant names. diff --git a/program/src/processor.rs b/program/src/processor.rs index f421ecb28..dca01400f 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -675,9 +675,10 @@ pub fn process_update_validator_perf( perf.computed_in_epoch ); msg!("It can only be done once per epoch, so we are going to abort this transaction."); - return Err(LidoError::InstructionIsDeprecated.into()); + return Err(LidoError::ValidatorPerfAlreadyUpdatedForEpoch.into()); } + perf.computed_in_epoch = clock.epoch; perf.block_production_rate = block_production_rate; perf.vote_success_rate = vote_success_rate; perf.uptime = uptime; diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 719fc21f8..94f746b39 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -225,6 +225,41 @@ async fn test_update_uptime() { assert!(perf.uptime == 98); } +#[tokio::test] +async fn test_uptime_updates_at_most_once_per_epoch() { + // Given a Solido context and an active validator: + let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); + let validator = &context.get_solido().await.validators.entries[0]; + assert!(validator.active); + + // When the uptime of a validator gets updated: + let result = context + .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) + .await; + assert!(result.is_ok()); + + // And when the uptime of the same validator gets updated again in the same epoch: + let result = context + .try_update_validator_perf(*validator.pubkey(), 0, 0, 99) + .await; + + // Then the second update fails: + assert_solido_error!( + result, + LidoError::ValidatorPerfAlreadyUpdatedForEpoch + ); + + // But when the epoch changes: + context.advance_to_normal_epoch(1); + + // Then the second update succeeds: + let result = context + .try_update_validator_perf(*validator.pubkey(), 0, 0, 99) + .await; + assert!(result.is_ok()); +} + #[tokio::test] async fn test_close_vote_account() { let mut context = Context::new_with_maintainer_and_validator().await; From 89dae8e2dc3deccfc086a209829edb60ab0d0d58 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 7 Jun 2023 19:13:01 +0300 Subject: [PATCH 060/131] metric now count off-chain perf transactions --- cli/maintainer/src/daemon.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index 191f1d7af..6cdb28f00 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -43,8 +43,8 @@ struct MaintenanceMetrics { /// Number of times we performed `UpdateExchangeRate`. transactions_update_exchange_rate: u64, - /// Number of times we performed `UpdateBlockProductionRate`. - transactions_update_block_production_rate: u64, + /// Number of times we performed `UpdateValidatorPerf`. + transactions_update_validator_perf: u64, /// Number of times we performed `UpdateStakeAccountBalance`. transactions_update_stake_account_balance: u64, @@ -125,8 +125,8 @@ impl MaintenanceMetrics { MaintenanceOutput::UpdateExchangeRate => { self.transactions_update_exchange_rate += 1; } - MaintenanceOutput::UpdateBlockProductionRate { .. } => { - self.transactions_update_block_production_rate += 1; + MaintenanceOutput::UpdateValidatorPerf { .. } => { + self.transactions_update_validator_perf += 1; } MaintenanceOutput::UpdateStakeAccountBalance { .. } => { self.transactions_update_stake_account_balance += 1; @@ -301,7 +301,7 @@ impl<'a, 'b> Daemon<'a, 'b> { errors: 0, transactions_stake_deposit: 0, transactions_update_exchange_rate: 0, - transactions_update_block_production_rate: 0, + transactions_update_validator_perf: 0, transactions_update_stake_account_balance: 0, transactions_merge_stake: 0, transactions_unstake_from_inactive_validator: 0, From 4b3c2b56ced14f0773173772a87b400d14042ce9 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 8 Jun 2023 23:09:10 +0300 Subject: [PATCH 061/131] load validator perfs in maintainer --- cli/common/src/snapshot.rs | 20 ++++++++++++++++++++ cli/maintainer/src/maintenance.rs | 1 + 2 files changed, 21 insertions(+) diff --git a/cli/common/src/snapshot.rs b/cli/common/src/snapshot.rs index 574726622..3092636ac 100644 --- a/cli/common/src/snapshot.rs +++ b/cli/common/src/snapshot.rs @@ -348,6 +348,26 @@ impl<'a> Snapshot<'a> { } } + /// Load and parse the validator performance data with the given address. + /// If there is no such data, return `None`. + pub fn get_validator_perf( + &mut self, + address: &Pubkey, + ) -> crate::Result { + let account = self.get_account(address)?; + try_from_slice_unchecked::(&account.data).map_err(|err| { + let error: Error = Box::new(SerializationError { + cause: Some(err.into()), + address: *address, + context: format!( + "Failed to deserialize ValidatorPerf struct, data length is {} bytes.", + account.data.len() + ), + }); + error.into() + }) + } + /// Read the account and deserialize the Solido struct. pub fn get_solido(&mut self, solido_address: &Pubkey) -> crate::Result { let account = self.get_account(solido_address)?; diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index d90c87c1c..99c030980 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -467,6 +467,7 @@ impl SolidoState { let mut validator_identity_account_balances = Vec::new(); let mut validator_vote_accounts = Vec::new(); let mut validator_infos = Vec::new(); + let mut validator_perfs = Vec::new(); for validator in validators.entries.iter() { match config.client.get_account(validator.pubkey()) { Ok(vote_account) => { From f9b783ffcfda551b66047209900b31542fbcebaf Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 9 Jun 2023 11:59:37 +0300 Subject: [PATCH 062/131] maintainer now writes zeroes once in an epoch --- cli/common/src/snapshot.rs | 7 ++--- cli/maintainer/src/maintenance.rs | 45 ++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/cli/common/src/snapshot.rs b/cli/common/src/snapshot.rs index 3092636ac..963ef4ef1 100644 --- a/cli/common/src/snapshot.rs +++ b/cli/common/src/snapshot.rs @@ -46,7 +46,7 @@ use solana_sdk::transaction::Transaction; use solana_transaction_status::{TransactionDetails, UiTransactionEncoding}; use solana_vote_program::vote_state::VoteState; -use lido::state::{AccountList, Lido, ListEntry}; +use lido::state::{AccountList, Lido, ListEntry, ValidatorPerf}; use lido::token::Lamports; use spl_token::solana_program::hash::Hash; @@ -350,10 +350,7 @@ impl<'a> Snapshot<'a> { /// Load and parse the validator performance data with the given address. /// If there is no such data, return `None`. - pub fn get_validator_perf( - &mut self, - address: &Pubkey, - ) -> crate::Result { + pub fn get_validator_perf(&mut self, address: &Pubkey) -> crate::Result { let account = self.get_account(address)?; try_from_slice_unchecked::(&account.data).map_err(|err| { let error: Error = Box::new(SerializationError { diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 99c030980..a02ab1965 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -10,6 +10,7 @@ use std::time::SystemTime; use itertools::izip; +use lido::state::ValidatorPerf; use serde::Serialize; use solana_program::{ clock::{Clock, Slot}, @@ -64,11 +65,13 @@ pub enum MaintenanceOutput { UpdateExchangeRate, - UpdateBlockProductionRate { + UpdateValidatorPerf { // The vote account of the validator we want to update. #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, block_production_rate: u8, + vote_success_rate: u8, + uptime: u8, }, UpdateStakeAccountBalance { @@ -184,13 +187,17 @@ impl fmt::Display for MaintenanceOutput { unstake_withdrawn_to_reserve )?; } - MaintenanceOutput::UpdateBlockProductionRate { + MaintenanceOutput::UpdateValidatorPerf { validator_vote_account, block_production_rate, + vote_success_rate, + uptime, } => { writeln!(f, "Updated block production rate.")?; writeln!(f, " Validator vote account: {}", validator_vote_account)?; writeln!(f, " New block production rate: {}", block_production_rate)?; + writeln!(f, " New vote success rate: {}", vote_success_rate)?; + writeln!(f, " New uptime: {}", uptime)?; } MaintenanceOutput::MergeStake { validator_vote_account, @@ -332,6 +339,7 @@ pub struct SolidoState { /// Parsed list entries from list accounts pub validators: AccountList, + pub validator_perfs: AccountList, pub maintainers: AccountList, /// Threshold for when to consider the end of an epoch. @@ -446,6 +454,9 @@ impl SolidoState { let validators = config .client .get_account_list::(&solido.validator_list)?; + let validator_perfs = config + .client + .get_account_list::(&solido.validator_perf_list)?; let maintainers = config .client .get_account_list::(&solido.maintainer_list)?; @@ -467,7 +478,6 @@ impl SolidoState { let mut validator_identity_account_balances = Vec::new(); let mut validator_vote_accounts = Vec::new(); let mut validator_infos = Vec::new(); - let mut validator_perfs = Vec::new(); for validator in validators.entries.iter() { match config.client.get_account(validator.pubkey()) { Ok(vote_account) => { @@ -552,6 +562,7 @@ impl SolidoState { maintainer_address, stake_time, validators, + validator_perfs, maintainers, end_of_epoch_threshold, }) @@ -905,26 +916,39 @@ impl SolidoState { Some(MaintenanceInstruction::new(instruction, task)) } - /// Tell the program the newest block production rates. - pub fn try_update_block_production_rate(&self) -> Option { + /// Tell the program how well the validators are performing. + pub fn try_update_validator_perfs(&self) -> Option { for validator in self.validators.entries.iter() { - if false { + let perf = self + .validator_perfs + .entries + .iter() + .find(|perf| perf.validator_vote_account_address == *validator.pubkey()); + if perf + .map(|perf| perf.computed_in_epoch >= self.clock.epoch) + .unwrap_or(false) + { + // This validator's performance has already been updated in this epoch, nothing to do. continue; } - let instruction = lido::instruction::update_block_production_rate( + let instruction = lido::instruction::update_validator_perf( &self.solido_program_id, 0, - &lido::instruction::UpdateBlockProductionRateAccountsMeta { + 0, + 0, + &lido::instruction::UpdateValidatorPerfAccountsMeta { lido: self.solido_address, validator_vote_account_to_update: *validator.pubkey(), validator_list: self.solido.validator_list, validator_perf_list: self.solido.validator_perf_list, }, ); - let task = MaintenanceOutput::UpdateBlockProductionRate { + let task = MaintenanceOutput::UpdateValidatorPerf { validator_vote_account: *validator.pubkey(), block_production_rate: 0, + vote_success_rate: 0, + uptime: 0, }; return Some(MaintenanceInstruction::new(instruction, task)); } @@ -1540,7 +1564,7 @@ pub fn try_perform_maintenance( // as possible. .or_else(|| state.try_merge_on_all_stakes()) .or_else(|| state.try_update_exchange_rate()) - .or_else(|| state.try_update_block_production_rate()) + .or_else(|| state.try_update_validator_perfs()) .or_else(|| state.try_unstake_from_inactive_validator()) // Collecting validator fees goes after updating the exchange rate, // because it may be rejected if the exchange rate is outdated. @@ -1615,6 +1639,7 @@ mod test { maintainer_address: Pubkey::new_unique(), stake_time: StakeTime::Anytime, validators: AccountList::::new_default(0), + validator_perfs: AccountList::::new_default(0), maintainers: AccountList::::new_default(0), end_of_epoch_threshold: 95, }; From 980a4a64a71c906b37a5db6186d9174fecd8b5c3 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 13 Jun 2023 23:28:41 +0300 Subject: [PATCH 063/131] make `update_perf` into one --- testlib/src/solido_context.rs | 84 ++++++----------------------------- 1 file changed, 13 insertions(+), 71 deletions(-) diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 9f891b02b..bb991f52d 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1199,17 +1199,21 @@ impl Context { } /// Update the perf account for the given validator with the given reading. - pub async fn try_update_validator_block_production_rate( + pub async fn try_update_validator_perf( &mut self, validator_vote_account: Pubkey, new_block_production_rate: u8, + new_vote_success_rate: u8, + new_uptime: u8, ) -> transport::Result<()> { send_transaction( &mut self.context, - &[instruction::update_block_production_rate( + &[instruction::update_validator_perf( &id(), new_block_production_rate, - &instruction::UpdateBlockProductionRateAccountsMeta { + new_vote_success_rate, + new_uptime, + &instruction::UpdateValidatorPerfAccountsMeta { lido: self.solido.pubkey(), validator_vote_account_to_update: validator_vote_account, validator_list: self.validator_list.pubkey(), @@ -1221,85 +1225,23 @@ impl Context { .await } - pub async fn update_validator_block_production_rate( + pub async fn update_validator_perf( &mut self, validator_vote_account: Pubkey, new_block_production_rate: u8, + new_vote_success_rate: u8, + new_uptime: u8, ) { - self.try_update_validator_block_production_rate( + self.try_update_validator_perf( validator_vote_account, new_block_production_rate, + new_vote_success_rate, + new_uptime, ) .await .expect("Validator performance metrics could always be updated"); } - /// Update the perf account for the given validator with the given reading. - pub async fn try_update_validator_vote_success_rate( - &mut self, - validator_vote_account: Pubkey, - new_vote_success_rate: u8, - ) -> transport::Result<()> { - send_transaction( - &mut self.context, - &[instruction::update_vote_success_rate( - &id(), - new_vote_success_rate, - &instruction::UpdateVoteSuccessRateAccountsMeta { - lido: self.solido.pubkey(), - validator_vote_account_to_update: validator_vote_account, - validator_list: self.validator_list.pubkey(), - validator_perf_list: self.validator_perf_list.pubkey(), - }, - )], - vec![], - ) - .await - } - - pub async fn update_validator_vote_success_rate( - &mut self, - validator_vote_account: Pubkey, - new_vote_success_rate: u8, - ) { - self.try_update_validator_vote_success_rate(validator_vote_account, new_vote_success_rate) - .await - .expect("Validator performance metrics could always be updated"); - } - - /// Update the perf account for the given validator with the given reading. - pub async fn try_update_validator_uptime( - &mut self, - validator_vote_account: Pubkey, - new_uptime: u8, - ) -> transport::Result<()> { - send_transaction( - &mut self.context, - &[instruction::update_uptime( - &id(), - new_uptime, - &instruction::UpdateUptimeAccountsMeta { - lido: self.solido.pubkey(), - validator_vote_account_to_update: validator_vote_account, - validator_list: self.validator_list.pubkey(), - validator_perf_list: self.validator_perf_list.pubkey(), - }, - )], - vec![], - ) - .await - } - - pub async fn update_validator_uptime( - &mut self, - validator_vote_account: Pubkey, - new_vote_success_rate: u8, - ) { - self.try_update_validator_vote_success_rate(validator_vote_account, new_vote_success_rate) - .await - .expect("Validator performance metrics could always be updated"); - } - pub async fn try_get_account(&mut self, address: Pubkey) -> Option { self.context .banks_client From 91bf3f1f9a9442cc35923cd64b42a533a40ee32f Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 14 Jun 2023 10:01:20 +0300 Subject: [PATCH 064/131] `BigVec::iter_mut` --- program/src/big_vec.rs | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/program/src/big_vec.rs b/program/src/big_vec.rs index 12a0bc508..41408fabf 100644 --- a/program/src/big_vec.rs +++ b/program/src/big_vec.rs @@ -133,6 +133,17 @@ impl<'data, T: Pack + Clone> BigVec<'data, T> { } } + /// Get a mutable iterator for the type provided + pub fn iter_mut<'vec>(&'vec mut self) -> IterMut<'data, 'vec, T> { + IterMut { + len: self.len() as usize, + current: 0, + current_index: VEC_SIZE_BYTES, + inner: self, + phantom: PhantomData, + } + } + /// Find matching data in the array pub fn find(&self, data: &[u8], predicate: fn(&[u8], &[u8]) -> bool) -> Option<&T> { let len = self.len() as usize; @@ -178,6 +189,33 @@ impl<'data, 'vec, T: Pack + 'data> Iterator for Iter<'data, 'vec, T> { } } +/// Mutating iterator wrapper over a BigVec +pub struct IterMut<'data, 'vec, T> { + len: usize, + current: usize, + current_index: usize, + inner: &'vec mut BigVec<'data, T>, + phantom: PhantomData, +} + +impl<'data, 'vec, T: Pack + 'data> Iterator for IterMut<'data, 'vec, T> { + type Item = &'data mut T; + + fn next(&mut self) -> Option { + if self.current == self.len { + None + } else { + let end_index = self.current_index + T::LEN; + let value = Some(unsafe { + &mut *(self.inner.data[self.current_index..end_index].as_ptr() as *mut T) + }); + self.current += 1; + self.current_index = end_index; + value + } + } +} + #[cfg(test)] mod tests { use { @@ -246,6 +284,18 @@ mod tests { ); } + #[test] + fn iter_mut() { + let mut data = [0u8; 4 + 8 * 3]; + let mut v = BigVec::new(&mut data); + v.push(TestStruct::new(1)).unwrap(); + v.push(TestStruct::new(2)).unwrap(); + v.push(TestStruct::new(3)).unwrap(); + check_big_vec_eq(&v, &[1, 2, 3]); + v.iter_mut().for_each(|x| x.value += 1); + check_big_vec_eq(&v, &[2, 3, 4]); + } + #[test] fn at_position() { let mut data = [0u8; 4 + 8 * 3]; From d83e555ade7c6581f47118427d883f2e65e9572e Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 14 Jun 2023 10:02:55 +0300 Subject: [PATCH 065/131] `deactivate_if_violates` now only looks at the address, not the index, of the validator --- program/src/instruction.rs | 9 ++------- program/src/process_management.rs | 21 ++++++++++++++++----- program/src/processor.rs | 4 ++-- program/src/state.rs | 4 ++++ testlib/src/solido_context.rs | 3 --- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 4a6e6fc22..4f59e0bea 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -107,11 +107,7 @@ pub enum LidoInstruction { /// or if vote account is closed, then deactivate it /// /// Requires no permission - DeactivateIfViolates { - // Index of a validator in validator list - #[allow(dead_code)] // but it's not - validator_index: u32, - }, + DeactivateIfViolates, /// Set the curation thresholds to control validator's desired performance. /// If validators fall below the threshold they will be deactivated by @@ -1018,12 +1014,11 @@ accounts_struct! { pub fn deactivate_if_violates( program_id: &Pubkey, accounts: &DeactivateIfViolatesMeta, - validator_index: u32, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::DeactivateIfViolates { validator_index }.to_vec(), + data: LidoInstruction::DeactivateIfViolates.to_vec(), } } diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 5dea9aff8..088d8a433 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -139,7 +139,6 @@ pub fn process_deactivate_validator( /// removing the validator once no stake is delegated to it anymore. pub fn process_deactivate_if_violates( program_id: &Pubkey, - validator_index: u32, accounts_raw: &[AccountInfo], ) -> ProgramResult { let accounts = DeactivateIfViolatesInfo::try_from_slice(accounts_raw)?; @@ -159,15 +158,27 @@ pub fn process_deactivate_if_violates( validator_list_data, )?; - let validator = validators.get_mut( - validator_index, - accounts.validator_vote_account_to_deactivate.key, - )?; + // Find the validator in the list of validators. + let validator = validators + .iter_mut() + .find(|validator| validator.pubkey() == accounts.validator_vote_account_to_deactivate.key); + let validator = match validator { + Some(validator) => validator, + None => { + msg!( + "No such validator: {}.", + accounts.validator_vote_account_to_deactivate.key + ); + return Err(LidoError::InvalidAccountInfo.into()); + } + }; + // Nothing to do if the validator is already inactive. if !validator.active { return Ok(()); } + // Find the validator's performance metrics. let validator_perf = validator_perfs.iter().find(|perf| { &perf.validator_vote_account_address == accounts.validator_vote_account_to_deactivate.key }); diff --git a/program/src/processor.rs b/program/src/processor.rs index dca01400f..2fdf5b84a 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1294,8 +1294,8 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::MergeStakeV2 { validator_index } => { process_merge_stake(program_id, validator_index, accounts) } - LidoInstruction::DeactivateIfViolates { validator_index } => { - process_deactivate_if_violates(program_id, validator_index, accounts) + LidoInstruction::DeactivateIfViolates => { + process_deactivate_if_violates(program_id, accounts) } LidoInstruction::ChangeCriteria { new_criteria } => { process_change_criteria(program_id, new_criteria, accounts) diff --git a/program/src/state.rs b/program/src/state.rs index 196aa3f75..ca7f26af7 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -224,6 +224,10 @@ impl<'data, T: ListEntry> BigVecWithHeader<'data, T> { self.big_vec.iter() } + pub fn iter_mut(&'data mut self) -> impl Iterator { + self.big_vec.iter_mut() + } + pub fn find(&'data self, pubkey: &Pubkey) -> Result<&'data T, LidoError> { self.big_vec .find(&pubkey.to_bytes(), T::memcmp_pubkey) diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index bb991f52d..96860f932 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1469,8 +1469,6 @@ impl Context { &mut self, vote_account: Pubkey, ) -> transport::Result<()> { - let solido = self.get_solido().await; - let validator_index = solido.validators.position(&vote_account).unwrap(); send_transaction( &mut self.context, &[lido::instruction::deactivate_if_violates( @@ -1481,7 +1479,6 @@ impl Context { validator_list: self.validator_list.pubkey(), validator_perf_list: self.validator_perf_list.pubkey(), }, - validator_index, )], vec![], ) From e8357c605f19c2512cbb64ce16f2da3a04449578 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 15 Jun 2023 23:14:08 +0300 Subject: [PATCH 066/131] factor out `ValidatorPerf::meets_criteria` --- program/src/process_management.rs | 44 +++++++++++++++---------------- program/src/state.rs | 9 +++++++ 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 088d8a433..96d1d1425 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -178,32 +178,32 @@ pub fn process_deactivate_if_violates( return Ok(()); } - // Find the validator's performance metrics. - let validator_perf = validator_perfs.iter().find(|perf| { - &perf.validator_vote_account_address == accounts.validator_vote_account_to_deactivate.key - }); - - if accounts.validator_vote_account_to_deactivate.owner == &solana_program::vote::program::id() { + let should_deactivate = if accounts.validator_vote_account_to_deactivate.owner + == &solana_program::vote::program::id() + { + // Find the validator's performance metrics. + let validator_perf = validator_perfs.iter().find(|perf| { + &perf.validator_vote_account_address + == accounts.validator_vote_account_to_deactivate.key + }); + + // And its commission. let data = accounts.validator_vote_account_to_deactivate.data.borrow(); let commission = get_vote_account_commission(&data)?; - let does_perform_well = match validator_perf { - Some(validator_perf) => { - validator_perf.block_production_rate - >= (lido.criteria.min_block_production_rate as u64) - && validator_perf.vote_success_rate - >= (lido.criteria.min_vote_success_rate as u64) - && validator_perf.uptime >= (lido.criteria.min_uptime as u64) - } - None => true, - }; - - if commission <= lido.criteria.max_commission && does_perform_well { - // Does not violate. - return Ok(()); - } + // Check if the validator violates the criteria. + let does_perform_well = + validator_perf.map_or(true, |perf| perf.meets_criteria(&lido.criteria)); + let does_perform_well = does_perform_well && commission <= lido.criteria.max_commission; + + // If the validator does not perform well, deactivate it. + !does_perform_well } else { - // The vote account is closed by node operator + // The vote account is closed by node operator. + true + }; + if !should_deactivate { + return Ok(()); } validator.active = false; diff --git a/program/src/state.rs b/program/src/state.rs index ca7f26af7..fe2e1f380 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -385,6 +385,15 @@ pub struct ValidatorPerf { pub uptime: u64, } +impl ValidatorPerf { + /// True only if these metrics meet the criteria. + pub fn meets_criteria(&self, criteria: &Criteria) -> bool { + self.block_production_rate >= (criteria.min_block_production_rate as u64) + && self.vote_success_rate >= (criteria.min_vote_success_rate as u64) + && self.uptime >= (criteria.min_uptime as u64) + } +} + /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS /// THERE'S AN EXTREMELY GOOD REASON. /// From 88bb99bcfbcdd14209612476d7575c310a7b41bf Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 15 Jun 2023 23:26:19 +0300 Subject: [PATCH 067/131] propagate index argument removal to maintainer as well --- cli/maintainer/src/commands_solido.rs | 3 +-- cli/maintainer/src/maintenance.rs | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 01804f059..ff9ee5eec 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -1085,7 +1085,7 @@ pub fn command_deactivate_if_violates( let mut violations = vec![]; let mut instructions = vec![]; - for (validator_index, validator) in validators.entries.iter().enumerate() { + for validator in validators.entries.iter() { let vote_pubkey = validator.pubkey(); let validator_account = config.client.get_account(vote_pubkey)?; let commission = get_vote_account_commission(&validator_account.data) @@ -1104,7 +1104,6 @@ pub fn command_deactivate_if_violates( validator_list: solido.validator_list, validator_perf_list: solido.validator_perf_list, }, - u32::try_from(validator_index).expect("Too many validators"), ); instructions.push(instruction); violations.push(ValidatorViolationInfo { diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index a02ab1965..2bb5510af 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -763,12 +763,11 @@ impl SolidoState { /// If there is a validator which exceeded commission limit or it's vote account is closed, /// try to deactivate it. pub fn try_deactivate_if_violates(&self) -> Option { - for (validator_index, (validator, vote_state)) in self + for (validator, vote_state) in self .validators .entries .iter() .zip(self.validator_vote_accounts.iter()) - .enumerate() { if !validator.active { continue; @@ -795,7 +794,6 @@ impl SolidoState { validator_list: self.solido.validator_list, validator_perf_list: self.solido.validator_perf_list, }, - u32::try_from(validator_index).expect("Too many validators"), ); return Some(MaintenanceInstruction::new(instruction, task)); } From 33bda607b815d504c265bd09df331a06f00a1ff0 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 16 Jun 2023 22:06:59 +0300 Subject: [PATCH 068/131] clearer logging in `update_validator_perf` --- program/src/processor.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 2fdf5b84a..02fefd6cf 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -682,13 +682,13 @@ pub fn process_update_validator_perf( perf.block_production_rate = block_production_rate; perf.vote_success_rate = vote_success_rate; perf.uptime = uptime; - msg!("For validator {}, updated:", validator_vote_account_address); msg!( - "block production rate to {},", - validator_vote_account_address + "Validator {} gets new perf: block_production_rate={}, vote_success_rate={}, uptime={}", + validator_vote_account_address, + block_production_rate, + vote_success_rate, + uptime, ); - msg!("vote success rate to {},", vote_success_rate); - msg!("uptime to {}.", uptime); Ok(()) } From a399d76989ba9733d502118fb491e298e27c7d0b Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 19 Jun 2023 21:26:30 +0300 Subject: [PATCH 069/131] commentary --- program/src/process_management.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 96d1d1425..c70cbdbbd 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -127,6 +127,8 @@ pub fn process_deactivate_validator( accounts.validator_vote_account_to_deactivate.key, )?; + // Mark the validator as inactive so that no new stake can be delegated to it, + // and the existing stake shall be unstaked by the maintainer. validator.active = false; msg!("Validator {} deactivated.", validator.pubkey()); Ok(()) @@ -206,6 +208,8 @@ pub fn process_deactivate_if_violates( return Ok(()); } + // Mark the validator as inactive so that no new stake can be delegated to it, + // and the existing stake shall be unstaked by the maintainer. validator.active = false; msg!("Validator {} deactivated.", validator.pubkey()); From a72a024f2490fa892f658d002e36a9394e2e0ab4 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 22 Jun 2023 09:03:54 +0300 Subject: [PATCH 070/131] factor out `validator.deactivate` to a separate method --- program/src/process_management.rs | 8 ++------ program/src/state.rs | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/program/src/process_management.rs b/program/src/process_management.rs index c70cbdbbd..79c4a1c73 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -127,9 +127,7 @@ pub fn process_deactivate_validator( accounts.validator_vote_account_to_deactivate.key, )?; - // Mark the validator as inactive so that no new stake can be delegated to it, - // and the existing stake shall be unstaked by the maintainer. - validator.active = false; + validator.deactivate(); msg!("Validator {} deactivated.", validator.pubkey()); Ok(()) } @@ -208,9 +206,7 @@ pub fn process_deactivate_if_violates( return Ok(()); } - // Mark the validator as inactive so that no new stake can be delegated to it, - // and the existing stake shall be unstaked by the maintainer. - validator.active = false; + validator.deactivate(); msg!("Validator {} deactivated.", validator.pubkey()); Ok(()) diff --git a/program/src/state.rs b/program/src/state.rs index fe2e1f380..cd3d4952e 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -525,6 +525,12 @@ impl Validator { let authority = [VALIDATOR_STAKE_ACCOUNT, &epoch.to_le_bytes()[..]].concat(); self.find_stake_account_address_with_authority(program_id, solido_account, &authority, seed) } + + /// Mark the validator as inactive so that no new stake can be delegated to it, + /// and the existing stake shall be unstaked by the maintainer. + pub fn deactivate(&mut self) { + self.active = false; + } } impl Sealed for Validator {} From 1ffc88cb4691969f2997ccc4611012b7fd518c3f Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 22 Jun 2023 09:07:38 +0300 Subject: [PATCH 071/131] `does_perform_well` is now shared between the program and the bot --- cli/maintainer/src/maintenance.rs | 13 +++++++++---- program/src/logic.rs | 6 ++++++ program/src/process_management.rs | 7 +------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 2bb5510af..e62a240d5 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -773,13 +773,18 @@ impl SolidoState { continue; } - // We are only interested in validators that violate commission limit - if let Some(state) = vote_state { - if state.commission <= self.solido.criteria.max_commission { + // We are only interested in validators that violate the criteria. + if let Some(vote_state) = vote_state { + if does_perform_well( + &self.solido.criteria, + vote_state.commission, + self.validator_perfs.find(validator.pubkey()), + ) { + // Validator is performing well, no need to deactivate. continue; } } else { - // Vote account is closed + // Vote account is closed -- falling through to deactivation. } let task = MaintenanceOutput::DeactivateIfViolates { diff --git a/program/src/logic.rs b/program/src/logic.rs index 1fa6db94c..ed68bf3b0 100644 --- a/program/src/logic.rs +++ b/program/src/logic.rs @@ -100,6 +100,12 @@ pub fn get_reserve_available_balance( } } +/// True only if the validator meets the criteria. +pub fn does_perform_well(criteria: &Criteria, commission: u8, perf: Option<&ValidatorPerf>) -> bool { + perf.map_or(true, |perf| perf.meets_criteria(criteria)) + && commission <= criteria.max_commission +} + pub struct CreateAccountOptions<'a, 'b> { /// The amount to transfer from the reserve to the new account. pub fund_amount: Lamports, diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 79c4a1c73..b40a05240 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -191,13 +191,8 @@ pub fn process_deactivate_if_violates( let data = accounts.validator_vote_account_to_deactivate.data.borrow(); let commission = get_vote_account_commission(&data)?; - // Check if the validator violates the criteria. - let does_perform_well = - validator_perf.map_or(true, |perf| perf.meets_criteria(&lido.criteria)); - let does_perform_well = does_perform_well && commission <= lido.criteria.max_commission; - // If the validator does not perform well, deactivate it. - !does_perform_well + !does_perform_well(&lido.criteria, commission, validator_perf) } else { // The vote account is closed by node operator. true From f06710351f2021d355e42234307409217c5a3b61 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 22 Jun 2023 09:10:37 +0300 Subject: [PATCH 072/131] use --- cli/maintainer/src/commands_solido.rs | 1 - cli/maintainer/src/maintenance.rs | 4 ++-- program/src/lib.rs | 2 +- program/src/logic.rs | 1 + program/src/process_management.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index ff9ee5eec..6f9e11ceb 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2021 Chorus One AG // SPDX-License-Identifier: GPL-3.0 -use std::convert::TryFrom; use std::{fmt, path::PathBuf}; use serde::Serialize; diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index e62a240d5..035f1c8c3 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -10,7 +10,6 @@ use std::time::SystemTime; use itertools::izip; -use lido::state::ValidatorPerf; use serde::Serialize; use solana_program::{ clock::{Clock, Slot}, @@ -35,10 +34,11 @@ use solido_cli_common::{ use spl_token::state::Mint; use lido::{ + logic::does_perform_well, processor::StakeType, stake_account::StakeAccount, stake_account::{deserialize_stake_account, StakeBalance}, - state::{AccountList, Lido, ListEntry, Maintainer, Validator}, + state::{AccountList, Lido, ListEntry, Maintainer, Validator, ValidatorPerf}, token::Lamports, token::Rational, token::StLamports, diff --git a/program/src/lib.rs b/program/src/lib.rs index f2b9a5204..6469175f6 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -10,7 +10,7 @@ pub mod accounts; pub mod balance; pub mod error; pub mod instruction; -pub(crate) mod logic; +pub mod logic; pub mod metrics; pub(crate) mod process_management; pub mod processor; diff --git a/program/src/logic.rs b/program/src/logic.rs index ed68bf3b0..5a567ccf7 100644 --- a/program/src/logic.rs +++ b/program/src/logic.rs @@ -12,6 +12,7 @@ use solana_program::{ }; use crate::processor::StakeType; +use crate::state::{Criteria, ValidatorPerf}; use crate::STAKE_AUTHORITY; use crate::{ error::LidoError, diff --git a/program/src/process_management.rs b/program/src/process_management.rs index b40a05240..bfc00aff9 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -6,7 +6,7 @@ use solana_program::rent::Rent; use solana_program::sysvar::Sysvar; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; -use crate::logic::check_rent_exempt; +use crate::logic::{check_rent_exempt, does_perform_well}; use crate::processor::StakeType; use crate::state::{Criteria, Lido, ValidatorPerf}; use crate::vote_state::PartialVoteState; From bdbb1857d5b2e580ad082555b811b94f23635af8 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 22 Jun 2023 10:17:23 +0300 Subject: [PATCH 073/131] same but different --- program/src/processor.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 02fefd6cf..8fb40ee40 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -649,23 +649,22 @@ pub fn process_update_validator_perf( validator_perf_list_data, )?; - // Find the existing perf record for the validator: - let perf_index = validator_perfs - .iter() - .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); - // If there is no existing perf record, create a new one: - let perf_index = match perf_index { - None => { - validator_perfs.push(ValidatorPerf { - validator_vote_account_address, - block_production_rate, - ..Default::default() - })?; - (validator_perfs.len() as usize) - 1 + let mut perf = { + let index = validator_perfs + .iter() + .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); + match index { + None => { + validator_perfs.push(ValidatorPerf { + validator_vote_account_address, + ..Default::default() + })?; + validator_perfs.iter_mut().last().unwrap() + } + Some(index) => validator_perfs + .get_mut(index as u32, accounts.validator_vote_account_to_update.key)?, } - Some(index) => index, }; - let mut perf = validator_perfs.get_mut(perf_index as u32, &validator_vote_account_address)?; // Update could happen at most once per epoch: let clock = Clock::get()?; From 1ee9be3c58e1069b0eea8d2359cb5a1088487a08 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 22 Jun 2023 10:17:41 +0300 Subject: [PATCH 074/131] fmt --- program/tests/tests/validators_curation.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 94f746b39..1c4fd37f6 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -245,10 +245,7 @@ async fn test_uptime_updates_at_most_once_per_epoch() { .await; // Then the second update fails: - assert_solido_error!( - result, - LidoError::ValidatorPerfAlreadyUpdatedForEpoch - ); + assert_solido_error!(result, LidoError::ValidatorPerfAlreadyUpdatedForEpoch); // But when the epoch changes: context.advance_to_normal_epoch(1); From 6ab475e7aa17e5a4ebfc77b6b1645d6ea793cfcc Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 23 Jun 2023 09:13:11 +0300 Subject: [PATCH 075/131] stub `reactivate_if_complies` --- program/src/instruction.rs | 39 ++++++++++++++++++++++ program/src/process_management.rs | 9 +++++ program/src/processor.rs | 7 ++-- program/tests/tests/validators_curation.rs | 39 ++++++++++++++++++++++ testlib/src/solido_context.rs | 20 +++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 4f59e0bea..8bb01ef2d 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -109,6 +109,12 @@ pub enum LidoInstruction { /// Requires no permission DeactivateIfViolates, + /// Check if validator lowered their commission back in the allowed range, + /// then activate it back. + /// + /// Requires no permission + ReactivateIfComplies, + /// Set the curation thresholds to control validator's desired performance. /// If validators fall below the threshold they will be deactivated by /// `DeactivateIfViolates`. @@ -1022,6 +1028,39 @@ pub fn deactivate_if_violates( } } +accounts_struct! { + ReactivateIfCompliesMeta, + ReactivateIfCompliesInfo { + pub lido { + is_signer: false, + is_writable: false, + }, + pub validator_vote_account_to_reactivate { + is_signer: false, + is_writable: false, + }, + pub validator_list { + is_signer: false, + is_writable: true, + }, + pub validator_perf_list { + is_signer: false, + is_writable: false, + }, + } +} + +pub fn reactivate_if_complies( + program_id: &Pubkey, + accounts: &ReactivateIfCompliesMeta, +) -> Instruction { + Instruction { + program_id: *program_id, + accounts: accounts.to_vec(), + data: LidoInstruction::ReactivateIfComplies.to_vec(), + } +} + accounts_struct! { ChangeCriteriaMeta, ChangeCriteriaInfo { pub lido { diff --git a/program/src/process_management.rs b/program/src/process_management.rs index bfc00aff9..a2f594d6d 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -207,6 +207,15 @@ pub fn process_deactivate_if_violates( Ok(()) } +/// If necessary, reactivate a validator that was deactivated by +/// `DeactivateIfViolates`. +pub fn process_reactivate_if_complies( + _program_id: &Pubkey, + _accounts_raw: &[AccountInfo], +) -> ProgramResult { + todo!() +} + /// Adds a maintainer to the list of maintainers pub fn process_add_maintainer(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ProgramResult { let accounts = AddMaintainerInfoV2::try_from_slice(accounts_raw)?; diff --git a/program/src/processor.rs b/program/src/processor.rs index 8fb40ee40..0fd607b6d 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -22,8 +22,8 @@ use crate::{ process_management::{ process_add_maintainer, process_add_validator, process_change_criteria, process_change_reward_distribution, process_deactivate_if_violates, - process_deactivate_validator, process_merge_stake, process_remove_maintainer, - process_remove_validator, + process_deactivate_validator, process_merge_stake, process_reactivate_if_complies, + process_remove_maintainer, process_remove_validator, }, stake_account::{deserialize_stake_account, StakeAccount}, state::{ @@ -1296,6 +1296,9 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::DeactivateIfViolates => { process_deactivate_if_violates(program_id, accounts) } + LidoInstruction::ReactivateIfComplies => { + process_reactivate_if_complies(program_id, accounts) + } LidoInstruction::ChangeCriteria { new_criteria } => { process_change_criteria(program_id, new_criteria, accounts) } diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 1c4fd37f6..493379d2c 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -257,6 +257,45 @@ async fn test_uptime_updates_at_most_once_per_epoch() { assert!(result.is_ok()); } +#[tokio::test] +async fn test_bring_back() { + // Given a previously deactivated validator: + let mut context = Context::new_with_maintainer_and_validator().await; + context.advance_to_normal_epoch(0); + let validator = &context.get_solido().await.validators.entries[0]; + + let result = context + .try_change_criteria(&Criteria { + min_uptime: 99, + ..context.criteria + }) + .await; + assert_eq!(result.is_ok(), true); + + let result = context + .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) + .await; + assert_eq!(result.is_ok(), true); + + let result = context.try_deactivate_if_violates(*validator.pubkey()); + assert_eq!(result.await.is_ok(), true); + + let validator = &context.get_solido().await.validators.entries[0]; + assert_eq!(validator.active, false); + + // When the validator's performance is back to normal: + let result = context.try_reactivate_if_complies(*validator.pubkey()); + assert_eq!(result.await.is_ok(), true); + + // And when the instruction is issued: + let result = context.try_reactivate_if_complies(*validator.pubkey()); + assert_eq!(result.await.is_ok(), true); + + // Then the validator is reactivated: + let validator = &context.get_solido().await.validators.entries[0]; + assert_eq!(validator.active, true); +} + #[tokio::test] async fn test_close_vote_account() { let mut context = Context::new_with_maintainer_and_validator().await; diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 96860f932..54d1c9d64 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1485,6 +1485,26 @@ impl Context { .await } + pub async fn try_reactivate_if_complies( + &mut self, + vote_account: Pubkey, + ) -> transport::Result<()> { + send_transaction( + &mut self.context, + &[lido::instruction::reactivate_if_complies( + &id(), + &lido::instruction::ReactivateIfCompliesMeta { + lido: self.solido.pubkey(), + validator_vote_account_to_reactivate: vote_account, + validator_list: self.validator_list.pubkey(), + validator_perf_list: self.validator_perf_list.pubkey(), + }, + )], + vec![], + ) + .await + } + pub async fn try_close_vote_account( &mut self, vote_account: &Pubkey, From 896e86c99ee51983c1c58b010454201670dd0508 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 25 Jun 2023 09:12:30 +0300 Subject: [PATCH 076/131] stub `reactivate_if_complies` --- cli/maintainer/src/maintenance.rs | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 035f1c8c3..96d9bbf25 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -805,6 +805,54 @@ impl SolidoState { None } + /// If a validator is back into the commission limit, try to bring it back. + pub fn try_reactivate_if_complies(&self) -> Option { + for (validator, vote_state) in self + .validators + .entries + .iter() + .zip(self.validator_vote_accounts.iter()) + { + if validator.active { + // Already active, so nothing to do. + continue; + } + + // We are only interested in previously deactivated validators + // that are now performing well. + if let Some(vote_state) = vote_state { + if !does_perform_well( + &self.solido.criteria, + vote_state.commission, + self.validator_perfs.find(validator.pubkey()), + ) { + // Validator is still not performing well, no need to reactivate. + continue; + } + } else { + // Vote account is closed -- nothing to do here. + continue; + } + + let task = MaintenanceOutput::DeactivateIfViolates { + validator_vote_account: *validator.pubkey(), + }; + + let instruction = lido::instruction::deactivate_if_violates( + &self.solido_program_id, + &lido::instruction::DeactivateIfViolatesMeta { + lido: self.solido_address, + validator_vote_account_to_deactivate: *validator.pubkey(), + validator_list: self.solido.validator_list, + validator_perf_list: self.solido.validator_perf_list, + }, + ); + return Some(MaintenanceInstruction::new(instruction, task)); + } + + None + } + /// If there is a validator ready for removal, try to remove it. pub fn try_remove_validator(&self) -> Option { for (validator_index, validator) in self.validators.entries.iter().enumerate() { @@ -1568,6 +1616,7 @@ pub fn try_perform_maintenance( .or_else(|| state.try_merge_on_all_stakes()) .or_else(|| state.try_update_exchange_rate()) .or_else(|| state.try_update_validator_perfs()) + .or_else(|| state.try_reactivate_if_complies()) .or_else(|| state.try_unstake_from_inactive_validator()) // Collecting validator fees goes after updating the exchange rate, // because it may be rejected if the exchange rate is outdated. From 3ef2860c7f6dae39a5f56f8fd05580ea30fc3945 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 25 Jun 2023 09:13:16 +0300 Subject: [PATCH 077/131] split off perf updating into two separate subroutines --- cli/maintainer/src/maintenance.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 96d9bbf25..2c2c8640b 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -1007,6 +1007,12 @@ impl SolidoState { None } + /// Tell the program how well the validators are performing. + pub fn try_update_validator_perfs(&self) -> Option { + None.or_else(|| self.do_update_validator_perfs()) + .or_else(|| self.do_update_validator_commission()) + } + /// Check if any validator's balance is outdated, and if so, update it. /// /// Merging stakes generates inactive stake that could be withdrawn with this transaction, From 2c87316132009f82fb4287e4efca1fcb7aa4963a Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 25 Jun 2023 09:14:10 +0300 Subject: [PATCH 078/131] `SolidoState::is_at_epoch_end` --- cli/maintainer/src/maintenance.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 2c2c8640b..4a4fac0ce 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -1545,6 +1545,25 @@ impl SolidoState { } } + pub fn is_at_epoch_end(&self) -> bool { + let first_slot_in_current_epoch = self + .epoch_schedule + .get_first_slot_in_epoch(self.clock.epoch); + let slots_into_current_epoch = self.clock.slot - first_slot_in_current_epoch; + let slots_per_epoch = self.epoch_schedule.get_slots_in_epoch(self.clock.epoch); + + let epoch_progress = Rational { + numerator: slots_into_current_epoch, + denominator: slots_per_epoch, + }; + let near_epoch_end = Rational { + numerator: self.end_of_epoch_threshold as u64, + denominator: 100, // `end_of_epoch_threshold` argument supplied as a percentage. + }; + + epoch_progress >= near_epoch_end + } + /// Return None if we observe we moved past `1 - /// config.end_of_epoch_threshold`%. Return Some(()) if the above /// condition fails or `self.stake_unstake_any_time` is set to From 659f52478faee226f1534e7b6e56a565adec47f9 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 25 Jun 2023 09:15:04 +0300 Subject: [PATCH 079/131] write down mock data but at the right intervals --- cli/maintainer/src/maintenance.rs | 81 ++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 4a4fac0ce..b78e32f36 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -967,8 +967,69 @@ impl SolidoState { Some(MaintenanceInstruction::new(instruction, task)) } - /// Tell the program how well the validators are performing. - pub fn try_update_validator_perfs(&self) -> Option { + fn do_update_validator_commission(&self) -> Option { + for (validator, vote_state) in self + .validators + .entries + .iter() + .zip(self.validator_vote_accounts.iter()) + { + let commission = vote_state + .as_ref() + .map(|vote_state| vote_state.commission) + .unwrap_or_default(); + + let perf = self + .validator_perfs + .entries + .iter() + .find(|perf| perf.validator_vote_account_address == *validator.pubkey()); + + // We should only overwrite the stored commission + // if it is beyond the allowed range, or if it is the epoch's end. + let should_update = + commission > self.solido.criteria.max_commission || self.is_at_epoch_end(); + if !should_update { + continue; + } + + let ValidatorPerf { + block_production_rate, + vote_success_rate, + uptime, + .. + } = perf.cloned().unwrap_or_default(); + + let instruction = lido::instruction::update_validator_perf( + &self.solido_program_id, + block_production_rate as u8, + vote_success_rate as u8, + uptime as u8, + &lido::instruction::UpdateValidatorPerfAccountsMeta { + lido: self.solido_address, + validator_vote_account_to_update: *validator.pubkey(), + validator_list: self.solido.validator_list, + validator_perf_list: self.solido.validator_perf_list, + }, + ); + let task = MaintenanceOutput::UpdateValidatorPerf { + validator_vote_account: *validator.pubkey(), + block_production_rate: block_production_rate as u8, + vote_success_rate: vote_success_rate as u8, + uptime: uptime as u8, + }; + return Some(MaintenanceInstruction::new(instruction, task)); + } + + None + } + + fn do_update_validator_perfs(&self) -> Option { + if !self.is_at_epoch_end() { + // We only update the off-chain part of the validator performance at the end of the epoch. + return None; + } + for validator in self.validators.entries.iter() { let perf = self .validator_perfs @@ -983,11 +1044,15 @@ impl SolidoState { continue; } + let block_production_rate = 77; + let vote_success_rate = 78; + let uptime = 79; + let instruction = lido::instruction::update_validator_perf( &self.solido_program_id, - 0, - 0, - 0, + block_production_rate, + vote_success_rate, + uptime, &lido::instruction::UpdateValidatorPerfAccountsMeta { lido: self.solido_address, validator_vote_account_to_update: *validator.pubkey(), @@ -997,9 +1062,9 @@ impl SolidoState { ); let task = MaintenanceOutput::UpdateValidatorPerf { validator_vote_account: *validator.pubkey(), - block_production_rate: 0, - vote_success_rate: 0, - uptime: 0, + block_production_rate, + vote_success_rate, + uptime, }; return Some(MaintenanceInstruction::new(instruction, task)); } From b9df2fe438afc44d35c5d63b63a14769786169a2 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 25 Jun 2023 09:55:35 +0300 Subject: [PATCH 080/131] `show-solido` now shows perfs as well --- cli/maintainer/src/commands_solido.rs | 49 ++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 6f9e11ceb..726e8bb02 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -465,8 +465,9 @@ pub struct ShowSolidoOutput { pub mint_authority: Pubkey, /// Validator structure as the program sees it, along with the validator's - /// identity account address, their info, and their commission percentage. - pub validators: Vec<(Validator, Pubkey, ValidatorInfo, u8)>, + /// identity account address, their info, their performance data, + /// and their commission percentage. + pub validators: Vec<(Validator, Pubkey, ValidatorInfo, Option, u8)>, pub validators_max: u32, pub maintainers: Vec, @@ -624,7 +625,7 @@ impl fmt::Display for ShowSolidoOutput { self.validators.len(), self.validators_max, )?; - for (v, identity, info, commission) in self.validators.iter() { + for (v, identity, info, perf, commission) in self.validators.iter() { writeln!( f, "\n - \ @@ -688,6 +689,32 @@ impl fmt::Display for ShowSolidoOutput { .0 )?; } + + writeln!(f, " Performance readings:")?; + if let Some(perf) = perf { + writeln!( + f, + " For the epoch: {}", + perf.computed_in_epoch + )?; + writeln!( + f, + " Block Production Rate: {}/epoch", + perf.block_production_rate + )?; + writeln!( + f, + " Vote Success Rate: {}/epoch", + perf.vote_success_rate + )?; + writeln!( + f, + " Uptime: {}s/epoch", // -- + perf.uptime + )?; + } else { + writeln!(f, " Not yet collected.")?; + } } writeln!(f, "\nMaintainer list {}", self.solido.maintainer_list)?; writeln!( @@ -720,6 +747,9 @@ pub fn command_show_solido( let validators = config .client .get_account_list::(&lido.validator_list)?; + let available_perfs = config + .client + .get_account_list::(&lido.validator_perf_list)?; let validators_max = validators.header.max_entries; let validators = validators.entries; let maintainers = config @@ -731,6 +761,7 @@ pub fn command_show_solido( let mut validator_identities = Vec::new(); let mut validator_infos = Vec::new(); let mut validator_commission_percentages = Vec::new(); + let mut validator_perfs = Vec::new(); for validator in validators.iter() { let vote_state = config.client.get_vote_account(validator.pubkey())?; validator_identities.push(vote_state.node_pubkey); @@ -741,13 +772,23 @@ pub fn command_show_solido( .ok() .ok_or_else(|| CliError::new("Validator account data too small"))?; validator_commission_percentages.push(commission); + // On the chain, the validator's performance is stored in a separate + // account list, and it is written down in "first come, first serve" order. + // But here in the CLI, we join the two lists by validator pubkey, + // so that the two lists have the same indices. + let perf = available_perfs + .entries + .iter() + .find(|perf| &perf.validator_vote_account_address == validator.pubkey()); + validator_perfs.push(perf.cloned()); } let validators = validators .into_iter() .zip(validator_identities.into_iter()) .zip(validator_infos.into_iter()) + .zip(validator_perfs.into_iter()) .zip(validator_commission_percentages.into_iter()) - .map(|(((v, identity), info), commission)| (v, identity, info, commission)) + .map(|((((v, identity), info), perf), commission)| (v, identity, info, perf, commission)) .collect(); Ok(ShowSolidoOutput { From 7f87cea6f79f6943da83807390f7acbab57f62a3 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 28 Jun 2023 23:09:20 +0300 Subject: [PATCH 081/131] split off `update_validator_perf` into two --- program/src/processor.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/program/src/processor.rs b/program/src/processor.rs index 0fd607b6d..d3766d3e3 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -627,6 +627,32 @@ pub fn process_update_exchange_rate( lido.save(accounts.lido) } +/// Mutably get the existing perf for the validator, or create a new one. +fn perf_for( + validator_vote_account_address: Pubkey, + mut validator_perfs: crate::state::BigVecWithHeader<'_, ValidatorPerf>, +) -> Result<&mut ValidatorPerf, ProgramError> { + let mut perf = { + let index = validator_perfs + .iter() + .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); + match index { + None => { + validator_perfs.push(ValidatorPerf { + validator_vote_account_address, + ..Default::default() + })?; + validator_perfs.iter_mut().last().unwrap() + } + Some(index) => { + validator_perfs.get_mut(index as u32, &validator_vote_account_address)? + } + } + }; + Ok(perf) +} + +/// Update the off-chain part of the validator performance metrics. pub fn process_update_validator_perf( program_id: &Pubkey, block_production_rate: u8, @@ -692,6 +718,15 @@ pub fn process_update_validator_perf( Ok(()) } +/// Update the on-chain part of the validator performance metrics. +pub fn process_update_validator_perf_commission( + program_id: &Pubkey, + commission: u8, + raw_accounts: &[AccountInfo], +) -> ProgramResult { + Ok(()) +} + #[derive(PartialEq, Clone, Copy)] pub enum StakeType { Stake, From 5e32ddde82d7d332f1db57575e45b0ae987d8afc Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 29 Jun 2023 23:38:22 +0300 Subject: [PATCH 082/131] separate `OffchainValidatorPerf` into a separate structure --- program/src/instruction.rs | 43 +++++++++++++++++ program/src/processor.rs | 96 ++++++++++++++++++++++++++------------ program/src/state.rs | 44 ++++++++++++----- 3 files changed, 140 insertions(+), 43 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 8bb01ef2d..d3d4d6a17 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -657,6 +657,49 @@ pub fn update_validator_perf( } } +accounts_struct! { + UpdateValidatorPerfCommissionAccountsMeta, + UpdateValidatorPerfCommissionAccountsInfo { + pub lido { + is_signer: false, + is_writable: true, + }, + pub validator_vote_account_to_update { + is_signer: false, + is_writable: false, + }, + pub validator_list { + is_signer: false, + is_writable: false, + }, + pub validator_perf_list { + is_signer: false, + is_writable: true, + }, + + const sysvar_clock = sysvar::clock::id(), + } +} + +pub fn update_validator_perf_commission( + program_id: &Pubkey, + block_production_rate: u8, + vote_success_rate: u8, + uptime: u8, + accounts: &UpdateValidatorPerfCommissionAccountsMeta, +) -> Instruction { + Instruction { + program_id: *program_id, + accounts: accounts.to_vec(), + data: LidoInstruction::UpdateValidatorPerf { + block_production_rate, + vote_success_rate, + uptime, + } + .to_vec(), + } +} + // Changes the Fee spec // The new Fee structure is passed by argument and the recipients are passed here accounts_struct! { diff --git a/program/src/processor.rs b/program/src/processor.rs index d3766d3e3..2e9bce7cf 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -10,7 +10,8 @@ use crate::{ instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, - UpdateStakeAccountBalanceInfo, UpdateValidatorPerfAccountsInfo, WithdrawAccountsInfoV2, + UpdateStakeAccountBalanceInfo, UpdateValidatorPerfAccountsInfo, + UpdateValidatorPerfCommissionAccountsInfo, WithdrawAccountsInfoV2, }, logic::{ burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, @@ -28,10 +29,11 @@ use crate::{ stake_account::{deserialize_stake_account, StakeAccount}, state::{ AccountType, Criteria, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, - MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, ValidatorPerf, - ValidatorPerfList, + MaintainerList, OffchainValidatorPerf, RewardDistribution, StakeDeposit, Validator, + ValidatorList, ValidatorPerf, ValidatorPerfList, }, token::{Lamports, Rational, StLamports}, + vote_state::get_vote_account_commission, MAXIMUM_UNSTAKE_ACCOUNTS, MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, }; @@ -663,9 +665,6 @@ pub fn process_update_validator_perf( let accounts = UpdateValidatorPerfAccountsInfo::try_from_slice(raw_accounts)?; let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - let block_production_rate = block_production_rate as u64; - let vote_success_rate = vote_success_rate as u64; - let uptime = uptime as u64; let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); @@ -675,41 +674,40 @@ pub fn process_update_validator_perf( validator_perf_list_data, )?; - let mut perf = { - let index = validator_perfs - .iter() - .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); - match index { - None => { - validator_perfs.push(ValidatorPerf { - validator_vote_account_address, - ..Default::default() - })?; - validator_perfs.iter_mut().last().unwrap() - } - Some(index) => validator_perfs - .get_mut(index as u32, accounts.validator_vote_account_to_update.key)?, - } - }; + let mut perf = perf_for(validator_vote_account_address, validator_perfs)?; - // Update could happen at most once per epoch: + let data = accounts.validator_vote_account_to_update.data.borrow(); + let commission = get_vote_account_commission(&data)?; + + // Update could happen at most once per epoch, or if the commission worsened: let clock = Clock::get()?; - if perf.computed_in_epoch >= clock.epoch { + if perf + .rest + .map_or(false, |rest| rest.updated_at >= clock.epoch) + { msg!( - "The block production rate was already updated in epoch {}.", - perf.computed_in_epoch + "The perf was already updated in epoch {}.", + perf.rest.unwrap().updated_at ); msg!("It can only be done once per epoch, so we are going to abort this transaction."); return Err(LidoError::ValidatorPerfAlreadyUpdatedForEpoch.into()); } - perf.computed_in_epoch = clock.epoch; - perf.block_production_rate = block_production_rate; - perf.vote_success_rate = vote_success_rate; - perf.uptime = uptime; + let block_production_rate = block_production_rate as u64; + let vote_success_rate = vote_success_rate as u64; + let uptime = uptime as u64; + + perf.rest = Some(OffchainValidatorPerf { + updated_at: clock.epoch, + block_production_rate, + vote_success_rate, + uptime, + }); + msg!( - "Validator {} gets new perf: block_production_rate={}, vote_success_rate={}, uptime={}", + "Validator {} gets new perf: commission={}, block_production_rate={}, vote_success_rate={}, uptime={}", validator_vote_account_address, + commission, block_production_rate, vote_success_rate, uptime, @@ -724,6 +722,42 @@ pub fn process_update_validator_perf_commission( commission: u8, raw_accounts: &[AccountInfo], ) -> ProgramResult { + let accounts = UpdateValidatorPerfCommissionAccountsInfo::try_from_slice(raw_accounts)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + + let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; + + let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); + let mut validator_perfs = lido.deserialize_account_list_info::( + program_id, + accounts.validator_perf_list, + validator_perf_list_data, + )?; + + let mut perf = perf_for(validator_vote_account_address, validator_perfs)?; + + let data = accounts.validator_vote_account_to_update.data.borrow(); + let commission = get_vote_account_commission(&data)?; + + // Update could happen at most once per epoch, or if the commission worsened: + let clock = Clock::get()?; + if perf.commission_updated_at >= clock.epoch { + msg!( + "The commission was already updated in epoch {}.", + perf.commission_updated_at + ); + msg!("It can only be done once per epoch, so we are going to abort this transaction."); + return Err(LidoError::ValidatorPerfAlreadyUpdatedForEpoch.into()); + } + + perf.commission_updated_at = clock.epoch; + perf.commission = commission; + msg!( + "Validator {} gets new commission in their perf: commission={}", + validator_vote_account_address, + commission + ); + Ok(()) } diff --git a/program/src/state.rs b/program/src/state.rs index cd3d4952e..e2939a73d 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -363,17 +363,10 @@ pub struct Validator { /// undeclared alignment-padding in its representation. #[repr(C)] #[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] -pub struct ValidatorPerf { - /// The associated validator's vote account address. - /// It might not be present in the validator list. - /// Do not reorder this field, it should be first in the struct - #[serde(serialize_with = "serialize_b58")] - #[serde(rename = "pubkey")] - pub validator_vote_account_address: Pubkey, - +pub struct OffchainValidatorPerf { /// The epoch in which the off-chain part /// of the validator's performance was computed. - pub computed_in_epoch: Epoch, + pub updated_at: Epoch, /// The number of slots the validator has produced in the last epoch. pub block_production_rate: u64, @@ -385,12 +378,39 @@ pub struct ValidatorPerf { pub uptime: u64, } +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] +pub struct ValidatorPerf { + /// The associated validator's vote account address. + /// It might not be present in the validator list. + /// Do not reorder this field, it should be first in the struct + #[serde(serialize_with = "serialize_b58")] + #[serde(rename = "pubkey")] + pub validator_vote_account_address: Pubkey, + + /// The commission is updated at its own pace. + pub commission: u8, + pub commission_updated_at: Epoch, + + /// The off-chain part of the validator's performance, if available. + pub rest: Option, +} + impl ValidatorPerf { /// True only if these metrics meet the criteria. pub fn meets_criteria(&self, criteria: &Criteria) -> bool { - self.block_production_rate >= (criteria.min_block_production_rate as u64) - && self.vote_success_rate >= (criteria.min_vote_success_rate as u64) - && self.uptime >= (criteria.min_uptime as u64) + self.commission <= criteria.max_commission + && self.rest.map_or(true, |perf| { + perf.vote_success_rate >= (criteria.min_vote_success_rate as u64) + && perf.block_production_rate >= (criteria.min_block_production_rate as u64) + && perf.uptime >= (criteria.min_uptime as u64) + }) } } From fba116f61c8a296ed2a0e1db17521709b00a90e6 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 30 Jun 2023 11:05:53 +0300 Subject: [PATCH 083/131] split off `update_validator_perf` into two --- program/src/instruction.rs | 3 +++ program/src/processor.rs | 50 +++++++++++++++++++------------------- program/src/state.rs | 9 +++---- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index d3d4d6a17..ec33c2126 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -164,6 +164,9 @@ pub enum LidoInstruction { uptime: u8, }, + /// Update the performance metrics for a validator, but only its on-chain part. + UpdateValidatorPerfCommission, + /// Withdraw a given amount of stSOL. /// /// Caller provides some `amount` of StLamports that are to be burned in diff --git a/program/src/processor.rs b/program/src/processor.rs index 2e9bce7cf..cd382bd07 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -630,28 +630,25 @@ pub fn process_update_exchange_rate( } /// Mutably get the existing perf for the validator, or create a new one. -fn perf_for( - validator_vote_account_address: Pubkey, - mut validator_perfs: crate::state::BigVecWithHeader<'_, ValidatorPerf>, -) -> Result<&mut ValidatorPerf, ProgramError> { - let mut perf = { - let index = validator_perfs - .iter() - .position(|perf| perf.validator_vote_account_address == validator_vote_account_address); - match index { - None => { - validator_perfs.push(ValidatorPerf { - validator_vote_account_address, - ..Default::default() - })?; - validator_perfs.iter_mut().last().unwrap() - } - Some(index) => { - validator_perfs.get_mut(index as u32, &validator_vote_account_address)? - } +fn perf_for<'vec>( + validator_vote_account_address: &Pubkey, + validator_perfs: &'vec mut crate::state::BigVecWithHeader<'vec, ValidatorPerf>, +) -> Result<&'vec mut ValidatorPerf, ProgramError> { + let index = validator_perfs + .iter() + .position(|perf| &perf.validator_vote_account_address == validator_vote_account_address); + let element = match index { + None => { + let validator_vote_account_address = validator_vote_account_address.to_owned(); + validator_perfs.push(ValidatorPerf { + validator_vote_account_address, + ..Default::default() + })?; + validator_perfs.iter_mut().last().unwrap() } + Some(index) => validator_perfs.get_mut(index as u32, &validator_vote_account_address)?, }; - Ok(perf) + Ok(element) } /// Update the off-chain part of the validator performance metrics. @@ -674,7 +671,7 @@ pub fn process_update_validator_perf( validator_perf_list_data, )?; - let mut perf = perf_for(validator_vote_account_address, validator_perfs)?; + let mut perf = perf_for(&validator_vote_account_address, &mut validator_perfs)?; let data = accounts.validator_vote_account_to_update.data.borrow(); let commission = get_vote_account_commission(&data)?; @@ -683,11 +680,12 @@ pub fn process_update_validator_perf( let clock = Clock::get()?; if perf .rest + .as_ref() .map_or(false, |rest| rest.updated_at >= clock.epoch) { msg!( "The perf was already updated in epoch {}.", - perf.rest.unwrap().updated_at + perf.rest.as_ref().unwrap().updated_at ); msg!("It can only be done once per epoch, so we are going to abort this transaction."); return Err(LidoError::ValidatorPerfAlreadyUpdatedForEpoch.into()); @@ -719,7 +717,6 @@ pub fn process_update_validator_perf( /// Update the on-chain part of the validator performance metrics. pub fn process_update_validator_perf_commission( program_id: &Pubkey, - commission: u8, raw_accounts: &[AccountInfo], ) -> ProgramResult { let accounts = UpdateValidatorPerfCommissionAccountsInfo::try_from_slice(raw_accounts)?; @@ -734,14 +731,14 @@ pub fn process_update_validator_perf_commission( validator_perf_list_data, )?; - let mut perf = perf_for(validator_vote_account_address, validator_perfs)?; + let mut perf = perf_for(&validator_vote_account_address, &mut validator_perfs)?; let data = accounts.validator_vote_account_to_update.data.borrow(); let commission = get_vote_account_commission(&data)?; // Update could happen at most once per epoch, or if the commission worsened: let clock = Clock::get()?; - if perf.commission_updated_at >= clock.epoch { + if commission <= perf.commission && perf.commission_updated_at >= clock.epoch { msg!( "The commission was already updated in epoch {}.", perf.commission_updated_at @@ -1341,6 +1338,9 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P uptime, accounts, ), + LidoInstruction::UpdateValidatorPerfCommission => { + process_update_validator_perf_commission(program_id, accounts) + } LidoInstruction::WithdrawV2 { amount, validator_index, diff --git a/program/src/state.rs b/program/src/state.rs index e2939a73d..b42932959 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -406,7 +406,7 @@ impl ValidatorPerf { /// True only if these metrics meet the criteria. pub fn meets_criteria(&self, criteria: &Criteria) -> bool { self.commission <= criteria.max_commission - && self.rest.map_or(true, |perf| { + && self.rest.as_ref().map_or(true, |perf| { perf.vote_success_rate >= (criteria.min_vote_success_rate as u64) && perf.block_production_rate >= (criteria.min_block_production_rate as u64) && perf.uptime >= (criteria.min_uptime as u64) @@ -616,10 +616,9 @@ impl Default for ValidatorPerf { fn default() -> Self { ValidatorPerf { validator_vote_account_address: Pubkey::default(), - computed_in_epoch: Epoch::default(), - block_production_rate: u64::MAX as _, - vote_success_rate: u64::MAX as _, - uptime: u64::MAX as _, + commission: 0, + commission_updated_at: Epoch::default(), + rest: None, } } } From 429556f0ba95e85af60dcf505a69f2c9b2b5cf56 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 30 Jun 2023 11:11:58 +0300 Subject: [PATCH 084/131] pass through the validator commission updating to testlib --- testlib/src/solido_context.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 54d1c9d64..c74956d2b 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -1198,6 +1198,33 @@ impl Context { .expect("Failed to withdraw inactive stake."); } + /// Update the commission in the performance readings for the given validator. + pub async fn try_update_validator_perf_commission( + &mut self, + validator_vote_account: Pubkey, + ) -> transport::Result<()> { + send_transaction( + &mut self.context, + &[instruction::update_validator_perf_commission( + &id(), + &instruction::UpdateValidatorPerfCommissionAccountsMeta { + lido: self.solido.pubkey(), + validator_vote_account_to_update: validator_vote_account, + validator_list: self.validator_list.pubkey(), + validator_perf_list: self.validator_perf_list.pubkey(), + }, + )], + vec![], + ) + .await + } + + pub async fn update_validator_perf_commission(&mut self, validator_vote_account: Pubkey) { + self.try_update_validator_perf_commission(validator_vote_account) + .await + .expect("Validator performance metrics could always be updated"); + } + /// Update the perf account for the given validator with the given reading. pub async fn try_update_validator_perf( &mut self, From 76ca2a3f295b4fa3adbc57e61f3123bc51f2a810 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 2 Jul 2023 17:40:07 +0300 Subject: [PATCH 085/131] simpler entrypoint --- program/src/entrypoint.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs index ec7608ede..6138f85d6 100644 --- a/program/src/entrypoint.rs +++ b/program/src/entrypoint.rs @@ -16,9 +16,5 @@ fn process_instruction( accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { - if let Err(error) = processor::process(program_id, accounts, instruction_data) { - Err(error) - } else { - Ok(()) - } + processor::process(program_id, accounts, instruction_data) } From 95c8d5f054099bee648c3c75fd696de88520ce1f Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 2 Jul 2023 17:45:03 +0300 Subject: [PATCH 086/131] `reactivate_if_complies` --- program/src/instruction.rs | 20 ++----- program/src/logic.rs | 9 ++- program/src/process_management.rs | 70 +++++++++++++++++++++- program/src/processor.rs | 12 ++-- program/src/state.rs | 5 ++ program/tests/tests/validators_curation.rs | 43 ++++++++----- 6 files changed, 117 insertions(+), 42 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index ec33c2126..56b9b1e5f 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -155,7 +155,7 @@ pub enum LidoInstruction { UpdateExchangeRateV2, /// Update the off-chain performance metrics for a validator. - UpdateValidatorPerf { + UpdateOffchainValidatorPerf { #[allow(dead_code)] block_production_rate: u8, #[allow(dead_code)] @@ -165,7 +165,7 @@ pub enum LidoInstruction { }, /// Update the performance metrics for a validator, but only its on-chain part. - UpdateValidatorPerfCommission, + UpdateOnchainValidatorPerf, /// Withdraw a given amount of stSOL. /// @@ -641,7 +641,7 @@ accounts_struct! { } } -pub fn update_validator_perf( +pub fn update_offchain_validator_perf( program_id: &Pubkey, block_production_rate: u8, vote_success_rate: u8, @@ -651,7 +651,7 @@ pub fn update_validator_perf( Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::UpdateValidatorPerf { + data: LidoInstruction::UpdateOffchainValidatorPerf { block_production_rate, vote_success_rate, uptime, @@ -684,22 +684,14 @@ accounts_struct! { } } -pub fn update_validator_perf_commission( +pub fn update_onchain_validator_perf( program_id: &Pubkey, - block_production_rate: u8, - vote_success_rate: u8, - uptime: u8, accounts: &UpdateValidatorPerfCommissionAccountsMeta, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::UpdateValidatorPerf { - block_production_rate, - vote_success_rate, - uptime, - } - .to_vec(), + data: LidoInstruction::UpdateOnchainValidatorPerf.to_vec(), } } diff --git a/program/src/logic.rs b/program/src/logic.rs index 5a567ccf7..07c2cdc35 100644 --- a/program/src/logic.rs +++ b/program/src/logic.rs @@ -102,9 +102,12 @@ pub fn get_reserve_available_balance( } /// True only if the validator meets the criteria. -pub fn does_perform_well(criteria: &Criteria, commission: u8, perf: Option<&ValidatorPerf>) -> bool { - perf.map_or(true, |perf| perf.meets_criteria(criteria)) - && commission <= criteria.max_commission +pub fn does_perform_well( + criteria: &Criteria, + commission: u8, + perf: Option<&ValidatorPerf>, +) -> bool { + perf.map_or(true, |perf| perf.meets_criteria(criteria)) && commission <= criteria.max_commission } pub struct CreateAccountOptions<'a, 'b> { diff --git a/program/src/process_management.rs b/program/src/process_management.rs index a2f594d6d..6dcd0e116 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -210,10 +210,74 @@ pub fn process_deactivate_if_violates( /// If necessary, reactivate a validator that was deactivated by /// `DeactivateIfViolates`. pub fn process_reactivate_if_complies( - _program_id: &Pubkey, - _accounts_raw: &[AccountInfo], + program_id: &Pubkey, + accounts_raw: &[AccountInfo], ) -> ProgramResult { - todo!() + let accounts = DeactivateIfViolatesInfo::try_from_slice(accounts_raw)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + + let validator_perf_list_data = &mut *accounts.validator_perf_list.data.borrow_mut(); + let validator_perfs = lido.deserialize_account_list_info::( + program_id, + accounts.validator_perf_list, + validator_perf_list_data, + )?; + + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + + // Find the validator in the list of validators. + let validator = validators + .iter_mut() + .find(|validator| validator.pubkey() == accounts.validator_vote_account_to_deactivate.key); + let validator = match validator { + Some(validator) => validator, + None => { + msg!( + "No such validator: {}.", + accounts.validator_vote_account_to_deactivate.key + ); + return Err(LidoError::InvalidAccountInfo.into()); + } + }; + + // Nothing to do if the validator is already active. + if validator.active { + return Ok(()); + } + + let should_be_inactive = if accounts.validator_vote_account_to_deactivate.owner + == &solana_program::vote::program::id() + { + // Find the validator's performance metrics. + let validator_perf = validator_perfs.iter().find(|perf| { + &perf.validator_vote_account_address + == accounts.validator_vote_account_to_deactivate.key + }); + + // And its commission. + let data = accounts.validator_vote_account_to_deactivate.data.borrow(); + let commission = get_vote_account_commission(&data)?; + + // If the validator does not perform well, deactivate it. + !does_perform_well(&lido.criteria, commission, validator_perf) + } else { + // The vote account is closed by node operator. + true + }; + if should_be_inactive { + // Does not comply with the criteria, so do not reactivate. + return Ok(()); + } + + validator.activate(); + msg!("Validator {} activated back.", validator.pubkey()); + + Ok(()) } /// Adds a maintainer to the list of maintainers diff --git a/program/src/processor.rs b/program/src/processor.rs index cd382bd07..3a71e6f2d 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -652,7 +652,7 @@ fn perf_for<'vec>( } /// Update the off-chain part of the validator performance metrics. -pub fn process_update_validator_perf( +pub fn process_update_offchain_validator_perf( program_id: &Pubkey, block_production_rate: u8, vote_success_rate: u8, @@ -715,7 +715,7 @@ pub fn process_update_validator_perf( } /// Update the on-chain part of the validator performance metrics. -pub fn process_update_validator_perf_commission( +pub fn process_update_onchain_validator_perf( program_id: &Pubkey, raw_accounts: &[AccountInfo], ) -> ProgramResult { @@ -1327,19 +1327,19 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::UpdateStakeAccountBalance { validator_index } => { process_update_stake_account_balance(program_id, validator_index, accounts) } - LidoInstruction::UpdateValidatorPerf { + LidoInstruction::UpdateOffchainValidatorPerf { block_production_rate, vote_success_rate, uptime, - } => process_update_validator_perf( + } => process_update_offchain_validator_perf( program_id, block_production_rate, vote_success_rate, uptime, accounts, ), - LidoInstruction::UpdateValidatorPerfCommission => { - process_update_validator_perf_commission(program_id, accounts) + LidoInstruction::UpdateOnchainValidatorPerf => { + process_update_onchain_validator_perf(program_id, accounts) } LidoInstruction::WithdrawV2 { amount, diff --git a/program/src/state.rs b/program/src/state.rs index b42932959..91cec525c 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -546,6 +546,11 @@ impl Validator { self.find_stake_account_address_with_authority(program_id, solido_account, &authority, seed) } + /// Mark the validator as active so that they could receive new stake. + pub fn activate(&mut self) { + self.active = true; + } + /// Mark the validator as inactive so that no new stake can be delegated to it, /// and the existing stake shall be unstaked by the maintainer. pub fn deactivate(&mut self) { diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 493379d2c..9b310e37a 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -67,7 +67,7 @@ async fn test_curate_by_min_block_production_rate() { // And when the validator's block production rate for the epoch is observed: let result = context - .try_update_validator_perf(*validator.pubkey(), 98, 0, 0) + .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0, 0) .await; assert!(result.is_ok()); @@ -101,7 +101,7 @@ async fn test_curate_by_min_vote_success_rate() { // And when the validator's vote success rate for the epoch is observed: let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 98, 0) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 98, 0) .await; assert!(result.is_ok()); @@ -135,7 +135,7 @@ async fn test_curate_by_min_uptime() { // And when the validator's uptime for the epoch is observed: let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) .await; assert!(result.is_ok()); @@ -160,7 +160,7 @@ async fn test_update_block_production_rate() { // When an epoch passes, and the validator's block production rate is observed: let result = context - .try_update_validator_perf(*validator.pubkey(), 98, 0, 0) + .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0, 0) .await; assert!(result.is_ok()); @@ -172,7 +172,10 @@ async fn test_update_block_production_rate() { .iter() .find(|x| x.validator_vote_account_address == *validator.pubkey()) .unwrap(); - assert!(perf.block_production_rate == 98); + assert!(perf + .rest + .as_ref() + .map_or(false, |x| x.block_production_rate == 98)); } #[tokio::test] @@ -185,7 +188,7 @@ async fn test_update_vote_success_rate() { // When an epoch passes, and the validator's vote success rate is observed: let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 98, 0) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 98, 0) .await; assert!(result.is_ok()); @@ -197,7 +200,10 @@ async fn test_update_vote_success_rate() { .iter() .find(|x| x.validator_vote_account_address == *validator.pubkey()) .unwrap(); - assert!(perf.vote_success_rate == 98); + assert!(perf + .rest + .as_ref() + .map_or(false, |x| x.vote_success_rate == 98)); } #[tokio::test] @@ -210,7 +216,7 @@ async fn test_update_uptime() { // When an epoch passes, and the validator's uptime is observed: let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) .await; assert!(result.is_ok()); @@ -222,7 +228,7 @@ async fn test_update_uptime() { .iter() .find(|x| x.validator_vote_account_address == *validator.pubkey()) .unwrap(); - assert!(perf.uptime == 98); + assert!(perf.rest.as_ref().map_or(false, |x| x.uptime == 98)); } #[tokio::test] @@ -235,13 +241,13 @@ async fn test_uptime_updates_at_most_once_per_epoch() { // When the uptime of a validator gets updated: let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) .await; assert!(result.is_ok()); // And when the uptime of the same validator gets updated again in the same epoch: let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 0, 99) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 99) .await; // Then the second update fails: @@ -252,7 +258,7 @@ async fn test_uptime_updates_at_most_once_per_epoch() { // Then the second update succeeds: let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 0, 99) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 99) .await; assert!(result.is_ok()); } @@ -273,7 +279,7 @@ async fn test_bring_back() { assert_eq!(result.is_ok(), true); let result = context - .try_update_validator_perf(*validator.pubkey(), 0, 0, 98) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) .await; assert_eq!(result.is_ok(), true); @@ -283,9 +289,14 @@ async fn test_bring_back() { let validator = &context.get_solido().await.validators.entries[0]; assert_eq!(validator.active, false); - // When the validator's performance is back to normal: - let result = context.try_reactivate_if_complies(*validator.pubkey()); - assert_eq!(result.await.is_ok(), true); + // When the epoch passes: + context.advance_to_normal_epoch(1); + + // And when the validator's performance is back to normal: + let result = context + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 101) + .await; + assert_eq!(result.is_ok(), true); // And when the instruction is issued: let result = context.try_reactivate_if_complies(*validator.pubkey()); From d2c7a14299507a4e7e915c94ce1f0ae3ab63a6f6 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sun, 2 Jul 2023 21:42:49 +0300 Subject: [PATCH 087/131] normalize naming across instruction-related structs --- program/src/instruction.rs | 11 ++++++----- program/src/processor.rs | 10 +++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 56b9b1e5f..463f3865e 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -619,7 +619,8 @@ pub fn update_exchange_rate( } accounts_struct! { - UpdateValidatorPerfAccountsMeta, UpdateValidatorPerfAccountsInfo { + UpdateOffchainValidatorPerfAccountsMeta, + UpdateOffchainValidatorPerfAccountsInfo { pub lido { is_signer: false, is_writable: true, @@ -646,7 +647,7 @@ pub fn update_offchain_validator_perf( block_production_rate: u8, vote_success_rate: u8, uptime: u8, - accounts: &UpdateValidatorPerfAccountsMeta, + accounts: &UpdateOffchainValidatorPerfAccountsMeta, ) -> Instruction { Instruction { program_id: *program_id, @@ -661,8 +662,8 @@ pub fn update_offchain_validator_perf( } accounts_struct! { - UpdateValidatorPerfCommissionAccountsMeta, - UpdateValidatorPerfCommissionAccountsInfo { + UpdateOnchainValidatorPerfAccountsMeta, + UpdateOnchainValidatorPerfAccountsInfo { pub lido { is_signer: false, is_writable: true, @@ -686,7 +687,7 @@ accounts_struct! { pub fn update_onchain_validator_perf( program_id: &Pubkey, - accounts: &UpdateValidatorPerfCommissionAccountsMeta, + accounts: &UpdateOnchainValidatorPerfAccountsMeta, ) -> Instruction { Instruction { program_id: *program_id, diff --git a/program/src/processor.rs b/program/src/processor.rs index 3a71e6f2d..ea12380d6 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -10,8 +10,8 @@ use crate::{ instruction::{ DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, - UpdateStakeAccountBalanceInfo, UpdateValidatorPerfAccountsInfo, - UpdateValidatorPerfCommissionAccountsInfo, WithdrawAccountsInfoV2, + UpdateOffchainValidatorPerfAccountsInfo, UpdateOnchainValidatorPerfAccountsInfo, + UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, }, logic::{ burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, @@ -646,7 +646,7 @@ fn perf_for<'vec>( })?; validator_perfs.iter_mut().last().unwrap() } - Some(index) => validator_perfs.get_mut(index as u32, &validator_vote_account_address)?, + Some(index) => validator_perfs.get_mut(index as u32, validator_vote_account_address)?, }; Ok(element) } @@ -659,7 +659,7 @@ pub fn process_update_offchain_validator_perf( uptime: u8, raw_accounts: &[AccountInfo], ) -> ProgramResult { - let accounts = UpdateValidatorPerfAccountsInfo::try_from_slice(raw_accounts)?; + let accounts = UpdateOffchainValidatorPerfAccountsInfo::try_from_slice(raw_accounts)?; let lido = Lido::deserialize_lido(program_id, accounts.lido)?; let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; @@ -719,7 +719,7 @@ pub fn process_update_onchain_validator_perf( program_id: &Pubkey, raw_accounts: &[AccountInfo], ) -> ProgramResult { - let accounts = UpdateValidatorPerfCommissionAccountsInfo::try_from_slice(raw_accounts)?; + let accounts = UpdateOnchainValidatorPerfAccountsInfo::try_from_slice(raw_accounts)?; let lido = Lido::deserialize_lido(program_id, accounts.lido)?; let validator_vote_account_address = *accounts.validator_vote_account_to_update.key; From 32a5e7e1e659a6ec73d763b796fdadb00858bad5 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 3 Jul 2023 18:50:00 +0300 Subject: [PATCH 088/131] derive default for `ValidatorPerf` --- program/src/state.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/program/src/state.rs b/program/src/state.rs index 91cec525c..8ea9be060 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -385,7 +385,9 @@ pub struct OffchainValidatorPerf { /// unsafe pointer cast, which means that this structure cannot have any /// undeclared alignment-padding in its representation. #[repr(C)] -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] +#[derive( + Clone, Debug, Default, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, +)] pub struct ValidatorPerf { /// The associated validator's vote account address. /// It might not be present in the validator list. @@ -617,17 +619,6 @@ impl Pack for ValidatorPerf { } } -impl Default for ValidatorPerf { - fn default() -> Self { - ValidatorPerf { - validator_vote_account_address: Pubkey::default(), - commission: 0, - commission_updated_at: Epoch::default(), - rest: None, - } - } -} - impl ListEntry for ValidatorPerf { const TYPE: AccountType = AccountType::ValidatorPerf; From 67208e7860c35a26c6a32f5d5c9dc30db1dd8ab6 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 7 Jul 2023 09:27:37 +0300 Subject: [PATCH 089/131] in SolidoState, join ValidatorPerfs to their validators by index --- cli/listener/src/lib.rs | 2 +- cli/maintainer/src/commands_solido.rs | 23 +++++- cli/maintainer/src/daemon.rs | 19 +++-- cli/maintainer/src/maintenance.rs | 110 ++++++++++++++------------ 4 files changed, 93 insertions(+), 61 deletions(-) diff --git a/cli/listener/src/lib.rs b/cli/listener/src/lib.rs index bc64b599e..5822d3cf4 100644 --- a/cli/listener/src/lib.rs +++ b/cli/listener/src/lib.rs @@ -832,7 +832,7 @@ fn start_http_server(opts: &Opts, metrics_mutex: Arc) -> Vec { self.transactions_update_exchange_rate += 1; } - MaintenanceOutput::UpdateValidatorPerf { .. } => { - self.transactions_update_validator_perf += 1; + MaintenanceOutput::UpdateOffchainValidatorPerf { .. } => { + self.transactions_update_offchain_validator_perf += 1; + } + MaintenanceOutput::UpdateOnchainValidatorPerf { .. } => { + self.transactions_update_onchain_validator_perf += 1; } MaintenanceOutput::UpdateStakeAccountBalance { .. } => { self.transactions_update_stake_account_balance += 1; @@ -301,7 +307,8 @@ impl<'a, 'b> Daemon<'a, 'b> { errors: 0, transactions_stake_deposit: 0, transactions_update_exchange_rate: 0, - transactions_update_validator_perf: 0, + transactions_update_offchain_validator_perf: 0, + transactions_update_onchain_validator_perf: 0, transactions_update_stake_account_balance: 0, transactions_merge_stake: 0, transactions_unstake_from_inactive_validator: 0, @@ -533,7 +540,7 @@ fn start_http_server( for request in server_clone.incoming_requests() { // Ignore any errors; if we fail to respond, then there's little // we can do about it here ... the client should just retry. - let _ = serve_request(request, &*snapshot_mutex_clone); + let _ = serve_request(request, &snapshot_mutex_clone); } }) .expect("Failed to spawn http handler thread.") diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index b78e32f36..7ca69188b 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -65,7 +65,7 @@ pub enum MaintenanceOutput { UpdateExchangeRate, - UpdateValidatorPerf { + UpdateOffchainValidatorPerf { // The vote account of the validator we want to update. #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, @@ -74,6 +74,12 @@ pub enum MaintenanceOutput { uptime: u8, }, + UpdateOnchainValidatorPerf { + // The vote account of the validator we want to update. + #[serde(serialize_with = "serialize_b58")] + validator_vote_account: Pubkey, + }, + UpdateStakeAccountBalance { /// The vote account of the validator that we want to update. #[serde(serialize_with = "serialize_b58")] @@ -187,17 +193,27 @@ impl fmt::Display for MaintenanceOutput { unstake_withdrawn_to_reserve )?; } - MaintenanceOutput::UpdateValidatorPerf { + MaintenanceOutput::UpdateOffchainValidatorPerf { validator_vote_account, block_production_rate, vote_success_rate, uptime, } => { - writeln!(f, "Updated block production rate.")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; + writeln!(f, "Updated off-chain validator performance.")?; + writeln!( + f, + " Validator vote account: {}", + validator_vote_account + )?; writeln!(f, " New block production rate: {}", block_production_rate)?; - writeln!(f, " New vote success rate: {}", vote_success_rate)?; - writeln!(f, " New uptime: {}", uptime)?; + writeln!(f, " New vote success rate: {}", vote_success_rate)?; + writeln!(f, " New uptime: {}", uptime)?; + } + MaintenanceOutput::UpdateOnchainValidatorPerf { + validator_vote_account, + } => { + writeln!(f, "Updated on-chain validator performance.")?; + writeln!(f, " Validator vote account: {}", validator_vote_account)?; } MaintenanceOutput::MergeStake { validator_vote_account, @@ -309,6 +325,10 @@ pub struct SolidoState { /// votes). pub validator_identity_account_balances: Vec, + /// For each validator, in the same order as in `solido.validators`, holds + /// the performance metrics for it. + pub validator_perfs: Vec>, + /// For each validator, in the same order as in `solido.validators`, holds /// the validator info (name and Keybase username). pub validator_infos: Vec, @@ -339,7 +359,6 @@ pub struct SolidoState { /// Parsed list entries from list accounts pub validators: AccountList, - pub validator_perfs: AccountList, pub maintainers: AccountList, /// Threshold for when to consider the end of an epoch. @@ -454,7 +473,7 @@ impl SolidoState { let validators = config .client .get_account_list::(&solido.validator_list)?; - let validator_perfs = config + let all_validator_perfs = config .client .get_account_list::(&solido.validator_perf_list)?; let maintainers = config @@ -477,12 +496,19 @@ impl SolidoState { let mut validator_vote_account_balances = Vec::new(); let mut validator_identity_account_balances = Vec::new(); let mut validator_vote_accounts = Vec::new(); + let mut validator_perfs = Vec::new(); let mut validator_infos = Vec::new(); for validator in validators.entries.iter() { match config.client.get_account(validator.pubkey()) { Ok(vote_account) => { let vote_state = config.client.get_vote_account(validator.pubkey())?; + let maybe_perf = all_validator_perfs + .entries + .iter() + .find(|perf| perf.pubkey() == validator.pubkey()); + validator_perfs.push(maybe_perf.cloned()); + // prometheus validator_vote_account_balances .push(get_account_balance_except_rent(&rent, vote_account)); @@ -550,6 +576,7 @@ impl SolidoState { validator_vote_account_balances, validator_vote_accounts, validator_identity_account_balances, + validator_perfs, validator_infos, maintainer_balances, reserve_address, @@ -562,7 +589,6 @@ impl SolidoState { maintainer_address, stake_time, validators, - validator_perfs, maintainers, end_of_epoch_threshold, }) @@ -763,11 +789,12 @@ impl SolidoState { /// If there is a validator which exceeded commission limit or it's vote account is closed, /// try to deactivate it. pub fn try_deactivate_if_violates(&self) -> Option { - for (validator, vote_state) in self + for (i, (validator, vote_state)) in self .validators .entries .iter() .zip(self.validator_vote_accounts.iter()) + .enumerate() { if !validator.active { continue; @@ -778,7 +805,7 @@ impl SolidoState { if does_perform_well( &self.solido.criteria, vote_state.commission, - self.validator_perfs.find(validator.pubkey()), + self.validator_perfs[i].as_ref(), ) { // Validator is performing well, no need to deactivate. continue; @@ -807,11 +834,12 @@ impl SolidoState { /// If a validator is back into the commission limit, try to bring it back. pub fn try_reactivate_if_complies(&self) -> Option { - for (validator, vote_state) in self + for (i, (validator, vote_state)) in self .validators .entries .iter() .zip(self.validator_vote_accounts.iter()) + .enumerate() { if validator.active { // Already active, so nothing to do. @@ -824,7 +852,7 @@ impl SolidoState { if !does_perform_well( &self.solido.criteria, vote_state.commission, - self.validator_perfs.find(validator.pubkey()), + self.validator_perfs[i].as_ref(), ) { // Validator is still not performing well, no need to reactivate. continue; @@ -967,7 +995,7 @@ impl SolidoState { Some(MaintenanceInstruction::new(instruction, task)) } - fn do_update_validator_commission(&self) -> Option { + fn do_update_onchain_validator_perfs(&self) -> Option { for (validator, vote_state) in self .validators .entries @@ -979,12 +1007,6 @@ impl SolidoState { .map(|vote_state| vote_state.commission) .unwrap_or_default(); - let perf = self - .validator_perfs - .entries - .iter() - .find(|perf| perf.validator_vote_account_address == *validator.pubkey()); - // We should only overwrite the stored commission // if it is beyond the allowed range, or if it is the epoch's end. let should_update = @@ -993,30 +1015,17 @@ impl SolidoState { continue; } - let ValidatorPerf { - block_production_rate, - vote_success_rate, - uptime, - .. - } = perf.cloned().unwrap_or_default(); - - let instruction = lido::instruction::update_validator_perf( + let instruction = lido::instruction::update_onchain_validator_perf( &self.solido_program_id, - block_production_rate as u8, - vote_success_rate as u8, - uptime as u8, - &lido::instruction::UpdateValidatorPerfAccountsMeta { + &lido::instruction::UpdateOnchainValidatorPerfAccountsMeta { lido: self.solido_address, validator_vote_account_to_update: *validator.pubkey(), validator_list: self.solido.validator_list, validator_perf_list: self.solido.validator_perf_list, }, ); - let task = MaintenanceOutput::UpdateValidatorPerf { + let task = MaintenanceOutput::UpdateOnchainValidatorPerf { validator_vote_account: *validator.pubkey(), - block_production_rate: block_production_rate as u8, - vote_success_rate: vote_success_rate as u8, - uptime: uptime as u8, }; return Some(MaintenanceInstruction::new(instruction, task)); } @@ -1024,20 +1033,21 @@ impl SolidoState { None } - fn do_update_validator_perfs(&self) -> Option { + fn do_update_offchain_validator_perfs(&self) -> Option { if !self.is_at_epoch_end() { // We only update the off-chain part of the validator performance at the end of the epoch. return None; } - for validator in self.validators.entries.iter() { - let perf = self - .validator_perfs - .entries - .iter() - .find(|perf| perf.validator_vote_account_address == *validator.pubkey()); + for (i, validator) in self.validators.entries.iter().enumerate() { + let perf = self.validator_perfs[i].as_ref(); if perf - .map(|perf| perf.computed_in_epoch >= self.clock.epoch) + .map(|perf| { + perf.rest + .as_ref() + .map(|rest| rest.updated_at >= self.clock.epoch) + .unwrap_or(false) + }) .unwrap_or(false) { // This validator's performance has already been updated in this epoch, nothing to do. @@ -1048,19 +1058,19 @@ impl SolidoState { let vote_success_rate = 78; let uptime = 79; - let instruction = lido::instruction::update_validator_perf( + let instruction = lido::instruction::update_offchain_validator_perf( &self.solido_program_id, block_production_rate, vote_success_rate, uptime, - &lido::instruction::UpdateValidatorPerfAccountsMeta { + &lido::instruction::UpdateOffchainValidatorPerfAccountsMeta { lido: self.solido_address, validator_vote_account_to_update: *validator.pubkey(), validator_list: self.solido.validator_list, validator_perf_list: self.solido.validator_perf_list, }, ); - let task = MaintenanceOutput::UpdateValidatorPerf { + let task = MaintenanceOutput::UpdateOffchainValidatorPerf { validator_vote_account: *validator.pubkey(), block_production_rate, vote_success_rate, @@ -1074,8 +1084,8 @@ impl SolidoState { /// Tell the program how well the validators are performing. pub fn try_update_validator_perfs(&self) -> Option { - None.or_else(|| self.do_update_validator_perfs()) - .or_else(|| self.do_update_validator_commission()) + None.or_else(|| self.do_update_offchain_validator_perfs()) + .or_else(|| self.do_update_onchain_validator_perfs()) } /// Check if any validator's balance is outdated, and if so, update it. @@ -1769,6 +1779,7 @@ mod test { validator_vote_account_balances: vec![], validator_vote_accounts: vec![], validator_identity_account_balances: vec![], + validator_perfs: vec![], validator_infos: vec![], maintainer_balances: vec![], st_sol_mint: Mint::default(), @@ -1781,7 +1792,6 @@ mod test { maintainer_address: Pubkey::new_unique(), stake_time: StakeTime::Anytime, validators: AccountList::::new_default(0), - validator_perfs: AccountList::::new_default(0), maintainers: AccountList::::new_default(0), end_of_epoch_threshold: 95, }; From d4d1c4066f9099180f35c846428e0cd67be62634 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 10 Jul 2023 18:44:02 +0300 Subject: [PATCH 090/131] update perfs only in its time --- cli/maintainer/src/maintenance.rs | 34 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 7ca69188b..6b8524392 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -996,21 +996,34 @@ impl SolidoState { } fn do_update_onchain_validator_perfs(&self) -> Option { - for (validator, vote_state) in self + for (i, (validator, vote_state)) in self .validators .entries .iter() .zip(self.validator_vote_accounts.iter()) + .enumerate() { - let commission = vote_state - .as_ref() - .map(|vote_state| vote_state.commission) - .unwrap_or_default(); + let Some(vote_state) = vote_state else { + // Vote account is closed, so this validator shall be removed + // by a the subsequent `deactivate_if_violates` step. + continue; + }; + + let maybe_perf = self.validator_perfs[i].as_ref(); + + let reading_expired = maybe_perf + .map_or(true, |perf| self.clock.epoch > perf.commission_updated_at) + && self.is_at_epoch_end(); + + let current_commission = vote_state.commission; + let reading_worsened = + maybe_perf.map_or(false, |perf| current_commission > perf.commission); + + let commission_exceeds_max = current_commission > self.solido.criteria.max_commission; // We should only overwrite the stored commission - // if it is beyond the allowed range, or if it is the epoch's end. - let should_update = - commission > self.solido.criteria.max_commission || self.is_at_epoch_end(); + // if it is beyond the allowed range, or at the epoch's end. + let should_update = reading_expired || reading_worsened || commission_exceeds_max; if !should_update { continue; } @@ -1050,6 +1063,7 @@ impl SolidoState { }) .unwrap_or(false) { + dbg!("already updated"); // This validator's performance has already been updated in this epoch, nothing to do. continue; } @@ -1084,8 +1098,8 @@ impl SolidoState { /// Tell the program how well the validators are performing. pub fn try_update_validator_perfs(&self) -> Option { - None.or_else(|| self.do_update_offchain_validator_perfs()) - .or_else(|| self.do_update_onchain_validator_perfs()) + None.or_else(|| self.do_update_onchain_validator_perfs()) + .or_else(|| self.do_update_offchain_validator_perfs()) } /// Check if any validator's balance is outdated, and if so, update it. From 266c57dac2018483a7fb786313a7a4677c021345 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 11 Jul 2023 16:16:02 +0300 Subject: [PATCH 091/131] use Rust@1.66.1 on CI --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7067960af..03ab7dbb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: submodules: true - uses: actions-rs/toolchain@v1 with: - toolchain: 1.60.0 + toolchain: 1.66.1 override: true components: rustfmt @@ -50,7 +50,7 @@ jobs: submodules: true - uses: actions-rs/toolchain@v1 with: - toolchain: 1.60.0 + toolchain: 1.66.1 override: true - name: cache-build-artifacts @@ -138,7 +138,7 @@ jobs: submodules: true - uses: actions-rs/toolchain@v1 with: - toolchain: 1.60.0 + toolchain: 1.66.1 override: true components: rustfmt, clippy @@ -183,4 +183,4 @@ jobs: - name: Check license compatibility run: | - scripts/check_licenses.py \ No newline at end of file + scripts/check_licenses.py From 3c94f61708b10bf5474e384321aca26402cf8b3e Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 11 Jul 2023 19:10:45 +0300 Subject: [PATCH 092/131] not not remove double negation --- cli/maintainer/src/maintenance.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 6b8524392..2dc42c6fb 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -1054,15 +1054,12 @@ impl SolidoState { for (i, validator) in self.validators.entries.iter().enumerate() { let perf = self.validator_perfs[i].as_ref(); - if perf - .map(|perf| { - perf.rest - .as_ref() - .map(|rest| rest.updated_at >= self.clock.epoch) - .unwrap_or(false) - }) - .unwrap_or(false) - { + let should_update = perf.map_or(true, |perf| { + perf.rest + .as_ref() + .map_or(true, |rest| self.clock.epoch > rest.updated_at) + }); + if !should_update { dbg!("already updated"); // This validator's performance has already been updated in this epoch, nothing to do. continue; From 5a42bc993bcdbb33716c8846d38269a569ebd7f1 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 11 Jul 2023 19:11:20 +0300 Subject: [PATCH 093/131] load block production rates while collecting the snapshot --- cli/common/src/error.rs | 7 +++++++ cli/common/src/snapshot.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/cli/common/src/error.rs b/cli/common/src/error.rs index 44459235b..5623422ab 100644 --- a/cli/common/src/error.rs +++ b/cli/common/src/error.rs @@ -88,6 +88,13 @@ impl AsPrettyError for MaintenanceError { } } +impl AsPrettyError for ParsePubkeyError { + fn print_pretty(&self) { + print_red("Could parse a Pubkey:"); + println!(" {:?}", self); + } +} + /// Something went wrong either while reading CLI arguments, or while using them. /// /// This can be a user error (e.g. an invalid Ledger path), or it can be something diff --git a/cli/common/src/snapshot.rs b/cli/common/src/snapshot.rs index 963ef4ef1..c71856a75 100644 --- a/cli/common/src/snapshot.rs +++ b/cli/common/src/snapshot.rs @@ -216,6 +216,43 @@ impl<'a> Snapshot<'a> { } } + /// Get a map of leader identity pubkeys to a tuple of `(number of leader slots, number of blocks produced)` + /// along with the total number of slots. + pub fn get_all_block_production_rates(&self) -> crate::Result> { + let response = self + .rpc_client + .get_block_production() + .map_err(|err| { + let wrapped_err = Error::from(err); + let result: Error = Box::new(wrapped_err); + result + })? + .value; + + // Map the keys from textual form returned by the RPC to the decoded `Pubkey`, + // and the values to the rate. + let rates_of = response + .by_identity + .into_iter() + .map(|(key, (leader_slots, blocks_produced))| { + Pubkey::from_str(&key).map(|key| { + let rate = if blocks_produced > 0 { + leader_slots / blocks_produced + } else { + 0 + }; + (key, rate) + }) + }) + .collect::, _>>() + .map_err(|err| { + let result: Error = Box::new(err); + result + })?; + + Ok(rates_of) + } + /// Get list of accounts of type T from Solido pub fn get_account_list( &mut self, From 86e67af2f00d4003c5b1aa479cc9cd5762130825 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 12 Jul 2023 17:52:05 +0300 Subject: [PATCH 094/131] block production rates now get written as per-2^64 --- cli/common/src/error.rs | 2 +- cli/common/src/snapshot.rs | 41 ++++++++++++++++++-------------------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/cli/common/src/error.rs b/cli/common/src/error.rs index 5623422ab..ed1e59ed3 100644 --- a/cli/common/src/error.rs +++ b/cli/common/src/error.rs @@ -8,7 +8,7 @@ use solana_client::client_error::{ClientError, ClientErrorKind}; use solana_client::rpc_request::{RpcError, RpcResponseErrorData}; use solana_program::instruction::InstructionError; use solana_program::program_error::ProgramError; -use solana_program::pubkey::PubkeyError; +use solana_program::pubkey::{ParsePubkeyError, PubkeyError}; use solana_sdk::hash::ParseHashError; use solana_sdk::pubkey::Pubkey; use solana_sdk::signer::presigner::PresignerError; diff --git a/cli/common/src/snapshot.rs b/cli/common/src/snapshot.rs index c71856a75..fc807d582 100644 --- a/cli/common/src/snapshot.rs +++ b/cli/common/src/snapshot.rs @@ -23,6 +23,7 @@ //! rare, and when they do happen, they shouldn’t happen repeatedly. use std::collections::{HashMap, HashSet}; +use std::ops::{Div, Mul}; use std::str::FromStr; use std::time::Duration; @@ -46,7 +47,7 @@ use solana_sdk::transaction::Transaction; use solana_transaction_status::{TransactionDetails, UiTransactionEncoding}; use solana_vote_program::vote_state::VoteState; -use lido::state::{AccountList, Lido, ListEntry, ValidatorPerf}; +use lido::state::{AccountList, Lido, ListEntry}; use lido::token::Lamports; use spl_token::solana_program::hash::Hash; @@ -135,6 +136,17 @@ impl std::ops::Deref for OrderedSet { } } +/// Scale the fraction of `numerator / denominator` +/// to the range of `[0..u64::MAX]` and return just the numerator. +fn per64(numerator: u64, denominator: u64) -> u64 { + let numerator = numerator as u128; + let denominator = denominator as u128; + let range_max = u64::MAX as u128; + let result = numerator.mul(range_max).div(denominator); + assert!(result <= range_max); + result as u64 +} + /// A snapshot of one or more accounts. pub struct Snapshot<'a> { /// Addresses, and their values, at the time of the snapshot. @@ -216,9 +228,11 @@ impl<'a> Snapshot<'a> { } } - /// Get a map of leader identity pubkeys to a tuple of `(number of leader slots, number of blocks produced)` - /// along with the total number of slots. - pub fn get_all_block_production_rates(&self) -> crate::Result> { + /// Get a map of validator identities to a numerator of their block production rate. + /// It's like percentage, but it's per-2^64. + pub fn get_all_block_production_rates(&self) -> crate::Result> { + assert!(std::mem::size_of::() >= std::mem::size_of::()); + let response = self .rpc_client .get_block_production() @@ -237,7 +251,7 @@ impl<'a> Snapshot<'a> { .map(|(key, (leader_slots, blocks_produced))| { Pubkey::from_str(&key).map(|key| { let rate = if blocks_produced > 0 { - leader_slots / blocks_produced + per64(leader_slots as u64, blocks_produced as u64) } else { 0 }; @@ -385,23 +399,6 @@ impl<'a> Snapshot<'a> { } } - /// Load and parse the validator performance data with the given address. - /// If there is no such data, return `None`. - pub fn get_validator_perf(&mut self, address: &Pubkey) -> crate::Result { - let account = self.get_account(address)?; - try_from_slice_unchecked::(&account.data).map_err(|err| { - let error: Error = Box::new(SerializationError { - cause: Some(err.into()), - address: *address, - context: format!( - "Failed to deserialize ValidatorPerf struct, data length is {} bytes.", - account.data.len() - ), - }); - error.into() - }) - } - /// Read the account and deserialize the Solido struct. pub fn get_solido(&mut self, solido_address: &Pubkey) -> crate::Result { let account = self.get_account(solido_address)?; From 91648612b0b9f47c86022401e8569f7cf903d5a8 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 12 Jul 2023 18:27:31 +0300 Subject: [PATCH 095/131] a separate file --- cli/common/src/lib.rs | 1 + cli/common/src/per64.rs | 12 ++++++++++++ cli/common/src/snapshot.rs | 13 +------------ 3 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 cli/common/src/per64.rs diff --git a/cli/common/src/lib.rs b/cli/common/src/lib.rs index 41ea12030..937727666 100644 --- a/cli/common/src/lib.rs +++ b/cli/common/src/lib.rs @@ -1,6 +1,7 @@ use snapshot::SnapshotError; pub mod error; +pub mod per64; pub mod prometheus; pub mod snapshot; pub mod validator_info_utils; diff --git a/cli/common/src/per64.rs b/cli/common/src/per64.rs new file mode 100644 index 000000000..66c7953a9 --- /dev/null +++ b/cli/common/src/per64.rs @@ -0,0 +1,12 @@ +use std::ops::{Div, Mul}; + +/// Scale the fraction of `numerator / denominator` +/// to the range of `[0..u64::MAX]` and return just the numerator. +pub fn per64(numerator: u64, denominator: u64) -> u64 { + let numerator = numerator as u128; + let denominator = denominator as u128; + let range_max = u64::MAX as u128; + let result = numerator.mul(range_max).div(denominator); + assert!(result <= range_max); + result as u64 +} diff --git a/cli/common/src/snapshot.rs b/cli/common/src/snapshot.rs index fc807d582..4d120b412 100644 --- a/cli/common/src/snapshot.rs +++ b/cli/common/src/snapshot.rs @@ -23,7 +23,6 @@ //! rare, and when they do happen, they shouldn’t happen repeatedly. use std::collections::{HashMap, HashSet}; -use std::ops::{Div, Mul}; use std::str::FromStr; use std::time::Duration; @@ -54,6 +53,7 @@ use spl_token::solana_program::hash::Hash; use crate::error::{ self, Error, MissingAccountError, MissingValidatorInfoError, SerializationError, }; +use crate::per64::per64; use crate::validator_info_utils::ValidatorInfo; pub enum SnapshotError { @@ -136,17 +136,6 @@ impl std::ops::Deref for OrderedSet { } } -/// Scale the fraction of `numerator / denominator` -/// to the range of `[0..u64::MAX]` and return just the numerator. -fn per64(numerator: u64, denominator: u64) -> u64 { - let numerator = numerator as u128; - let denominator = denominator as u128; - let range_max = u64::MAX as u128; - let result = numerator.mul(range_max).div(denominator); - assert!(result <= range_max); - result as u64 -} - /// A snapshot of one or more accounts. pub struct Snapshot<'a> { /// Addresses, and their values, at the time of the snapshot. From 520307ac0cb99bbe1c59fd0677c9dea7b3f8d214 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 14 Jul 2023 22:11:41 +0300 Subject: [PATCH 096/131] commands related to curation criteria now parse inputs from percentage --- cli/common/src/per64.rs | 52 +++++++++++++++++++++++++++++++ cli/maintainer/src/config.rs | 28 ++++++++--------- cli/maintainer/src/maintenance.rs | 8 ++--- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/cli/common/src/per64.rs b/cli/common/src/per64.rs index 66c7953a9..aa981fe4b 100644 --- a/cli/common/src/per64.rs +++ b/cli/common/src/per64.rs @@ -10,3 +10,55 @@ pub fn per64(numerator: u64, denominator: u64) -> u64 { assert!(result <= range_max); result as u64 } + +/// `per64` equal to `percentage` percents. +pub fn from_percentage(percentage: u64) -> u64 { + per64(percentage, 100) +} + +/// Fraction equal to the given `per64`. +pub fn to_f64(x: u64) -> f64 { + let per64 = x as f64; + let range_max = u64::MAX as f64; + per64 / range_max +} + +/// Parse a string like "50.5" into a `per64`. +pub fn parse_from_fractional_percentage(s: &str) -> Result { + let x = s + .parse::() + .map_err(|_| "expected a percentage, like `6.9`")?; + if x < 0.0 || x > 100.0 { + return Err("expected a percentage between 0 and 100"); + } + Ok(from_percentage((x * 100.0) as u64)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip() { + assert_eq!(per64(0, 100), 0); + assert_eq!(per64(1, 100), 184467440737095516); + assert_eq!(per64(50, 100), 9223372036854775807); + assert_eq!(per64(100, 100), 18446744073709551615); + } + + #[test] + fn percentage() { + assert_eq!(from_percentage(0), 0); + assert_eq!(from_percentage(1), 184467440737095516); + assert_eq!(from_percentage(50), 9223372036854775807); + assert_eq!(from_percentage(100), 18446744073709551615); + } + + #[test] + fn floats() { + assert!((to_f64(0) - 0.00).abs() < 0.001); + assert!((to_f64(184467440737095516) - 0.01).abs() < 0.001); + assert!((to_f64(9223372036854775807) - 0.5).abs() < 0.001); + assert!((to_f64(18446744073709551615) - 1.0).abs() < 0.001); + } +} diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index 07f684062..ebcd28e8e 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -13,7 +13,7 @@ use solana_sdk::pubkey::{ParsePubkeyError, Pubkey}; use lido::token::Lamports; use lido::token::StLamports; -use solido_cli_common::snapshot::OutputMode; +use solido_cli_common::{per64::parse_from_fractional_percentage, snapshot::OutputMode}; pub fn get_option_from_config( name: &'static str, @@ -253,20 +253,20 @@ cli_opt_struct! { max_maintainers: u32, /// The maximum validator fee a validator can have to be accepted by protocol. - #[clap(long, value_name = "int")] + #[clap(long, value_name = "percentage")] max_commission: u8, /// The minimum vote success rate a validator must have to not be deactivated. - #[clap(long, value_name = "int")] - min_vote_success_rate: u8, + #[clap(long, value_name = "percentage", value_parser = parse_from_fractional_percentage)] + min_vote_success_rate: u64, /// The minimum block production rate a validator must have to not be deactivated. - #[clap(long, value_name = "int")] - min_block_production_rate: u8, + #[clap(long, value_name = "percentage", value_parser = parse_from_fractional_percentage)] + min_block_production_rate: u64, /// The minimum block production rate a validator must have to not be deactivated. - #[clap(long, value_name = "int")] - min_uptime: u8, + #[clap(long, value_name = "percentage", value_parser = parse_from_fractional_percentage)] + min_uptime: u64, // See also the docs section of `create-solido` in main.rs for a description // of the fee shares. @@ -502,16 +502,16 @@ cli_opt_struct! { max_commission: u8, /// Min block production rate that a validator must uphold. - #[clap(long, value_name = "rate")] - min_block_production_rate: u8, + #[clap(long, value_name = "percentage", value_parser = parse_from_fractional_percentage)] + min_block_production_rate: u64, /// Min vote success rate that a validator must uphold. - #[clap(long, value_name = "rate")] - min_vote_success_rate: u8, + #[clap(long, value_name = "percentage", value_parser = parse_from_fractional_percentage)] + min_vote_success_rate: u64, /// Min uptime that a validator must maintain. - #[clap(long, value_name = "rate")] - min_uptime: u8, + #[clap(long, value_name = "percentage", value_parser = parse_from_fractional_percentage)] + min_uptime: u64, /// Multisig instance. #[clap(long, value_name = "address")] diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 2dc42c6fb..244cbdab6 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -28,7 +28,7 @@ use solana_sdk::{ }; use solana_vote_program::vote_state::VoteState; use solido_cli_common::{ - error::MaintenanceError, snapshot::SnapshotConfig, snapshot::SnapshotError, + error::MaintenanceError, per64::per64, snapshot::SnapshotConfig, snapshot::SnapshotError, validator_info_utils::ValidatorInfo, Result, }; use spl_token::state::Mint; @@ -69,9 +69,9 @@ pub enum MaintenanceOutput { // The vote account of the validator we want to update. #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, - block_production_rate: u8, - vote_success_rate: u8, - uptime: u8, + block_production_rate: u64, + vote_success_rate: u64, + uptime: u64, }, UpdateOnchainValidatorPerf { From 49146114960bdc55d6ed745e0c4d6d944c38559a Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 14 Jul 2023 23:08:05 +0300 Subject: [PATCH 097/131] `RangeInclusive::contains` --- cli/common/src/per64.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/common/src/per64.rs b/cli/common/src/per64.rs index aa981fe4b..c07459ee3 100644 --- a/cli/common/src/per64.rs +++ b/cli/common/src/per64.rs @@ -28,7 +28,7 @@ pub fn parse_from_fractional_percentage(s: &str) -> Result { let x = s .parse::() .map_err(|_| "expected a percentage, like `6.9`")?; - if x < 0.0 || x > 100.0 { + if !(0.0..=100.0).contains(&x) { return Err("expected a percentage between 0 and 100"); } Ok(from_percentage((x * 100.0) as u64)) From e0dfdf16dc7be7cce667c275867b967ed7040438 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 00:24:34 +0300 Subject: [PATCH 098/131] offchain perfs are now sixty-four-bit deep --- program/src/instruction.rs | 12 ++++++------ program/src/processor.rs | 10 +++------- program/src/state.rs | 21 ++++++++++---------- testlib/src/solido_context.rs | 37 +++++++++++++++++++---------------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 463f3865e..cf2fc7d7a 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -157,11 +157,11 @@ pub enum LidoInstruction { /// Update the off-chain performance metrics for a validator. UpdateOffchainValidatorPerf { #[allow(dead_code)] - block_production_rate: u8, + block_production_rate: u64, #[allow(dead_code)] - vote_success_rate: u8, + vote_success_rate: u64, #[allow(dead_code)] - uptime: u8, + uptime: u64, }, /// Update the performance metrics for a validator, but only its on-chain part. @@ -644,9 +644,9 @@ accounts_struct! { pub fn update_offchain_validator_perf( program_id: &Pubkey, - block_production_rate: u8, - vote_success_rate: u8, - uptime: u8, + block_production_rate: u64, + vote_success_rate: u64, + uptime: u64, accounts: &UpdateOffchainValidatorPerfAccountsMeta, ) -> Instruction { Instruction { diff --git a/program/src/processor.rs b/program/src/processor.rs index ea12380d6..dfd2c8db9 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -654,9 +654,9 @@ fn perf_for<'vec>( /// Update the off-chain part of the validator performance metrics. pub fn process_update_offchain_validator_perf( program_id: &Pubkey, - block_production_rate: u8, - vote_success_rate: u8, - uptime: u8, + block_production_rate: u64, + vote_success_rate: u64, + uptime: u64, raw_accounts: &[AccountInfo], ) -> ProgramResult { let accounts = UpdateOffchainValidatorPerfAccountsInfo::try_from_slice(raw_accounts)?; @@ -691,10 +691,6 @@ pub fn process_update_offchain_validator_perf( return Err(LidoError::ValidatorPerfAlreadyUpdatedForEpoch.into()); } - let block_production_rate = block_production_rate as u64; - let vote_success_rate = vote_success_rate as u64; - let uptime = uptime as u64; - perf.rest = Some(OffchainValidatorPerf { updated_at: clock.epoch, block_production_rate, diff --git a/program/src/state.rs b/program/src/state.rs index 8ea9be060..437b718cb 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -409,9 +409,9 @@ impl ValidatorPerf { pub fn meets_criteria(&self, criteria: &Criteria) -> bool { self.commission <= criteria.max_commission && self.rest.as_ref().map_or(true, |perf| { - perf.vote_success_rate >= (criteria.min_vote_success_rate as u64) - && perf.block_production_rate >= (criteria.min_block_production_rate as u64) - && perf.uptime >= (criteria.min_uptime as u64) + perf.vote_success_rate >= criteria.min_vote_success_rate + && perf.block_production_rate >= criteria.min_block_production_rate + && perf.uptime >= criteria.min_uptime }) } } @@ -794,13 +794,13 @@ pub struct Criteria { pub max_commission: u8, /// If a validator has `block_production_rate` lower than this, then it gets deactivated. - pub min_block_production_rate: u8, + pub min_block_production_rate: u64, /// If a validator has `vote_success_rate` lower than this, then it gets deactivated. - pub min_vote_success_rate: u8, + pub min_vote_success_rate: u64, /// If a validator has the uptime lower than this, then it gets deactivated. - pub min_uptime: u8, + pub min_uptime: u64, } impl Default for Criteria { @@ -817,14 +817,15 @@ impl Default for Criteria { impl Criteria { pub fn new( max_commission: u8, - min_vote_success_rate: u8, - min_block_production_rate: u8, + min_vote_success_rate: u64, + min_block_production_rate: u64, + min_uptime: u64, ) -> Self { Self { max_commission, min_vote_success_rate, min_block_production_rate, - min_uptime: 0, + min_uptime, } } } @@ -1653,7 +1654,7 @@ mod test_lido { developer_account: Pubkey::new_unique(), }, metrics: Metrics::new(), - criteria: Criteria::new(5, 0, 0), + criteria: Criteria::new(5, 0, 0, 0), validator_list: Pubkey::new_unique(), validator_perf_list: Pubkey::new_unique(), maintainer_list: Pubkey::new_unique(), diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index c74956d2b..6af3ccb9b 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -246,7 +246,7 @@ impl Context { stake_authority, mint_authority, deterministic_keypair, - criteria: Criteria::new(5, 0, 0), + criteria: Criteria::new(5, 0, 0, 0), }; result.st_sol_mint = result.create_mint(result.mint_authority).await; @@ -1199,15 +1199,15 @@ impl Context { } /// Update the commission in the performance readings for the given validator. - pub async fn try_update_validator_perf_commission( + pub async fn try_update_onchain_validator_perf( &mut self, validator_vote_account: Pubkey, ) -> transport::Result<()> { send_transaction( &mut self.context, - &[instruction::update_validator_perf_commission( + &[instruction::update_onchain_validator_perf( &id(), - &instruction::UpdateValidatorPerfCommissionAccountsMeta { + &instruction::UpdateOnchainValidatorPerfAccountsMeta { lido: self.solido.pubkey(), validator_vote_account_to_update: validator_vote_account, validator_list: self.validator_list.pubkey(), @@ -1219,28 +1219,31 @@ impl Context { .await } - pub async fn update_validator_perf_commission(&mut self, validator_vote_account: Pubkey) { - self.try_update_validator_perf_commission(validator_vote_account) + pub async fn update_onchain_validator_perf_commission( + &mut self, + validator_vote_account: Pubkey, + ) { + self.try_update_onchain_validator_perf(validator_vote_account) .await .expect("Validator performance metrics could always be updated"); } /// Update the perf account for the given validator with the given reading. - pub async fn try_update_validator_perf( + pub async fn try_update_offchain_validator_perf( &mut self, validator_vote_account: Pubkey, - new_block_production_rate: u8, - new_vote_success_rate: u8, - new_uptime: u8, + new_block_production_rate: u64, + new_vote_success_rate: u64, + new_uptime: u64, ) -> transport::Result<()> { send_transaction( &mut self.context, - &[instruction::update_validator_perf( + &[instruction::update_offchain_validator_perf( &id(), new_block_production_rate, new_vote_success_rate, new_uptime, - &instruction::UpdateValidatorPerfAccountsMeta { + &instruction::UpdateOffchainValidatorPerfAccountsMeta { lido: self.solido.pubkey(), validator_vote_account_to_update: validator_vote_account, validator_list: self.validator_list.pubkey(), @@ -1252,14 +1255,14 @@ impl Context { .await } - pub async fn update_validator_perf( + pub async fn update_offchain_validator_perf( &mut self, validator_vote_account: Pubkey, - new_block_production_rate: u8, - new_vote_success_rate: u8, - new_uptime: u8, + new_block_production_rate: u64, + new_vote_success_rate: u64, + new_uptime: u64, ) { - self.try_update_validator_perf( + self.try_update_offchain_validator_perf( validator_vote_account, new_block_production_rate, new_vote_success_rate, From 7721c3b392c48f21124f6910c13727bb506c8771 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 00:47:21 +0300 Subject: [PATCH 099/131] =?UTF-8?q?Lido::LEN=20=E2=86=92=20474?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- program/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/state.rs b/program/src/state.rs index 437b718cb..21742df97 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -898,7 +898,7 @@ impl Lido { /// Size of a serialized `Lido` struct excluding validators and maintainers. /// /// To update this, run the tests and replace the value here with the test output. - pub const LEN: usize = 453; + pub const LEN: usize = 474; pub fn deserialize_lido(program_id: &Pubkey, lido: &AccountInfo) -> Result { check_account_owner(lido, program_id)?; From f300c47d6198921545d823131172bd01024a2e97 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 01:27:16 +0300 Subject: [PATCH 100/131] commission in the perf now updates at the same pace both in the maintainer and in the program --- program/src/processor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index dfd2c8db9..b15773489 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -734,7 +734,11 @@ pub fn process_update_onchain_validator_perf( // Update could happen at most once per epoch, or if the commission worsened: let clock = Clock::get()?; - if commission <= perf.commission && perf.commission_updated_at >= clock.epoch { + let current_expired = perf.commission_updated_at < clock.epoch; + let new_is_worse = commission > perf.commission; + let new_exceeds_max = commission > lido.criteria.max_commission; + let should_update = new_is_worse || new_exceeds_max || current_expired; + if !should_update { msg!( "The commission was already updated in epoch {}.", perf.commission_updated_at From 4b4c9b682c26b49eec3e8caf1edb65148dbe0eee Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 02:03:31 +0300 Subject: [PATCH 101/131] do not record the commission mid-epoch unless there is a reason in doing so --- program/src/processor.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index b15773489..a4a5fcae2 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -735,9 +735,8 @@ pub fn process_update_onchain_validator_perf( // Update could happen at most once per epoch, or if the commission worsened: let clock = Clock::get()?; let current_expired = perf.commission_updated_at < clock.epoch; - let new_is_worse = commission > perf.commission; - let new_exceeds_max = commission > lido.criteria.max_commission; - let should_update = new_is_worse || new_exceeds_max || current_expired; + let new_exceeds_max = commission > lido.criteria.max_commission && commission > perf.commission; + let should_update = new_exceeds_max || current_expired; if !should_update { msg!( "The commission was already updated in epoch {}.", From 2325cdec4035f17ef0072ffe8f74ea215438c6d1 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 02:20:27 +0300 Subject: [PATCH 102/131] show performance metrics in percentages --- cli/maintainer/src/commands_solido.rs | 25 +++++++++-------- cli/maintainer/src/maintenance.rs | 40 +++++++++++++++++++-------- program/src/processor.rs | 2 +- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 8caeafee8..6153d4c46 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -26,6 +26,7 @@ use lido::{ }; use solido_cli_common::{ error::{CliError, Error}, + per64::to_f64, snapshot::{SnapshotClientConfig, SnapshotConfig}, validator_info_utils::ValidatorInfo, }; @@ -604,18 +605,18 @@ impl fmt::Display for ShowSolidoOutput { )?; writeln!( f, - " Min block production rate: {}/epoch", - self.solido.criteria.min_block_production_rate, + " Min block production rate: {:.2}%", + 100.0 * to_f64(self.solido.criteria.min_block_production_rate), )?; writeln!( f, - " Min vote success rate: {}/epoch", - self.solido.criteria.min_vote_success_rate, + " Min vote success rate: {:.2}%", + 100.0 * to_f64(self.solido.criteria.min_vote_success_rate), )?; writeln!( f, - " Min uptime: {}s/epoch", - self.solido.criteria.min_uptime, + " Min uptime: {:.2}%", + 100.0 * to_f64(self.solido.criteria.min_uptime), )?; writeln!(f, "\nValidator list {}", self.solido.validator_list)?; @@ -699,18 +700,18 @@ impl fmt::Display for ShowSolidoOutput { )?; writeln!( f, - " Block Production Rate: {}/epoch", - perf.block_production_rate + " Block Production Rate: {:.2}%", + 100.0 * to_f64(perf.block_production_rate) )?; writeln!( f, - " Vote Success Rate: {}/epoch", - perf.vote_success_rate + " Vote Success Rate: {:.2}%", + 100.0 * to_f64(perf.vote_success_rate) )?; writeln!( f, - " Uptime: {}s/epoch", // -- - perf.uptime + " Uptime: {:.2}%", // -- + 100.0 * to_f64(perf.uptime) )?; } else { writeln!(f, " Not yet collected.")?; diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 244cbdab6..a82ac2a34 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -28,8 +28,12 @@ use solana_sdk::{ }; use solana_vote_program::vote_state::VoteState; use solido_cli_common::{ - error::MaintenanceError, per64::per64, snapshot::SnapshotConfig, snapshot::SnapshotError, - validator_info_utils::ValidatorInfo, Result, + error::MaintenanceError, + per64::{per64, to_f64}, + snapshot::SnapshotConfig, + snapshot::SnapshotError, + validator_info_utils::ValidatorInfo, + Result, }; use spl_token::state::Mint; @@ -205,9 +209,21 @@ impl fmt::Display for MaintenanceOutput { " Validator vote account: {}", validator_vote_account )?; - writeln!(f, " New block production rate: {}", block_production_rate)?; - writeln!(f, " New vote success rate: {}", vote_success_rate)?; - writeln!(f, " New uptime: {}", uptime)?; + writeln!( + f, + " New block production rate: {:.2}%", + 100.0 * to_f64(*block_production_rate) + )?; + writeln!( + f, + " New vote success rate: {:.2}%", + 100.0 * to_f64(*vote_success_rate) + )?; + writeln!( + f, + " New uptime: {:.2}%", + 100.0 * to_f64(*uptime) + )?; } MaintenanceOutput::UpdateOnchainValidatorPerf { validator_vote_account, @@ -1011,19 +1027,19 @@ impl SolidoState { let maybe_perf = self.validator_perfs[i].as_ref(); - let reading_expired = maybe_perf - .map_or(true, |perf| self.clock.epoch > perf.commission_updated_at) + let expired = maybe_perf + .map_or(true, |perf| perf.commission_updated_at < self.clock.epoch) && self.is_at_epoch_end(); let current_commission = vote_state.commission; - let reading_worsened = - maybe_perf.map_or(false, |perf| current_commission > perf.commission); - - let commission_exceeds_max = current_commission > self.solido.criteria.max_commission; + let exceeds_max = maybe_perf.map_or(false, |perf| { + current_commission > self.solido.criteria.max_commission + && current_commission > perf.commission + }); // We should only overwrite the stored commission // if it is beyond the allowed range, or at the epoch's end. - let should_update = reading_expired || reading_worsened || commission_exceeds_max; + let should_update = expired || exceeds_max; if !should_update { continue; } diff --git a/program/src/processor.rs b/program/src/processor.rs index a4a5fcae2..12fea4add 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -676,7 +676,7 @@ pub fn process_update_offchain_validator_perf( let data = accounts.validator_vote_account_to_update.data.borrow(); let commission = get_vote_account_commission(&data)?; - // Update could happen at most once per epoch, or if the commission worsened: + // Update could happen at most once per epoch: let clock = Clock::get()?; if perf .rest From 5e1d3603308c05f47b942a114b3174f0f09932f6 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 02:21:15 +0300 Subject: [PATCH 103/131] parse percentages from the CLI --- cli/common/src/per64.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/common/src/per64.rs b/cli/common/src/per64.rs index c07459ee3..09449bc54 100644 --- a/cli/common/src/per64.rs +++ b/cli/common/src/per64.rs @@ -25,13 +25,14 @@ pub fn to_f64(x: u64) -> f64 { /// Parse a string like "50.5" into a `per64`. pub fn parse_from_fractional_percentage(s: &str) -> Result { - let x = s + let percentage = s .parse::() .map_err(|_| "expected a percentage, like `6.9`")?; - if !(0.0..=100.0).contains(&x) { + if !(0.0..=100.0).contains(&percentage) { return Err("expected a percentage between 0 and 100"); } - Ok(from_percentage((x * 100.0) as u64)) + let part = percentage / 100.0; + Ok((part * (u64::MAX as f64)) as u64) } #[cfg(test)] From a1236f845a01fdcbaef52263bed973868871a77d Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 02:24:50 +0300 Subject: [PATCH 104/131] pretty --- cli/maintainer/src/maintenance.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index a82ac2a34..bcb7933ae 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -229,7 +229,11 @@ impl fmt::Display for MaintenanceOutput { validator_vote_account, } => { writeln!(f, "Updated on-chain validator performance.")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; + writeln!( + f, + " Validator vote account: {}", + validator_vote_account + )?; } MaintenanceOutput::MergeStake { validator_vote_account, From aa3009deb0e6c713839216d6b9f3fcb2325b3285 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Sat, 15 Jul 2023 16:01:36 +0300 Subject: [PATCH 105/131] clarity --- cli/common/src/per64.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/common/src/per64.rs b/cli/common/src/per64.rs index 09449bc54..fae1e8d20 100644 --- a/cli/common/src/per64.rs +++ b/cli/common/src/per64.rs @@ -32,7 +32,10 @@ pub fn parse_from_fractional_percentage(s: &str) -> Result { return Err("expected a percentage between 0 and 100"); } let part = percentage / 100.0; - Ok((part * (u64::MAX as f64)) as u64) + let range_max = u64::MAX as f64; + let x = part * range_max; + let x = x as u64; + Ok(x) } #[cfg(test)] From d1d6986b7543a0fdf2db67e1557cb6f6947b6a16 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 16:11:08 +0300 Subject: [PATCH 106/131] do not remove inactive validators from the pool automatically --- cli/maintainer/src/maintenance.rs | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index bcb7933ae..4c533bfec 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -901,31 +901,6 @@ impl SolidoState { None } - /// If there is a validator ready for removal, try to remove it. - pub fn try_remove_validator(&self) -> Option { - for (validator_index, validator) in self.validators.entries.iter().enumerate() { - // We are only interested in validators that can be removed. - if validator.check_can_be_removed().is_err() { - continue; - } - let task = MaintenanceOutput::RemoveValidator { - validator_vote_account: *validator.pubkey(), - }; - - let instruction = lido::instruction::remove_validator( - &self.solido_program_id, - &lido::instruction::RemoveValidatorMetaV2 { - lido: self.solido_address, - validator_vote_account_to_remove: *validator.pubkey(), - validator_list: self.solido.validator_list, - }, - u32::try_from(validator_index).expect("Too many validators"), - ); - return Some(MaintenanceInstruction::new(instruction, task)); - } - None - } - /// Get an instruction to merge accounts. fn get_merge_instruction( &self, @@ -1755,8 +1730,7 @@ pub fn try_perform_maintenance( .or_else(|| state.try_update_stake_account_balance()) .or_else(|| state.try_deactivate_if_violates()) .or_else(|| state.try_stake_deposit()) - .or_else(|| state.try_unstake_from_active_validators()) - .or_else(|| state.try_remove_validator()); + .or_else(|| state.try_unstake_from_active_validators()); match instruction_output { Some(maintenance_instruction) => { From f9952ffe04820d10eb78419f9961f314d8296ff8 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 16:16:03 +0300 Subject: [PATCH 107/131] prune dead code --- cli/maintainer/src/daemon.rs | 3 --- cli/maintainer/src/maintenance.rs | 16 +--------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index f814aa362..3d76f24cc 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -143,9 +143,6 @@ impl MaintenanceMetrics { MaintenanceOutput::UnstakeFromInactiveValidator { .. } => { self.transactions_unstake_from_inactive_validator += 1; } - MaintenanceOutput::RemoveValidator { .. } => { - self.transactions_remove_validator += 1; - } MaintenanceOutput::DeactivateIfViolates { .. } => { self.transactions_deactivate_if_violates += 1; } diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 4c533bfec..252c9be9b 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -113,10 +113,6 @@ pub enum MaintenanceOutput { }, UnstakeFromInactiveValidator(Unstake), - RemoveValidator { - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - }, DeactivateIfViolates { #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, @@ -261,20 +257,10 @@ impl fmt::Display for MaintenanceOutput { MaintenanceOutput::UnstakeFromActiveValidator(unstake) => { writeln!(f, "Unstake from active validator\n{}", unstake)?; } - MaintenanceOutput::RemoveValidator { - validator_vote_account, - } => { - writeln!(f, "Remove validator")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; - } MaintenanceOutput::DeactivateIfViolates { validator_vote_account, } => { - writeln!(f, "Check max commission violation.")?; - writeln!( - f, - "Deactivate validator that charges more commission than we allow." - )?; + writeln!(f, "Deactivate a validator that fails to meet our criteria.")?; writeln!(f, " Validator vote account: {}", validator_vote_account)?; } } From e5c43437c1a9f607a45a76c71a984d89e1eaca6c Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 16:17:01 +0300 Subject: [PATCH 108/131] record real BPR and VSR --- cli/maintainer/src/maintenance.rs | 41 +++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 252c9be9b..18d1747cb 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -335,6 +335,10 @@ pub struct SolidoState { /// the performance metrics for it. pub validator_perfs: Vec>, + /// For each validator, in the same order as in `solido.validators`, holds + /// the block production rate scaled by `u64::MAX`. + pub validator_block_production_rates: Vec>, + /// For each validator, in the same order as in `solido.validators`, holds /// the validator info (name and Keybase username). pub validator_infos: Vec, @@ -466,7 +470,7 @@ impl SolidoState { denominator: 10, }; - /// Read the state from the on-chain data. + /// Capture the snapshot of the state related to Solido. pub fn new( config: &mut SnapshotConfig, solido_program_id: &Pubkey, @@ -497,12 +501,15 @@ impl SolidoState { let epoch_schedule = config.client.get_epoch_schedule()?; let stake_history = config.client.get_stake_history()?; + let all_block_production_rates = config.client.get_all_block_production_rates()?; + let mut validator_stake_accounts = Vec::new(); let mut validator_unstake_accounts = Vec::new(); let mut validator_vote_account_balances = Vec::new(); let mut validator_identity_account_balances = Vec::new(); let mut validator_vote_accounts = Vec::new(); let mut validator_perfs = Vec::new(); + let mut validator_block_production_rates = Vec::new(); let mut validator_infos = Vec::new(); for validator in validators.entries.iter() { match config.client.get_account(validator.pubkey()) { @@ -515,6 +522,10 @@ impl SolidoState { .find(|perf| perf.pubkey() == validator.pubkey()); validator_perfs.push(maybe_perf.cloned()); + let maybe_block_production_rate = + all_block_production_rates.get(&vote_state.node_pubkey); + validator_block_production_rates.push(maybe_block_production_rate.cloned()); + // prometheus validator_vote_account_balances .push(get_account_balance_except_rent(&rent, vote_account)); @@ -583,6 +594,7 @@ impl SolidoState { validator_vote_accounts, validator_identity_account_balances, validator_perfs, + validator_block_production_rates, validator_infos, maintainer_balances, reserve_address, @@ -1041,14 +1053,24 @@ impl SolidoState { .map_or(true, |rest| self.clock.epoch > rest.updated_at) }); if !should_update { - dbg!("already updated"); // This validator's performance has already been updated in this epoch, nothing to do. continue; } - let block_production_rate = 77; - let vote_success_rate = 78; - let uptime = 79; + let Some(vote_state) = self.validator_vote_accounts[i].as_ref() else { + // Vote account is closed, so this validator shall be removed + // by a subsequent `deactivate_if_violates` step. + continue; + }; + + let block_production_rate = + self.validator_block_production_rates[i].unwrap_or(u64::MAX); + + let slots_per_epoch = self.epoch_schedule.get_slots_in_epoch(self.clock.epoch); + let vote_success_rate = + per64(vote_state.credits().min(slots_per_epoch), slots_per_epoch); + + let uptime = 0; let instruction = lido::instruction::update_offchain_validator_perf( &self.solido_program_id, @@ -1612,11 +1634,15 @@ impl SolidoState { } } - pub fn is_at_epoch_end(&self) -> bool { + pub fn slots_into_current_epoch(&self) -> u64 { let first_slot_in_current_epoch = self .epoch_schedule .get_first_slot_in_epoch(self.clock.epoch); - let slots_into_current_epoch = self.clock.slot - first_slot_in_current_epoch; + self.clock.slot - first_slot_in_current_epoch + } + + pub fn is_at_epoch_end(&self) -> bool { + let slots_into_current_epoch = self.slots_into_current_epoch(); let slots_per_epoch = self.epoch_schedule.get_slots_in_epoch(self.clock.epoch); let epoch_progress = Rational { @@ -1771,6 +1797,7 @@ mod test { validator_vote_accounts: vec![], validator_identity_account_balances: vec![], validator_perfs: vec![], + validator_block_production_rates: vec![], validator_infos: vec![], maintainer_balances: vec![], st_sol_mint: Mint::default(), From 34c7f6bbbd57c8fdacd10f0bd7ad8a73390832d0 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 21:58:56 +0300 Subject: [PATCH 109/131] add `reactivate_if_complies` to stats as well --- cli/maintainer/src/daemon.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index 3d76f24cc..e583254da 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -67,6 +67,9 @@ struct MaintenanceMetrics { /// Number of times we performed `DeactivateIfViolates`. transactions_deactivate_if_violates: u64, + /// Number of times we performed `DeactivateIfViolates`. + transactions_reactivate_if_complies: u64, + /// Number of times we performed `Unstake` on an active validator for balancing purposes. transactions_unstake_from_active_validator: u64, } @@ -146,6 +149,9 @@ impl MaintenanceMetrics { MaintenanceOutput::DeactivateIfViolates { .. } => { self.transactions_deactivate_if_violates += 1; } + MaintenanceOutput::ReactivateIfComplies { .. } => { + self.transactions_reactivate_if_complies += 1; + } MaintenanceOutput::UnstakeFromActiveValidator { .. } => { self.transactions_unstake_from_active_validator += 1; } @@ -311,6 +317,7 @@ impl<'a, 'b> Daemon<'a, 'b> { transactions_unstake_from_inactive_validator: 0, transactions_remove_validator: 0, transactions_deactivate_if_violates: 0, + transactions_reactivate_if_complies: 0, transactions_unstake_from_active_validator: 0, }; Daemon { From 3ac5657a68bcc9299fe782c7a1301f39ff7ca1f2 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 21:59:48 +0300 Subject: [PATCH 110/131] rework `reactivate_if_complies` --- cli/maintainer/src/maintenance.rs | 75 +++++++++++++++++-------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 18d1747cb..2651c91be 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -117,6 +117,10 @@ pub enum MaintenanceOutput { #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, }, + ReactivateIfComplies { + #[serde(serialize_with = "serialize_b58")] + validator_vote_account: Pubkey, + }, UnstakeFromActiveValidator(Unstake), } @@ -263,6 +267,12 @@ impl fmt::Display for MaintenanceOutput { writeln!(f, "Deactivate a validator that fails to meet our criteria.")?; writeln!(f, " Validator vote account: {}", validator_vote_account)?; } + MaintenanceOutput::ReactivateIfComplies { + validator_vote_account, + } => { + writeln!(f, "Reactivate a validator that meets our criteria.")?; + writeln!(f, " Validator vote account: {}", validator_vote_account)?; + } } Ok(()) } @@ -852,6 +862,11 @@ impl SolidoState { /// If a validator is back into the commission limit, try to bring it back. pub fn try_reactivate_if_complies(&self) -> Option { + if !self.is_at_epoch_end() { + // We only try to reactivate them at the end of the epoch. + return None; + } + for (i, (validator, vote_state)) in self .validators .entries @@ -859,41 +874,33 @@ impl SolidoState { .zip(self.validator_vote_accounts.iter()) .enumerate() { - if validator.active { - // Already active, so nothing to do. - continue; - } - - // We are only interested in previously deactivated validators - // that are now performing well. - if let Some(vote_state) = vote_state { - if !does_perform_well( - &self.solido.criteria, - vote_state.commission, - self.validator_perfs[i].as_ref(), - ) { - // Validator is still not performing well, no need to reactivate. - continue; - } - } else { - // Vote account is closed -- nothing to do here. - continue; + // We are only interested in validators that are inactive, + // and are now performing well. + // + // If the vote account is closed, no need to reactivate. + if !validator.active + && vote_state.as_ref().map_or(false, |vote_state| { + does_perform_well( + &self.solido.criteria, + vote_state.commission, + self.validator_perfs[i].as_ref(), + ) + }) + { + let task = MaintenanceOutput::ReactivateIfComplies { + validator_vote_account: *validator.pubkey(), + }; + let instruction = lido::instruction::reactivate_if_complies( + &self.solido_program_id, + &lido::instruction::ReactivateIfCompliesMeta { + lido: self.solido_address, + validator_vote_account_to_reactivate: *validator.pubkey(), + validator_list: self.solido.validator_list, + validator_perf_list: self.solido.validator_perf_list, + }, + ); + return Some(MaintenanceInstruction::new(instruction, task)); } - - let task = MaintenanceOutput::DeactivateIfViolates { - validator_vote_account: *validator.pubkey(), - }; - - let instruction = lido::instruction::deactivate_if_violates( - &self.solido_program_id, - &lido::instruction::DeactivateIfViolatesMeta { - lido: self.solido_address, - validator_vote_account_to_deactivate: *validator.pubkey(), - validator_list: self.solido.validator_list, - validator_perf_list: self.solido.validator_perf_list, - }, - ); - return Some(MaintenanceInstruction::new(instruction, task)); } None From 4d15a300f50bf032fca8cd9daf799f8f621ee8d1 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 22:07:21 +0300 Subject: [PATCH 111/131] pythonesque, also --- scripts/deploy_local_instance.py | 19 ++++++++++++++++++- solido_testnet_config.json | 8 -------- 2 files changed, 18 insertions(+), 9 deletions(-) delete mode 100644 solido_testnet_config.json diff --git a/scripts/deploy_local_instance.py b/scripts/deploy_local_instance.py index 39d018c16..7768487ca 100755 --- a/scripts/deploy_local_instance.py +++ b/scripts/deploy_local_instance.py @@ -59,8 +59,14 @@ def __init__(self) -> None: '9', '--max-maintainers', '3', - '--max-commission-percentage', + '--max-commission', str(util.MAX_VALIDATION_COMMISSION_PERCENTAGE), + '--min-block-production-rate', + '0', + '--min-vote-success-rate', + '0', + '--min-uptime', + '0', '--treasury-fee-share', '5', '--developer-fee-share', @@ -173,6 +179,17 @@ def __init__(self) -> None: output = { "cluster": util.get_network(), + + "max_commission": "5", + "treasury_fee_share": "1", + "developer_fee_share": "1", + "max_validators": "256", + "max_maintainers": "16", + "st_sol_appreciation_share": "1", + + "treasury_account_owner": util.solana('address').strip(), + "developer_account_owner": util.solana('address').strip(), + "multisig_program_id": self.multisig_program_id, "multisig_address": self.multisig_instance, "solido_program_id": self.solido_program_id, diff --git a/solido_testnet_config.json b/solido_testnet_config.json deleted file mode 100644 index e6c384099..000000000 --- a/solido_testnet_config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "cluster": "https://api.testnet.solana.com", - "multisig_program_id": "AM9wZbsi3BaTYpgXWpRW7y78iBqjqEgwvu48RkAydNPo", - "multisig_address": "9dsg6WJjdK3sxvfs72Nu1B2CsqVk17L4arjFUxpZVEZ4", - "solido_program_id": "79u3pW4w3avGnayP5JysRAWAmPi6RhMr81psKRMEbmGj", - "solido_address": "AbEGtj1dLmJ9yMvQyYFisEerJBU4g3v273kSSzJ3Jtvk", - "st_sol_mint": "EvNUCAwhosjHQnbzbQdKL2dUsQcipbLgV5aEzxdeVQ4g" -} \ No newline at end of file From b40552848a952bb3f8e7ed1db06c64ecd520a502 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 22:39:11 +0300 Subject: [PATCH 112/131] add criteria setup to integration tests as well --- scripts/deploy_local_instance.py | 4 ++-- scripts/test_solido.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/deploy_local_instance.py b/scripts/deploy_local_instance.py index 7768487ca..6dc0e38a9 100755 --- a/scripts/deploy_local_instance.py +++ b/scripts/deploy_local_instance.py @@ -110,11 +110,11 @@ def __init__(self) -> None: # validator that is actually voting, and earning rewards. current_validators = json.loads(util.solana('validators', '--output', 'json')) - # If we're running on localhost, change the comission + # If we're running on localhost, change the commission if util.get_network() == 'http://127.0.0.1:8899': solido_instance = self.pull_solido() print( - '> Changing validator\'s comission to {}% ...'.format( + '> Changing validator\'s commission to {}% ...'.format( util.MAX_VALIDATION_COMMISSION_PERCENTAGE ) ) diff --git a/scripts/test_solido.py b/scripts/test_solido.py index 34af3b26a..531bf74a8 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -19,7 +19,6 @@ from util import ( TestAccount, - create_spl_token_account, create_test_account, create_vote_account, get_solido_program_path, @@ -143,8 +142,14 @@ def approve_and_execute(transaction_to_approve: str, signer: TestAccount) -> Non '9', '--max-maintainers', '1', - '--max-commission-percentage', + '--max-commission', str(MAX_VALIDATION_COMMISSION_PERCENTAGE), + '--min-block-production-rate', + '0', + '--min-vote-success-rate', + '0', + '--min-uptime', + '0', '--treasury-fee-share', '5', '--developer-fee-share', @@ -177,8 +182,14 @@ def approve_and_execute(transaction_to_approve: str, signer: TestAccount) -> Non '9', '--max-maintainers', '1', - '--max-commission-percentage', + '--max-commission', str(MAX_VALIDATION_COMMISSION_PERCENTAGE), + '--min-block-production-rate', + '0', + '--min-vote-success-rate', + '0', + '--min-uptime', + '0', '--treasury-fee-share', '5', '--developer-fee-share', From a3cdc556b5ed9f64f2cc51378f04ccd253860f59 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 23:27:14 +0300 Subject: [PATCH 113/131] factor out rich validator into a separate struct --- cli/maintainer/src/commands_solido.rs | 105 ++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 14 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 6153d4c46..13cef254f 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -16,8 +16,8 @@ use lido::{ metrics::LamportsHistogram, processor::StakeType, state::{ - AccountList, Criteria, Lido, ListEntry, Maintainer, RewardDistribution, Validator, - ValidatorPerf, + AccountList, Criteria, Lido, ListEntry, Maintainer, RewardDistribution, SeedRange, + Validator, ValidatorPerf, }, token::{Lamports, StLamports}, util::serialize_b58, @@ -446,6 +446,28 @@ pub fn command_remove_maintainer( ) } +/// `Validator` structure with all the fields from its related struct +/// joined by its `Pubkey`. +#[derive(Serialize)] +pub struct RichValidator { + #[serde(serialize_with = "serialize_b58")] + pub vote_account_address: Pubkey, + pub stake_seeds: SeedRange, + pub unstake_seeds: SeedRange, + pub stake_accounts_balance: Lamports, + pub unstake_accounts_balance: Lamports, + pub effective_stake_balance: Lamports, + pub active: bool, + + #[serde(serialize_with = "serialize_b58")] + pub identity_account_address: Pubkey, + + pub info: ValidatorInfo, + pub perf: Option, + + pub commission: u8, +} + #[derive(Serialize)] pub struct ShowSolidoOutput { pub solido: Lido, @@ -468,7 +490,7 @@ pub struct ShowSolidoOutput { /// Validator structure as the program sees it, along with the validator's /// identity account address, their info, their performance data, /// and their commission percentage. - pub validators: Vec<(Validator, Pubkey, ValidatorInfo, Option, u8)>, + pub validators: Vec, pub validators_max: u32, pub maintainers: Vec, @@ -477,6 +499,45 @@ pub struct ShowSolidoOutput { pub reserve_account_balance: Lamports, } +pub const VALIDATOR_STAKE_ACCOUNT: &[u8] = b"validator_stake_account"; +pub const VALIDATOR_UNSTAKE_ACCOUNT: &[u8] = b"validator_unstake_account"; + +fn find_stake_account_address_with_authority( + vote_account_address: &Pubkey, + program_id: &Pubkey, + solido_account: &Pubkey, + authority: &[u8], + seed: u64, +) -> (Pubkey, u8) { + let seeds = [ + &solido_account.to_bytes(), + &vote_account_address.to_bytes(), + authority, + &seed.to_le_bytes()[..], + ]; + Pubkey::find_program_address(&seeds, program_id) +} + +fn find_stake_account_address( + vote_account_address: &Pubkey, + program_id: &Pubkey, + solido_account: &Pubkey, + seed: u64, + stake_type: StakeType, +) -> (Pubkey, u8) { + let authority = match stake_type { + StakeType::Stake => VALIDATOR_STAKE_ACCOUNT, + StakeType::Unstake => VALIDATOR_UNSTAKE_ACCOUNT, + }; + find_stake_account_address_with_authority( + vote_account_address, + program_id, + solido_account, + authority, + seed, + ) +} + impl fmt::Display for ShowSolidoOutput { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Manager: {}", self.solido.manager)?; @@ -626,7 +687,7 @@ impl fmt::Display for ShowSolidoOutput { self.validators.len(), self.validators_max, )?; - for (v, identity, info, perf, commission) in self.validators.iter() { + for v in self.validators.iter() { writeln!( f, "\n - \ @@ -639,14 +700,14 @@ impl fmt::Display for ShowSolidoOutput { Stake in all accounts: {}\n \ Stake in stake accounts: {}\n \ Stake in unstake accounts: {}", - info.name, - match &info.keybase_username { + v.info.name, + match &v.info.keybase_username { Some(username) => &username[..], None => "not set", }, - v.pubkey(), - identity, - commission, + v.vote_account_address, + v.identity_account_address, + v.commission, v.active, v.stake_accounts_balance, v.effective_stake_balance, @@ -662,7 +723,8 @@ impl fmt::Display for ShowSolidoOutput { f, " - {}: {}", seed, - v.find_stake_account_address( + find_stake_account_address( + &v.vote_account_address, &self.solido_program_id, &self.solido_address, seed, @@ -681,7 +743,8 @@ impl fmt::Display for ShowSolidoOutput { f, " - {}: {}", seed, - v.find_stake_account_address( + find_stake_account_address( + &v.vote_account_address, &self.solido_program_id, &self.solido_address, seed, @@ -692,7 +755,7 @@ impl fmt::Display for ShowSolidoOutput { } writeln!(f, " Off-chain performance readings:")?; - if let Some(Some(perf)) = perf.as_ref().map(|perf| &perf.rest) { + if let Some(Some(perf)) = v.perf.as_ref().map(|perf| &perf.rest) { writeln!( f, " For epoch #{}", // -- @@ -717,7 +780,7 @@ impl fmt::Display for ShowSolidoOutput { writeln!(f, " Not yet collected.")?; } writeln!(f, " On-chain performance readings:")?; - if let Some(perf) = perf { + if let Some(perf) = &v.perf { writeln!( f, " For epoch #{}", @@ -804,7 +867,21 @@ pub fn command_show_solido( .zip(validator_infos.into_iter()) .zip(validator_perfs.into_iter()) .zip(validator_commission_percentages.into_iter()) - .map(|((((v, identity), info), perf), commission)| (v, identity, info, perf, commission)) + .map( + |((((v, identity), info), perf), commission)| RichValidator { + vote_account_address: v.pubkey().to_owned(), + stake_seeds: v.stake_seeds, + unstake_seeds: v.unstake_seeds, + stake_accounts_balance: v.stake_accounts_balance, + unstake_accounts_balance: v.unstake_accounts_balance, + effective_stake_balance: v.effective_stake_balance, + active: v.active, + identity_account_address: identity, + info, + perf, + commission, + }, + ) .collect(); Ok(ShowSolidoOutput { From 3a58e07a0ba16cf5f75a3c5e300002f54044ebdc Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 17 Jul 2023 23:44:32 +0300 Subject: [PATCH 114/131] lax comparison in integration tests --- scripts/test_solido.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/scripts/test_solido.py b/scripts/test_solido.py index 531bf74a8..6b5ba1eeb 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -341,15 +341,18 @@ def add_validator( solido_address, ) -assert solido_instance['validators']['entries'][0] == { - 'pubkey': validator.vote_account.pubkey, - 'stake_seeds': {'begin': 0, 'end': 0}, - 'unstake_seeds': {'begin': 0, 'end': 0}, - 'stake_accounts_balance': 0, - 'unstake_accounts_balance': 0, - 'effective_stake_balance': 0, - 'active': True, -}, f'Unexpected validator entry, in {json.dumps(solido_instance, indent=True)}' +v = solido_instance['validators'][0] +assert ( + True + and v['vote_account_address'] == validator.vote_account.pubkey + and v['stake_seeds'] == {'begin': 0, 'end': 0} + and v['unstake_seeds'] == {'begin': 0, 'end': 0} + and v['stake_accounts_balance'] == 0 + and v['unstake_accounts_balance'] == 0 + and v['effective_stake_balance'] == 0 + and v['active'] == True + and v['commission'] == 5 +), f'Unexpected validator entry, in {json.dumps(solido_instance, indent=True)}' maintainer = create_test_account('tests/.keys/maintainer-account-key.json') @@ -381,7 +384,7 @@ def add_validator( solido_address, ) -assert solido_instance['maintainers']['entries'][0] == {'pubkey': maintainer.pubkey} +assert solido_instance['maintainers'][0] == {'pubkey': maintainer.pubkey} print(f'> Removing maintainer {maintainer}') transaction_result = solido( @@ -408,7 +411,7 @@ def add_validator( solido_address, ) -assert len(solido_instance['maintainers']['entries']) == 0 +assert len(solido_instance['maintainers']) == 0 print(f'> Adding maintainer {maintainer} again') transaction_result = solido( @@ -580,7 +583,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida 'UpdateStakeAccountBalance': { 'validator_vote_account': validator.vote_account.pubkey, 'expected_difference_stake_lamports': 100_000_000, # We donated 0.1 SOL. - 'unstake_withdrawn_to_reserve_lamports': 1_499_750_000, # Amount that was unstaked for the newcomming validator. + 'unstake_withdrawn_to_reserve_lamports': 1_499_750_000, # Amount that was unstaked for the newcoming validator. } } @@ -661,7 +664,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida '--solido-address', solido_address, ) -assert not solido_instance['validators']['entries'][0][ +assert not solido_instance['validators'][0][ 'active' ], 'Validator should be inactive after deactivation.' print('> Validator is inactive as expected.') @@ -689,7 +692,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida solido_address, ) # Should have bumped the validator's `stake_seeds` and `unstake_seeds`. -val = solido_instance['validators']['entries'][0] +val = solido_instance['validators'][0] assert val['stake_seeds'] == {'begin': 1, 'end': 1} assert val['unstake_seeds'] == {'begin': 1, 'end': 2} @@ -729,7 +732,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida '--solido-address', solido_address, ) -number_validators = len(solido_instance['validators']['entries']) +number_validators = len(solido_instance['validators']) assert ( number_validators == 1 ), f'\nExpected no validators\nGot: {number_validators} validators' @@ -838,7 +841,7 @@ def set_max_validation_commission(fee: int) -> Any: '--solido-address', solido_address, ) -number_validators = len(solido_instance['validators']['entries']) +number_validators = len(solido_instance['validators']) assert ( number_validators == 2 ), f'\nExpected 2 validators\nGot: {number_validators} validators' @@ -861,7 +864,7 @@ def set_max_validation_commission(fee: int) -> Any: '--solido-address', solido_address, ) -number_validators = len(solido_instance['validators']['entries']) +number_validators = len(solido_instance['validators']) assert number_validators == 0 From 57bec32839e5d4f42e92e13d32e0119666080081 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 18 Jul 2023 00:01:28 +0300 Subject: [PATCH 115/131] not today --- scripts/test_solido.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test_solido.py b/scripts/test_solido.py index 6b5ba1eeb..7d621b0ae 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -718,12 +718,12 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida } } -print('\nRunning maintenance (should remove the validator) ...') +print('\nRunning maintenance (should not remove the validator) ...') result = perform_maintenance() expected_result = { 'RemoveValidator': {'validator_vote_account': validator.vote_account.pubkey} } -assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' +assert result is None solido_instance = solido( 'show-solido', From 5bc3458a8bf22fa1496636e70be98b80db3ba7b5 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Tue, 18 Jul 2023 23:51:23 +0300 Subject: [PATCH 116/131] add `RemoveValidator` variant to `SolidoInstruction` --- cli/maintainer/src/commands_multisig.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cli/maintainer/src/commands_multisig.rs b/cli/maintainer/src/commands_multisig.rs index 80bf3db0b..ead94069c 100644 --- a/cli/maintainer/src/commands_multisig.rs +++ b/cli/maintainer/src/commands_multisig.rs @@ -437,6 +437,16 @@ enum SolidoInstruction { validator_index: u32, }, + RemoveValidator { + #[serde(serialize_with = "serialize_b58")] + solido_instance: Pubkey, + + #[serde(serialize_with = "serialize_b58")] + manager: Pubkey, + + #[serde(serialize_with = "serialize_b58")] + validator_vote_account: Pubkey, + }, AddMaintainer { #[serde(serialize_with = "serialize_b58")] solido_instance: Pubkey, From be5dc602f68ca7575cd7b2bc62e1346b7f85f3d1 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 15:27:06 +0300 Subject: [PATCH 117/131] plurals --- cli/maintainer/src/commands_solido.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 13cef254f..4377fe2d1 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -446,7 +446,7 @@ pub fn command_remove_maintainer( ) } -/// `Validator` structure with all the fields from its related struct +/// `Validator` structure with all the fields from its related structs /// joined by its `Pubkey`. #[derive(Serialize)] pub struct RichValidator { From 2d95b3de388f2ddd401274a99ab6728fadb6bac6 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 15:51:05 +0300 Subject: [PATCH 118/131] split off --- program/src/state.rs | 396 +--------------------------- program/src/state/maintainer.rs | 82 ++++++ program/src/state/validator.rs | 244 +++++++++++++++++ program/src/state/validator_perf.rs | 178 +++++++++++++ 4 files changed, 513 insertions(+), 387 deletions(-) create mode 100644 program/src/state/maintainer.rs create mode 100644 program/src/state/validator.rs create mode 100644 program/src/state/validator_perf.rs diff --git a/program/src/state.rs b/program/src/state.rs index 21742df97..e88aee398 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -40,6 +40,15 @@ use crate::{ VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, }; +mod validator; +pub use validator::Validator; + +mod validator_perf; +pub use validator_perf::{Criteria, OffchainValidatorPerf, ValidatorPerf}; + +mod maintainer; +pub use maintainer::Maintainer; + /// Types of list entries /// Uninitialized should always be a first enum field as it catches empty list data errors #[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, BorshSchema)] @@ -320,346 +329,6 @@ impl ValidatorList { } } -/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS -/// THERE'S AN EXTREMELY GOOD REASON. -/// -/// To save on BPF instructions, the serialized bytes are reinterpreted with an -/// unsafe pointer cast, which means that this structure cannot have any -/// undeclared alignment-padding in its representation. -#[repr(C)] -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] -pub struct Validator { - /// Validator vote account address. - /// Do not reorder this field, it should be first in the struct - #[serde(serialize_with = "serialize_b58")] - #[serde(rename = "pubkey")] - pub vote_account_address: Pubkey, - - /// Seeds for active stake accounts. - pub stake_seeds: SeedRange, - /// Seeds for inactive stake accounts. - pub unstake_seeds: SeedRange, - - /// Sum of the balances of the stake accounts and unstake accounts. - pub stake_accounts_balance: Lamports, - - /// Sum of the balances of the unstake accounts. - pub unstake_accounts_balance: Lamports, - - /// Effective stake balance is stake_accounts_balance - unstake_accounts_balance. - /// The result is stored on-chain to optimize compute budget - pub effective_stake_balance: Lamports, - - /// Controls if a validator is allowed to have new stake deposits. - /// When removing a validator, this flag should be set to `false`. - pub active: bool, -} - -/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS -/// THERE'S AN EXTREMELY GOOD REASON. -/// -/// To save on BPF instructions, the serialized bytes are reinterpreted with an -/// unsafe pointer cast, which means that this structure cannot have any -/// undeclared alignment-padding in its representation. -#[repr(C)] -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] -pub struct OffchainValidatorPerf { - /// The epoch in which the off-chain part - /// of the validator's performance was computed. - pub updated_at: Epoch, - - /// The number of slots the validator has produced in the last epoch. - pub block_production_rate: u64, - - /// Ratio of successful votes to total votes. - pub vote_success_rate: u64, - - /// Ratio of how long the validator has been available to the total time in the epoch. - pub uptime: u64, -} - -/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS -/// THERE'S AN EXTREMELY GOOD REASON. -/// -/// To save on BPF instructions, the serialized bytes are reinterpreted with an -/// unsafe pointer cast, which means that this structure cannot have any -/// undeclared alignment-padding in its representation. -#[repr(C)] -#[derive( - Clone, Debug, Default, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, -)] -pub struct ValidatorPerf { - /// The associated validator's vote account address. - /// It might not be present in the validator list. - /// Do not reorder this field, it should be first in the struct - #[serde(serialize_with = "serialize_b58")] - #[serde(rename = "pubkey")] - pub validator_vote_account_address: Pubkey, - - /// The commission is updated at its own pace. - pub commission: u8, - pub commission_updated_at: Epoch, - - /// The off-chain part of the validator's performance, if available. - pub rest: Option, -} - -impl ValidatorPerf { - /// True only if these metrics meet the criteria. - pub fn meets_criteria(&self, criteria: &Criteria) -> bool { - self.commission <= criteria.max_commission - && self.rest.as_ref().map_or(true, |perf| { - perf.vote_success_rate >= criteria.min_vote_success_rate - && perf.block_production_rate >= criteria.min_block_production_rate - && perf.uptime >= criteria.min_uptime - }) - } -} - -/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS -/// THERE'S AN EXTREMELY GOOD REASON. -/// -/// To save on BPF instructions, the serialized bytes are reinterpreted with an -/// unsafe pointer cast, which means that this structure cannot have any -/// undeclared alignment-padding in its representation. -#[repr(C)] -#[derive( - Clone, Default, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, -)] -pub struct Maintainer { - /// Address of maintainer account. - /// Do not reorder this field, it should be first in the struct - #[serde(serialize_with = "serialize_b58")] - pub pubkey: Pubkey, -} - -impl Validator { - /// Return the balance in only the stake accounts, excluding the unstake accounts. - pub fn compute_effective_stake_balance(&self) -> Lamports { - (self.stake_accounts_balance - self.unstake_accounts_balance) - .expect("Unstake balance cannot exceed the validator's total stake balance.") - } - - pub fn observe_balance(observed: Lamports, tracked: Lamports, info: &str) -> ProgramResult { - if observed < tracked { - msg!( - "{}: observed balance of {} is less than tracked balance of {}.", - info, - observed, - tracked - ); - msg!("This should not happen, aborting ..."); - return Err(LidoError::ValidatorBalanceDecreased.into()); - } - Ok(()) - } - - pub fn has_stake_accounts(&self) -> bool { - self.stake_seeds.begin != self.stake_seeds.end - } - pub fn has_unstake_accounts(&self) -> bool { - self.unstake_seeds.begin != self.unstake_seeds.end - } - - pub fn check_can_be_removed(&self) -> Result<(), LidoError> { - if self.active { - return Err(LidoError::ValidatorIsStillActive); - } - if self.has_stake_accounts() { - return Err(LidoError::ValidatorShouldHaveNoStakeAccounts); - } - if self.has_unstake_accounts() { - return Err(LidoError::ValidatorShouldHaveNoUnstakeAccounts); - } - // If not, this is a bug. - assert_eq!(self.stake_accounts_balance, Lamports(0)); - Ok(()) - } - - pub fn show_removed_error_msg(error: &Result<(), LidoError>) { - if let Err(err) = error { - match err { - LidoError::ValidatorIsStillActive => { - msg!( - "Refusing to remove validator because it is still active, deactivate it first." - ); - } - LidoError::ValidatorHasUnclaimedCredit => { - msg!( - "Validator still has tokens to claim. Reclaim tokens before removing the validator" - ); - } - LidoError::ValidatorShouldHaveNoStakeAccounts => { - msg!("Refusing to remove validator because it still has stake accounts, unstake them first."); - } - LidoError::ValidatorShouldHaveNoUnstakeAccounts => { - msg!("Refusing to remove validator because it still has unstake accounts, withdraw them first."); - } - _ => { - msg!("Invalid error when removing a validator: shouldn't happen."); - } - } - } - } - - pub fn find_stake_account_address_with_authority( - &self, - program_id: &Pubkey, - solido_account: &Pubkey, - authority: &[u8], - seed: u64, - ) -> (Pubkey, u8) { - let seeds = [ - &solido_account.to_bytes(), - &self.vote_account_address.to_bytes(), - authority, - &seed.to_le_bytes()[..], - ]; - Pubkey::find_program_address(&seeds, program_id) - } - - pub fn find_stake_account_address( - &self, - program_id: &Pubkey, - solido_account: &Pubkey, - seed: u64, - stake_type: StakeType, - ) -> (Pubkey, u8) { - let authority = match stake_type { - StakeType::Stake => VALIDATOR_STAKE_ACCOUNT, - StakeType::Unstake => VALIDATOR_UNSTAKE_ACCOUNT, - }; - self.find_stake_account_address_with_authority(program_id, solido_account, authority, seed) - } - - /// Get stake account address that should be merged into another right after creation. - /// This function should be used to create temporary stake accounts - /// tied to the epoch that should be merged into another account and destroyed - /// after a transaction. So that each epoch would have a different - /// generation of stake accounts. This is done for security purpose - pub fn find_temporary_stake_account_address( - &self, - program_id: &Pubkey, - solido_account: &Pubkey, - seed: u64, - epoch: Epoch, - ) -> (Pubkey, u8) { - let authority = [VALIDATOR_STAKE_ACCOUNT, &epoch.to_le_bytes()[..]].concat(); - self.find_stake_account_address_with_authority(program_id, solido_account, &authority, seed) - } - - /// Mark the validator as active so that they could receive new stake. - pub fn activate(&mut self) { - self.active = true; - } - - /// Mark the validator as inactive so that no new stake can be delegated to it, - /// and the existing stake shall be unstaked by the maintainer. - pub fn deactivate(&mut self) { - self.active = false; - } -} - -impl Sealed for Validator {} - -impl Pack for Validator { - const LEN: usize = 89; - fn pack_into_slice(&self, data: &mut [u8]) { - let mut data = data; - BorshSerialize::serialize(&self, &mut data).unwrap(); - } - fn unpack_from_slice(src: &[u8]) -> Result { - let unpacked = Self::try_from_slice(src)?; - Ok(unpacked) - } -} - -impl Default for Validator { - fn default() -> Self { - Validator { - stake_seeds: SeedRange { begin: 0, end: 0 }, - unstake_seeds: SeedRange { begin: 0, end: 0 }, - stake_accounts_balance: Lamports(0), - unstake_accounts_balance: Lamports(0), - effective_stake_balance: Lamports(0), - active: true, - vote_account_address: Pubkey::default(), - } - } -} - -impl ListEntry for Validator { - const TYPE: AccountType = AccountType::Validator; - - fn new(vote_account_address: Pubkey) -> Self { - Self { - vote_account_address, - ..Default::default() - } - } - - fn pubkey(&self) -> &Pubkey { - &self.vote_account_address - } -} - -impl ValidatorPerf {} - -impl Sealed for ValidatorPerf {} - -impl Pack for ValidatorPerf { - const LEN: usize = 64; - fn pack_into_slice(&self, data: &mut [u8]) { - let mut data = data; - BorshSerialize::serialize(&self, &mut data).unwrap(); - } - fn unpack_from_slice(src: &[u8]) -> Result { - let unpacked = Self::try_from_slice(src)?; - Ok(unpacked) - } -} - -impl ListEntry for ValidatorPerf { - const TYPE: AccountType = AccountType::ValidatorPerf; - - fn new(validator_vote_account_address: Pubkey) -> Self { - Self { - validator_vote_account_address, - ..Default::default() - } - } - - fn pubkey(&self) -> &Pubkey { - &self.validator_vote_account_address - } -} - -impl Sealed for Maintainer {} - -impl Pack for Maintainer { - const LEN: usize = PUBKEY_BYTES; - fn pack_into_slice(&self, data: &mut [u8]) { - let mut data = data; - BorshSerialize::serialize(&self, &mut data).unwrap(); - } - fn unpack_from_slice(src: &[u8]) -> Result { - let unpacked = Self::try_from_slice(src)?; - Ok(unpacked) - } -} - -impl ListEntry for Maintainer { - const TYPE: AccountType = AccountType::Maintainer; - - fn new(pubkey: Pubkey) -> Self { - Self { pubkey } - } - - fn pubkey(&self) -> &Pubkey { - &self.pubkey - } -} - /// The exchange rate used for deposits and rewards distribution. /// /// The exchange rate of SOL to stSOL is determined by the SOL balance of @@ -783,53 +452,6 @@ impl ExchangeRate { } } -/// Each field is an optimum for a metric. -/// If a validator has a value for a metric that does not meet the threshold, -/// then the validator gets deactivated. -/// -#[repr(C)] -#[derive(Clone, Debug, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize)] -pub struct Criteria { - /// If a validator has the commission higher than this, then it gets deactivated. - pub max_commission: u8, - - /// If a validator has `block_production_rate` lower than this, then it gets deactivated. - pub min_block_production_rate: u64, - - /// If a validator has `vote_success_rate` lower than this, then it gets deactivated. - pub min_vote_success_rate: u64, - - /// If a validator has the uptime lower than this, then it gets deactivated. - pub min_uptime: u64, -} - -impl Default for Criteria { - fn default() -> Self { - Self { - max_commission: 100, - min_vote_success_rate: 0, - min_block_production_rate: 0, - min_uptime: 0, - } - } -} - -impl Criteria { - pub fn new( - max_commission: u8, - min_vote_success_rate: u64, - min_block_production_rate: u64, - min_uptime: u64, - ) -> Self { - Self { - max_commission, - min_vote_success_rate, - min_block_production_rate, - min_uptime, - } - } -} - #[repr(C)] #[derive( Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize, diff --git a/program/src/state/maintainer.rs b/program/src/state/maintainer.rs new file mode 100644 index 000000000..b37304403 --- /dev/null +++ b/program/src/state/maintainer.rs @@ -0,0 +1,82 @@ +//! Maintainer representation in the program state. + +use std::convert::TryFrom; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Range; + +use serde::Serialize; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, + borsh::{get_instance_packed_len, try_from_slice_unchecked}, + clock::Clock, + clock::Epoch, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_memory::sol_memcmp, + program_pack::Pack, + program_pack::Sealed, + pubkey::{Pubkey, PUBKEY_BYTES}, + rent::Rent, + sysvar::Sysvar, +}; +use spl_token::state::Mint; + +use crate::big_vec::BigVec; +use crate::error::LidoError; +use crate::logic::{check_account_owner, get_reserve_available_balance}; +use crate::metrics::Metrics; +use crate::processor::StakeType; +use crate::state::{AccountType, ListEntry, SeedRange}; +use crate::token::{self, Lamports, Rational, StLamports}; +use crate::util::serialize_b58; +use crate::{ + MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, + VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, +}; + +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive( + Clone, Default, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, +)] +pub struct Maintainer { + /// Address of maintainer account. + /// Do not reorder this field, it should be first in the struct + #[serde(serialize_with = "serialize_b58")] + pub pubkey: Pubkey, +} + +impl Sealed for Maintainer {} + +impl Pack for Maintainer { + const LEN: usize = PUBKEY_BYTES; + fn pack_into_slice(&self, data: &mut [u8]) { + let mut data = data; + BorshSerialize::serialize(&self, &mut data).unwrap(); + } + fn unpack_from_slice(src: &[u8]) -> Result { + let unpacked = Self::try_from_slice(src)?; + Ok(unpacked) + } +} + +impl ListEntry for Maintainer { + const TYPE: AccountType = AccountType::Maintainer; + + fn new(pubkey: Pubkey) -> Self { + Self { pubkey } + } + + fn pubkey(&self) -> &Pubkey { + &self.pubkey + } +} diff --git a/program/src/state/validator.rs b/program/src/state/validator.rs new file mode 100644 index 000000000..5f1991026 --- /dev/null +++ b/program/src/state/validator.rs @@ -0,0 +1,244 @@ +//! Types describing the state of the validator with respect to the pool. + +use std::convert::TryFrom; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Range; + +use serde::Serialize; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, + borsh::{get_instance_packed_len, try_from_slice_unchecked}, + clock::Clock, + clock::Epoch, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_memory::sol_memcmp, + program_pack::Pack, + program_pack::Sealed, + pubkey::{Pubkey, PUBKEY_BYTES}, + rent::Rent, + sysvar::Sysvar, +}; +use spl_token::state::Mint; + +use crate::big_vec::BigVec; +use crate::error::LidoError; +use crate::logic::{check_account_owner, get_reserve_available_balance}; +use crate::metrics::Metrics; +use crate::processor::StakeType; +use crate::state::{AccountType, ListEntry, SeedRange}; +use crate::token::{self, Lamports, Rational, StLamports}; +use crate::util::serialize_b58; +use crate::{ + MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, + VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, +}; + +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] +pub struct Validator { + /// Validator vote account address. + /// Do not reorder this field, it should be first in the struct + #[serde(serialize_with = "serialize_b58")] + #[serde(rename = "pubkey")] + pub vote_account_address: Pubkey, + + /// Seeds for active stake accounts. + pub stake_seeds: SeedRange, + /// Seeds for inactive stake accounts. + pub unstake_seeds: SeedRange, + + /// Sum of the balances of the stake accounts and unstake accounts. + pub stake_accounts_balance: Lamports, + + /// Sum of the balances of the unstake accounts. + pub unstake_accounts_balance: Lamports, + + /// Effective stake balance is stake_accounts_balance - unstake_accounts_balance. + /// The result is stored on-chain to optimize compute budget + pub effective_stake_balance: Lamports, + + /// Controls if a validator is allowed to have new stake deposits. + /// When removing a validator, this flag should be set to `false`. + pub active: bool, +} + +impl Validator { + /// Return the balance in only the stake accounts, excluding the unstake accounts. + pub fn compute_effective_stake_balance(&self) -> Lamports { + (self.stake_accounts_balance - self.unstake_accounts_balance) + .expect("Unstake balance cannot exceed the validator's total stake balance.") + } + + pub fn observe_balance(observed: Lamports, tracked: Lamports, info: &str) -> ProgramResult { + if observed < tracked { + msg!( + "{}: observed balance of {} is less than tracked balance of {}.", + info, + observed, + tracked + ); + msg!("This should not happen, aborting ..."); + return Err(LidoError::ValidatorBalanceDecreased.into()); + } + Ok(()) + } + + pub fn has_stake_accounts(&self) -> bool { + self.stake_seeds.begin != self.stake_seeds.end + } + pub fn has_unstake_accounts(&self) -> bool { + self.unstake_seeds.begin != self.unstake_seeds.end + } + + pub fn check_can_be_removed(&self) -> Result<(), LidoError> { + if self.active { + return Err(LidoError::ValidatorIsStillActive); + } + if self.has_stake_accounts() { + return Err(LidoError::ValidatorShouldHaveNoStakeAccounts); + } + if self.has_unstake_accounts() { + return Err(LidoError::ValidatorShouldHaveNoUnstakeAccounts); + } + // If not, this is a bug. + assert_eq!(self.stake_accounts_balance, Lamports(0)); + Ok(()) + } + + pub fn show_removed_error_msg(error: &Result<(), LidoError>) { + if let Err(err) = error { + match err { + LidoError::ValidatorIsStillActive => { + msg!( + "Refusing to remove validator because it is still active, deactivate it first." + ); + } + LidoError::ValidatorHasUnclaimedCredit => { + msg!( + "Validator still has tokens to claim. Reclaim tokens before removing the validator" + ); + } + LidoError::ValidatorShouldHaveNoStakeAccounts => { + msg!("Refusing to remove validator because it still has stake accounts, unstake them first."); + } + LidoError::ValidatorShouldHaveNoUnstakeAccounts => { + msg!("Refusing to remove validator because it still has unstake accounts, withdraw them first."); + } + _ => { + msg!("Invalid error when removing a validator: shouldn't happen."); + } + } + } + } + + pub fn find_stake_account_address_with_authority( + &self, + program_id: &Pubkey, + solido_account: &Pubkey, + authority: &[u8], + seed: u64, + ) -> (Pubkey, u8) { + let seeds = [ + &solido_account.to_bytes(), + &self.vote_account_address.to_bytes(), + authority, + &seed.to_le_bytes()[..], + ]; + Pubkey::find_program_address(&seeds, program_id) + } + + pub fn find_stake_account_address( + &self, + program_id: &Pubkey, + solido_account: &Pubkey, + seed: u64, + stake_type: StakeType, + ) -> (Pubkey, u8) { + let authority = match stake_type { + StakeType::Stake => VALIDATOR_STAKE_ACCOUNT, + StakeType::Unstake => VALIDATOR_UNSTAKE_ACCOUNT, + }; + self.find_stake_account_address_with_authority(program_id, solido_account, authority, seed) + } + + /// Get stake account address that should be merged into another right after creation. + /// This function should be used to create temporary stake accounts + /// tied to the epoch that should be merged into another account and destroyed + /// after a transaction. So that each epoch would have a different + /// generation of stake accounts. This is done for security purpose + pub fn find_temporary_stake_account_address( + &self, + program_id: &Pubkey, + solido_account: &Pubkey, + seed: u64, + epoch: Epoch, + ) -> (Pubkey, u8) { + let authority = [VALIDATOR_STAKE_ACCOUNT, &epoch.to_le_bytes()[..]].concat(); + self.find_stake_account_address_with_authority(program_id, solido_account, &authority, seed) + } + + /// Mark the validator as active so that they could receive new stake. + pub fn activate(&mut self) { + self.active = true; + } + + /// Mark the validator as inactive so that no new stake can be delegated to it, + /// and the existing stake shall be unstaked by the maintainer. + pub fn deactivate(&mut self) { + self.active = false; + } +} + +impl Sealed for Validator {} + +impl Pack for Validator { + const LEN: usize = 89; + fn pack_into_slice(&self, data: &mut [u8]) { + let mut data = data; + BorshSerialize::serialize(&self, &mut data).unwrap(); + } + fn unpack_from_slice(src: &[u8]) -> Result { + let unpacked = Self::try_from_slice(src)?; + Ok(unpacked) + } +} + +impl Default for Validator { + fn default() -> Self { + Validator { + stake_seeds: SeedRange { begin: 0, end: 0 }, + unstake_seeds: SeedRange { begin: 0, end: 0 }, + stake_accounts_balance: Lamports(0), + unstake_accounts_balance: Lamports(0), + effective_stake_balance: Lamports(0), + active: true, + vote_account_address: Pubkey::default(), + } + } +} + +impl ListEntry for Validator { + const TYPE: AccountType = AccountType::Validator; + + fn new(vote_account_address: Pubkey) -> Self { + Self { + vote_account_address, + ..Default::default() + } + } + + fn pubkey(&self) -> &Pubkey { + &self.vote_account_address + } +} diff --git a/program/src/state/validator_perf.rs b/program/src/state/validator_perf.rs new file mode 100644 index 000000000..935a8763c --- /dev/null +++ b/program/src/state/validator_perf.rs @@ -0,0 +1,178 @@ +//! Validator performance metrics. + +use std::convert::TryFrom; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Range; + +use serde::Serialize; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, + borsh::{get_instance_packed_len, try_from_slice_unchecked}, + clock::Clock, + clock::Epoch, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_memory::sol_memcmp, + program_pack::Pack, + program_pack::Sealed, + pubkey::{Pubkey, PUBKEY_BYTES}, + rent::Rent, + sysvar::Sysvar, +}; +use spl_token::state::Mint; + +use crate::big_vec::BigVec; +use crate::error::LidoError; +use crate::logic::{check_account_owner, get_reserve_available_balance}; +use crate::metrics::Metrics; +use crate::processor::StakeType; +use crate::state::{AccountType, ListEntry, SeedRange}; +use crate::token::{self, Lamports, Rational, StLamports}; +use crate::util::serialize_b58; +use crate::{ + MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, + VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, +}; + +/// Each field is an optimum for a metric. +/// If a validator has a value for a metric that does not meet the threshold, +/// then the validator gets deactivated. +/// +#[repr(C)] +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize)] +pub struct Criteria { + /// If a validator has the commission higher than this, then it gets deactivated. + pub max_commission: u8, + + /// If a validator has `block_production_rate` lower than this, then it gets deactivated. + pub min_block_production_rate: u64, + + /// If a validator has `vote_success_rate` lower than this, then it gets deactivated. + pub min_vote_success_rate: u64, + + /// If a validator has the uptime lower than this, then it gets deactivated. + pub min_uptime: u64, +} + +impl Default for Criteria { + fn default() -> Self { + Self { + max_commission: 100, + min_vote_success_rate: 0, + min_block_production_rate: 0, + min_uptime: 0, + } + } +} + +impl Criteria { + pub fn new( + max_commission: u8, + min_vote_success_rate: u64, + min_block_production_rate: u64, + min_uptime: u64, + ) -> Self { + Self { + max_commission, + min_vote_success_rate, + min_block_production_rate, + min_uptime, + } + } +} + +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] +pub struct OffchainValidatorPerf { + /// The epoch in which the off-chain part + /// of the validator's performance was computed. + pub updated_at: Epoch, + + /// The number of slots the validator has produced in the last epoch. + pub block_production_rate: u64, + + /// Ratio of successful votes to total votes. + pub vote_success_rate: u64, + + /// Ratio of how long the validator has been available to the total time in the epoch. + pub uptime: u64, +} + +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive( + Clone, Debug, Default, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, +)] +pub struct ValidatorPerf { + /// The associated validator's vote account address. + /// It might not be present in the validator list. + /// Do not reorder this field, it should be first in the struct + #[serde(serialize_with = "serialize_b58")] + #[serde(rename = "pubkey")] + pub validator_vote_account_address: Pubkey, + + /// The commission is updated at its own pace. + pub commission: u8, + pub commission_updated_at: Epoch, + + /// The off-chain part of the validator's performance, if available. + pub rest: Option, +} + +impl ValidatorPerf { + /// True only if these metrics meet the criteria. + pub fn meets_criteria(&self, criteria: &Criteria) -> bool { + self.commission <= criteria.max_commission + && self.rest.as_ref().map_or(true, |perf| { + perf.vote_success_rate >= criteria.min_vote_success_rate + && perf.block_production_rate >= criteria.min_block_production_rate + && perf.uptime >= criteria.min_uptime + }) + } +} + +impl ValidatorPerf {} + +impl Sealed for ValidatorPerf {} + +impl Pack for ValidatorPerf { + const LEN: usize = 64; + fn pack_into_slice(&self, data: &mut [u8]) { + let mut data = data; + BorshSerialize::serialize(&self, &mut data).unwrap(); + } + fn unpack_from_slice(src: &[u8]) -> Result { + let unpacked = Self::try_from_slice(src)?; + Ok(unpacked) + } +} + +impl ListEntry for ValidatorPerf { + const TYPE: AccountType = AccountType::ValidatorPerf; + + fn new(validator_vote_account_address: Pubkey) -> Self { + Self { + validator_vote_account_address, + ..Default::default() + } + } + + fn pubkey(&self) -> &Pubkey { + &self.validator_vote_account_address + } +} From b846aa07afe5f4c04034033fd53b4f34e05e45cf Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 15:55:49 +0300 Subject: [PATCH 119/131] leaner --- program/src/state.rs | 7 +------ program/src/state/maintainer.rs | 25 +--------------------- program/src/state/validator.rs | 29 ++++---------------------- program/src/state/validator_perf.rs | 32 +++-------------------------- 4 files changed, 9 insertions(+), 84 deletions(-) diff --git a/program/src/state.rs b/program/src/state.rs index e88aee398..97adceadc 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -21,7 +21,6 @@ use solana_program::{ program_error::ProgramError, program_memory::sol_memcmp, program_pack::Pack, - program_pack::Sealed, pubkey::{Pubkey, PUBKEY_BYTES}, rent::Rent, sysvar::Sysvar, @@ -32,13 +31,9 @@ use crate::big_vec::BigVec; use crate::error::LidoError; use crate::logic::{check_account_owner, get_reserve_available_balance}; use crate::metrics::Metrics; -use crate::processor::StakeType; use crate::token::{self, Lamports, Rational, StLamports}; use crate::util::serialize_b58; -use crate::{ - MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, - VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, -}; +use crate::{MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY}; mod validator; pub use validator::Validator; diff --git a/program/src/state/maintainer.rs b/program/src/state/maintainer.rs index b37304403..6a9e61574 100644 --- a/program/src/state/maintainer.rs +++ b/program/src/state/maintainer.rs @@ -1,42 +1,19 @@ //! Maintainer representation in the program state. -use std::convert::TryFrom; use std::fmt::Debug; -use std::marker::PhantomData; -use std::ops::Range; use serde::Serialize; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::{ - account_info::AccountInfo, - borsh::{get_instance_packed_len, try_from_slice_unchecked}, - clock::Clock, - clock::Epoch, - entrypoint::ProgramResult, - msg, program_error::ProgramError, - program_memory::sol_memcmp, program_pack::Pack, program_pack::Sealed, pubkey::{Pubkey, PUBKEY_BYTES}, - rent::Rent, - sysvar::Sysvar, }; -use spl_token::state::Mint; -use crate::big_vec::BigVec; -use crate::error::LidoError; -use crate::logic::{check_account_owner, get_reserve_available_balance}; -use crate::metrics::Metrics; -use crate::processor::StakeType; -use crate::state::{AccountType, ListEntry, SeedRange}; -use crate::token::{self, Lamports, Rational, StLamports}; +use crate::state::{AccountType, ListEntry}; use crate::util::serialize_b58; -use crate::{ - MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, - VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, -}; /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS /// THERE'S AN EXTREMELY GOOD REASON. diff --git a/program/src/state/validator.rs b/program/src/state/validator.rs index 5f1991026..2ef7709bf 100644 --- a/program/src/state/validator.rs +++ b/program/src/state/validator.rs @@ -1,42 +1,21 @@ //! Types describing the state of the validator with respect to the pool. -use std::convert::TryFrom; use std::fmt::Debug; -use std::marker::PhantomData; -use std::ops::Range; use serde::Serialize; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::{ - account_info::AccountInfo, - borsh::{get_instance_packed_len, try_from_slice_unchecked}, - clock::Clock, - clock::Epoch, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - program_memory::sol_memcmp, - program_pack::Pack, - program_pack::Sealed, - pubkey::{Pubkey, PUBKEY_BYTES}, - rent::Rent, - sysvar::Sysvar, + clock::Epoch, entrypoint::ProgramResult, msg, program_error::ProgramError, program_pack::Pack, + program_pack::Sealed, pubkey::Pubkey, }; -use spl_token::state::Mint; -use crate::big_vec::BigVec; use crate::error::LidoError; -use crate::logic::{check_account_owner, get_reserve_available_balance}; -use crate::metrics::Metrics; use crate::processor::StakeType; use crate::state::{AccountType, ListEntry, SeedRange}; -use crate::token::{self, Lamports, Rational, StLamports}; +use crate::token::Lamports; use crate::util::serialize_b58; -use crate::{ - MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, - VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, -}; +use crate::{VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT}; /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS /// THERE'S AN EXTREMELY GOOD REASON. diff --git a/program/src/state/validator_perf.rs b/program/src/state/validator_perf.rs index 935a8763c..e9f73ea72 100644 --- a/program/src/state/validator_perf.rs +++ b/program/src/state/validator_perf.rs @@ -1,42 +1,16 @@ //! Validator performance metrics. -use std::convert::TryFrom; use std::fmt::Debug; -use std::marker::PhantomData; -use std::ops::Range; use serde::Serialize; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::{ - account_info::AccountInfo, - borsh::{get_instance_packed_len, try_from_slice_unchecked}, - clock::Clock, - clock::Epoch, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - program_memory::sol_memcmp, - program_pack::Pack, - program_pack::Sealed, - pubkey::{Pubkey, PUBKEY_BYTES}, - rent::Rent, - sysvar::Sysvar, + program_error::ProgramError, program_pack::Pack, program_pack::Sealed, pubkey::Pubkey, }; -use spl_token::state::Mint; - -use crate::big_vec::BigVec; -use crate::error::LidoError; -use crate::logic::{check_account_owner, get_reserve_available_balance}; -use crate::metrics::Metrics; -use crate::processor::StakeType; -use crate::state::{AccountType, ListEntry, SeedRange}; -use crate::token::{self, Lamports, Rational, StLamports}; + +use crate::state::{AccountType, Epoch, ListEntry}; use crate::util::serialize_b58; -use crate::{ - MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, - VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, -}; /// Each field is an optimum for a metric. /// If a validator has a value for a metric that does not meet the threshold, From 7f873cee5ce7f6ce286fbaaa02e32e5f8452a0c1 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 20:11:34 +0300 Subject: [PATCH 120/131] separate validator deactivation from removal --- cli/maintainer/src/commands_multisig.rs | 10 ---- program/src/balance.rs | 31 ++++++------ program/src/instruction.rs | 39 +++++++++++++++ program/src/process_management.rs | 35 ++++++++++++- program/src/processor.rs | 13 +++-- program/src/state.rs | 8 +-- program/src/state/validator.rs | 54 +++++++++++++++++++-- program/tests/tests/add_remove_validator.rs | 8 +-- program/tests/tests/validators_curation.rs | 54 ++++++++++----------- testlib/src/solido_context.rs | 43 ++++++++++++++++ 10 files changed, 225 insertions(+), 70 deletions(-) diff --git a/cli/maintainer/src/commands_multisig.rs b/cli/maintainer/src/commands_multisig.rs index ead94069c..80bf3db0b 100644 --- a/cli/maintainer/src/commands_multisig.rs +++ b/cli/maintainer/src/commands_multisig.rs @@ -437,16 +437,6 @@ enum SolidoInstruction { validator_index: u32, }, - RemoveValidator { - #[serde(serialize_with = "serialize_b58")] - solido_instance: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - manager: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - }, AddMaintainer { #[serde(serialize_with = "serialize_b58")] solido_instance: Pubkey, diff --git a/program/src/balance.rs b/program/src/balance.rs index da566d82b..4dc9a65d0 100644 --- a/program/src/balance.rs +++ b/program/src/balance.rs @@ -50,7 +50,7 @@ pub fn get_target_balance( .entries .iter() .map(|validator| { - if validator.active { + if validator.is_active() { lamports_per_validator } else { Lamports(0) @@ -82,7 +82,7 @@ pub fn get_target_balance( if remainder == Lamports(0) { break; } - if validator.active { + if validator.is_active() { *target = (*target + Lamports(1)).expect( "Does not overflow because per-validator balance is at most total_lamports.", ); @@ -176,10 +176,11 @@ pub fn get_minimum_stake_validator_index_amount( // Our initial index, that will be returned when no validator is below its target, // is the first active validator. - let mut index = - validators.entries.iter().position(|v| v.active).expect( - "get_minimum_stake_validator_index_amount requires at least one active validator.", - ); + let mut index = validators + .entries + .iter() + .position(|v| v.is_active()) + .expect("get_minimum_stake_validator_index_amount requires at least one active validator."); let mut lowest_balance = validators.entries[index].compute_effective_stake_balance(); let mut amount = Lamports( target_balance[index].0.saturating_sub( @@ -190,7 +191,9 @@ pub fn get_minimum_stake_validator_index_amount( ); for (i, (validator, target)) in validators.entries.iter().zip(target_balance).enumerate() { - if validator.active && validator.compute_effective_stake_balance() < lowest_balance { + if validator.is_active() + && validator.compute_effective_stake_balance() < lowest_balance + { index = i; amount = Lamports( target @@ -217,7 +220,7 @@ pub fn get_validator_to_withdraw( #[cfg(test)] mod test { use super::*; - use crate::state::ValidatorList; + use crate::state::{ValidatorList, ValidatorStatus}; use crate::token::Lamports; #[test] @@ -294,7 +297,7 @@ mod test { let mut validators = ValidatorList::new_default(3); validators.entries[0].stake_accounts_balance = Lamports(101); validators.entries[1].stake_accounts_balance = Lamports(0); - validators.entries[1].active = false; + validators.entries[1].status = ValidatorStatus::StakesSuspended; validators.entries[2].stake_accounts_balance = Lamports(99); let undelegated_stake = Lamports(51); @@ -314,7 +317,7 @@ mod test { let mut validators = ValidatorList::new_default(3); validators.entries[0].stake_accounts_balance = Lamports(100); validators.entries[1].stake_accounts_balance = Lamports(100); - validators.entries[1].active = false; + validators.entries[1].status = ValidatorStatus::StakesSuspended; validators.entries[2].stake_accounts_balance = Lamports(300); let undelegated_stake = Lamports(0); @@ -334,9 +337,9 @@ mod test { validators.entries[0].stake_accounts_balance = Lamports(1); validators.entries[1].stake_accounts_balance = Lamports(2); validators.entries[2].stake_accounts_balance = Lamports(3); - validators.entries[0].active = false; - validators.entries[1].active = false; - validators.entries[2].active = false; + validators.entries[0].status = ValidatorStatus::StakesSuspended; + validators.entries[1].status = ValidatorStatus::StakesSuspended; + validators.entries[2].status = ValidatorStatus::StakesSuspended; let undelegated_stake = Lamports(0); let result = get_target_balance(undelegated_stake, &validators); @@ -351,7 +354,7 @@ mod test { let mut validators = ValidatorList::new_default(2); validators.entries[0].stake_accounts_balance = Lamports(0); validators.entries[1].stake_accounts_balance = Lamports(10); - validators.entries[0].active = false; + validators.entries[0].status = ValidatorStatus::StakesSuspended; let undelegated_stake = Lamports(0); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); diff --git a/program/src/instruction.rs b/program/src/instruction.rs index cf2fc7d7a..2736aab43 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -179,6 +179,12 @@ pub enum LidoInstruction { validator_index: u32, }, + EnqueueValidatorForRemovalV2 { + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + }, + RemoveValidatorV2 { // Index of a validator in validator list #[allow(dead_code)] // but it's not @@ -798,6 +804,39 @@ pub fn deactivate_validator( } } +accounts_struct! { + EnqueueValidatorForRemovalMetaV2, EnqueueValidatorForRemovalInfoV2 { + pub lido { + is_signer: false, + is_writable: false, + }, + pub manager { + is_signer: true, + is_writable: false, + }, + pub validator_vote_account_to_deactivate { + is_signer: false, + is_writable: false, + }, + pub validator_list { + is_signer: false, + is_writable: true, + }, + } +} + +pub fn enqueue_validator_for_removal( + program_id: &Pubkey, + accounts: &EnqueueValidatorForRemovalMetaV2, + validator_index: u32, +) -> Instruction { + Instruction { + program_id: *program_id, + accounts: accounts.to_vec(), + data: LidoInstruction::EnqueueValidatorForRemovalV2 { validator_index }.to_vec(), + } +} + accounts_struct! { AddMaintainerMetaV2, AddMaintainerInfoV2 { pub lido { diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 6dcd0e116..7a725d016 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -102,6 +102,37 @@ pub fn process_remove_validator( Ok(()) } +/// Enqueue a validator for removal. +/// +/// This deactivates the validator as well, so that no new funds can be staked +/// with it. Once the validator has no more stake delegated to it, it can be +/// removed from the list by calling `RemoveValidator`. +pub fn process_enqueue_validator_for_removal( + program_id: &Pubkey, + validator_index: u32, + accounts_raw: &[AccountInfo], +) -> ProgramResult { + let accounts = DeactivateValidatorInfoV2::try_from_slice(accounts_raw)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + lido.check_manager(accounts.manager)?; + + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + + let validator = validators.get_mut( + validator_index, + accounts.validator_vote_account_to_deactivate.key, + )?; + + validator.enqueue_for_removal(); + msg!("Validator {} enqueued for removal.", validator.pubkey()); + Ok(()) +} + /// Set the `active` flag to false for a given validator. /// /// This prevents new funds from being staked with this validator, and enables @@ -174,7 +205,7 @@ pub fn process_deactivate_if_violates( }; // Nothing to do if the validator is already inactive. - if !validator.active { + if !validator.is_active() { return Ok(()); } @@ -246,7 +277,7 @@ pub fn process_reactivate_if_complies( }; // Nothing to do if the validator is already active. - if validator.active { + if validator.is_active() { return Ok(()); } diff --git a/program/src/processor.rs b/program/src/processor.rs index 12fea4add..26706b116 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -23,8 +23,8 @@ use crate::{ process_management::{ process_add_maintainer, process_add_validator, process_change_criteria, process_change_reward_distribution, process_deactivate_if_violates, - process_deactivate_validator, process_merge_stake, process_reactivate_if_complies, - process_remove_maintainer, process_remove_validator, + process_deactivate_validator, process_enqueue_validator_for_removal, process_merge_stake, + process_reactivate_if_complies, process_remove_maintainer, process_remove_validator, }, stake_account::{deserialize_stake_account, StakeAccount}, state::{ @@ -261,7 +261,7 @@ pub fn process_stake_deposit( // the same StakeDeposit transaction, only one of them succeeds. let minimum_stake_validator = validators .iter() - .filter(|&v| v.active) + .filter(|&v| v.is_active()) .min_by_key(|v| v.effective_stake_balance) .ok_or(LidoError::NoActiveValidators)?; let minimum_stake_pubkey = *minimum_stake_validator.pubkey(); @@ -269,7 +269,7 @@ pub fn process_stake_deposit( let validator = validators.get_mut(validator_index, accounts.validator_vote_account.key)?; - if !validator.active { + if !validator.is_active() { msg!( "Validator {} is inactive, new deposits are not allowed", validator.pubkey() @@ -557,7 +557,7 @@ pub fn process_unstake( ]], )?; - if validator.active { + if validator.is_active() { // For active validators, we don't allow their stake accounts to contain // less than the minimum stake account balance. let new_source_balance = (source_balance - amount)?; @@ -1348,6 +1348,9 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P new_reward_distribution, } => process_change_reward_distribution(program_id, new_reward_distribution, accounts), LidoInstruction::AddValidatorV2 => process_add_validator(program_id, accounts), + LidoInstruction::EnqueueValidatorForRemovalV2 { validator_index } => { + process_enqueue_validator_for_removal(program_id, validator_index, accounts) + } LidoInstruction::RemoveValidatorV2 { validator_index } => { process_remove_validator(program_id, validator_index, accounts) } diff --git a/program/src/state.rs b/program/src/state.rs index 97adceadc..1e1307654 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -36,7 +36,7 @@ use crate::util::serialize_b58; use crate::{MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY}; mod validator; -pub use validator::Validator; +pub use validator::{Validator, ValidatorStatus}; mod validator_perf; pub use validator_perf::{Criteria, OffchainValidatorPerf, ValidatorPerf}; @@ -320,7 +320,7 @@ impl ListHeader { impl ValidatorList { pub fn iter_active(&self) -> impl Iterator { - self.entries.iter().filter(|&v| v.active) + self.entries.iter().filter(|&v| v.is_active()) } } @@ -1653,7 +1653,7 @@ mod test_lido { elem.vote_account_address = Pubkey::new_unique(); elem.effective_stake_balance = Lamports(34453); elem.stake_accounts_balance = Lamports(234525); - elem.active = true; + elem.status = ValidatorStatus::AcceptingStakes; // allocate space for future elements let mut buffer: Vec = @@ -1678,7 +1678,7 @@ mod test_lido { stake_accounts_balance: Lamports(1111), unstake_accounts_balance: Lamports(3333), effective_stake_balance: Lamports(3465468), - active: false, + status: ValidatorStatus::StakesSuspended, }; accounts.entries.push(elem.clone()); diff --git a/program/src/state/validator.rs b/program/src/state/validator.rs index 2ef7709bf..ac65bd22c 100644 --- a/program/src/state/validator.rs +++ b/program/src/state/validator.rs @@ -17,6 +17,30 @@ use crate::token::Lamports; use crate::util::serialize_b58; use crate::{VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT}; +/// How well the pool accepts a certain validator. +#[repr(i8)] +#[derive( + Clone, Copy, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, +)] +pub enum ValidatorStatus { + /// The validator is fully accepted by the pool, and can receive new stake. + AcceptingStakes, + + /// New stakes are not accepted for this validator. Existing stakes should be unstaked. + StakesSuspended, + + /// The validator is queued for removal. Existing stakes should be unstaked, + /// and once unstaking is complete, the validator should be removed. + /// This status is irreversible. + PendingRemoval = -1, +} + +impl Default for ValidatorStatus { + fn default() -> Self { + Self::AcceptingStakes + } +} + /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS /// THERE'S AN EXTREMELY GOOD REASON. /// @@ -49,7 +73,7 @@ pub struct Validator { /// Controls if a validator is allowed to have new stake deposits. /// When removing a validator, this flag should be set to `false`. - pub active: bool, + pub status: ValidatorStatus, } impl Validator { @@ -81,7 +105,7 @@ impl Validator { } pub fn check_can_be_removed(&self) -> Result<(), LidoError> { - if self.active { + if self.status != ValidatorStatus::PendingRemoval { return Err(LidoError::ValidatorIsStillActive); } if self.has_stake_accounts() { @@ -167,15 +191,35 @@ impl Validator { self.find_stake_account_address_with_authority(program_id, solido_account, &authority, seed) } + /// True only if the validator is accepting new stake. + pub fn is_active(&self) -> bool { + self.status == ValidatorStatus::AcceptingStakes + } + /// Mark the validator as active so that they could receive new stake. pub fn activate(&mut self) { - self.active = true; + if self.status != ValidatorStatus::StakesSuspended { + msg!("Validator is {:?}, so not activating ...", self.status); + return; + } + + self.status = ValidatorStatus::AcceptingStakes; } /// Mark the validator as inactive so that no new stake can be delegated to it, /// and the existing stake shall be unstaked by the maintainer. pub fn deactivate(&mut self) { - self.active = false; + if self.status != ValidatorStatus::AcceptingStakes { + msg!("Validator is {:?}, so not deactivating ...", self.status); + return; + } + + self.status = ValidatorStatus::StakesSuspended; + } + + /// Mark the validator as queued for removal. + pub fn enqueue_for_removal(&mut self) { + self.status = ValidatorStatus::PendingRemoval; } } @@ -201,8 +245,8 @@ impl Default for Validator { stake_accounts_balance: Lamports(0), unstake_accounts_balance: Lamports(0), effective_stake_balance: Lamports(0), - active: true, vote_account_address: Pubkey::default(), + status: ValidatorStatus::default(), } } } diff --git a/program/tests/tests/add_remove_validator.rs b/program/tests/tests/add_remove_validator.rs index bf4e67b96..4d2d2900b 100644 --- a/program/tests/tests/add_remove_validator.rs +++ b/program/tests/tests/add_remove_validator.rs @@ -70,7 +70,9 @@ async fn test_add_validator_with_invalid_owner() { async fn test_successful_remove_validator() { let mut context = Context::new_with_maintainer_and_validator().await; let validator = &context.get_solido().await.validators.entries[0]; - context.deactivate_validator(*validator.pubkey()).await; + context + .enqueue_validator_for_removal(*validator.pubkey()) + .await; context .try_remove_validator(*validator.pubkey()) .await @@ -100,14 +102,14 @@ async fn test_deactivate_validator() { // Initially, the validator should be active. let solido = context.get_solido().await; assert_eq!(solido.validators.len(), 1); - assert!(solido.validators.entries[0].active); + assert!(solido.validators.entries[0].is_active()); context.deactivate_validator(validator.vote_account).await; // After deactivation, it should be inactive. let solido = context.get_solido().await; assert_eq!(solido.validators.len(), 1); - assert!(!solido.validators.entries[0].active); + assert!(!solido.validators.entries[0].is_active()); // Deactivation is idempotent. context.deactivate_validator(validator.vote_account).await; diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 9b310e37a..8baff6cae 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -15,7 +15,7 @@ async fn test_curate_by_max_commission_percentage() { // increase max_commission_percentage let result = context.try_set_max_commission_percentage(context.criteria.max_commission + 1); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); let solido = context.get_solido().await.lido; assert_eq!( @@ -24,11 +24,11 @@ async fn test_curate_by_max_commission_percentage() { ); let result = context.try_deactivate_if_violates(*validator.pubkey()); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); // check validator is not deactivated let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.active, true); + assert!(validator.is_active()); // Increase max_commission_percentage above 100% assert_solido_error!( @@ -38,14 +38,14 @@ async fn test_curate_by_max_commission_percentage() { // decrease max_commission_percentage let result = context.try_set_max_commission_percentage(context.criteria.max_commission - 1); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); let result = context.try_deactivate_if_violates(*validator.pubkey()); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); // check validator is deactivated let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.active, false); + assert!(!validator.is_active()); } #[tokio::test] @@ -54,7 +54,7 @@ async fn test_curate_by_min_block_production_rate() { let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.active); + assert!(validator.is_active()); // When Solido imposes a minimum block production rate: let result = context @@ -79,7 +79,7 @@ async fn test_curate_by_min_block_production_rate() { // Then the validators with a lower block production rate are deactivated: let validator = &context.get_solido().await.validators.entries[0]; - assert!(!validator.active); + assert!(!validator.is_active()); } #[tokio::test] @@ -88,7 +88,7 @@ async fn test_curate_by_min_vote_success_rate() { let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.active); + assert!(validator.is_active()); // When Solido imposes a minimum vote success rate: let result = context @@ -113,7 +113,7 @@ async fn test_curate_by_min_vote_success_rate() { // Then the validators with a lower vote success rate are deactivated: let validator = &context.get_solido().await.validators.entries[0]; - assert!(!validator.active); + assert!(!validator.is_active()); } #[tokio::test] @@ -122,7 +122,7 @@ async fn test_curate_by_min_uptime() { let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.active); + assert!(validator.is_active()); // When Solido imposes a minimum uptime: let result = context @@ -147,7 +147,7 @@ async fn test_curate_by_min_uptime() { // Then the validators with a lower vote success rate are deactivated: let validator = &context.get_solido().await.validators.entries[0]; - assert!(!validator.active); + assert!(!validator.is_active()); } #[tokio::test] @@ -156,7 +156,7 @@ async fn test_update_block_production_rate() { let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.active); + assert!(validator.is_active()); // When an epoch passes, and the validator's block production rate is observed: let result = context @@ -184,7 +184,7 @@ async fn test_update_vote_success_rate() { let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.active); + assert!(validator.is_active()); // When an epoch passes, and the validator's vote success rate is observed: let result = context @@ -212,7 +212,7 @@ async fn test_update_uptime() { let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.active); + assert!(validator.is_active()); // When an epoch passes, and the validator's uptime is observed: let result = context @@ -237,7 +237,7 @@ async fn test_uptime_updates_at_most_once_per_epoch() { let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.active); + assert!(validator.is_active()); // When the uptime of a validator gets updated: let result = context @@ -276,18 +276,18 @@ async fn test_bring_back() { ..context.criteria }) .await; - assert_eq!(result.is_ok(), true); + assert!(result.is_ok()); let result = context .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) .await; - assert_eq!(result.is_ok(), true); + assert!(result.is_ok()); let result = context.try_deactivate_if_violates(*validator.pubkey()); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.active, false); + assert!(!validator.is_active()); // When the epoch passes: context.advance_to_normal_epoch(1); @@ -296,15 +296,15 @@ async fn test_bring_back() { let result = context .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 101) .await; - assert_eq!(result.is_ok(), true); + assert!(result.is_ok()); // And when the instruction is issued: let result = context.try_reactivate_if_complies(*validator.pubkey()); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); // Then the validator is reactivated: let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.active, true); + assert!(validator.is_active()); } #[tokio::test] @@ -313,7 +313,7 @@ async fn test_close_vote_account() { let vote_account = context.validator.as_ref().unwrap().vote_account; let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.active, true); + assert!(validator.is_active()); let keypair_bytes = context .validator @@ -325,11 +325,11 @@ async fn test_close_vote_account() { let withdraw_authority = Keypair::from_bytes(&keypair_bytes).unwrap(); let result = context.try_close_vote_account(&vote_account, &withdraw_authority); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); let result = context.try_deactivate_if_violates(*validator.pubkey()); - assert_eq!(result.await.is_ok(), true); + assert!(result.await.is_ok()); let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.active, false); + assert!(!validator.is_active()); } diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 6af3ccb9b..36b9e458c 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -798,6 +798,49 @@ impl Context { .await } + pub async fn enqueue_validator_for_removal(&mut self, vote_account: Pubkey) { + let solido = self.get_solido().await; + let validator_index = solido.validators.position(&vote_account).unwrap(); + send_transaction( + &mut self.context, + &[lido::instruction::enqueue_validator_for_removal( + &id(), + &lido::instruction::EnqueueValidatorForRemovalMetaV2 { + lido: self.solido.pubkey(), + manager: self.manager.pubkey(), + validator_vote_account_to_deactivate: vote_account, + validator_list: self.validator_list.pubkey(), + }, + validator_index, + )], + vec![&self.manager], + ) + .await + .expect("Failed to deactivate validator."); + } + + pub async fn try_enqueue_validator_for_removal( + &mut self, + vote_account: Pubkey, + ) -> transport::Result<()> { + let solido = self.get_solido().await; + let validator_index = solido.validators.position(&vote_account).unwrap(); + send_transaction( + &mut self.context, + &[lido::instruction::remove_validator( + &id(), + &lido::instruction::RemoveValidatorMetaV2 { + lido: self.solido.pubkey(), + validator_vote_account_to_remove: vote_account, + validator_list: self.validator_list.pubkey(), + }, + validator_index, + )], + vec![], + ) + .await + } + /// Create a new account, deposit from it, and return the resulting owner and stSOL account. pub async fn try_deposit(&mut self, amount: Lamports) -> transport::Result<(Keypair, Pubkey)> { // Create a new user who is going to do the deposit. The user's account From 26eed33c01dac40a39243e1fa5dd42618b03cbb2 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 20:19:04 +0300 Subject: [PATCH 121/131] `validator.is_active()` across maintainer --- cli/maintainer/src/maintenance.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 2651c91be..8fbdb2c9b 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -781,7 +781,7 @@ impl SolidoState { { // We are only interested in unstaking from inactive validators that // have stake accounts. - if validator.active { + if validator.is_active() { continue; } // Validator already has 3 unstake accounts. @@ -824,7 +824,7 @@ impl SolidoState { .zip(self.validator_vote_accounts.iter()) .enumerate() { - if !validator.active { + if !validator.is_active() { continue; } @@ -878,7 +878,7 @@ impl SolidoState { // and are now performing well. // // If the vote account is closed, no need to reactivate. - if !validator.active + if !validator.is_active() && vote_state.as_ref().map_or(false, |vote_state| { does_perform_well( &self.solido.criteria, From d6c73d8746d002e4d882e1be2c8d322f6501c652 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 21:19:20 +0300 Subject: [PATCH 122/131] add `remove-validator` as a multisig subcommand --- cli/maintainer/src/commands_solido.rs | 39 +++++++++++++++++++++++++-- cli/maintainer/src/config.rs | 24 +++++++++++++++++ cli/maintainer/src/daemon.rs | 3 +++ cli/maintainer/src/main.rs | 21 ++++++++++----- cli/maintainer/src/maintenance.rs | 38 +++++++++++++++++++++++++- program/src/instruction.rs | 2 +- testlib/src/solido_context.rs | 2 +- 7 files changed, 118 insertions(+), 11 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 4377fe2d1..14b5d1d53 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -35,6 +35,7 @@ use crate::{ commands_multisig::{ get_multisig_program_address, propose_instruction, ProposeInstructionOutput, }, + config::RemoveValidatorOpts, spl_token_utils::{push_create_spl_token_account, push_create_spl_token_mint}, }; use crate::{ @@ -869,13 +870,13 @@ pub fn command_show_solido( .zip(validator_commission_percentages.into_iter()) .map( |((((v, identity), info), perf), commission)| RichValidator { + active: v.is_active(), vote_account_address: v.pubkey().to_owned(), stake_seeds: v.stake_seeds, unstake_seeds: v.unstake_seeds, stake_accounts_balance: v.stake_accounts_balance, unstake_accounts_balance: v.unstake_accounts_balance, effective_stake_balance: v.effective_stake_balance, - active: v.active, identity_account_address: identity, info, perf, @@ -1225,7 +1226,7 @@ pub fn command_deactivate_if_violates( .ok() .ok_or_else(|| CliError::new("Validator account data too small"))?; - if !validator.active || commission <= solido.criteria.max_commission { + if !validator.is_active() || commission <= solido.criteria.max_commission { continue; } @@ -1257,6 +1258,40 @@ pub fn command_deactivate_if_violates( }) } +/// CLI entry point to mark a validator as subject to removal. +pub fn command_remove_validator( + config: &mut SnapshotConfig, + opts: &RemoveValidatorOpts, +) -> solido_cli_common::Result { + let solido = config.client.get_solido(opts.solido_address())?; + + let validators = config + .client + .get_account_list::(&solido.validator_list)?; + + let (multisig_address, _) = + get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); + + let instruction = lido::instruction::enqueue_validator_for_removal( + opts.solido_program_id(), + &lido::instruction::EnqueueValidatorForRemovalMetaV2 { + lido: *opts.solido_address(), + manager: multisig_address, + validator_vote_account_to_remove: *opts.validator_vote_account(), + validator_list: solido.validator_list, + }, + validators + .position(opts.validator_vote_account()) + .ok_or_else(|| CliError::new("Pubkey not found in validator list"))?, + ); + propose_instruction( + config, + opts.multisig_program_id(), + *opts.multisig_address(), + instruction, + ) +} + /// CLI entry point to change the thresholds of curating out the validators pub fn command_change_criteria( config: &mut SnapshotConfig, diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index ebcd28e8e..e0ed0b76b 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -523,6 +523,30 @@ cli_opt_struct! { } } +cli_opt_struct! { + RemoveValidatorOpts { + /// Address of the Solido program. + #[clap(long, value_name = "address")] + solido_program_id: Pubkey, + + /// Account that stores the data for this Solido instance. + #[clap(long, value_name = "address")] + solido_address: Pubkey, + + /// Address of the validator vote account. + #[clap(long, value_name = "address")] + validator_vote_account: Pubkey, + + /// Multisig instance. + #[clap(long, value_name = "address")] + multisig_address: Pubkey, + + /// Address of the Multisig program. + #[clap(long, value_name = "address")] + multisig_program_id: Pubkey, + } +} + // Multisig opts cli_opt_struct! { diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index e583254da..b9fd7f052 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -152,6 +152,9 @@ impl MaintenanceMetrics { MaintenanceOutput::ReactivateIfComplies { .. } => { self.transactions_reactivate_if_complies += 1; } + MaintenanceOutput::RemoveValidator { .. } => { + self.transactions_remove_validator += 1; + } MaintenanceOutput::UnstakeFromActiveValidator { .. } => { self.transactions_unstake_from_active_validator += 1; } diff --git a/cli/maintainer/src/main.rs b/cli/maintainer/src/main.rs index 74b4e273b..bb3f5d889 100644 --- a/cli/maintainer/src/main.rs +++ b/cli/maintainer/src/main.rs @@ -21,8 +21,9 @@ use crate::commands_multisig::MultisigOpts; use crate::commands_solido::{ command_add_maintainer, command_add_validator, command_change_criteria, command_create_solido, command_create_v2_accounts, command_deactivate_if_violates, command_deactivate_validator, - command_deposit, command_migrate_state_to_v2, command_remove_maintainer, command_show_solido, - command_show_solido_authorities, command_withdraw, + command_deposit, command_migrate_state_to_v2, command_remove_maintainer, + command_remove_validator, command_show_solido, command_show_solido_authorities, + command_withdraw, }; use crate::config::*; @@ -159,12 +160,14 @@ REWARDS /// Adds a new validator. AddValidator(AddValidatorOpts), - /// Deactivates a validator and initiates the removal process. + /// Deactivates a validator for the current epoch. DeactivateValidator(DeactivateValidatorOpts), - /// Deactivates a validator and initiates the removal process if - /// validator exceeds maximum validation commission or any other performance metric - /// as per the thresholds. + /// Deactivates a validator and initiates its removal process. + RemoveValidator(RemoveValidatorOpts), + + /// Deactivates a validator if the validator exceeds maximum validation commission + /// or any other performance metric as per the thresholds. /// Requires no permission. DeactivateIfViolates(DeactivateIfViolatesOpts), @@ -321,6 +324,11 @@ fn main() { let output = result.ok_or_abort_with("Failed to check max commission violation."); print_output(output_mode, &output); } + SubCommand::RemoveValidator(cmd_opts) => { + let result = config.with_snapshot(|config| command_remove_validator(config, &cmd_opts)); + let output = result.ok_or_abort_with("Failed to remove validator."); + print_output(output_mode, &output); + } SubCommand::AddMaintainer(cmd_opts) => { let result = config.with_snapshot(|config| command_add_maintainer(config, &cmd_opts)); let output = result.ok_or_abort_with("Failed to add maintainer."); @@ -386,6 +394,7 @@ fn merge_with_config_and_environment( SubCommand::DeactivateIfViolates(opts) => { opts.merge_with_config_and_environment(config_file) } + SubCommand::RemoveValidator(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::AddMaintainer(opts) | SubCommand::RemoveMaintainer(opts) => { opts.merge_with_config_and_environment(config_file) } diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 8fbdb2c9b..4c41186c7 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -121,6 +121,10 @@ pub enum MaintenanceOutput { #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, }, + RemoveValidator { + #[serde(serialize_with = "serialize_b58")] + validator_vote_account: Pubkey, + }, UnstakeFromActiveValidator(Unstake), } @@ -273,6 +277,12 @@ impl fmt::Display for MaintenanceOutput { writeln!(f, "Reactivate a validator that meets our criteria.")?; writeln!(f, " Validator vote account: {}", validator_vote_account)?; } + MaintenanceOutput::RemoveValidator { + validator_vote_account, + } => { + writeln!(f, "Remove a validator.")?; + writeln!(f, " Validator vote account: {}", validator_vote_account)?; + } } Ok(()) } @@ -906,6 +916,31 @@ impl SolidoState { None } + /// If there is a validator ready for removal, try to remove it. + pub fn try_remove_pending_validators(&self) -> Option { + for (validator_index, validator) in self.validators.entries.iter().enumerate() { + // We are only interested in validators that can be removed. + if validator.check_can_be_removed().is_err() { + continue; + } + let task = MaintenanceOutput::RemoveValidator { + validator_vote_account: *validator.pubkey(), + }; + + let instruction = lido::instruction::remove_validator( + &self.solido_program_id, + &lido::instruction::RemoveValidatorMetaV2 { + lido: self.solido_address, + validator_vote_account_to_remove: *validator.pubkey(), + validator_list: self.solido.validator_list, + }, + u32::try_from(validator_index).expect("Too many validators"), + ); + return Some(MaintenanceInstruction::new(instruction, task)); + } + None + } + /// Get an instruction to merge accounts. fn get_merge_instruction( &self, @@ -1749,7 +1784,8 @@ pub fn try_perform_maintenance( .or_else(|| state.try_update_stake_account_balance()) .or_else(|| state.try_deactivate_if_violates()) .or_else(|| state.try_stake_deposit()) - .or_else(|| state.try_unstake_from_active_validators()); + .or_else(|| state.try_unstake_from_active_validators()) + .or_else(|| state.try_remove_pending_validators()); match instruction_output { Some(maintenance_instruction) => { diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 2736aab43..7ede7071d 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -814,7 +814,7 @@ accounts_struct! { is_signer: true, is_writable: false, }, - pub validator_vote_account_to_deactivate { + pub validator_vote_account_to_remove { is_signer: false, is_writable: false, }, diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 36b9e458c..713ab9b19 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -808,7 +808,7 @@ impl Context { &lido::instruction::EnqueueValidatorForRemovalMetaV2 { lido: self.solido.pubkey(), manager: self.manager.pubkey(), - validator_vote_account_to_deactivate: vote_account, + validator_vote_account_to_remove: vote_account, validator_list: self.validator_list.pubkey(), }, validator_index, From e8e373aa00ff10aee85794afcb5f23f270d72a57 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 23:11:51 +0300 Subject: [PATCH 123/131] cover validator removal with end-end test --- scripts/test_solido.py | 148 +++++++++++------------------------------ 1 file changed, 39 insertions(+), 109 deletions(-) diff --git a/scripts/test_solido.py b/scripts/test_solido.py index 7d621b0ae..8b5d2da2e 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -68,13 +68,13 @@ print('\nUploading Solido program ...') solido_program_id = solana_program_deploy(get_solido_program_path() + '/lido.so') -print(f'> Solido program id is {solido_program_id}.') +print(f'> Solido program id is {solido_program_id}') print('\nUploading Multisig program ...') multisig_program_id = solana_program_deploy( get_solido_program_path() + '/serum_multisig.so' ) -print(f'> Multisig program id is {multisig_program_id}.') +print(f'> Multisig program id is {multisig_program_id}') print('\nCreating new multisig ...') multisig_data = multisig( @@ -88,7 +88,7 @@ ) multisig_instance = multisig_data['multisig_address'] multisig_pda = multisig_data['multisig_program_derived_address'] -print(f'> Created instance at {multisig_instance}.') +print(f'> Created instance at {multisig_instance}') def approve_and_execute(transaction_to_approve: str, signer: TestAccount) -> None: @@ -216,7 +216,7 @@ def approve_and_execute(transaction_to_approve: str, signer: TestAccount) -> Non validator_list_address = result['validator_list_address'] maintainer_list_address = result['maintainer_list_address'] -print(f'> Created instance at {solido_address}.') +print(f'> Created instance at {solido_address}') output = { "multisig_program_id": multisig_program_id, @@ -289,7 +289,6 @@ def add_validator( ) return (validator, transaction_result) - print('> Call function to add validator') (validator, transaction_result) = add_validator( 'validator-account-key', 'validator-vote-account-key' @@ -466,14 +465,14 @@ def consume_maintainence_instructions(verbose: bool = False) -> Any: result = perform_maintenance() if solido_instance['solido']['exchange_rate']['computed_in_epoch'] == current_epoch: assert result is None, f'Huh, perform-maintenance performed {result}' - print('> There was nothing to do, as expected.') + print('> There was nothing to do, as expected') else: update_exchange_rate_result = 'UpdateExchangeRate' # Epoch is likely to be > 0 for the test-net runs assert ( result == update_exchange_rate_result ), f'\nExpected: {update_exchange_rate_result}\nActual: {result}' - print('> Updated the exchange rate, as expected in a change of Epoch.') + print('> Updated the exchange rate, as expected in a change of Epoch') def deposit(lamports: int, expect_created_token_account: bool = False) -> None: @@ -496,7 +495,7 @@ def deposit(lamports: int, expect_created_token_account: bool = False) -> None: } assert deposit_result == expected, f'{deposit_result} == {expected}' print( - f'> Got {deposit_result["st_lamports_balance_increase"]/1_000_000_000} stSOL.' + f'> Got {deposit_result["st_lamports_balance_increase"]/1_000_000_000} stSOL' ) @@ -515,7 +514,7 @@ def deposit(lamports: int, expect_created_token_account: bool = False) -> None: 'stake_account' ] # This one we can't easily predict, don't compare it. assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' -print(f'> Staked deposit with {validator.vote_account}.') +print(f'> Staked deposit with {validator.vote_account}') print( '\nSimulating 0.0005 SOL deposit (too little to stake), then running maintenance ...' @@ -526,7 +525,7 @@ def deposit(lamports: int, expect_created_token_account: bool = False) -> None: # is not empty, we can't stake what's in the reserve. result = perform_maintenance() assert result is None, f'Huh, perform-maintenance performed {result}' -print('> There was nothing to do, as expected.') +print('> There was nothing to do, as expected') def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Validator: @@ -547,6 +546,27 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida assert transaction_status['did_execute'] == True return validator +def remove_validator_and_approve(vote_account_address: str, keypair_path: str) -> str: + transaction_result = solido( + 'remove-validator', + '--validator-vote-account', + vote_account_address, + keypair_path=keypair_path, + ) + transaction_address = transaction_result['transaction_address'] + approve_and_execute(transaction_address, test_addrs[0]) + transaction_status = multisig( + 'show-transaction', + '--multisig-program-id', + multisig_program_id, + '--solido-program-id', + solido_program_id, + '--transaction-address', + transaction_address, + ) + assert transaction_status['did_execute'] + return transaction_address + validator_1 = add_validator_and_approve( 'validator-account-key-1', 'validator-vote-account-key-1' @@ -593,7 +613,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' -print('> Performed UpdateStakeAccountBalance as expected.') +print('> Performed UpdateStakeAccountBalance as expected') print('\nDonating 1.0 SOL to reserve, then running maintenance ...') @@ -617,13 +637,13 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida } } assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' -print('> Deposited to the second validator, as expected.') +print('> Deposited to the second validator, as expected') print('\nRunning maintenance (should be no-op) ...') result = perform_maintenance() assert result is None, f'Huh, perform-maintenance performed {result}' -print('> There was nothing to do, as expected.') +print('> There was nothing to do, as expected') print(f'\nDeactivating validator {validator.vote_account.pubkey} ...') transaction_result = solido( @@ -641,7 +661,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida keypair_path=test_addrs[0].keypair_path, ) transaction_address = transaction_result['transaction_address'] -print(f'> Deactivation multisig transaction address is {transaction_address}.') +print(f'> Deactivation multisig transaction address is {transaction_address}') transaction_status = multisig( 'show-transaction', '--multisig-program-id', @@ -667,7 +687,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida assert not solido_instance['validators'][0][ 'active' ], 'Validator should be inactive after deactivation.' -print('> Validator is inactive as expected.') +print('> Validator is inactive as expected') print('\nRunning maintenance (should unstake from inactive validator) ...') result = perform_maintenance() @@ -720,23 +740,8 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida print('\nRunning maintenance (should not remove the validator) ...') result = perform_maintenance() -expected_result = { - 'RemoveValidator': {'validator_vote_account': validator.vote_account.pubkey} -} assert result is None -solido_instance = solido( - 'show-solido', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, -) -number_validators = len(solido_instance['validators']) -assert ( - number_validators == 1 -), f'\nExpected no validators\nGot: {number_validators} validators' - # change validator commission above limit solana( "vote-update-commission", @@ -746,7 +751,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida ) print( - '\nRunning maintenance (should deactivate a validator that exceed max validation commission) ...' + '\nRunning maintenance (should record the exceeded commission) ...' ) result = perform_maintenance() # check validator_1 is deactivated @@ -757,80 +762,11 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida } assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' - -print('\nConsuming all maintainence instructions') -consume_maintainence_instructions(False) - # Adding another validator validator_2 = add_validator_and_approve( 'validator-account-key-2', 'validator-vote-account-key-2' ) - -def set_max_validation_commission(fee: int) -> Any: - transaction_result = solido( - 'set-max-validation-commission', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--max-commission-percentage', - str(fee), - '--multisig-address', - multisig_instance, - keypair_path=test_addrs[1].keypair_path, - ) - assert transaction_result['transaction_address'] != None - - approve_and_execute(transaction_result['transaction_address'], test_addrs[0]) - transaction_status = multisig( - 'show-transaction', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--transaction-address', - transaction_result['transaction_address'], - ) - return transaction_status - - -print( - '\nLowering max validation commission to %d%% ...' - % (MAX_VALIDATION_COMMISSION_PERCENTAGE - 1) -) -transaction_status = set_max_validation_commission( - MAX_VALIDATION_COMMISSION_PERCENTAGE - 1 -) -assert transaction_status['did_execute'] == True - - -print( - '\nRunning maintenance (should deactivate all validators, because they exceed max validation commission) ...' -) - -maintainance_result = perform_maintenance() -expected_result = { - 'DeactivateIfViolates': { - 'validator_vote_account': validator_2.vote_account.pubkey - } -} -# check validator_2 is deactivated -assert ( - maintainance_result == expected_result -), f'\nExpected: {expected_result}\nActual: {maintainance_result}' - -############################################################################# - -print( - '\nRestore max validation commission to %d%% ...' - % (MAX_VALIDATION_COMMISSION_PERCENTAGE) -) -transaction_status = set_max_validation_commission(MAX_VALIDATION_COMMISSION_PERCENTAGE) -assert transaction_status['did_execute'] == True - validator_3 = add_validator_and_approve( 'validator-account-key-3', 'validator-vote-account-key-3' ) @@ -843,17 +779,11 @@ def set_max_validation_commission(fee: int) -> Any: ) number_validators = len(solido_instance['validators']) assert ( - number_validators == 2 + number_validators == 4 ), f'\nExpected 2 validators\nGot: {number_validators} validators' -print(f'\nClosing vote account {validator_3.vote_account.pubkey}') -solana( - "close-vote-account", - validator_3.vote_account.pubkey, - "tests/.keys/test-key-1.json", - "--authorized-withdrawer", - validator_3.withdrawer_account.keypair_path, -) +print(f'\nRemoving validator {validator_1.vote_account.pubkey} ...') +remove_validator_and_approve(validator_1.vote_account.pubkey, test_addrs[0].keypair_path) print('\nConsuming all maintainence instructions (should remove all validators) ...') consume_maintainence_instructions(False) From 5246cf5215564451be365a2d1e02f4ad6ab72217 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Wed, 19 Jul 2023 23:13:12 +0300 Subject: [PATCH 124/131] fmt --- program/src/balance.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/program/src/balance.rs b/program/src/balance.rs index 4dc9a65d0..6476c8f4a 100644 --- a/program/src/balance.rs +++ b/program/src/balance.rs @@ -191,9 +191,7 @@ pub fn get_minimum_stake_validator_index_amount( ); for (i, (validator, target)) in validators.entries.iter().zip(target_balance).enumerate() { - if validator.is_active() - && validator.compute_effective_stake_balance() < lowest_balance - { + if validator.is_active() && validator.compute_effective_stake_balance() < lowest_balance { index = i; amount = Lamports( target From ca7e6e011205fa8caa06205d176d173823a50b3d Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 20 Jul 2023 00:03:04 +0300 Subject: [PATCH 125/131] args --- scripts/test_solido.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/test_solido.py b/scripts/test_solido.py index 8b5d2da2e..afd0d0240 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -549,6 +549,14 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida def remove_validator_and_approve(vote_account_address: str, keypair_path: str) -> str: transaction_result = solido( 'remove-validator', + '--solido-program-id', + solido_program_id, + '--solido-address', + solido_address, + '--multisig-address', + multisig_instance, + '--multisig-program-id', + multisig_program_id, '--validator-vote-account', vote_account_address, keypair_path=keypair_path, From 962c3ed3c1c7484aaf0d367f1b732ca29637d298 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 20 Jul 2023 00:11:02 +0300 Subject: [PATCH 126/131] singers --- scripts/test_solido.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test_solido.py b/scripts/test_solido.py index afd0d0240..143be96bd 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -791,7 +791,7 @@ def remove_validator_and_approve(vote_account_address: str, keypair_path: str) - ), f'\nExpected 2 validators\nGot: {number_validators} validators' print(f'\nRemoving validator {validator_1.vote_account.pubkey} ...') -remove_validator_and_approve(validator_1.vote_account.pubkey, test_addrs[0].keypair_path) +remove_validator_and_approve(validator_1.vote_account.pubkey, maintainer.keypair_path) print('\nConsuming all maintainence instructions (should remove all validators) ...') consume_maintainence_instructions(False) From 4816ded3d77b8e051e0ca35e6cb4982c967878de Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Thu, 20 Jul 2023 00:54:12 +0300 Subject: [PATCH 127/131] remove the validators in test before checking for removal --- scripts/test_solido.py | 56 +++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/scripts/test_solido.py b/scripts/test_solido.py index 143be96bd..5bbae2bac 100755 --- a/scripts/test_solido.py +++ b/scripts/test_solido.py @@ -461,19 +461,9 @@ def consume_maintainence_instructions(verbose: bool = False) -> Any: return last_result -print('\nRunning maintenance (should be no-op if epoch is unchanged) ...') +print('\nRunning maintenance (should be no-op) ...') result = perform_maintenance() -if solido_instance['solido']['exchange_rate']['computed_in_epoch'] == current_epoch: - assert result is None, f'Huh, perform-maintenance performed {result}' - print('> There was nothing to do, as expected') -else: - update_exchange_rate_result = 'UpdateExchangeRate' - # Epoch is likely to be > 0 for the test-net runs - assert ( - result == update_exchange_rate_result - ), f'\nExpected: {update_exchange_rate_result}\nActual: {result}' - print('> Updated the exchange rate, as expected in a change of Epoch') - +assert result is None, f'Huh, perform-maintenance performed {result}' def deposit(lamports: int, expect_created_token_account: bool = False) -> None: print(f'\nDepositing {lamports/1_000_000_000} SOL ...') @@ -562,7 +552,36 @@ def remove_validator_and_approve(vote_account_address: str, keypair_path: str) - keypair_path=keypair_path, ) transaction_address = transaction_result['transaction_address'] - approve_and_execute(transaction_address, test_addrs[0]) + multisig( + 'approve', + '--multisig-program-id', + multisig_program_id, + '--multisig-address', + multisig_instance, + '--transaction-address', + transaction_address, + keypair_path=test_addrs[0].keypair_path, + ) + multisig( + 'approve', + '--multisig-program-id', + multisig_program_id, + '--multisig-address', + multisig_instance, + '--transaction-address', + transaction_address, + keypair_path=test_addrs[1].keypair_path, + ) + multisig( + 'execute-transaction', + '--multisig-program-id', + multisig_program_id, + '--multisig-address', + multisig_instance, + '--transaction-address', + transaction_address, + keypair_path=keypair_path, + ) transaction_status = multisig( 'show-transaction', '--multisig-program-id', @@ -791,7 +810,16 @@ def remove_validator_and_approve(vote_account_address: str, keypair_path: str) - ), f'\nExpected 2 validators\nGot: {number_validators} validators' print(f'\nRemoving validator {validator_1.vote_account.pubkey} ...') -remove_validator_and_approve(validator_1.vote_account.pubkey, maintainer.keypair_path) +remove_validator_and_approve(validator_1.vote_account.pubkey, test_addrs[0].keypair_path) + +print(f'\nRemoving validator {validator_2.vote_account.pubkey} ...') +remove_validator_and_approve(validator_2.vote_account.pubkey, test_addrs[0].keypair_path) + +print(f'\nRemoving validator {validator_3.vote_account.pubkey} ...') +remove_validator_and_approve(validator_3.vote_account.pubkey, test_addrs[0].keypair_path) + +print(f'\nRemoving validator {validator.vote_account.pubkey} ...') +remove_validator_and_approve(validator.vote_account.pubkey, test_addrs[0].keypair_path) print('\nConsuming all maintainence instructions (should remove all validators) ...') consume_maintainence_instructions(False) From 6ec4f80884571cc89d54d1b3a3c5d98922bb18ed Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 21 Jul 2023 15:35:27 +0300 Subject: [PATCH 128/131] Lido::LEN -> 466 --- program/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/state.rs b/program/src/state.rs index 1e1307654..83ab0dae5 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -515,7 +515,7 @@ impl Lido { /// Size of a serialized `Lido` struct excluding validators and maintainers. /// /// To update this, run the tests and replace the value here with the test output. - pub const LEN: usize = 474; + pub const LEN: usize = 466; pub fn deserialize_lido(program_id: &Pubkey, lido: &AccountInfo) -> Result { check_account_owner(lido, program_id)?; From ef78cbe0dbf07d02f307be747bab42c2b3cb6e86 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 21 Jul 2023 15:48:22 +0300 Subject: [PATCH 129/131] unimplement uptime recording --- cli/maintainer/src/commands_solido.rs | 12 ---- cli/maintainer/src/maintenance.rs | 1 - program/src/instruction.rs | 4 -- program/src/processor.rs | 7 +- program/src/state.rs | 2 +- program/src/state/validator_perf.rs | 10 --- program/tests/tests/validators_curation.rs | 81 +++------------------- testlib/src/solido_context.rs | 6 +- 8 files changed, 14 insertions(+), 109 deletions(-) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 14b5d1d53..9460789d2 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -280,7 +280,6 @@ pub fn command_create_solido( max_commission: *opts.max_commission(), min_block_production_rate: *opts.min_block_production_rate(), min_vote_success_rate: *opts.min_vote_success_rate(), - min_uptime: *opts.min_uptime(), }, *opts.max_validators(), *opts.max_maintainers(), @@ -675,11 +674,6 @@ impl fmt::Display for ShowSolidoOutput { " Min vote success rate: {:.2}%", 100.0 * to_f64(self.solido.criteria.min_vote_success_rate), )?; - writeln!( - f, - " Min uptime: {:.2}%", - 100.0 * to_f64(self.solido.criteria.min_uptime), - )?; writeln!(f, "\nValidator list {}", self.solido.validator_list)?; writeln!( @@ -772,11 +766,6 @@ impl fmt::Display for ShowSolidoOutput { " Vote Success Rate: {:.2}%", 100.0 * to_f64(perf.vote_success_rate) )?; - writeln!( - f, - " Uptime: {:.2}%", // -- - 100.0 * to_f64(perf.uptime) - )?; } else { writeln!(f, " Not yet collected.")?; } @@ -1310,7 +1299,6 @@ pub fn command_change_criteria( max_commission: *opts.max_commission(), min_block_production_rate: *opts.min_block_production_rate(), min_vote_success_rate: *opts.min_vote_success_rate(), - min_uptime: *opts.min_uptime(), }, ); propose_instruction( diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 4c41186c7..ac86d7d80 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -1118,7 +1118,6 @@ impl SolidoState { &self.solido_program_id, block_production_rate, vote_success_rate, - uptime, &lido::instruction::UpdateOffchainValidatorPerfAccountsMeta { lido: self.solido_address, validator_vote_account_to_update: *validator.pubkey(), diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 7ede7071d..623cf3c2e 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -160,8 +160,6 @@ pub enum LidoInstruction { block_production_rate: u64, #[allow(dead_code)] vote_success_rate: u64, - #[allow(dead_code)] - uptime: u64, }, /// Update the performance metrics for a validator, but only its on-chain part. @@ -652,7 +650,6 @@ pub fn update_offchain_validator_perf( program_id: &Pubkey, block_production_rate: u64, vote_success_rate: u64, - uptime: u64, accounts: &UpdateOffchainValidatorPerfAccountsMeta, ) -> Instruction { Instruction { @@ -661,7 +658,6 @@ pub fn update_offchain_validator_perf( data: LidoInstruction::UpdateOffchainValidatorPerf { block_production_rate, vote_success_rate, - uptime, } .to_vec(), } diff --git a/program/src/processor.rs b/program/src/processor.rs index 26706b116..43e27bc2d 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -656,7 +656,6 @@ pub fn process_update_offchain_validator_perf( program_id: &Pubkey, block_production_rate: u64, vote_success_rate: u64, - uptime: u64, raw_accounts: &[AccountInfo], ) -> ProgramResult { let accounts = UpdateOffchainValidatorPerfAccountsInfo::try_from_slice(raw_accounts)?; @@ -695,16 +694,14 @@ pub fn process_update_offchain_validator_perf( updated_at: clock.epoch, block_production_rate, vote_success_rate, - uptime, }); msg!( - "Validator {} gets new perf: commission={}, block_production_rate={}, vote_success_rate={}, uptime={}", + "Validator {} gets new perf: commission={}, block_production_rate={}, vote_success_rate={}", validator_vote_account_address, commission, block_production_rate, vote_success_rate, - uptime, ); Ok(()) @@ -1329,12 +1326,10 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P LidoInstruction::UpdateOffchainValidatorPerf { block_production_rate, vote_success_rate, - uptime, } => process_update_offchain_validator_perf( program_id, block_production_rate, vote_success_rate, - uptime, accounts, ), LidoInstruction::UpdateOnchainValidatorPerf => { diff --git a/program/src/state.rs b/program/src/state.rs index 83ab0dae5..cafb0a60b 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -1271,7 +1271,7 @@ mod test_lido { developer_account: Pubkey::new_unique(), }, metrics: Metrics::new(), - criteria: Criteria::new(5, 0, 0, 0), + criteria: Criteria::new(5, 0, 0), validator_list: Pubkey::new_unique(), validator_perf_list: Pubkey::new_unique(), maintainer_list: Pubkey::new_unique(), diff --git a/program/src/state/validator_perf.rs b/program/src/state/validator_perf.rs index e9f73ea72..2ca8d0167 100644 --- a/program/src/state/validator_perf.rs +++ b/program/src/state/validator_perf.rs @@ -27,9 +27,6 @@ pub struct Criteria { /// If a validator has `vote_success_rate` lower than this, then it gets deactivated. pub min_vote_success_rate: u64, - - /// If a validator has the uptime lower than this, then it gets deactivated. - pub min_uptime: u64, } impl Default for Criteria { @@ -38,7 +35,6 @@ impl Default for Criteria { max_commission: 100, min_vote_success_rate: 0, min_block_production_rate: 0, - min_uptime: 0, } } } @@ -48,13 +44,11 @@ impl Criteria { max_commission: u8, min_vote_success_rate: u64, min_block_production_rate: u64, - min_uptime: u64, ) -> Self { Self { max_commission, min_vote_success_rate, min_block_production_rate, - min_uptime, } } } @@ -77,9 +71,6 @@ pub struct OffchainValidatorPerf { /// Ratio of successful votes to total votes. pub vote_success_rate: u64, - - /// Ratio of how long the validator has been available to the total time in the epoch. - pub uptime: u64, } /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS @@ -115,7 +106,6 @@ impl ValidatorPerf { && self.rest.as_ref().map_or(true, |perf| { perf.vote_success_rate >= criteria.min_vote_success_rate && perf.block_production_rate >= criteria.min_block_production_rate - && perf.uptime >= criteria.min_uptime }) } } diff --git a/program/tests/tests/validators_curation.rs b/program/tests/tests/validators_curation.rs index 8baff6cae..88a5129fe 100644 --- a/program/tests/tests/validators_curation.rs +++ b/program/tests/tests/validators_curation.rs @@ -67,7 +67,7 @@ async fn test_curate_by_min_block_production_rate() { // And when the validator's block production rate for the epoch is observed: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0, 0) + .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0) .await; assert!(result.is_ok()); @@ -101,41 +101,7 @@ async fn test_curate_by_min_vote_success_rate() { // And when the validator's vote success rate for the epoch is observed: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 98, 0) - .await; - assert!(result.is_ok()); - - // And when the validator's vote success rate is below the minimum: - let result = context - .try_deactivate_if_violates(*validator.pubkey()) - .await; - assert!(result.is_ok()); - - // Then the validators with a lower vote success rate are deactivated: - let validator = &context.get_solido().await.validators.entries[0]; - assert!(!validator.is_active()); -} - -#[tokio::test] -async fn test_curate_by_min_uptime() { - // Given a Solido context and an active validator: - let mut context = Context::new_with_maintainer_and_validator().await; - context.advance_to_normal_epoch(0); - let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.is_active()); - - // When Solido imposes a minimum uptime: - let result = context - .try_change_criteria(&Criteria { - min_uptime: 99, - ..context.criteria - }) - .await; - assert!(result.is_ok()); - - // And when the validator's uptime for the epoch is observed: - let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 98) .await; assert!(result.is_ok()); @@ -160,7 +126,7 @@ async fn test_update_block_production_rate() { // When an epoch passes, and the validator's block production rate is observed: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0, 0) + .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0) .await; assert!(result.is_ok()); @@ -188,7 +154,7 @@ async fn test_update_vote_success_rate() { // When an epoch passes, and the validator's vote success rate is observed: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 98, 0) + .try_update_offchain_validator_perf(*validator.pubkey(), 0, 98) .await; assert!(result.is_ok()); @@ -207,32 +173,7 @@ async fn test_update_vote_success_rate() { } #[tokio::test] -async fn test_update_uptime() { - // Given a Solido context and an active validator: - let mut context = Context::new_with_maintainer_and_validator().await; - context.advance_to_normal_epoch(0); - let validator = &context.get_solido().await.validators.entries[0]; - assert!(validator.is_active()); - - // When an epoch passes, and the validator's uptime is observed: - let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) - .await; - assert!(result.is_ok()); - - // Then the validator's uptime is updated: - let solido = &context.get_solido().await; - let perf = &solido - .validator_perfs - .entries - .iter() - .find(|x| x.validator_vote_account_address == *validator.pubkey()) - .unwrap(); - assert!(perf.rest.as_ref().map_or(false, |x| x.uptime == 98)); -} - -#[tokio::test] -async fn test_uptime_updates_at_most_once_per_epoch() { +async fn test_perf_updates_at_most_once_per_epoch() { // Given a Solido context and an active validator: let mut context = Context::new_with_maintainer_and_validator().await; context.advance_to_normal_epoch(0); @@ -241,13 +182,13 @@ async fn test_uptime_updates_at_most_once_per_epoch() { // When the uptime of a validator gets updated: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) + .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0) .await; assert!(result.is_ok()); // And when the uptime of the same validator gets updated again in the same epoch: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 99) + .try_update_offchain_validator_perf(*validator.pubkey(), 99, 0) .await; // Then the second update fails: @@ -258,7 +199,7 @@ async fn test_uptime_updates_at_most_once_per_epoch() { // Then the second update succeeds: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 99) + .try_update_offchain_validator_perf(*validator.pubkey(), 99, 0) .await; assert!(result.is_ok()); } @@ -272,14 +213,14 @@ async fn test_bring_back() { let result = context .try_change_criteria(&Criteria { - min_uptime: 99, + min_block_production_rate: 99, ..context.criteria }) .await; assert!(result.is_ok()); let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 98) + .try_update_offchain_validator_perf(*validator.pubkey(), 98, 0) .await; assert!(result.is_ok()); @@ -294,7 +235,7 @@ async fn test_bring_back() { // And when the validator's performance is back to normal: let result = context - .try_update_offchain_validator_perf(*validator.pubkey(), 0, 0, 101) + .try_update_offchain_validator_perf(*validator.pubkey(), 101, 0) .await; assert!(result.is_ok()); diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 713ab9b19..f5c7a3a09 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -246,7 +246,7 @@ impl Context { stake_authority, mint_authority, deterministic_keypair, - criteria: Criteria::new(5, 0, 0, 0), + criteria: Criteria::new(5, 0, 0), }; result.st_sol_mint = result.create_mint(result.mint_authority).await; @@ -1277,7 +1277,6 @@ impl Context { validator_vote_account: Pubkey, new_block_production_rate: u64, new_vote_success_rate: u64, - new_uptime: u64, ) -> transport::Result<()> { send_transaction( &mut self.context, @@ -1285,7 +1284,6 @@ impl Context { &id(), new_block_production_rate, new_vote_success_rate, - new_uptime, &instruction::UpdateOffchainValidatorPerfAccountsMeta { lido: self.solido.pubkey(), validator_vote_account_to_update: validator_vote_account, @@ -1303,13 +1301,11 @@ impl Context { validator_vote_account: Pubkey, new_block_production_rate: u64, new_vote_success_rate: u64, - new_uptime: u64, ) { self.try_update_offchain_validator_perf( validator_vote_account, new_block_production_rate, new_vote_success_rate, - new_uptime, ) .await .expect("Validator performance metrics could always be updated"); From e31385402a653c4c1ba4309b302cf3c3e1bd41d5 Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Fri, 21 Jul 2023 15:55:45 +0300 Subject: [PATCH 130/131] maintainer forgets about uptime --- cli/maintainer/src/maintenance.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index ac86d7d80..9ed00a9ef 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -75,7 +75,6 @@ pub enum MaintenanceOutput { validator_vote_account: Pubkey, block_production_rate: u64, vote_success_rate: u64, - uptime: u64, }, UpdateOnchainValidatorPerf { @@ -205,7 +204,6 @@ impl fmt::Display for MaintenanceOutput { validator_vote_account, block_production_rate, vote_success_rate, - uptime, } => { writeln!(f, "Updated off-chain validator performance.")?; writeln!( @@ -223,11 +221,6 @@ impl fmt::Display for MaintenanceOutput { " New vote success rate: {:.2}%", 100.0 * to_f64(*vote_success_rate) )?; - writeln!( - f, - " New uptime: {:.2}%", - 100.0 * to_f64(*uptime) - )?; } MaintenanceOutput::UpdateOnchainValidatorPerf { validator_vote_account, @@ -1112,8 +1105,6 @@ impl SolidoState { let vote_success_rate = per64(vote_state.credits().min(slots_per_epoch), slots_per_epoch); - let uptime = 0; - let instruction = lido::instruction::update_offchain_validator_perf( &self.solido_program_id, block_production_rate, @@ -1129,7 +1120,6 @@ impl SolidoState { validator_vote_account: *validator.pubkey(), block_production_rate, vote_success_rate, - uptime, }; return Some(MaintenanceInstruction::new(instruction, task)); } From a03b3bc93e58e9782675c483371d379493f77d4c Mon Sep 17 00:00:00 2001 From: Denis Makarenko Date: Mon, 31 Jul 2023 19:44:36 +0300 Subject: [PATCH 131/131] do not try to revive a doomed validator --- cli/maintainer/src/maintenance.rs | 2 +- program/src/state/validator.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index 9ed00a9ef..622bbd51e 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -881,7 +881,7 @@ impl SolidoState { // and are now performing well. // // If the vote account is closed, no need to reactivate. - if !validator.is_active() + if validator.is_inactive() && vote_state.as_ref().map_or(false, |vote_state| { does_perform_well( &self.solido.criteria, diff --git a/program/src/state/validator.rs b/program/src/state/validator.rs index ac65bd22c..3cc617fff 100644 --- a/program/src/state/validator.rs +++ b/program/src/state/validator.rs @@ -196,6 +196,12 @@ impl Validator { self.status == ValidatorStatus::AcceptingStakes } + /// True only if the validator has been suppressed, and not accepting new stake, + /// but they still has not been queued for removal. + pub fn is_inactive(&self) -> bool { + self.status == ValidatorStatus::StakesSuspended + } + /// Mark the validator as active so that they could receive new stake. pub fn activate(&mut self) { if self.status != ValidatorStatus::StakesSuspended {