use std::ops::Mul;
use crate::account_map::PubkeyAndEntry;
use crate::state::{Validator, Validators};
use crate::{
error::LidoError,
token,
token::{Lamports, Rational},
};
pub fn get_target_balance(
undelegated_lamports: Lamports,
validators: &Validators,
) -> Result<Vec<Lamports>, LidoError> {
let total_delegated_lamports: token::Result<Lamports> = validators
.iter_entries()
.map(|v| v.stake_accounts_balance)
.sum();
let total_lamports = total_delegated_lamports.and_then(|t| t + undelegated_lamports)?;
let num_active_validators = validators.iter_active().count() as u64;
if num_active_validators == 0 {
return Err(LidoError::NoActiveValidators);
}
let lamports_per_validator = total_lamports
.mul(Rational {
numerator: 1,
denominator: num_active_validators,
})
.expect("Does not divide by zero because `num_active_validators != 0`");
let mut target_balance: Vec<Lamports> = validators
.iter_entries()
.map(|validator| {
if validator.active {
lamports_per_validator
} else {
Lamports(0)
}
})
.collect();
let total_lamports_distributed = target_balance
.iter()
.cloned()
.sum::<token::Result<Lamports>>()
.expect("Does not overflow, is at most total_lamports.");
let mut remainder = (total_lamports - total_lamports_distributed)
.expect("Does not underflow because we distribute at most total_lamports.");
assert!(remainder.0 < num_active_validators);
for (target, validator) in target_balance.iter_mut().zip(validators.iter_entries()) {
if remainder == Lamports(0) {
break;
}
if validator.active {
*target = (*target + Lamports(1)).expect(
"Does not overflow because per-validator balance is at most total_lamports.",
);
remainder =
(remainder - Lamports(1)).expect("Does not underflow due to loop condition.");
}
}
let total_lamports_distributed = target_balance
.iter()
.cloned()
.sum::<token::Result<Lamports>>()
.expect("Does not overflow, is at most total_lamports.");
assert_eq!(total_lamports_distributed, total_lamports);
Ok(target_balance)
}
pub fn get_unstake_validator_index(
validators: &Validators,
target_balance: &[Lamports],
threshold: Rational,
) -> Option<(usize, Lamports)> {
let needs_unstake =
validators
.entries
.iter()
.zip(target_balance.iter())
.any(|(validator, target)| {
let target_difference = target
.0
.saturating_sub(validator.entry.effective_stake_balance().0);
if target == &Lamports(0) {
return false;
}
Rational {
numerator: target_difference,
denominator: target.0,
} >= threshold
});
let ((idx, validator), target) = validators
.entries
.iter()
.enumerate()
.zip(target_balance)
.max_by_key(|((_idx, validator), target)| {
validator
.entry
.effective_stake_balance()
.0
.saturating_sub(target.0)
})?;
let amount = validator
.entry
.effective_stake_balance()
.0
.saturating_sub(target.0);
let ratio = Rational {
numerator: amount,
denominator: target.0,
};
if ratio >= threshold || needs_unstake {
Some((idx, Lamports(amount)))
} else {
None
}
}
pub fn get_minimum_stake_validator_index_amount(
validators: &Validators,
target_balance: &[Lamports],
) -> (usize, Lamports) {
assert_eq!(
validators.len(),
target_balance.len(),
"Must have as many target balances as current balances."
);
let mut index = validators
.iter_entries()
.position(|v| v.active)
.expect("get_minimum_stake_validator_index_amount requires at least one active validator.");
let mut lowest_balance = validators.entries[index].entry.effective_stake_balance();
let mut amount = Lamports(
target_balance[index]
.0
.saturating_sub(validators.entries[index].entry.effective_stake_balance().0),
);
for (i, (validator, target)) in validators.iter_entries().zip(target_balance).enumerate() {
if validator.active && validator.effective_stake_balance() < lowest_balance {
index = i;
amount = Lamports(
target
.0
.saturating_sub(validator.effective_stake_balance().0),
);
lowest_balance = validator.effective_stake_balance();
}
}
(index, amount)
}
pub fn get_validator_to_withdraw(
validators: &Validators,
) -> Result<&PubkeyAndEntry<Validator>, crate::error::LidoError> {
validators
.entries
.iter()
.max_by_key(|v| v.entry.effective_stake_balance())
.ok_or(LidoError::NoActiveValidators)
}
#[cfg(test)]
mod test {
use super::*;
use crate::state::Validators;
use crate::token::Lamports;
#[test]
fn get_target_balance_works_for_single_validator() {
let mut validators = Validators::new_fill_default(1);
validators.entries[0].entry.stake_accounts_balance = Lamports(100);
let undelegated_stake = Lamports(50);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(targets[0], Lamports(150));
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(0, Lamports(50))
);
}
#[test]
fn get_target_balance_works_for_integer_multiple() {
let mut validators = Validators::new_fill_default(2);
validators.entries[0].entry.stake_accounts_balance = Lamports(101);
validators.entries[1].entry.stake_accounts_balance = Lamports(99);
let undelegated_stake = Lamports(50);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(targets, [Lamports(125), Lamports(125)]);
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(1, Lamports(26))
);
}
#[test]
fn get_target_balance_works_for_non_integer_multiple() {
let mut validators = Validators::new_fill_default(2);
validators.entries[0].entry.stake_accounts_balance = Lamports(101);
validators.entries[1].entry.stake_accounts_balance = Lamports(99);
let undelegated_stake = Lamports(51);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(targets, [Lamports(126), Lamports(125)]);
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(1, Lamports(26))
);
}
#[test]
fn get_target_balance_already_balanced() {
let mut validators = Validators::new_fill_default(2);
validators.entries[0].entry.stake_accounts_balance = Lamports(50);
validators.entries[1].entry.stake_accounts_balance = Lamports(50);
let undelegated_stake = Lamports(0);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(targets, [Lamports(50), Lamports(50)]);
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(0, Lamports(0))
);
}
#[test]
fn get_target_balance_works_with_inactive_for_non_integer_multiple() {
let mut validators = Validators::new_fill_default(3);
validators.entries[0].entry.stake_accounts_balance = Lamports(101);
validators.entries[1].entry.stake_accounts_balance = Lamports(0);
validators.entries[1].entry.active = false;
validators.entries[2].entry.stake_accounts_balance = Lamports(99);
let undelegated_stake = Lamports(51);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(targets, [Lamports(126), Lamports(0), Lamports(125)]);
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(2, Lamports(26))
);
}
#[test]
fn get_target_balance_works_with_inactive_for_integer_multiple() {
let mut validators = Validators::new_fill_default(3);
validators.entries[0].entry.stake_accounts_balance = Lamports(100);
validators.entries[1].entry.stake_accounts_balance = Lamports(100);
validators.entries[1].entry.active = false;
validators.entries[2].entry.stake_accounts_balance = Lamports(300);
let undelegated_stake = Lamports(0);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(targets, [Lamports(250), Lamports(0), Lamports(250)]);
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(0, Lamports(150))
);
}
#[test]
fn get_target_balance_all_inactive() {
let mut validators = Validators::new_fill_default(3);
validators.entries[0].entry.stake_accounts_balance = Lamports(1);
validators.entries[1].entry.stake_accounts_balance = Lamports(2);
validators.entries[2].entry.stake_accounts_balance = Lamports(3);
validators.entries[0].entry.active = false;
validators.entries[1].entry.active = false;
validators.entries[2].entry.active = false;
let undelegated_stake = Lamports(0);
let result = get_target_balance(undelegated_stake, &validators);
assert!(result.is_err());
}
#[test]
fn get_target_balance_no_preference_but_some_inactive() {
let mut validators = Validators::new_fill_default(2);
validators.entries[0].entry.stake_accounts_balance = Lamports(0);
validators.entries[1].entry.stake_accounts_balance = Lamports(10);
validators.entries[0].entry.active = false;
let undelegated_stake = Lamports(0);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(1, Lamports(0)),
);
}
#[test]
fn get_target_balance_works_for_minimum_staked_validator() {
let mut validators = Validators::new_fill_default(3);
validators.entries[0].entry.stake_accounts_balance = Lamports(101);
validators.entries[1].entry.stake_accounts_balance = Lamports(101);
validators.entries[2].entry.stake_accounts_balance = Lamports(100);
let undelegated_stake = Lamports(200);
let targets = get_target_balance(undelegated_stake, &validators).unwrap();
assert_eq!(targets, [Lamports(168), Lamports(167), Lamports(167)]);
assert_eq!(
get_minimum_stake_validator_index_amount(&validators, &targets[..]),
(2, Lamports(67))
);
}
#[test]
fn get_unstake_from_active_validator_above_or_equal_threshold() {
let mut validators = Validators::new_fill_default(3);
validators.entries[0].entry.stake_accounts_balance = Lamports(10);
validators.entries[1].entry.stake_accounts_balance = Lamports(16);
validators.entries[2].entry.stake_accounts_balance = Lamports(10);
let targets = get_target_balance(Lamports(0), &validators).unwrap();
let minimum_unstake = get_unstake_validator_index(
&validators,
&targets,
Rational {
numerator: 1,
denominator: 4,
},
);
assert_eq!(minimum_unstake, Some((1, Lamports(4))));
let minimum_unstake = get_unstake_validator_index(
&validators,
&targets,
Rational {
numerator: 1,
denominator: 5,
},
);
assert_eq!(minimum_unstake, Some((1, Lamports(4))));
}
#[test]
fn get_unstake_from_active_validator_below_threshold() {
let mut validators = Validators::new_fill_default(3);
validators.entries[0].entry.stake_accounts_balance = Lamports(10);
validators.entries[1].entry.stake_accounts_balance = Lamports(16);
validators.entries[2].entry.stake_accounts_balance = Lamports(10);
let targets = get_target_balance(Lamports(0), &validators).unwrap();
let minimum_unstake = get_unstake_validator_index(
&validators,
&targets,
Rational {
numerator: 1,
denominator: 2,
},
);
assert_eq!(minimum_unstake, None);
}
#[test]
fn get_unstake_from_active_validator_because_another_needs_stake() {
let mut validators = Validators::new_fill_default(3);
validators.entries[0].entry.stake_accounts_balance = Lamports(17);
validators.entries[1].entry.stake_accounts_balance = Lamports(15);
validators.entries[2].entry.stake_accounts_balance = Lamports(0);
let targets = get_target_balance(Lamports(0), &validators).unwrap();
let minimum_unstake = get_unstake_validator_index(
&validators,
&targets,
Rational {
numerator: 1,
denominator: 1,
},
);
assert_eq!(minimum_unstake, Some((0, Lamports(6))))
}
}