Runtime State Test + Integration with try-runtime (#10174)

* add missing version to dependencies

* Huh

* add features more

* more fixing

* last touches

* it all finally works

* remove some feature gates

* remove unused

* fix old macro

* make it work again

* fmt

* remove unused import

* ".git/.scripts/fmt.sh" 1

* Cleanup more

* fix and rename everything

* a few clippy fixes

* Add try-runtime feature

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* small fixes

* fmt

* Update bin/node-template/runtime/src/lib.rs

* fix build

* Update utils/frame/try-runtime/cli/src/lib.rs

Co-authored-by: David <dvdplm@gmail.com>

* Update utils/frame/try-runtime/cli/src/commands/execute_block.rs

Co-authored-by: David <dvdplm@gmail.com>

* address all review comments

* fix typos

* revert spec change

* last touches

* update docs

* fmt

* remove some debug_assertions

* fmt

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: command-bot <>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: David <dvdplm@gmail.com>
This commit is contained in:
Kian Paimani
2022-09-01 11:33:22 +01:00
committed by GitHub
parent d8e951758c
commit f67c06ce22
39 changed files with 651 additions and 209 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ fn main() {
},
}
assert!(BagsList::sanity_check().is_ok());
assert!(BagsList::try_state().is_ok());
})
});
}
@@ -24,8 +24,8 @@ use sp_std::prelude::*;
pub const LOG_TARGET: &str = "runtime::bags-list::remote-tests";
pub mod migration;
pub mod sanity_check;
pub mod snapshot;
pub mod try_state;
/// A wrapper for a runtime that the functions of this crate expect.
///
@@ -44,7 +44,7 @@ pub async fn execute<Runtime: crate::RuntimeT, Block: BlockT + DeserializeOwned>
ext.execute_with(|| {
sp_core::crypto::set_default_ss58_version(Runtime::SS58Prefix::get().try_into().unwrap());
pallet_bags_list::Pallet::<Runtime>::sanity_check().unwrap();
pallet_bags_list::Pallet::<Runtime>::try_state().unwrap();
log::info!(target: crate::LOG_TARGET, "executed bags-list sanity check with no errors.");
crate::display_and_check_bags::<Runtime>(currency_unit, currency_name);
+7 -8
View File
@@ -263,6 +263,11 @@ pub mod pallet {
"thresholds must strictly increase, and have no duplicates",
);
}
#[cfg(feature = "try-runtime")]
fn try_state(_: BlockNumberFor<T>) -> Result<(), &'static str> {
<Self as SortedListProvider<T::AccountId>>::try_state()
}
}
}
@@ -340,14 +345,8 @@ impl<T: Config<I>, I: 'static> SortedListProvider<T::AccountId> for Pallet<T, I>
List::<T, I>::unsafe_regenerate(all, score_of)
}
#[cfg(feature = "std")]
fn sanity_check() -> Result<(), &'static str> {
List::<T, I>::sanity_check()
}
#[cfg(not(feature = "std"))]
fn sanity_check() -> Result<(), &'static str> {
Ok(())
fn try_state() -> Result<(), &'static str> {
List::<T, I>::try_state()
}
fn unsafe_clear() {
+15 -28
View File
@@ -28,8 +28,8 @@ use crate::Config;
use codec::{Decode, Encode, MaxEncodedLen};
use frame_election_provider_support::ScoreProvider;
use frame_support::{
ensure,
traits::{Defensive, Get},
defensive, ensure,
traits::{Defensive, DefensiveOption, Get},
DefaultNoBound, PalletError,
};
use scale_info::TypeInfo;
@@ -220,7 +220,8 @@ impl<T: Config<I>, I: 'static> List<T, I> {
crate::ListBags::<T, I>::remove(removed_bag);
}
debug_assert_eq!(Self::sanity_check(), Ok(()));
#[cfg(feature = "std")]
debug_assert_eq!(Self::try_state(), Ok(()));
num_affected
}
@@ -325,8 +326,7 @@ impl<T: Config<I>, I: 'static> List<T, I> {
crate::log!(
debug,
"inserted {:?} with score {:?
} into bag {:?}, new count is {}",
"inserted {:?} with score {:?} into bag {:?}, new count is {}",
id,
score,
bag_score,
@@ -457,11 +457,8 @@ impl<T: Config<I>, I: 'static> List<T, I> {
// re-fetch `lighter_node` from storage since it may have been updated when `heavier_node`
// was removed.
let lighter_node = Node::<T, I>::get(lighter_id).ok_or_else(|| {
debug_assert!(false, "id that should exist cannot be found");
crate::log!(warn, "id that should exist cannot be found");
ListError::NodeNotFound
})?;
let lighter_node =
Node::<T, I>::get(lighter_id).defensive_ok_or_else(|| ListError::NodeNotFound)?;
// insert `heavier_node` directly in front of `lighter_node`. This will update both nodes
// in storage and update the node counter.
@@ -508,7 +505,7 @@ impl<T: Config<I>, I: 'static> List<T, I> {
node.put();
}
/// Sanity check the list.
/// Check the internal state of the list.
///
/// This should be called from the call-site, whenever one of the mutating apis (e.g. `insert`)
/// is being used, after all other staking data (such as counter) has been updated. It checks:
@@ -517,8 +514,7 @@ impl<T: Config<I>, I: 'static> List<T, I> {
/// * length of this list is in sync with `ListNodes::count()`,
/// * and sanity-checks all bags and nodes. This will cascade down all the checks and makes sure
/// all bags and nodes are checked per *any* update to `List`.
#[cfg(feature = "std")]
pub(crate) fn sanity_check() -> Result<(), &'static str> {
pub(crate) fn try_state() -> Result<(), &'static str> {
let mut seen_in_list = BTreeSet::new();
ensure!(
Self::iter().map(|node| node.id).all(|id| seen_in_list.insert(id)),
@@ -546,7 +542,7 @@ impl<T: Config<I>, I: 'static> List<T, I> {
thresholds.into_iter().filter_map(|t| Bag::<T, I>::get(t))
};
let _ = active_bags.clone().try_for_each(|b| b.sanity_check())?;
let _ = active_bags.clone().try_for_each(|b| b.try_state())?;
let nodes_in_bags_count =
active_bags.clone().fold(0u32, |acc, cur| acc + cur.iter().count() as u32);
@@ -557,17 +553,12 @@ impl<T: Config<I>, I: 'static> List<T, I> {
// check that all nodes are sane. We check the `ListNodes` storage item directly in case we
// have some "stale" nodes that are not in a bag.
for (_id, node) in crate::ListNodes::<T, I>::iter() {
node.sanity_check()?
node.try_state()?
}
Ok(())
}
#[cfg(not(feature = "std"))]
pub(crate) fn sanity_check() -> Result<(), &'static str> {
Ok(())
}
/// Returns the nodes of all non-empty bags. For testing and benchmarks.
#[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
#[allow(dead_code)]
@@ -701,8 +692,7 @@ impl<T: Config<I>, I: 'static> Bag<T, I> {
if *tail == node.id {
// this should never happen, but this check prevents one path to a worst case
// infinite loop.
debug_assert!(false, "system logic error: inserting a node who has the id of tail");
crate::log!(warn, "system logic error: inserting a node who has the id of tail");
defensive!("system logic error: inserting a node who has the id of tail");
return
};
}
@@ -753,7 +743,7 @@ impl<T: Config<I>, I: 'static> Bag<T, I> {
}
}
/// Sanity check this bag.
/// Check the internal state of the bag.
///
/// Should be called by the call-site, after any mutating operation on a bag. The call site of
/// this struct is always `List`.
@@ -761,8 +751,7 @@ impl<T: Config<I>, I: 'static> Bag<T, I> {
/// * Ensures head has no prev.
/// * Ensures tail has no next.
/// * Ensures there are no loops, traversal from head to tail is correct.
#[cfg(feature = "std")]
fn sanity_check(&self) -> Result<(), &'static str> {
fn try_state(&self) -> Result<(), &'static str> {
frame_support::ensure!(
self.head()
.map(|head| head.prev().is_none())
@@ -801,7 +790,6 @@ impl<T: Config<I>, I: 'static> Bag<T, I> {
}
/// Check if the bag contains a node with `id`.
#[cfg(feature = "std")]
fn contains(&self, id: &T::AccountId) -> bool {
self.iter().any(|n| n.id() == id)
}
@@ -906,8 +894,7 @@ impl<T: Config<I>, I: 'static> Node<T, I> {
self.bag_upper
}
#[cfg(feature = "std")]
fn sanity_check(&self) -> Result<(), &'static str> {
fn try_state(&self) -> Result<(), &'static str> {
let expected_bag = Bag::<T, I>::get(self.bag_upper).ok_or("bag not found for node")?;
let id = self.id();
+13 -13
View File
@@ -350,15 +350,15 @@ mod list {
}
#[test]
fn sanity_check_works() {
fn try_state_works() {
ExtBuilder::default().build_and_execute_no_post_check(|| {
assert_ok!(List::<Runtime>::sanity_check());
assert_ok!(List::<Runtime>::try_state());
});
// make sure there are no duplicates.
ExtBuilder::default().build_and_execute_no_post_check(|| {
Bag::<Runtime>::get(10).unwrap().insert_unchecked(2, 10);
assert_eq!(List::<Runtime>::sanity_check(), Err("duplicate identified"));
assert_eq!(List::<Runtime>::try_state(), Err("duplicate identified"));
});
// ensure count is in sync with `ListNodes::count()`.
@@ -372,7 +372,7 @@ mod list {
CounterForListNodes::<Runtime>::mutate(|counter| *counter += 1);
assert_eq!(crate::ListNodes::<Runtime>::count(), 5);
assert_eq!(List::<Runtime>::sanity_check(), Err("iter_count != stored_count"));
assert_eq!(List::<Runtime>::try_state(), Err("iter_count != stored_count"));
});
}
@@ -804,7 +804,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 13, 14]);
assert_ok!(bag_1000.sanity_check());
assert_ok!(bag_1000.try_state());
// and the node isn't mutated when its removed
assert_eq!(node_4, node_4_pre_remove);
@@ -814,7 +814,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_1000), vec![3, 13, 14]);
assert_ok!(bag_1000.sanity_check());
assert_ok!(bag_1000.try_state());
// when removing a tail that is not pointing at the head
let node_14 = Node::<Runtime>::get(&14).unwrap();
@@ -822,7 +822,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_1000), vec![3, 13]);
assert_ok!(bag_1000.sanity_check());
assert_ok!(bag_1000.try_state());
// when removing a tail that is pointing at the head
let node_13 = Node::<Runtime>::get(&13).unwrap();
@@ -830,7 +830,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_1000), vec![3]);
assert_ok!(bag_1000.sanity_check());
assert_ok!(bag_1000.try_state());
// when removing a node that is both the head & tail
let node_3 = Node::<Runtime>::get(&3).unwrap();
@@ -846,7 +846,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_10), vec![1, 12]);
assert_ok!(bag_10.sanity_check());
assert_ok!(bag_10.try_state());
// when removing a head that is pointing at the tail
let node_1 = Node::<Runtime>::get(&1).unwrap();
@@ -854,7 +854,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_10), vec![12]);
assert_ok!(bag_10.sanity_check());
assert_ok!(bag_10.try_state());
// and since we updated the bag's head/tail, we need to write this storage so we
// can correctly `get` it again in later checks
bag_10.put();
@@ -865,7 +865,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_2000), vec![15, 17, 18, 19]);
assert_ok!(bag_2000.sanity_check());
assert_ok!(bag_2000.try_state());
// when removing a node that is pointing at tail, but not head
let node_18 = Node::<Runtime>::get(&18).unwrap();
@@ -873,7 +873,7 @@ mod bags {
// then
assert_eq!(bag_as_ids(&bag_2000), vec![15, 17, 19]);
assert_ok!(bag_2000.sanity_check());
assert_ok!(bag_2000.try_state());
// finally, when reading from storage, the state of all bags is as expected
assert_eq!(
@@ -905,7 +905,7 @@ mod bags {
// .. and the bag it was removed from
let bag_1000 = Bag::<Runtime>::get(1_000).unwrap();
// is sane
assert_ok!(bag_1000.sanity_check());
assert_ok!(bag_1000.try_state());
// and has the correct head and tail.
assert_eq!(bag_1000.head, Some(3));
assert_eq!(bag_1000.tail, Some(4));
+1 -1
View File
@@ -147,7 +147,7 @@ impl ExtBuilder {
pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
self.build().execute_with(|| {
test();
List::<Runtime>::sanity_check().expect("Sanity check post condition failed")
List::<Runtime>::try_state().expect("Try-state post condition failed")
})
}
+1
View File
@@ -50,3 +50,4 @@ std = [
"sp-runtime/std",
"sp-std/std",
]
try-runtime = ["frame-support/try-runtime"]
+1
View File
@@ -37,3 +37,4 @@ std = [
"sp-runtime/std",
"sp-std/std",
]
try-runtime = ["frame-support/try-runtime"]
@@ -513,8 +513,8 @@ pub trait SortedListProvider<AccountId> {
/// unbounded amount of storage accesses.
fn unsafe_clear();
/// Sanity check internal state of list. Only meant for debug compilation.
fn sanity_check() -> Result<(), &'static str>;
/// Check internal state of list. Only meant for debugging.
fn try_state() -> Result<(), &'static str>;
/// If `who` changes by the returned amount they are guaranteed to have a worst case change
/// in their list position.
+2 -1
View File
@@ -19,6 +19,7 @@ codec = { package = "parity-scale-codec", version = "3.0.0", default-features =
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
frame-try-runtime = { version = "0.10.0-dev", default-features = false, path = "../try-runtime", optional = true }
sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" }
sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" }
sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" }
@@ -48,4 +49,4 @@ std = [
"sp-std/std",
"sp-tracing/std",
]
try-runtime = ["frame-support/try-runtime"]
try-runtime = ["frame-support/try-runtime", "frame-try-runtime" ]
+93 -44
View File
@@ -202,6 +202,99 @@ where
}
}
#[cfg(feature = "try-runtime")]
impl<
System: frame_system::Config + EnsureInherentsAreFirst<Block>,
Block: traits::Block<Header = System::Header, Hash = System::Hash>,
Context: Default,
UnsignedValidator,
AllPalletsWithSystem: OnRuntimeUpgrade
+ OnInitialize<System::BlockNumber>
+ OnIdle<System::BlockNumber>
+ OnFinalize<System::BlockNumber>
+ OffchainWorker<System::BlockNumber>
+ frame_support::traits::TryState<System::BlockNumber>,
COnRuntimeUpgrade: OnRuntimeUpgrade,
> Executive<System, Block, Context, UnsignedValidator, AllPalletsWithSystem, COnRuntimeUpgrade>
where
Block::Extrinsic: Checkable<Context> + Codec,
CheckedOf<Block::Extrinsic, Context>: Applyable + GetDispatchInfo,
CallOf<Block::Extrinsic, Context>:
Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
OriginOf<Block::Extrinsic, Context>: From<Option<System::AccountId>>,
UnsignedValidator: ValidateUnsigned<Call = CallOf<Block::Extrinsic, Context>>,
{
/// Execute given block, but don't as strict is the normal block execution.
///
/// Some consensus related checks such as the state root check can be switched off via
/// `try_state_root`. Some additional non-consensus checks can be additionally enabled via
/// `try_state`.
///
/// Should only be used for testing ONLY.
pub fn try_execute_block(
block: Block,
try_state_root: bool,
select: frame_try_runtime::TryStateSelect,
) -> Result<frame_support::weights::Weight, &'static str> {
use frame_support::traits::TryState;
Self::initialize_block(block.header());
Self::initial_checks(&block);
let (header, extrinsics) = block.deconstruct();
Self::execute_extrinsics_with_book_keeping(extrinsics, *header.number());
// run the try-state checks of all pallets.
<AllPalletsWithSystem as TryState<System::BlockNumber>>::try_state(
*header.number(),
select,
)
.map_err(|e| {
frame_support::log::error!(target: "runtime::executive", "failure: {:?}", e);
e
})?;
// do some of the checks that would normally happen in `final_checks`, but perhaps skip
// the state root check.
{
let new_header = <frame_system::Pallet<System>>::finalize();
let items_zip = header.digest().logs().iter().zip(new_header.digest().logs().iter());
for (header_item, computed_item) in items_zip {
header_item.check_equal(computed_item);
assert!(header_item == computed_item, "Digest item must match that calculated.");
}
if try_state_root {
let storage_root = new_header.state_root();
header.state_root().check_equal(storage_root);
assert!(
header.state_root() == storage_root,
"Storage root must match that calculated."
);
}
assert!(
header.extrinsics_root() == new_header.extrinsics_root(),
"Transaction trie root must be valid.",
);
}
Ok(frame_system::Pallet::<System>::block_weight().total())
}
/// Execute all `OnRuntimeUpgrade` of this runtime, including the pre and post migration checks.
///
/// This should only be used for testing.
pub fn try_runtime_upgrade() -> Result<frame_support::weights::Weight, &'static str> {
<(COnRuntimeUpgrade, AllPalletsWithSystem) as OnRuntimeUpgrade>::pre_upgrade().unwrap();
let weight = Self::execute_on_runtime_upgrade();
<(COnRuntimeUpgrade, AllPalletsWithSystem) as OnRuntimeUpgrade>::post_upgrade().unwrap();
Ok(weight)
}
}
impl<
System: frame_system::Config + EnsureInherentsAreFirst<Block>,
Block: traits::Block<Header = System::Header, Hash = System::Hash>,
@@ -227,50 +320,6 @@ where
<(COnRuntimeUpgrade, AllPalletsWithSystem) as OnRuntimeUpgrade>::on_runtime_upgrade()
}
/// Execute given block, but don't do any of the `final_checks`.
///
/// Should only be used for testing.
#[cfg(feature = "try-runtime")]
pub fn execute_block_no_check(block: Block) -> frame_support::weights::Weight {
Self::initialize_block(block.header());
Self::initial_checks(&block);
let (header, extrinsics) = block.deconstruct();
Self::execute_extrinsics_with_book_keeping(extrinsics, *header.number());
// do some of the checks that would normally happen in `final_checks`, but definitely skip
// the state root check.
{
let new_header = <frame_system::Pallet<System>>::finalize();
let items_zip = header.digest().logs().iter().zip(new_header.digest().logs().iter());
for (header_item, computed_item) in items_zip {
header_item.check_equal(computed_item);
assert!(header_item == computed_item, "Digest item must match that calculated.");
}
assert!(
header.extrinsics_root() == new_header.extrinsics_root(),
"Transaction trie root must be valid.",
);
}
frame_system::Pallet::<System>::block_weight().total()
}
/// Execute all `OnRuntimeUpgrade` of this runtime, including the pre and post migration checks.
///
/// This should only be used for testing.
#[cfg(feature = "try-runtime")]
pub fn try_runtime_upgrade() -> Result<frame_support::weights::Weight, &'static str> {
<(COnRuntimeUpgrade, AllPalletsWithSystem) as OnRuntimeUpgrade>::pre_upgrade().unwrap();
let weight = Self::execute_on_runtime_upgrade();
<(COnRuntimeUpgrade, AllPalletsWithSystem) as OnRuntimeUpgrade>::post_upgrade().unwrap();
Ok(weight)
}
/// Start the execution of a particular block.
pub fn initialize_block(header: &System::Header) {
sp_io::init_tracing();
+1 -1
View File
@@ -32,7 +32,7 @@ sp-tracing = { version = "5.0.0", path = "../../primitives/tracing" }
[features]
runtime-benchmarks = []
try-runtime = []
try-runtime = [ "frame-support/try-runtime" ]
default = ["std"]
std = [
"codec/std",
@@ -201,11 +201,11 @@ impl<T: Config> ListScenario<T> {
Pools::<T>::join(Origin::Signed(joiner.clone()).into(), amount, 1).unwrap();
// Sanity check that the vote weight is still the same as the original bonded
// check that the vote weight is still the same as the original bonded
let weight_of = pallet_staking::Pallet::<T>::weight_of_fn();
assert_eq!(vote_to_balance::<T>(weight_of(&self.origin1)).unwrap(), original_bonded);
// Sanity check the member was added correctly
// check the member was added correctly
let member = PoolMembers::<T>::get(&joiner).unwrap();
assert_eq!(member.points, amount);
assert_eq!(member.pool_id, 1);
+11 -5
View File
@@ -1110,7 +1110,7 @@ impl<T: Config> SubPools<T> {
}
/// The sum of all unbonding balance, regardless of whether they are actually unlocked or not.
#[cfg(any(test, debug_assertions))]
#[cfg(any(feature = "try-runtime", test, debug_assertions))]
fn sum_unbonding_balance(&self) -> BalanceOf<T> {
self.no_era.balance.saturating_add(
self.with_era
@@ -2138,6 +2138,11 @@ pub mod pallet {
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), &'static str> {
Self::do_try_state(u8::MAX)
}
fn integrity_test() {
assert!(
T::MaxPointsToBalance::get() > 0,
@@ -2389,9 +2394,9 @@ impl<T: Config> Pallet<T> {
///
/// To cater for tests that want to escape parts of these checks, this function is split into
/// multiple `level`s, where the higher the level, the more checks we performs. So,
/// `sanity_check(255)` is the strongest sanity check, and `0` performs no checks.
#[cfg(any(test, debug_assertions))]
pub fn sanity_checks(level: u8) -> Result<(), &'static str> {
/// `try_state(255)` is the strongest sanity check, and `0` performs no checks.
#[cfg(any(feature = "try-runtime", test, debug_assertions))]
pub fn do_try_state(level: u8) -> Result<(), &'static str> {
if level.is_zero() {
return Ok(())
}
@@ -2401,7 +2406,8 @@ impl<T: Config> Pallet<T> {
let reward_pools = RewardPools::<T>::iter_keys().collect::<Vec<_>>();
assert_eq!(bonded_pools, reward_pools);
assert!(Metadata::<T>::iter_keys().all(|k| bonded_pools.contains(&k)));
// TODO: can't check this right now: https://github.com/paritytech/substrate/issues/12077
// assert!(Metadata::<T>::iter_keys().all(|k| bonded_pools.contains(&k)));
assert!(SubPoolsStorage::<T>::iter_keys().all(|k| bonded_pools.contains(&k)));
assert!(MaxPools::<T>::get().map_or(true, |max| bonded_pools.len() <= (max as usize)));
+1 -1
View File
@@ -304,7 +304,7 @@ impl ExtBuilder {
pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
self.build().execute_with(|| {
test();
Pools::sanity_checks(CheckLevel::get()).unwrap();
Pools::do_try_state(CheckLevel::get()).unwrap();
})
}
}
+1
View File
@@ -41,3 +41,4 @@ std = [
"sp-runtime/std",
"sp-std/std",
]
try-runtime = [ "frame-support/try-runtime" ]
+2 -2
View File
@@ -153,7 +153,7 @@ pub mod v8 {
Nominators::<T>::iter().map(|(id, _)| id),
Pallet::<T>::weight_of_fn(),
);
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
debug_assert_eq!(T::VoterList::try_state(), Ok(()));
StorageVersion::<T>::put(crate::Releases::V8_0_0);
crate::log!(
@@ -170,7 +170,7 @@ pub mod v8 {
#[cfg(feature = "try-runtime")]
pub fn post_migrate<T: Config>() -> Result<(), &'static str> {
T::VoterList::sanity_check().map_err(|_| "VoterList is not in a sane state.")?;
T::VoterList::try_state().map_err(|_| "VoterList is not in a sane state.")?;
crate::log!(info, "👜 staking bags-list migration passes POST migrate checks ✅",);
Ok(())
}
+102 -5
View File
@@ -789,7 +789,6 @@ impl<T: Config> Pallet<T> {
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
);
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
}
/// This function will remove a nominator from the `Nominators` storage map,
@@ -809,7 +808,6 @@ impl<T: Config> Pallet<T> {
false
};
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
@@ -837,7 +835,6 @@ impl<T: Config> Pallet<T> {
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
);
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
}
/// This function will remove a validator from the `Validators` storage map.
@@ -856,7 +853,6 @@ impl<T: Config> Pallet<T> {
false
};
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
@@ -1369,7 +1365,7 @@ impl<T: Config> SortedListProvider<T::AccountId> for UseNominatorsAndValidatorsM
// nothing to do upon regenerate.
0
}
fn sanity_check() -> Result<(), &'static str> {
fn try_state() -> Result<(), &'static str> {
Ok(())
}
@@ -1452,3 +1448,104 @@ impl<T: Config> StakingInterface for Pallet<T> {
Nominators::<T>::get(who).map(|n| n.targets.into_inner())
}
}
#[cfg(feature = "try-runtime")]
impl<T: Config> Pallet<T> {
pub(crate) fn do_try_state(_: BlockNumberFor<T>) -> Result<(), &'static str> {
T::VoterList::try_state()?;
Self::check_nominators()?;
Self::check_exposures()?;
Self::check_ledgers()?;
Self::check_count()
}
fn check_count() -> Result<(), &'static str> {
ensure!(
<T as Config>::VoterList::count() ==
Nominators::<T>::count() + Validators::<T>::count(),
"wrong external count"
);
Ok(())
}
fn check_ledgers() -> Result<(), &'static str> {
Bonded::<T>::iter()
.map(|(_, ctrl)| Self::ensure_ledger_consistent(ctrl))
.collect::<Result<_, _>>()
}
fn check_exposures() -> Result<(), &'static str> {
// a check per validator to ensure the exposure struct is always sane.
let era = Self::active_era().unwrap().index;
ErasStakers::<T>::iter_prefix_values(era)
.map(|expo| {
ensure!(
expo.total ==
expo.own +
expo.others
.iter()
.map(|e| e.value)
.fold(Zero::zero(), |acc, x| acc + x),
"wrong total exposure.",
);
Ok(())
})
.collect::<Result<_, _>>()
}
fn check_nominators() -> Result<(), &'static str> {
// a check per nominator to ensure their entire stake is correctly distributed. Will only
// kick-in if the nomination was submitted before the current era.
let era = Self::active_era().unwrap().index;
<Nominators<T>>::iter()
.filter_map(
|(nominator, nomination)| {
if nomination.submitted_in > era {
Some(nominator)
} else {
None
}
},
)
.map(|nominator| {
// must be bonded.
Self::ensure_is_stash(&nominator)?;
let mut sum = BalanceOf::<T>::zero();
T::SessionInterface::validators()
.iter()
.map(|v| Self::eras_stakers(era, v))
.map(|e| {
let individual =
e.others.iter().filter(|e| e.who == nominator).collect::<Vec<_>>();
let len = individual.len();
match len {
0 => { /* not supporting this validator at all. */ },
1 => sum += individual[0].value,
_ => return Err("nominator cannot back a validator more than once."),
};
Ok(())
})
.collect::<Result<_, _>>()
})
.collect::<Result<_, _>>()
}
fn ensure_is_stash(who: &T::AccountId) -> Result<(), &'static str> {
ensure!(Self::bonded(who).is_some(), "Not a stash.");
Ok(())
}
fn ensure_ledger_consistent(ctrl: T::AccountId) -> Result<(), &'static str> {
// ensures ledger.total == ledger.active + sum(ledger.unlocking).
let ledger = Self::ledger(ctrl.clone()).ok_or("Not a controller.")?;
let real_total: BalanceOf<T> =
ledger.unlocking.iter().fold(ledger.active, |a, c| a + c.value);
ensure!(real_total == ledger.total, "ledger.total corrupt");
if !(ledger.active >= T::Currency::minimum_balance() || ledger.active.is_zero()) {
log!(warn, "ledger.active less than ED: {:?}, {:?}", ctrl, ledger)
}
Ok(())
}
}
+5 -1
View File
@@ -744,6 +744,11 @@ pub mod pallet {
);
}
}
#[cfg(feature = "try-runtime")]
fn try_state(n: BlockNumberFor<T>) -> Result<(), &'static str> {
Self::do_try_state(n)
}
}
#[pallet::call]
@@ -856,7 +861,6 @@ pub mod pallet {
if T::VoterList::contains(&stash) {
let _ =
T::VoterList::on_update(&stash, Self::weight_of(&ledger.stash)).defensive();
debug_assert_eq!(T::VoterList::sanity_check(), Ok(()));
}
Self::deposit_event(Event::<T>::Bonded(stash, extra));
@@ -17,7 +17,6 @@
use crate::pallet::Def;
///
/// * implement the individual traits using the Hooks trait
pub fn expand_hooks(def: &mut Def) -> proc_macro2::TokenStream {
let (where_clause, span, has_runtime_upgrade) = match def.hooks.as_ref() {
@@ -59,6 +58,19 @@ pub fn expand_hooks(def: &mut Def) -> proc_macro2::TokenStream {
}
};
let log_try_state = quote::quote! {
let pallet_name = <
<T as #frame_system::Config>::PalletInfo
as
#frame_support::traits::PalletInfo
>::name::<Self>().expect("Every active pallet has a name in the runtime; qed");
#frame_support::log::debug!(
target: #frame_support::LOG_TARGET,
"🩺 try-state pallet {:?}",
pallet_name,
);
};
let hooks_impl = if def.hooks.is_none() {
let frame_system = &def.frame_system;
quote::quote! {
@@ -191,5 +203,23 @@ pub fn expand_hooks(def: &mut Def) -> proc_macro2::TokenStream {
>::integrity_test()
}
}
#[cfg(feature = "try-runtime")]
impl<#type_impl_gen>
#frame_support::traits::TryState<<T as #frame_system::Config>::BlockNumber>
for #pallet_ident<#type_use_gen> #where_clause
{
fn try_state(
n: <T as #frame_system::Config>::BlockNumber,
_s: #frame_support::traits::TryStateSelect
) -> Result<(), &'static str> {
#log_try_state
<
Self as #frame_support::traits::Hooks<
<T as #frame_system::Config>::BlockNumber
>
>::try_state(n)
}
}
)
}
+36
View File
@@ -1549,6 +1549,35 @@ macro_rules! decl_module {
{}
};
(@impl_try_state_default
{ $system:ident }
$module:ident<$trait_instance:ident: $trait_name:ident$(<I>, $instance:ident: $instantiable:path)?>;
{ $( $other_where_bounds:tt )* }
) => {
#[cfg(feature = "try-runtime")]
impl<$trait_instance: $system::Config + $trait_name$(<I>, $instance: $instantiable)?>
$crate::traits::TryState<<$trait_instance as $system::Config>::BlockNumber>
for $module<$trait_instance$(, $instance)?> where $( $other_where_bounds )*
{
fn try_state(
_: <$trait_instance as $system::Config>::BlockNumber,
_: $crate::traits::TryStateSelect,
) -> Result<(), &'static str> {
let pallet_name = <<
$trait_instance
as
$system::Config
>::PalletInfo as $crate::traits::PalletInfo>::name::<Self>().unwrap_or("<unknown pallet name>");
$crate::log::debug!(
target: $crate::LOG_TARGET,
"⚠️ pallet {} cannot have try-state because it is using decl_module!",
pallet_name,
);
Ok(())
}
}
};
(@impl_on_runtime_upgrade
{ $system:ident }
$module:ident<$trait_instance:ident: $trait_name:ident$(<I>, $instance:ident: $instantiable:path)?>;
@@ -2026,6 +2055,13 @@ macro_rules! decl_module {
$( $on_initialize )*
}
$crate::decl_module! {
@impl_try_state_default
{ $system }
$mod_type<$trait_instance: $trait_name $(<I>, $instance: $instantiable)?>;
{ $( $other_where_bounds )* }
}
$crate::decl_module! {
@impl_on_runtime_upgrade
{ $system }
+5 -2
View File
@@ -84,8 +84,6 @@ pub use hooks::{
Hooks, IntegrityTest, OnFinalize, OnGenesis, OnIdle, OnInitialize, OnRuntimeUpgrade,
OnTimestampSet,
};
#[cfg(feature = "try-runtime")]
pub use hooks::{OnRuntimeUpgradeHelpersExt, ON_RUNTIME_UPGRADE_PREFIX};
pub mod schedule;
mod storage;
@@ -106,3 +104,8 @@ pub use voting::{
ClassCountOf, CurrencyToVote, PollStatus, Polling, SaturatingCurrencyToVote,
U128CurrencyToVote, VoteTally,
};
#[cfg(feature = "try-runtime")]
mod try_runtime;
#[cfg(feature = "try-runtime")]
pub use try_runtime::{OnRuntimeUpgradeHelpersExt, Select as TryStateSelect, TryState};
+12 -43
View File
@@ -20,6 +20,7 @@
use crate::weights::Weight;
use impl_trait_for_tuples::impl_for_tuples;
use sp_runtime::traits::AtLeast32BitUnsigned;
use sp_std::prelude::*;
/// The block initialization trait.
///
@@ -93,9 +94,9 @@ impl<BlockNumber: Copy + AtLeast32BitUnsigned> OnIdle<BlockNumber> for Tuple {
let start_index = start_index.try_into().ok().expect(
"`start_index % len` always fits into `usize`, because `len` can be in maximum `usize::MAX`; qed"
);
for on_idle in on_idle_functions.iter().cycle().skip(start_index).take(len) {
for on_idle_fn in on_idle_functions.iter().cycle().skip(start_index).take(len) {
let adjusted_remaining_weight = remaining_weight.saturating_sub(weight);
weight = weight.saturating_add(on_idle(n, adjusted_remaining_weight));
weight = weight.saturating_add(on_idle_fn(n, adjusted_remaining_weight));
}
weight
}
@@ -114,47 +115,6 @@ pub trait OnGenesis {
fn on_genesis() {}
}
/// Prefix to be used (optionally) for implementing [`OnRuntimeUpgradeHelpersExt::storage_key`].
#[cfg(feature = "try-runtime")]
pub const ON_RUNTIME_UPGRADE_PREFIX: &[u8] = b"__ON_RUNTIME_UPGRADE__";
/// Some helper functions for [`OnRuntimeUpgrade`] during `try-runtime` testing.
#[cfg(feature = "try-runtime")]
pub trait OnRuntimeUpgradeHelpersExt {
/// Generate a storage key unique to this runtime upgrade.
///
/// This can be used to communicate data from pre-upgrade to post-upgrade state and check
/// them. See [`Self::set_temp_storage`] and [`Self::get_temp_storage`].
#[cfg(feature = "try-runtime")]
fn storage_key(ident: &str) -> [u8; 32] {
crate::storage::storage_prefix(ON_RUNTIME_UPGRADE_PREFIX, ident.as_bytes())
}
/// Get temporary storage data written by [`Self::set_temp_storage`].
///
/// Returns `None` if either the data is unavailable or un-decodable.
///
/// A `at` storage identifier must be provided to indicate where the storage is being read from.
#[cfg(feature = "try-runtime")]
fn get_temp_storage<T: codec::Decode>(at: &str) -> Option<T> {
sp_io::storage::get(&Self::storage_key(at))
.and_then(|bytes| codec::Decode::decode(&mut &*bytes).ok())
}
/// Write some temporary data to a specific storage that can be read (potentially in
/// post-upgrade hook) via [`Self::get_temp_storage`].
///
/// A `at` storage identifier must be provided to indicate where the storage is being written
/// to.
#[cfg(feature = "try-runtime")]
fn set_temp_storage<T: codec::Encode>(data: T, at: &str) {
sp_io::storage::set(&Self::storage_key(at), &data.encode());
}
}
#[cfg(feature = "try-runtime")]
impl<U: OnRuntimeUpgrade> OnRuntimeUpgradeHelpersExt for U {}
/// The runtime upgrade trait.
///
/// Implementing this lets you express what should happen when the runtime upgrades,
@@ -272,6 +232,15 @@ pub trait Hooks<BlockNumber> {
Weight::new()
}
/// Execute the sanity checks of this pallet, per block.
///
/// It should focus on certain checks to ensure that the state is sensible. This is never
/// executed in a consensus code-path, therefore it can consume as much weight as it needs.
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumber) -> Result<(), &'static str> {
Ok(())
}
/// Execute some pre-checks prior to a runtime upgrade.
///
/// This hook is never meant to be executed on-chain but is meant to be used by testing tools.
@@ -0,0 +1,174 @@
// This file is part of Substrate.
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Try-runtime specific traits and types.
use super::*;
use impl_trait_for_tuples::impl_for_tuples;
use sp_arithmetic::traits::AtLeast32BitUnsigned;
use sp_std::prelude::*;
/// Prefix to be used (optionally) for implementing [`OnRuntimeUpgradeHelpersExt::storage_key`].
const ON_RUNTIME_UPGRADE_PREFIX: &[u8] = b"__ON_RUNTIME_UPGRADE__";
/// Some helper functions for [`OnRuntimeUpgrade`] during `try-runtime` testing.
pub trait OnRuntimeUpgradeHelpersExt {
/// Generate a storage key unique to this runtime upgrade.
///
/// This can be used to communicate data from pre-upgrade to post-upgrade state and check
/// them. See [`Self::set_temp_storage`] and [`Self::get_temp_storage`].
fn storage_key(ident: &str) -> [u8; 32] {
crate::storage::storage_prefix(ON_RUNTIME_UPGRADE_PREFIX, ident.as_bytes())
}
/// Get temporary storage data written by [`Self::set_temp_storage`].
///
/// Returns `None` if either the data is unavailable or un-decodable.
///
/// A `at` storage identifier must be provided to indicate where the storage is being read from.
fn get_temp_storage<T: codec::Decode>(at: &str) -> Option<T> {
sp_io::storage::get(&Self::storage_key(at))
.and_then(|bytes| codec::Decode::decode(&mut &*bytes).ok())
}
/// Write some temporary data to a specific storage that can be read (potentially in
/// post-upgrade hook) via [`Self::get_temp_storage`].
///
/// A `at` storage identifier must be provided to indicate where the storage is being written
/// to.
fn set_temp_storage<T: codec::Encode>(data: T, at: &str) {
sp_io::storage::set(&Self::storage_key(at), &data.encode());
}
}
impl<U: OnRuntimeUpgrade> OnRuntimeUpgradeHelpersExt for U {}
// Which state tests to execute.
#[derive(codec::Encode, codec::Decode, Clone)]
pub enum Select {
/// None of them.
None,
/// All of them.
All,
/// Run a fixed number of them in a round robin manner.
RoundRobin(u32),
/// Run only pallets who's name matches the given list.
///
/// Pallet names are obtained from [`PalletInfoAccess`].
Only(Vec<Vec<u8>>),
}
impl Default for Select {
fn default() -> Self {
Select::None
}
}
impl sp_std::fmt::Debug for Select {
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
match self {
Select::RoundRobin(x) => write!(f, "RoundRobin({})", x),
Select::Only(x) => write!(
f,
"Only({:?})",
x.iter()
.map(|x| sp_std::str::from_utf8(x).unwrap_or("<invalid?>"))
.collect::<Vec<_>>(),
),
Select::All => write!(f, "All"),
Select::None => write!(f, "None"),
}
}
}
#[cfg(feature = "std")]
impl sp_std::str::FromStr for Select {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"all" | "All" => Ok(Select::All),
"none" | "None" => Ok(Select::None),
_ =>
if s.starts_with("rr-") {
let count = s
.split_once('-')
.and_then(|(_, count)| count.parse::<u32>().ok())
.ok_or("failed to parse count")?;
Ok(Select::RoundRobin(count))
} else {
let pallets = s.split(',').map(|x| x.as_bytes().to_vec()).collect::<Vec<_>>();
Ok(Select::Only(pallets))
},
}
}
}
/// Execute some checks to ensure the internal state of a pallet is consistent.
///
/// Usually, these checks should check all of the invariants that are expected to be held on all of
/// the storage items of your pallet.
pub trait TryState<BlockNumber> {
/// Execute the state checks.
fn try_state(_: BlockNumber, _: Select) -> Result<(), &'static str>;
}
#[cfg_attr(all(not(feature = "tuples-96"), not(feature = "tuples-128")), impl_for_tuples(64))]
#[cfg_attr(all(feature = "tuples-96", not(feature = "tuples-128")), impl_for_tuples(96))]
#[cfg_attr(all(feature = "tuples-128"), impl_for_tuples(128))]
impl<BlockNumber: Clone + sp_std::fmt::Debug + AtLeast32BitUnsigned> TryState<BlockNumber>
for Tuple
{
for_tuples!( where #( Tuple: crate::traits::PalletInfoAccess )* );
fn try_state(n: BlockNumber, targets: Select) -> Result<(), &'static str> {
match targets {
Select::None => Ok(()),
Select::All => {
let mut result = Ok(());
for_tuples!( #( result = result.and(Tuple::try_state(n.clone(), targets.clone())); )* );
result
},
Select::RoundRobin(len) => {
let functions: &[fn(BlockNumber, Select) -> Result<(), &'static str>] =
&[for_tuples!(#( Tuple::try_state ),*)];
let skip = n.clone() % (functions.len() as u32).into();
let skip: u32 =
skip.try_into().unwrap_or_else(|_| sp_runtime::traits::Bounded::max_value());
let mut result = Ok(());
for try_state_fn in functions.iter().cycle().skip(skip as usize).take(len as usize)
{
result = result.and(try_state_fn(n.clone(), targets.clone()));
}
result
},
Select::Only(ref pallet_names) => {
let try_state_fns: &[(
&'static str,
fn(BlockNumber, Select) -> Result<(), &'static str>,
)] = &[for_tuples!(
#( (<Tuple as crate::traits::PalletInfoAccess>::name(), Tuple::try_state) ),*
)];
let mut result = Ok(());
for (name, try_state_fn) in try_state_fns {
if pallet_names.iter().any(|n| n == name.as_bytes()) {
result = result.and(try_state_fn(n.clone(), targets.clone()));
}
}
result
},
}
}
}
+3 -2
View File
@@ -1309,9 +1309,10 @@ impl<T: Config> Pallet<T> {
pub fn finalize() -> T::Header {
log::debug!(
target: "runtime::system",
"[{:?}] length: {} (normal {}%, op: {}%, mandatory {}%) / normal weight: {} ({}%) \
/ op weight {} ({}%) / mandatory weight {} ({}%)",
"[{:?}] {} extrinsics, length: {} (normal {}%, op: {}%, mandatory {}%) / normal weight:\
{} ({}%) op weight {} ({}%) / mandatory weight {} ({}%)",
Self::block_number(),
Self::extrinsic_index().unwrap_or_default(),
Self::all_extrinsics_len(),
sp_runtime::Percent::from_rational(
Self::all_extrinsics_len(),
@@ -48,3 +48,4 @@ std = [
"sp-std/std",
"sp-transaction-storage-proof/std",
]
try-runtime = ["frame-support/try-runtime"]
+3 -1
View File
@@ -13,7 +13,8 @@ readme = "README.md"
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"]}
frame-support = { version = "4.0.0-dev", default-features = false, features = [ "try-runtime" ], path = "../support" }
sp-api = { version = "4.0.0-dev", default-features = false, path = "../../primitives/api" }
sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" }
sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" }
@@ -21,6 +22,7 @@ sp-std = { version = "4.0.0", default-features = false, path = "../../primitives
[features]
default = [ "std" ]
std = [
"codec/std",
"frame-support/std",
"sp-api/std",
"sp-runtime/std",
+2 -1
View File
@@ -19,6 +19,7 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub use frame_support::traits::TryStateSelect;
use frame_support::weights::Weight;
sp_api::decl_runtime_apis! {
@@ -37,6 +38,6 @@ sp_api::decl_runtime_apis! {
///
/// This is only sensible where the incoming block is from a different network, yet it has
/// the same block format as the runtime implementing this API.
fn execute_block_no_check(block: Block) -> Weight;
fn execute_block(block: Block, state_root_check: bool, try_state: TryStateSelect) -> Weight;
}
}