Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding try_state hook for Core Fellowship #3276

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions prdoc/pr_3276.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
title: Adding `try-state` hook to core-fellowship pallet

doc:
- audience: Runtime User
description: |
Ranked members with zero demotion periods should not submit an evidence with a wish to retain, subsequently no promotion available for T::MaxRank members.
Candidate evidence should only be submitted with a Wish:Promotion.

crates:
- name: pallet-core-fellowship
bump: minor
92 changes: 90 additions & 2 deletions substrate/frame/core-fellowship/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ use scale_info::TypeInfo;
use sp_arithmetic::traits::{Saturating, Zero};
use sp_runtime::RuntimeDebug;

#[cfg(any(feature = "try-runtime", test))]
use sp_runtime::TryRuntimeError;

use frame_support::{
defensive,
dispatch::DispatchResultWithPostInfo,
Expand Down Expand Up @@ -316,6 +319,14 @@ pub mod pallet {
TooSoon,
}

#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
Self::do_try_state()
}
}

#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Bump the state of a member.
Expand All @@ -341,7 +352,7 @@ pub mod pallet {
};

if demotion_period.is_zero() {
return Err(Error::<T, I>::NothingDoing.into())
return Err(Error::<T, I>::NothingDoing.into());
}

let demotion_block = member.last_proof.saturating_add(demotion_period);
Expand All @@ -361,7 +372,7 @@ pub mod pallet {
Event::<T, I>::Offboarded { who }
};
Self::deposit_event(event);
return Ok(Pays::No.into())
return Ok(Pays::No.into());
}

Err(Error::<T, I>::NothingDoing.into())
Expand Down Expand Up @@ -678,6 +689,83 @@ pub mod pallet {
Self::deposit_event(e);
}
}

/// Ensure the correctness of the state of this pallet
/// The following expectations must always apply.
///
Doordashcon marked this conversation as resolved.
Show resolved Hide resolved
/// ## Expectations:
///
/// `ranked_member`:
/// * members with zero demotion period should not submit an evidence with a wish to retain,
/// subsequently no promotion
/// available for max_rank() members.
///
/// `unranked_member`:
/// * evidence should only be submitted with a `Wish:Promotion`.
#[cfg(any(feature = "try-runtime", test))]
pub fn do_try_state() -> Result<(), TryRuntimeError> {
// Check invariants for each member tracked by the pallet
let params = Params::<T, I>::get();

for who in Member::<T, I>::iter_keys() {
if let Some(_) = Member::<T, I>::get(who.clone()) {
Doordashcon marked this conversation as resolved.
Show resolved Hide resolved
let rank =
T::Members::rank_of(&who).ok_or("Member not found in rank registry")?;

if rank > T::Members::min_rank() {
Self::check_ranked_member(rank, &who, params.clone())?;
} else {
Self::check_unranked_member(&who)?;
}
} else {
return Err(TryRuntimeError::Other("Member should exist, inconsistent mapping"));
}
}
Ok(())
}
#[cfg(any(feature = "try-runtime", test))]
fn check_ranked_member(
rank: RankOf<T, I>,
who: &T::AccountId,
params: ParamsOf<T, I>,
) -> Result<(), TryRuntimeError> {
let rank_index = Self::rank_to_index(rank).ok_or(Error::<T, I>::InvalidRank)?;
let demotion_period = params.demotion_period[rank_index];

// Determine if a demotion period is applicable
let demotion_period_applicable = demotion_period != Zero::zero();

// If member has evidence submitted, ensure it aligns with their rank and wish
if let Some((wish, _)) = MemberEvidence::<T, I>::get(&who) {
match wish {
Wish::Retention => {
ensure!(
demotion_period_applicable,
TryRuntimeError::Other(
"Member with no demotion period can not wish to retain"
)
);
},
Wish::Promotion => {
ensure!(
rank < <T as Config<I>>::MaxRank::get() as u16,
TryRuntimeError::Other("Member at max rank cannot be promoted")
);
},
}
}
Ok(())
}

#[cfg(any(feature = "try-runtime", test))]
fn check_unranked_member(who: &T::AccountId) -> Result<(), TryRuntimeError> {
if let Some((wish, _evidence)) = MemberEvidence::<T, I>::get(&who) {
if wish == Wish::Retention {
return Err(TryRuntimeError::Other("Rentention disallowed for Rank < 1"));
}
}
Ok(())
}
}

impl<T: Config<I>, I: 'static> GetSalary<RankOf<T, I>, T::AccountId, T::Balance> for Pallet<T, I> {
Expand Down
74 changes: 60 additions & 14 deletions substrate/frame/core-fellowship/src/tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
ext
}

/// Run the function pointer inside externalities and asserts the try_state hook at the end.
pub fn build_and_execute(test: impl FnOnce() -> ()) {
new_test_ext().execute_with(|| {
test();
CoreFellowship::do_try_state().expect("All invariants must hold after a test");
});
}

fn next_block() {
System::set_block_number(System::block_number() + 1);
}
Expand All @@ -159,9 +167,20 @@ fn next_demotion(who: u64) -> u64 {
member.last_proof + demotion_period[TestClub::rank_of(&who).unwrap() as usize - 1]
}

/// Generate mocked evidence from some seed.
fn evidence(seed: u32) -> Evidence<Test, ()> {
seed.encode()
.into_iter()
.cycle()
.take(1024)
.collect::<Vec<_>>()
.try_into()
.expect("Static length matches")
}

