use crate::{
reward::EraRewardManager,
session_rotation::{Eras, Rotator},
tests::session_mock::{CurrentIndex, Timestamp},
POT_POOL_SIZE,
};
use codec::Encode;
use frame_support::{
traits::fungible::{Inspect, Mutate},
PalletId,
};
use super::*;
#[test]
fn forcing_force_none() {
ExtBuilder::default().build_and_execute(|| {
ForceEra::<T>::put(Forcing::ForceNone);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 6, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 7, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 8, active_era: 1, planned_era: 1 }]
);
});
}
#[test]
fn forcing_no_forcing_default() {
ExtBuilder::default().build_and_execute(|| {
ForceEra::<T>::put(Forcing::NotForcing);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 0 },
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
});
}
#[test]
fn forcing_force_always() {
ExtBuilder::default()
.session_per_era(6)
.no_flush_events()
.build_and_execute(|| {
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 1, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 2, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 3, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 4, active_era: 0, planned_era: 1 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 0, planned_era: 1 },
Event::EraPaid { era_index: 0, validator_payout: 15000, remainder: 0 },
Event::SessionRotated { starting_session: 6, active_era: 1, planned_era: 1 }
]
);
ForceEra::<T>::put(Forcing::ForceAlways);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 7, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 8, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 0 },
Event::SessionRotated { starting_session: 9, active_era: 2, planned_era: 3 }
]
);
});
}
#[test]
fn forcing_force_new() {
ExtBuilder::default()
.session_per_era(6)
.no_flush_events()
.build_and_execute(|| {
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 1, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 2, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 3, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 4, active_era: 0, planned_era: 1 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 0, planned_era: 1 },
Event::EraPaid { era_index: 0, validator_payout: 15000, remainder: 0 },
Event::SessionRotated { starting_session: 6, active_era: 1, planned_era: 1 }
]
);
ForceEra::<T>::put(Forcing::ForceNew);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 7, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 8, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 0 },
Event::SessionRotated { starting_session: 9, active_era: 2, planned_era: 2 }
]
);
Session::roll_until_active_era(3);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 10, active_era: 2, planned_era: 2 },
Event::SessionRotated { starting_session: 11, active_era: 2, planned_era: 2 },
Event::SessionRotated { starting_session: 12, active_era: 2, planned_era: 2 },
Event::SessionRotated { starting_session: 13, active_era: 2, planned_era: 3 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 14, active_era: 2, planned_era: 3 },
Event::EraPaid { era_index: 2, validator_payout: 15000, remainder: 0 },
Event::SessionRotated { starting_session: 15, active_era: 3, planned_era: 3 }
]
);
});
}
#[test]
fn activation_timestamp_when_no_planned_era() {
ExtBuilder::default().session_per_era(6).build_and_execute(|| {
Session::roll_until_active_era(2);
let current_index = CurrentIndex::get();
let _ = staking_events_since_last_call();
assert_eq!(Rotator::<T>::active_era(), 2);
assert_eq!(Rotator::<T>::planned_era(), 2);
<Staking as pallet_staking_async_rc_client::AHStakingInterface>::on_relay_session_report(
pallet_staking_async_rc_client::SessionReport::new_terminal(
current_index,
vec![],
Some((Timestamp::get() + time_per_session(), 3)),
),
);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::Unexpected(UnexpectedKind::UnknownValidatorActivation),
Event::SessionRotated {
starting_session: current_index + 1,
active_era: 2,
planned_era: 2
}
]
);
});
}
#[test]
#[should_panic]
fn activation_timestamp_when_era_planning_not_complete() {
todo!("what if we receive an activation timestamp when the era planning (election) is not complete?");
}
#[test]
fn era_cleanup_history_depth_works_with_prune_era_step_extrinsic() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(active_era(), 1);
Session::roll_until_active_era(HistoryDepth::get() - 1);
assert!(matches!(
&staking_events_since_last_call()[..],
&[
..,
Event::SessionRotated { starting_session: 236, active_era: 78, planned_era: 79 },
Event::EraPaid { era_index: 78, validator_payout: 7500, remainder: 0 },
Event::SessionRotated { starting_session: 237, active_era: 79, planned_era: 79 }
]
));
let staker_pot_78 = <Test as Config>::RewardPots::pot_account(RewardPot::Era(
78,
RewardKind::StakerRewards,
));
let ideal_validator_payout = validator_payout_for(time_per_era());
assert_eq!(Balances::balance(&staker_pot_78), ideal_validator_payout);
assert_ok!(Eras::<T>::era_fully_present(1));
assert_ok!(Eras::<T>::era_fully_present(2));
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get() - 1));
Session::roll_until_active_era(HistoryDepth::get());
assert_ok!(Eras::<T>::era_fully_present(1));
assert_ok!(Eras::<T>::era_fully_present(2));
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get()));
Session::roll_until_active_era(HistoryDepth::get() + 1);
assert_ok!(Eras::<T>::era_fully_present(1));
assert_ok!(Eras::<T>::era_fully_present(2));
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get() + 1));
assert!(matches!(
&staking_events_since_last_call()[..],
&[
..,
Event::EraPaid { era_index: 80, validator_payout: 7500, remainder: 0 },
Event::SessionRotated { starting_session: 243, active_era: 81, planned_era: 81 }
]
));
Session::roll_until_active_era(HistoryDepth::get() + 2);
assert_ok!(Eras::<T>::era_fully_present(1)); assert_ok!(Eras::<T>::era_fully_present(2));
assert_ok!(Eras::<T>::era_fully_present(3));
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get() + 2));
assert!(matches!(
&staking_events_since_last_call()[..],
&[
..,
Event::EraPaid { era_index: 81, validator_payout: 7500, remainder: 0 },
Event::SessionRotated { starting_session: 246, active_era: 82, planned_era: 82 }
]
));
let expected_per_era = validator_payout_for(time_per_era());
for era in 79..=81 {
let staker_pot = <Test as Config>::RewardPots::pot_account(RewardPot::Era(
era,
RewardKind::StakerRewards,
));
assert_eq!(
Balances::balance(&staker_pot),
expected_per_era,
"Era {era} staker pot should have {expected_per_era}"
);
}
assert_noop!(
Staking::prune_era_step(RuntimeOrigin::signed(99), 2),
Error::<T>::EraNotPrunable
);
assert_noop!(
Staking::prune_era_step(RuntimeOrigin::signed(99), HistoryDepth::get() + 2),
Error::<T>::EraNotPrunable
);
use crate::PruningStep::*;
let steps_order = [
ErasStakersPaged,
ErasStakersOverview,
ErasValidatorPrefs,
ClaimedRewards,
ErasValidatorReward,
ErasRewardPoints,
SingleEntryCleanups,
ValidatorSlashInEra,
ErasValidatorIncentiveWeight,
];
let _ = staking_events_since_last_call();
for expected_step in steps_order.iter() {
loop {
let current_state = EraPruningState::<T>::get(1)
.expect("Era 1 should be marked for pruning at this point");
assert_eq!(
current_state, *expected_step,
"Expected to be in step {:?} but was in {:?}",
expected_step, current_state
);
let result = Staking::prune_era_step(RuntimeOrigin::signed(99), 1);
assert_ok!(&result);
let post_info = result.unwrap();
assert_eq!(
post_info.pays_fee,
frame_support::dispatch::Pays::No,
"Should return Pays::No when work is done for step {:?}",
expected_step
);
assert!(
post_info.actual_weight.is_some(),
"Should report actual weight for {:?}",
expected_step
);
let actual_weight = post_info.actual_weight.unwrap();
assert!(
actual_weight.ref_time() > 0,
"Should report non-zero ref_time for {:?}",
expected_step
);
let new_state = EraPruningState::<T>::get(1).unwrap_or(ErasStakersPaged);
if new_state != current_state {
break; }
}
match expected_step {
ErasStakersPaged => assert_eq!(
crate::ErasStakersPaged::<T>::iter_prefix_values((1,)).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ErasStakersOverview => assert_eq!(
crate::ErasStakersOverview::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ErasValidatorPrefs => assert_eq!(
crate::ErasValidatorPrefs::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ClaimedRewards => assert_eq!(
crate::ClaimedRewards::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ErasValidatorReward => {
assert!(
!crate::ErasValidatorReward::<T>::contains_key(1),
"{expected_step:?} should be empty after completing step"
);
},
ErasRewardPoints => {
assert!(
!crate::ErasRewardPoints::<T>::contains_key(1),
"{expected_step:?} should be empty after completing step"
);
},
SingleEntryCleanups => {
assert!(
!crate::ErasTotalStake::<T>::contains_key(1),
"{expected_step:?} should be empty after completing step"
);
assert!(
!crate::ErasNominatorsSlashable::<T>::contains_key(1),
"ErasNominatorsSlashable should be empty after completing SingleEntryCleanups step"
);
assert!(
!crate::ErasValidatorIncentiveBudget::<T>::contains_key(1),
"ErasValidatorIncentiveBudget should be empty after completing SingleEntryCleanups step"
);
assert!(
!crate::ErasSumValidatorIncentiveWeight::<T>::contains_key(1),
"ErasSumValidatorIncentiveWeight should be empty after completing SingleEntryCleanups step"
);
},
ValidatorSlashInEra => assert_eq!(
crate::ValidatorSlashInEra::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ErasValidatorIncentiveWeight => assert_eq!(
crate::ErasValidatorIncentiveWeight::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
}
}
assert!(
EraPruningState::<T>::get(1).is_none(),
"EraPruningState should be removed after final step"
);
assert!(matches!(&staking_events_since_last_call()[..], &[Event::EraPruned { index: 1 }]));
let result = Staking::prune_era_step(RuntimeOrigin::signed(99), 1);
assert_noop!(result, Error::<T>::EraNotPrunable);
assert_ok!(Eras::<T>::era_absent(1));
assert_ok!(Eras::<T>::era_fully_present(2));
let result = Staking::prune_era_step(RuntimeOrigin::signed(99), 1);
assert_noop!(result, Error::<T>::EraNotPrunable);
});
}
#[test]
fn progress_many_eras_with_try_state() {
ExtBuilder::default().build_and_execute(|| {
Session::roll_until_active_era_with(
HistoryDepth::get().max(BondingDuration::get()) + 2,
|| {
Staking::do_try_state(System::block_number()).unwrap();
},
);
})
}
mod inflation {
use super::*;
#[test]
fn dap_budget_allocation_determines_staker_rewards() {
ExtBuilder::default().build_and_execute(|| {
let default_stakers_payout = validator_payout_for(time_per_era());
assert_eq!(default_stakers_payout, Balance::from(time_per_era()) / 2);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 0 },
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
assert_eq!(ErasValidatorReward::<Test>::get(0).unwrap(), default_stakers_payout);
})
}
}
#[test]
fn era_pot_drained_after_history_depth() {
ExtBuilder::default().build_and_execute(|| {
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
let staker_pot_1 =
<Test as Config>::RewardPots::pot_account(RewardPot::Era(1, RewardKind::StakerRewards));
let expected_per_era = validator_payout_for(time_per_era());
assert_eq!(Balances::balance(&staker_pot_1), expected_per_era);
let drained_era = 1;
let target_era = drained_era + HistoryDepth::get() + 1;
Session::roll_until_active_era(target_era);
let _ = staking_events_since_last_call();
let staker_pot = <Test as Config>::RewardPots::pot_account(RewardPot::Era(
drained_era,
RewardKind::StakerRewards,
));
assert_eq!(Balances::balance(&staker_pot), 0, "Staker pot should have zero balance");
assert_eq!(
System::providers(&staker_pot),
1,
"Staker pot is kept alive for slot reuse; provider must be retained"
);
});
}
#[test]
fn pot_slot_reuse_drain_then_recreate_is_idempotent() {
ExtBuilder::default().build_and_execute(|| {
let era_a = 5;
let era_b = era_a + POT_POOL_SIZE;
let pot = EraRewardManager::<Test>::create(era_a, RewardKind::StakerRewards);
assert_eq!(System::providers(&pot), 1);
let funded: Balance = 1_000;
Balances::set_balance(&pot, funded);
assert_eq!(Balances::balance(&pot), funded);
EraRewardManager::<Test>::cleanup_era(era_a);
assert_eq!(Balances::balance(&pot), 0);
assert_eq!(System::providers(&pot), 1, "drain must not release the provider");
EraRewardManager::<Test>::create(era_b, RewardKind::StakerRewards);
assert_eq!(
System::providers(&pot),
1,
"create must not double-increment provider on slot reuse"
);
Balances::set_balance(&pot, 2_000);
assert_eq!(Balances::balance(&pot), 2_000);
});
}
#[test]
fn era_pot_slots_collide_every_pool_size_eras() {
let seed_for = |era: u32, kind: RewardKind| -> Vec<u8> {
(
<PalletId as sp_runtime::TypeId>::TYPE_ID,
DapPalletId::get(),
RewardPot::Era(crate::pot_slot(era), kind),
)
.encode()
};
let base_era = 7u32;
for kind in [RewardKind::StakerRewards, RewardKind::ValidatorSelfStake] {
let base_seed = seed_for(base_era, kind);
for offset in 1..POT_POOL_SIZE {
assert_ne!(
seed_for(base_era + offset, kind),
base_seed,
"{:?} pot at era +{} must not collide within the pool window",
kind,
offset,
);
}
assert_eq!(
seed_for(base_era + POT_POOL_SIZE, kind),
base_seed,
"{:?} pot at era +POT_POOL_SIZE must reuse the base era's slot",
kind,
);
}
assert_ne!(
seed_for(base_era, RewardKind::StakerRewards),
seed_for(base_era, RewardKind::ValidatorSelfStake),
"staker-rewards and incentive pots within the same slot must be distinct",
);
}
#[test]
fn disable_legacy_minting_era_updates_correctly() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(DisableMintingGuard::<Test>::get(), Some(0));
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
assert_eq!(DisableMintingGuard::<Test>::get(), Some(0));
});
}
#[test]
fn disable_legacy_minting_era_write_once_semantics() {
ExtBuilder::default().build_and_execute(|| {
DisableMintingGuard::<Test>::kill();
assert_eq!(DisableMintingGuard::<Test>::get(), None);
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
assert_eq!(DisableMintingGuard::<Test>::get(), Some(1));
Session::roll_until_active_era(5);
let _ = staking_events_since_last_call();
assert_eq!(DisableMintingGuard::<Test>::get(), Some(1));
});
}
#[test]
fn dap_era_with_zero_rewards_still_sets_guard() {
ExtBuilder::default().build_and_execute(|| {
DisableMintingGuard::<Test>::kill();
pallet_dap::BudgetAllocation::<Test>::put(build_budget(&[(buffer_key(), 100)]));
assert_eq!(DisableMintingGuard::<Test>::get(), None);
Session::roll_until_active_era(2);
let _ = staking_events_since_last_call();
assert_eq!(ErasValidatorReward::<Test>::get(1), Some(0));
assert_eq!(DisableMintingGuard::<Test>::get(), Some(1));
});
}