use namada_sdk::chain::Epoch;
use namada_sdk::proof_of_stake::parameters::{OwnedPosParams, PosParams};
use namada_sdk::proof_of_stake::test_utils::test_init_genesis as init_genesis;
use namada_sdk::proof_of_stake::types::GenesisValidator;
use crate::tx::tx_host_env;
pub fn init_pos(
genesis_validators: &[GenesisValidator],
params: &OwnedPosParams,
start_epoch: Epoch,
) -> PosParams {
tx_host_env::init();
tx_host_env::with(|tx_env| {
let native_token = tx_env.state.in_mem().native_token.clone();
tx_env.spawn_accounts([&native_token]);
for validator in genesis_validators {
tx_env.spawn_accounts([&validator.address]);
tx_env.init_account_storage(
&validator.address,
vec![validator.consensus_key.clone()],
1,
)
}
tx_env.state.in_mem_mut().block.epoch = start_epoch;
let params = init_genesis::<
_,
crate::parameters::Store<_>,
crate::governance::Store<_>,
crate::token::Store<_>,
>(
&mut tx_env.state,
params.clone(),
genesis_validators.iter().cloned(),
start_epoch,
)
.unwrap();
tx_env.commit_genesis();
params
})
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use namada_sdk::gas::VpGasMeter;
use namada_sdk::governance::parameters::GovernanceParameters;
use namada_sdk::key::common::PublicKey;
use namada_sdk::validation::PosVp;
use namada_sdk::{address, token};
use namada_tx_prelude::Address;
use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params;
use proptest::prelude::*;
use proptest::test_runner::Config;
use proptest_state_machine::{
ReferenceStateMachine, StateMachineTest, prop_state_machine,
};
use test_log::test;
use super::testing::{
InvalidPosAction, ValidPosAction, arb_invalid_pos_action,
arb_valid_pos_action,
};
use super::*;
use crate::native_vp::TestNativeVpEnv;
prop_state_machine! {
#![proptest_config(Config {
// Instead of the default 256, we only run 5 because otherwise it
// takes too long and it's preferable to crank up the number of
// transitions instead, to allow each case to run for more epochs as
// some issues only manifest once the model progresses further.
// Additionally, more cases will be explored every time this test is
// executed in the CI.
cases: 5,
verbose: 1,
.. Config::default()
})]
#[test]
#[ignore]
fn pos_vp_state_machine_test(sequential 1..100 => ConcretePosState);
}
#[derive(Clone, Debug)]
struct AbstractPosState {
epoch: Epoch,
params: PosParams,
valid_actions: Vec<ValidPosAction>,
invalid_pos_changes: Vec<InvalidPosAction>,
invalid_arbitrary_changes: crate::storage::Changes,
committed_valid_actions: Vec<ValidPosAction>,
}
#[derive(Debug)]
struct ConcretePosState {
is_current_tx_valid: bool,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
enum Transition {
CommitTx,
NextEpoch,
Valid(ValidPosAction),
InvalidPos(InvalidPosAction),
#[allow(dead_code)]
InvalidArbitrary(crate::storage::Change),
}
impl StateMachineTest for ConcretePosState {
type Reference = AbstractPosState;
type SystemUnderTest = Self;
fn init_test(
initial_state: &<Self::Reference as ReferenceStateMachine>::State,
) -> Self::SystemUnderTest {
println!();
println!("New test case");
init_pos(&[], &initial_state.params, initial_state.epoch);
for change in &initial_state.committed_valid_actions {
println!("Apply init state change {:#?}", change);
change.clone().apply(true)
}
tx_host_env::commit_tx_and_block();
Self {
is_current_tx_valid: true,
}
}
fn apply(
mut test_state: Self::SystemUnderTest,
_ref_state: &<Self::Reference as ReferenceStateMachine>::State,
transition: <Self::Reference as ReferenceStateMachine>::Transition,
) -> Self::SystemUnderTest {
match transition {
Transition::CommitTx => {
if !test_state.is_current_tx_valid {
tx_host_env::with(|env| {
env.state.drop_tx_batch();
});
}
tx_host_env::commit_tx_and_block();
test_state.is_current_tx_valid = true;
}
Transition::NextEpoch => {
tx_host_env::with(|env| {
if !test_state.is_current_tx_valid {
env.state.drop_tx_batch();
}
env.commit_tx_and_block();
env.state.in_mem_mut().block.epoch =
env.state.in_mem().block.epoch.next();
});
test_state.is_current_tx_valid = true;
}
Transition::Valid(change) => {
change.apply(test_state.is_current_tx_valid);
test_state.validate_transitions();
}
Transition::InvalidPos(change) => {
test_state.is_current_tx_valid = false;
change.apply();
test_state.validate_transitions();
}
Transition::InvalidArbitrary(_) => {
test_state.is_current_tx_valid = false;
test_state.validate_transitions();
tx_host_env::with(|env| {
env.state.drop_tx_batch();
})
}
}
test_state
}
}
impl ReferenceStateMachine for AbstractPosState {
type State = Self;
type Transition = Transition;
fn init_state() -> BoxedStrategy<Self::State> {
(arb_pos_params(None), 0..100_u64)
.prop_flat_map(|(params, epoch)| {
let state = vec![];
let epoch = Epoch(epoch);
let params = PosParams {
owned: params,
max_proposal_period: GovernanceParameters::default()
.max_proposal_period,
};
arb_valid_pos_action(state.clone()).prop_map(
move |valid_action| Self {
epoch,
params: params.clone(),
valid_actions: vec![],
invalid_pos_changes: vec![],
invalid_arbitrary_changes: vec![],
committed_valid_actions: vec![valid_action],
},
)
})
.boxed()
}
fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition> {
let valid_actions = state.all_valid_actions();
prop_oneof![
Just(Transition::CommitTx),
Just(Transition::NextEpoch),
arb_valid_pos_action(valid_actions.clone())
.prop_map(Transition::Valid),
arb_invalid_pos_action(valid_actions.clone())
.prop_map(Transition::InvalidPos),
]
.boxed()
}
fn apply(
mut state: Self::State,
transition: &Self::Transition,
) -> Self::State {
match transition {
Transition::CommitTx => {
state.commit_tx();
}
Transition::NextEpoch => {
state.commit_tx();
state.epoch = state.epoch.next();
}
Transition::Valid(transition) => {
state.valid_actions.push(transition.clone());
}
Transition::InvalidPos(change) => {
state.invalid_pos_changes.push(change.clone());
}
Transition::InvalidArbitrary(transition) => {
state.invalid_arbitrary_changes.push(transition.clone());
}
}
state
}
fn preconditions(
state: &Self::State,
transition: &Self::Transition,
) -> bool {
match transition {
Transition::CommitTx => true,
Transition::NextEpoch => true,
Transition::Valid(action) => match action {
ValidPosAction::InitValidator {
address,
consensus_key,
commission_rate: _,
max_commission_rate_change: _,
} => {
!state.is_validator(address)
&& !state.is_used_key(consensus_key)
}
ValidPosAction::Bond {
amount: _,
owner: _,
validator,
} => state.is_validator(validator),
ValidPosAction::Unbond {
amount,
owner,
validator,
} => {
state.is_validator(validator)
&& state.has_enough_bonds(owner, validator, *amount)
}
ValidPosAction::Withdraw { owner, validator } => {
state.is_validator(validator)
&& state.has_withdrawable_unbonds(owner, validator)
}
},
Transition::InvalidPos(_) => true,
Transition::InvalidArbitrary(_) => true,
}
}
}
impl ConcretePosState {
fn validate_transitions(&self) {
let tx_env = tx_host_env::take();
let gas_meter = RefCell::new(VpGasMeter::new_from_meter(
&*tx_env.gas_meter.borrow(),
));
let vp_env = TestNativeVpEnv::from_tx_env(tx_env, address::POS);
let ctx = vp_env.ctx(&gas_meter);
let result = PosVp::validate_tx(
&ctx,
&vp_env.tx_env.batched_tx.to_ref(),
&vp_env.keys_changed,
&vp_env.verifiers,
);
tx_host_env::set(vp_env.tx_env);
match (self.is_current_tx_valid, result) {
(true, Ok(())) => {}
(true, Err(err)) => {
panic!(
"Validation of valid changes must pass! Got error: \
{err}"
);
}
(false, Err(_)) => {}
(false, Ok(())) => {
panic!("Validation of invalid changes must fail!");
}
}
}
}
impl AbstractPosState {
fn commit_tx(&mut self) {
let valid_actions_to_commit =
std::mem::take(&mut self.valid_actions);
if self.invalid_pos_changes.is_empty()
&& self.invalid_arbitrary_changes.is_empty()
{
self.committed_valid_actions.extend(valid_actions_to_commit);
}
self.invalid_pos_changes = vec![];
self.invalid_arbitrary_changes = vec![];
}
fn all_valid_actions(&self) -> Vec<ValidPosAction> {
[
self.committed_valid_actions.clone(),
self.valid_actions.clone(),
]
.concat()
}
fn is_validator(&self, addr: &Address) -> bool {
self.all_valid_actions().iter().any(|action| match action {
ValidPosAction::InitValidator { address, .. } => {
address == addr
}
_ => false,
})
}
fn is_used_key(&self, given_consensus_key: &PublicKey) -> bool {
self.all_valid_actions().iter().any(|action| match action {
ValidPosAction::InitValidator { consensus_key, .. } => {
consensus_key == given_consensus_key
}
_ => false,
})
}
fn has_enough_bonds(
&self,
owner: &Address,
validator: &Address,
amount: token::Amount,
) -> bool {
let raw_amount: u128 = amount.try_into().unwrap();
let mut total_bonds: u64 = 0;
for action in self.all_valid_actions().into_iter() {
match action {
ValidPosAction::Bond {
amount,
owner: bond_owner,
validator: bond_validator,
} => {
if owner == &bond_owner && validator == &bond_validator
{
let raw_amount: u128 = amount.try_into().unwrap();
total_bonds += raw_amount as u64;
}
}
ValidPosAction::Unbond {
amount,
owner: bond_owner,
validator: bond_validator,
} => {
if owner == &bond_owner && validator == &bond_validator
{
let raw_amount: u128 = amount.try_into().unwrap();
total_bonds -= raw_amount as u64;
}
}
_ => {}
}
}
total_bonds as u128 >= raw_amount
}
fn has_withdrawable_unbonds(
&self,
owner: &Address,
validator: &Address,
) -> bool {
self.all_valid_actions()
.into_iter()
.any(|action| match action {
ValidPosAction::Unbond {
amount: _,
owner: bond_owner,
validator: bond_validator,
} => owner == &bond_owner && validator == &bond_validator,
_ => false,
})
}
}
}
pub mod testing {
use std::cell::RefCell;
use derivative::Derivative;
use itertools::Either;
use namada_sdk::chain::Epoch;
use namada_sdk::dec::Dec;
use namada_sdk::gas::{GasMetering, TxGasMeter};
use namada_sdk::key::RefTo;
use namada_sdk::key::common::PublicKey;
use namada_sdk::proof_of_stake::ADDRESS as POS_ADDRESS;
use namada_sdk::proof_of_stake::epoched::DynEpochOffset;
use namada_sdk::proof_of_stake::parameters::PosParams;
use namada_sdk::proof_of_stake::parameters::testing::arb_rate;
use namada_sdk::proof_of_stake::storage::{
get_num_consensus_validators, read_pos_params, unbond_handle,
};
use namada_sdk::proof_of_stake::types::{BondId, ValidatorState};
use namada_sdk::token::{Amount, Change};
use namada_sdk::{address, governance, key, token};
use namada_tx_prelude::{Address, StorageRead, StorageWrite};
use namada_vm::host_env::gas_meter::GasMeter;
use proptest::prelude::*;
use crate::tx::{self, tx_host_env};
#[derive(Clone, Debug, Default)]
pub struct TestValidator {
pub address: Option<Address>,
pub stake: Option<token::Amount>,
pub unstaked_balances: Vec<(Address, token::Amount)>,
}
pub type PosStorageChanges = Vec<PosStorageChange>;
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ValidPosAction {
InitValidator {
address: Address,
consensus_key: PublicKey,
commission_rate: Dec,
max_commission_rate_change: Dec,
},
Bond {
amount: token::Amount,
owner: Address,
validator: Address,
},
Unbond {
amount: token::Amount,
owner: Address,
validator: Address,
},
Withdraw {
owner: Address,
validator: Address,
},
}
#[derive(Clone, Debug)]
pub struct InvalidPosAction {
changes: Vec<(Epoch, PosStorageChanges)>,
}
#[derive(Clone, Derivative)]
#[derivative(Debug)]
pub enum PosStorageChange {
SpawnAccount {
address: Address,
},
Bond {
owner: Address,
validator: Address,
delta: Change,
offset: DynEpochOffset,
},
Unbond {
owner: Address,
validator: Address,
delta: Change,
},
WithdrawUnbond {
owner: Address,
validator: Address,
},
TotalDeltas {
delta: Change,
offset: Either<DynEpochOffset, Epoch>,
},
ValidatorSet {
validator: Address,
token_delta: Change,
offset: DynEpochOffset,
},
ValidatorConsensusKey {
validator: Address,
#[derivative(Debug = "ignore")]
pk: PublicKey,
},
ValidatorDeltas {
validator: Address,
delta: Change,
offset: DynEpochOffset,
},
ValidatorState {
validator: Address,
state: ValidatorState,
},
StakingTokenPosBalance {
delta: Change,
},
ValidatorAddressRawHash {
address: Address,
#[derivative(Debug = "ignore")]
consensus_key: PublicKey,
},
ValidatorCommissionRate {
address: Address,
rate: Dec,
},
ValidatorMaxCommissionRateChange {
address: Address,
change: Dec,
},
}
pub fn arb_valid_pos_action(
valid_actions: Vec<ValidPosAction>,
) -> impl Strategy<Value = ValidPosAction> {
let validators: Vec<Address> = valid_actions
.iter()
.filter_map(|action| match action {
ValidPosAction::InitValidator { address, .. } => {
Some(address.clone())
}
_ => None,
})
.collect();
let init_validator = (
address::testing::arb_established_address(),
key::testing::arb_common_keypair(),
arb_rate(),
arb_rate(),
)
.prop_map(
|(
addr,
consensus_key,
commission_rate,
max_commission_rate_change,
)| {
ValidPosAction::InitValidator {
address: Address::Established(addr),
consensus_key: consensus_key.ref_to(),
commission_rate,
max_commission_rate_change,
}
},
);
if validators.is_empty() {
init_validator.boxed()
} else {
let arb_validator = proptest::sample::select(validators);
let arb_validator_or_address = prop_oneof![
arb_validator.clone(),
address::testing::arb_established_address()
.prop_map(Address::Established),
];
let arb_bond = (
(0..u64::MAX / 1_000),
arb_validator_or_address,
arb_validator,
)
.prop_map(|(amount, owner, validator)| ValidPosAction::Bond {
amount: Amount::from_uint(amount, 0).unwrap(),
owner,
validator,
});
let current_bonds: Vec<(BondId, token::Amount)> = valid_actions
.iter()
.filter_map(|action| match action {
ValidPosAction::Bond {
amount,
owner,
validator,
} => Some((
BondId {
source: owner.clone(),
validator: validator.clone(),
},
*amount,
)),
_ => None,
})
.collect();
if current_bonds.is_empty() {
prop_oneof![init_validator, arb_bond].boxed()
} else {
let arb_current_bond = proptest::sample::select(current_bonds);
let arb_unbond = arb_current_bond.prop_flat_map(
|(bond_id, current_bond_amount)| {
let current_bond_amount =
<Amount as TryInto<u128>>::try_into(
current_bond_amount,
)
.unwrap() as u64;
(0..current_bond_amount).prop_map(move |amount| {
ValidPosAction::Unbond {
amount: Amount::from_uint(amount, 0).unwrap(),
owner: bond_id.source.clone(),
validator: bond_id.validator.clone(),
}
})
},
);
let withdrawable_unbonds: Vec<BondId> = valid_actions
.iter()
.filter_map(|action| match action {
ValidPosAction::Unbond {
amount: _,
owner,
validator,
} => Some(BondId {
source: owner.clone(),
validator: validator.clone(),
}),
_ => None,
})
.collect();
if withdrawable_unbonds.is_empty() {
prop_oneof![init_validator, arb_bond, arb_unbond].boxed()
} else {
let arb_current_unbond =
proptest::sample::select(withdrawable_unbonds);
let arb_withdrawal =
arb_current_unbond.prop_map(|bond_id| {
ValidPosAction::Withdraw {
owner: bond_id.source.clone(),
validator: bond_id.validator,
}
});
prop_oneof![
init_validator,
arb_bond,
arb_unbond,
arb_withdrawal
]
.boxed()
}
}
}
}
impl ValidPosAction {
pub fn apply(self, is_current_tx_valid: bool) {
let params =
read_pos_params::<_, governance::Store<_>>(tx::ctx()).unwrap();
let current_epoch = tx_host_env::with(|env| {
let gas_limit = env.gas_meter.borrow().get_gas_limit();
env.gas_meter =
RefCell::new(GasMeter::Native(TxGasMeter::new(
gas_limit,
namada_sdk::parameters::get_gas_scale(tx::ctx())
.unwrap(),
)));
env.state.in_mem().block.epoch
});
println!("Current epoch {}", current_epoch);
let changes = self.into_storage_changes(current_epoch);
for change in changes {
apply_pos_storage_change(
change,
¶ms,
current_epoch,
is_current_tx_valid,
)
}
}
pub fn into_storage_changes(
self,
_current_epoch: Epoch,
) -> PosStorageChanges {
match self {
ValidPosAction::InitValidator {
address,
consensus_key,
commission_rate,
max_commission_rate_change,
} => {
let offset = DynEpochOffset::PipelineLen;
vec![
PosStorageChange::SpawnAccount {
address: address.clone(),
},
PosStorageChange::ValidatorAddressRawHash {
address: address.clone(),
consensus_key: consensus_key.clone(),
},
PosStorageChange::ValidatorSet {
validator: address.clone(),
token_delta: 0.into(),
offset,
},
PosStorageChange::ValidatorConsensusKey {
validator: address.clone(),
pk: consensus_key,
},
PosStorageChange::ValidatorState {
validator: address.clone(),
state: ValidatorState::Consensus,
},
PosStorageChange::ValidatorDeltas {
validator: address.clone(),
delta: 0.into(),
offset,
},
PosStorageChange::ValidatorCommissionRate {
address: address.clone(),
rate: commission_rate,
},
PosStorageChange::ValidatorMaxCommissionRateChange {
address,
change: max_commission_rate_change,
},
]
}
ValidPosAction::Bond {
amount,
owner,
validator,
} => {
let offset = DynEpochOffset::PipelineLen;
let token_delta = amount.change();
let mut changes = Vec::with_capacity(10);
changes.push(PosStorageChange::SpawnAccount {
address: owner.clone(),
});
changes.extend([
PosStorageChange::ValidatorSet {
validator: validator.clone(),
token_delta,
offset,
},
PosStorageChange::TotalDeltas {
delta: token_delta,
offset: Either::Left(offset),
},
PosStorageChange::ValidatorDeltas {
validator: validator.clone(),
delta: token_delta,
offset,
},
]);
changes.extend([
PosStorageChange::Bond {
owner,
validator,
delta: token_delta,
offset,
},
PosStorageChange::StakingTokenPosBalance {
delta: token_delta,
},
]);
changes
}
ValidPosAction::Unbond {
amount,
owner,
validator,
} => {
let offset = DynEpochOffset::UnbondingLen;
let token_delta = -amount.change();
let mut changes = Vec::with_capacity(6);
changes.extend([
PosStorageChange::ValidatorSet {
validator: validator.clone(),
token_delta,
offset,
},
PosStorageChange::TotalDeltas {
delta: token_delta,
offset: Either::Left(offset),
},
PosStorageChange::ValidatorDeltas {
validator: validator.clone(),
delta: token_delta,
offset,
},
]);
changes.extend([
PosStorageChange::Unbond {
owner: owner.clone(),
validator: validator.clone(),
delta: -token_delta,
},
PosStorageChange::Bond {
owner,
validator,
delta: token_delta,
offset,
},
]);
changes
}
ValidPosAction::Withdraw { owner, validator } => {
let _unbond = unbond_handle(&owner, &validator);
let token_delta = token::Change::zero();
vec![
PosStorageChange::WithdrawUnbond { owner, validator },
PosStorageChange::StakingTokenPosBalance {
delta: -token_delta,
},
]
}
}
}
}
pub fn apply_pos_storage_change(
change: PosStorageChange,
params: &PosParams,
current_epoch: Epoch,
_is_current_tx_valid: bool,
) {
match change {
PosStorageChange::SpawnAccount { address } => {
tx_host_env::with(move |env| {
env.spawn_accounts([&address]);
});
}
PosStorageChange::Bond {
owner,
validator,
delta: _,
offset: _,
} => {
let _bond_id = BondId {
source: owner,
validator,
};
}
PosStorageChange::Unbond {
owner: _,
validator: _,
delta: _,
} => {
}
PosStorageChange::TotalDeltas {
delta: _,
offset: _,
} => {
}
PosStorageChange::ValidatorAddressRawHash {
address: _,
consensus_key: _,
} => {
}
PosStorageChange::ValidatorSet {
validator,
token_delta,
offset,
} => {
apply_validator_set_change(
validator,
token_delta,
offset,
current_epoch,
params,
);
}
PosStorageChange::ValidatorConsensusKey {
validator: _,
pk: _,
} => {
}
PosStorageChange::ValidatorDeltas {
validator: _,
delta: _,
offset: _,
} => {
}
PosStorageChange::ValidatorState {
validator: _,
state: _,
} => {
}
PosStorageChange::StakingTokenPosBalance { delta } => {
let balance_key = token::storage_key::balance_key(
&tx::ctx().get_native_token().unwrap(),
&POS_ADDRESS,
);
let mut balance: token::Amount =
tx::ctx().read(&balance_key).unwrap().unwrap_or_default();
if !delta.non_negative() {
let to_spend = token::Amount::from_change(delta);
balance.spend(&to_spend).unwrap();
} else {
let to_recv = token::Amount::from_change(delta);
balance.receive(&to_recv).unwrap();
}
tx::ctx().write(&balance_key, balance).unwrap();
}
PosStorageChange::WithdrawUnbond {
owner: _,
validator: _,
} => {
}
PosStorageChange::ValidatorCommissionRate {
address: _,
rate: _,
} => {
}
PosStorageChange::ValidatorMaxCommissionRateChange {
address: _,
change: _,
} => {
}
}
}
pub fn apply_validator_set_change(
_validator: Address,
_token_delta: Change,
_offset: DynEpochOffset,
_current_epoch: Epoch,
_params: &PosParams,
) {
}
pub fn arb_invalid_pos_action(
valid_actions: Vec<ValidPosAction>,
) -> impl Strategy<Value = InvalidPosAction> {
let arb_epoch = 0..10_000_u64;
proptest::collection::vec(
(arb_epoch, arb_invalid_pos_storage_changes(valid_actions)),
1..=8,
)
.prop_map(|changes| InvalidPosAction {
changes: changes
.into_iter()
.map(|(epoch, changes)| (Epoch(epoch), changes))
.collect(),
})
}
pub fn arb_invalid_pos_storage_changes(
valid_actions: Vec<ValidPosAction>,
) -> impl Strategy<Value = PosStorageChanges> {
let validators: Vec<Address> = valid_actions
.iter()
.filter_map(|action| match action {
ValidPosAction::InitValidator { address, .. } => {
Some(address.clone())
}
_ => None,
})
.collect();
let arb_address = address::testing::arb_established_address()
.prop_map(Address::Established);
let arb_address_or_validator = if validators.is_empty() {
arb_address.boxed()
} else {
let arb_validator = proptest::sample::select(validators);
prop_oneof![arb_validator, arb_address].boxed()
};
let arb_offset = prop_oneof![
Just(DynEpochOffset::PipelineLen),
Just(DynEpochOffset::UnbondingLen)
];
let arb_delta =
prop_oneof![(-(u32::MAX as i128)..0), (1..=u32::MAX as i128),];
prop_oneof![
(
arb_address_or_validator.clone(),
arb_address_or_validator,
arb_offset,
arb_delta,
)
.prop_map(|(validator, owner, offset, delta)| {
vec![
PosStorageChange::SpawnAccount {
address: validator.clone(),
},
PosStorageChange::SpawnAccount {
address: owner.clone(),
},
PosStorageChange::Bond {
owner,
validator,
delta: delta.into(),
offset,
},
]
})
]
}
impl InvalidPosAction {
pub fn apply(self) {
let params =
read_pos_params::<_, governance::Store<_>>(tx::ctx()).unwrap();
for (epoch, changes) in self.changes {
for change in changes {
apply_pos_storage_change(change, ¶ms, epoch, false);
}
}
}
}
pub fn has_vacant_consensus_validator_slots(
params: &PosParams,
current_epoch: Epoch,
) -> bool {
let num_consensus_validators =
get_num_consensus_validators(tx::ctx(), current_epoch).unwrap();
params.max_validator_slots > num_consensus_validators
}
}