#[test]
fn basic_stuff() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
assert_eq!(CoreFellowship::rank_to_index(0), None);
assert_eq!(CoreFellowship::rank_to_index(1), Some(0));
assert_eq!(CoreFellowship::rank_to_index(9), Some(8));
Expand All @@ -172,7 +191,7 @@ fn basic_stuff() {

#[test]
fn set_params_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
let params = ParamsType {
active_salary: bounded_vec![10, 20, 30, 40, 50, 60, 70, 80, 90],
passive_salary: bounded_vec![1, 2, 3, 4, 5, 6, 7, 8, 9],
Expand Down Expand Up @@ -224,7 +243,7 @@ fn set_partial_params_works() {

#[test]
fn induct_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
set_rank(0, 0);
assert_ok!(CoreFellowship::import(signed(0)));
set_rank(1, 1);
Expand All @@ -239,16 +258,16 @@ fn induct_works() {

#[test]
fn promote_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
set_rank(1, 1);
set_rank(2, 2);
assert_ok!(CoreFellowship::import(signed(1)));
assert_noop!(CoreFellowship::promote(signed(1), 10, 1), Error::<Test>::Unranked);

assert_ok!(CoreFellowship::induct(signed(1), 10));
assert_noop!(CoreFellowship::promote(signed(10), 10, 1), DispatchError::BadOrigin);
assert_noop!(CoreFellowship::promote(signed(0), 10, 1), Error::<Test>::NoPermission);
assert_noop!(CoreFellowship::promote(signed(3), 10, 2), Error::<Test>::UnexpectedRank);
run_to(3);
assert_noop!(CoreFellowship::promote(signed(1), 10, 1), Error::<Test>::TooSoon);
run_to(4);
assert_ok!(CoreFellowship::promote(signed(1), 10, 1));
Expand Down Expand Up @@ -352,7 +371,7 @@ fn promote_fast_identical_to_promote() {

#[test]
fn sync_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
set_rank(10, 5);
assert_noop!(CoreFellowship::approve(signed(4), 10, 5), Error::<Test>::NoPermission);
assert_noop!(CoreFellowship::approve(signed(6), 10, 6), Error::<Test>::UnexpectedRank);
Expand All @@ -364,7 +383,7 @@ fn sync_works() {

#[test]
fn auto_demote_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
set_rank(10, 5);
assert_ok!(CoreFellowship::import(signed(10)));

Expand All @@ -380,7 +399,7 @@ fn auto_demote_works() {

#[test]
fn auto_demote_offboard_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
set_rank(10, 1);
assert_ok!(CoreFellowship::import(signed(10)));

Expand All @@ -396,7 +415,7 @@ fn auto_demote_offboard_works() {

#[test]
fn offboard_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
assert_noop!(CoreFellowship::offboard(signed(0), 10), Error::<Test>::NotTracked);
set_rank(10, 0);
assert_noop!(CoreFellowship::offboard(signed(0), 10), Error::<Test>::Ranked);
Expand All @@ -413,7 +432,7 @@ fn offboard_works() {

#[test]
fn infinite_demotion_period_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
let params = ParamsType {
active_salary: bounded_vec![10, 10, 10, 10, 10, 10, 10, 10, 10],
passive_salary: bounded_vec![10, 10, 10, 10, 10, 10, 10, 10, 10],
Expand All @@ -426,6 +445,7 @@ fn infinite_demotion_period_works() {
set_rank(0, 0);
assert_ok!(CoreFellowship::import(signed(0)));
set_rank(1, 1);

assert_ok!(CoreFellowship::import(signed(1)));

assert_noop!(CoreFellowship::bump(signed(0), 0), Error::<Test>::NothingDoing);
Expand All @@ -435,7 +455,7 @@ fn infinite_demotion_period_works() {

#[test]
fn proof_postpones_auto_demote() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
set_rank(10, 5);
assert_ok!(CoreFellowship::import(signed(10)));

Expand All @@ -448,7 +468,7 @@ fn proof_postpones_auto_demote() {

#[test]
fn promote_postpones_auto_demote() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
set_rank(10, 5);
assert_ok!(CoreFellowship::import(signed(10)));

Expand All @@ -461,7 +481,7 @@ fn promote_postpones_auto_demote() {

#[test]
fn get_salary_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
for i in 1..=9u64 {
set_rank(10 + i, i as u16);
assert_ok!(CoreFellowship::import(signed(10 + i)));
Expand All @@ -472,7 +492,7 @@ fn get_salary_works() {

#[test]
fn active_changing_get_salary_works() {
new_test_ext().execute_with(|| {
build_and_execute(|| {
for i in 1..=9u64 {
set_rank(10 + i, i as u16);
assert_ok!(CoreFellowship::import(signed(10 + i)));
Expand All @@ -483,3 +503,29 @@ fn active_changing_get_salary_works() {
}
});
}

#[test]
fn submit_evidence_candidate_retention_invariant() {
new_test_ext().execute_with(|| {
use frame_support::pallet_prelude::DispatchError::Other;

let params = ParamsType {
active_salary: bounded_vec![10, 20, 30, 40, 50, 60, 70, 80, 90],
passive_salary: bounded_vec![1, 2, 3, 4, 5, 6, 7, 8, 9],
demotion_period: bounded_vec![1, 2, 0, 4, 5, 6, 0, 0, 9],
min_promotion_period: bounded_vec![1, 2, 3, 4, 5, 10, 15, 20, 30],
offboard_timeout: 1,
};
assert_ok!(CoreFellowship::set_params(signed(1), Box::new(params)));

set_rank(10, 0);
assert_ok!(CoreFellowship::import(signed(10)));
CoreFellowship::submit_evidence(signed(10), Wish::Retention, evidence(1)).unwrap();

// Invariant violated
assert_eq!(
CoreFellowship::do_try_state(),
Err(Other("Rentention disallowed for Rank < 1"))
);
})
}
Loading