diff --git a/Cargo.lock b/Cargo.lock index 7fd0122f3cc0f..23f8d035fe533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3664,6 +3664,7 @@ dependencies = [ "pallet-session", "pallet-session-benchmarking", "pallet-society", + "pallet-stake-tracker", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-runtime-api", @@ -6372,6 +6373,7 @@ dependencies = [ "pallet-bags-list", "pallet-balances", "pallet-nomination-pools", + "pallet-stake-tracker", "pallet-staking", "pallet-staking-reward-curve", "pallet-timestamp", @@ -6730,6 +6732,30 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-stake-tracker" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-election-provider-support", + "frame-support", + "frame-system", + "log", + "pallet-bags-list", + "pallet-balances", + "pallet-staking", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-io", + "sp-npos-elections", + "sp-runtime", + "sp-staking", + "sp-std", + "sp-tracing", + "substrate-test-utils", +] + [[package]] name = "pallet-staking" version = "4.0.0-dev" @@ -6743,6 +6769,7 @@ dependencies = [ "pallet-bags-list", "pallet-balances", "pallet-session", + "pallet-stake-tracker", "pallet-staking-reward-curve", "pallet-timestamp", "parity-scale-codec", @@ -10717,6 +10744,7 @@ dependencies = [ name = "sp-staking" version = "4.0.0-dev" dependencies = [ + "impl-trait-for-tuples", "parity-scale-codec", "scale-info", "sp-core", diff --git a/Cargo.toml b/Cargo.toml index de562ad79e47e..a4e2de3fafdf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,6 +142,7 @@ members = [ "frame/session/benchmarking", "frame/society", "frame/staking", + "frame/stake-tracker", "frame/staking/reward-curve", "frame/staking/reward-fn", "frame/staking/runtime-api", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index bc9f3af9d879a..3a839d2df204d 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -101,6 +101,7 @@ pallet-salary = { version = "4.0.0-dev", default-features = false, path = "../.. pallet-session = { version = "4.0.0-dev", features = [ "historical" ], path = "../../../frame/session", default-features = false } pallet-session-benchmarking = { version = "4.0.0-dev", path = "../../../frame/session/benchmarking", default-features = false, optional = true } pallet-staking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking" } +pallet-stake-tracker = { version = "4.0.0-dev", default-features = false, path = "../../../frame/stake-tracker" } pallet-staking-reward-curve = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking/reward-curve" } pallet-staking-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking/runtime-api" } pallet-state-trie-migration = { version = "4.0.0-dev", default-features = false, path = "../../../frame/state-trie-migration" } @@ -184,6 +185,7 @@ std = [ "sp-runtime/std", "sp-staking/std", "pallet-staking/std", + "pallet-stake-tracker/std", "pallet-staking-runtime-api/std", "pallet-state-trie-migration/std", "pallet-salary/std", @@ -326,6 +328,7 @@ try-runtime = [ "pallet-salary/try-runtime", "pallet-session/try-runtime", "pallet-staking/try-runtime", + "pallet-stake-tracker/try-runtime", "pallet-state-trie-migration/try-runtime", "pallet-scheduler/try-runtime", "pallet-society/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 48bea5ddc101f..5288c8d017a4b 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -586,7 +586,7 @@ impl pallet_staking::Config for Runtime { type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type ElectionProvider = ElectionProviderMultiPhase; type GenesisElectionProvider = onchain::OnChainExecution; - type VoterList = VoterList; + type VoterList = pallet_stake_tracker::TrackedList; // This a placeholder, to be introduced in the next PR as an instance of bags-list type TargetList = pallet_staking::UseValidatorsMap; type MaxUnlockingChunks = ConstU32<32>; @@ -594,6 +594,13 @@ impl pallet_staking::Config for Runtime { type OnStakerSlash = NominationPools; type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = StakingBenchmarkingConfig; + type EventListeners = StakeTracker; +} + +impl pallet_stake_tracker::Config for Runtime { + type Currency = Balances; + type Staking = Staking; + type VoterList = VoterList; } impl pallet_fast_unstake::Config for Runtime { @@ -1763,6 +1770,7 @@ construct_runtime!( AssetTxPayment: pallet_asset_tx_payment, ElectionProviderMultiPhase: pallet_election_provider_multi_phase, Staking: pallet_staking, + StakeTracker: pallet_stake_tracker, Session: pallet_session, Democracy: pallet_democracy, Council: pallet_collective::, diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 94e748d0bca52..845300029164e 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -208,6 +208,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } impl pallet_offences::Config for Test { diff --git a/frame/bags-list/remote-tests/Cargo.toml b/frame/bags-list/remote-tests/Cargo.toml index 6e951b43a4aeb..26670c551f87d 100644 --- a/frame/bags-list/remote-tests/Cargo.toml +++ b/frame/bags-list/remote-tests/Cargo.toml @@ -16,6 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"] # frame pallet-staking = { path = "../../staking", version = "4.0.0-dev" } pallet-bags-list = { path = "../../bags-list", version = "4.0.0-dev", features = ["fuzz"] } + frame-election-provider-support = { path = "../../election-provider-support", version = "4.0.0-dev" } frame-system = { path = "../../system", version = "4.0.0-dev" } frame-support = { path = "../../support", version = "4.0.0-dev" } diff --git a/frame/bags-list/remote-tests/src/lib.rs b/frame/bags-list/remote-tests/src/lib.rs index 9f7c22d99dad1..49d60dc37a321 100644 --- a/frame/bags-list/remote-tests/src/lib.rs +++ b/frame/bags-list/remote-tests/src/lib.rs @@ -17,7 +17,7 @@ //! Utilities for remote-testing pallet-bags-list. -use frame_election_provider_support::ScoreProvider; +use frame_election_provider_support::{ScoreProvider, SortedListProvider}; use pallet_bags_list::Instance1; use sp_std::prelude::*; @@ -51,7 +51,6 @@ pub fn display_and_check_bags>( currency_unit: u64, currency_name: &'static str, ) { - use frame_election_provider_support::SortedListProvider; use frame_support::traits::Get; let min_nominator_bond = >::get(); diff --git a/frame/bags-list/remote-tests/src/migration.rs b/frame/bags-list/remote-tests/src/migration.rs index 7847fdc7591c0..e910935346912 100644 --- a/frame/bags-list/remote-tests/src/migration.rs +++ b/frame/bags-list/remote-tests/src/migration.rs @@ -17,6 +17,7 @@ //! Test to check the migration of the voter bag. use crate::{RuntimeT, LOG_TARGET}; +use frame_election_provider_support::SortedListProvider; use frame_support::traits::PalletInfoAccess; use pallet_staking::Nominators; use remote_externalities::{Builder, Mode, OnlineConfig}; @@ -48,7 +49,6 @@ pub async fn execute( let pre_migrate_nominator_count = >::iter().count() as u32; log::info!(target: LOG_TARGET, "Nominator count: {}", pre_migrate_nominator_count); - use frame_election_provider_support::SortedListProvider; // run the actual migration let moved = ::VoterList::unsafe_regenerate( pallet_staking::Nominators::::iter().map(|(n, _)| n), diff --git a/frame/bags-list/src/lib.rs b/frame/bags-list/src/lib.rs index 87eb2d1b341aa..1121c06e11b1d 100644 --- a/frame/bags-list/src/lib.rs +++ b/frame/bags-list/src/lib.rs @@ -328,45 +328,35 @@ impl, I: 'static> SortedListProvider for Pallet List::::contains(id) } - fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> { - List::::insert(id, score) - } - fn get_score(id: &T::AccountId) -> Result { List::::get_score(id) } - fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> { - Pallet::::do_rebag(id, new_score).map(|_| ()) + #[cfg(feature = "try-runtime")] + fn try_state() -> Result<(), &'static str> { + Self::do_try_state() } - fn on_remove(id: &T::AccountId) -> Result<(), ListError> { - List::::remove(id) + fn on_insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> { + List::::insert(id, score) } - fn unsafe_regenerate( - all: impl IntoIterator, - score_of: Box T::Score>, - ) -> u32 { - // NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_regenerate. - // I.e. because it can lead to many storage accesses. - // So it is ok to call it as caller must ensure the conditions. - List::::unsafe_regenerate(all, score_of) + fn on_update(id: &T::AccountId, new_score: T::Score) -> Result<(), ListError> { + Pallet::::do_rebag(id, new_score).map(|_| ()) } - #[cfg(feature = "try-runtime")] - fn try_state() -> Result<(), &'static str> { - Self::do_try_state() + fn on_remove(id: &T::AccountId) -> Result<(), ListError> { + List::::remove(id) } - fn unsafe_clear() { - // NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_clear. - // I.e. because it can lead to many storage accesses. - // So it is ok to call it as caller must ensure the conditions. - List::::unsafe_clear() - } + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + fn unsafe_clear() { + // NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_clear. + // I.e. because it can lead to many storage accesses. + // So it is ok to call it as caller must ensure the conditions. + List::::unsafe_clear() + } - frame_election_provider_support::runtime_benchmarks_enabled! { fn score_update_worst_case(who: &T::AccountId, is_increase: bool) -> Self::Score { use frame_support::traits::Get as _; let thresholds = T::BagThresholds::get(); @@ -388,6 +378,16 @@ impl, I: 'static> SortedListProvider for Pallet } } } + + fn unsafe_regenerate( + all: impl IntoIterator, + score_of: Box T::Score>, + ) -> u32 { + // NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_regenerate. + // I.e. because it can lead to many storage accesses. + // So it is ok to call it as caller must ensure the conditions. + List::::unsafe_regenerate(all, score_of) + } } impl, I: 'static> ScoreProvider for Pallet { diff --git a/frame/beefy/src/mock.rs b/frame/beefy/src/mock.rs index ceb95263e2436..f09b9849fc895 100644 --- a/frame/beefy/src/mock.rs +++ b/frame/beefy/src/mock.rs @@ -231,6 +231,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } impl pallet_offences::Config for Test { diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs index 750ccca46213f..b56e7b1b4d25d 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -476,10 +476,10 @@ where /// This is generic over `AccountId` and it can represent a validator, a nominator, or any other /// entity. /// -/// The scores (see [`Self::Score`]) are ascending, the higher, the better. -/// /// Something that implements this trait will do a best-effort sort over ids, and thus can be /// used on the implementing side of [`ElectionDataProvider`]. +/// +/// The scores (see [`Self::Score`]) are ascending, the higher, the better. pub trait SortedListProvider { /// The list's error type. type Error: sp_std::fmt::Debug; @@ -501,6 +501,13 @@ pub trait SortedListProvider { /// Return true if the list already contains `id`. fn contains(id: &AccountId) -> bool; + /// Get the score of `id`. + fn get_score(id: &AccountId) -> Result; + + /// Check internal state of list. Only meant for debugging. + #[cfg(feature = "try-runtime")] + fn try_state() -> Result<(), &'static str>; + /// Hook for inserting a new id. /// /// Implementation should return an error if duplicate item is being inserted. @@ -513,9 +520,6 @@ pub trait SortedListProvider { /// Returns `Ok(())` iff it successfully updates an item, an `Err(_)` otherwise. fn on_update(id: &AccountId, score: Self::Score) -> Result<(), Self::Error>; - /// Get the score of `id`. - fn get_score(id: &AccountId) -> Result; - /// Same as `on_update`, but incorporate some increased score. fn on_increase(id: &AccountId, additional: Self::Score) -> Result<(), Self::Error> { let old_score = Self::get_score(id)?; @@ -560,16 +564,13 @@ pub trait SortedListProvider { /// /// This function should never be called in production settings because it can lead to an /// unbounded amount of storage accesses. + #[cfg(any(feature = "runtime-benchmarks", test))] fn unsafe_clear(); - /// Check internal state of the list. Only meant for debugging. - #[cfg(feature = "try-runtime")] - 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. - #[cfg(feature = "runtime-benchmarks")] - fn score_update_worst_case(_who: &AccountId, _is_increase: bool) -> Self::Score; + #[cfg(any(feature = "runtime-benchmarks", test))] + fn score_update_worst_case(who: &AccountId, is_increase: bool) -> Self::Score; } /// Something that can provide the `Score` of an account. Similar to [`ElectionProvider`] and @@ -675,3 +676,4 @@ pub type BoundedSupportsOf = BoundedSupports< sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); sp_core::generate_feature_enabled_macro!(runtime_benchmarks_or_fuzz_enabled, any(feature = "runtime-benchmarks", feature = "fuzzing"), $); +sp_core::generate_feature_enabled_macro!(runtime_benchmarks_or_test_enabled, any(feature = "runtime-benchmarks", test), $); diff --git a/frame/fast-unstake/src/mock.rs b/frame/fast-unstake/src/mock.rs index fbe6c4592bf67..3c8d66dff395f 100644 --- a/frame/fast-unstake/src/mock.rs +++ b/frame/fast-unstake/src/mock.rs @@ -160,6 +160,7 @@ impl pallet_staking::Config for Runtime { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } pub struct BalanceToU256; diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index a7359f6896db7..711ac59b7393c 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -212,6 +212,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } impl pallet_offences::Config for Test { diff --git a/frame/nomination-pools/benchmarking/Cargo.toml b/frame/nomination-pools/benchmarking/Cargo.toml index b96024af7d3c5..287b2998d53f6 100644 --- a/frame/nomination-pools/benchmarking/Cargo.toml +++ b/frame/nomination-pools/benchmarking/Cargo.toml @@ -24,6 +24,7 @@ frame-support = { version = "4.0.0-dev", default-features = false, path = "../.. frame-system = { version = "4.0.0-dev", default-features = false, path = "../../system" } pallet-bags-list = { version = "4.0.0-dev", default-features = false, path = "../../bags-list" } pallet-staking = { version = "4.0.0-dev", default-features = false, path = "../../staking" } +pallet-stake-tracker = { version = "4.0.0-dev", default-features = false, path = "../../stake-tracker" } pallet-nomination-pools = { version = "1.0.0", default-features = false, path = "../" } # Substrate Primitives diff --git a/frame/nomination-pools/benchmarking/src/lib.rs b/frame/nomination-pools/benchmarking/src/lib.rs index d58bbaf3d117c..137b9e9af63e3 100644 --- a/frame/nomination-pools/benchmarking/src/lib.rs +++ b/frame/nomination-pools/benchmarking/src/lib.rs @@ -684,12 +684,12 @@ frame_benchmarking::benchmarks! { .collect(); assert_ok!(T::Staking::nominate(&pool_account, validators)); - assert!(T::Staking::nominations(Pools::::create_bonded_account(1)).is_some()); + assert!(T::Staking::nominations(&Pools::::create_bonded_account(1)).is_some()); whitelist_account!(depositor); }:_(RuntimeOrigin::Signed(depositor.clone()), 1) verify { - assert!(T::Staking::nominations(Pools::::create_bonded_account(1)).is_none()); + assert!(T::Staking::nominations(&Pools::::create_bonded_account(1)).is_none()); } set_commission { diff --git a/frame/nomination-pools/benchmarking/src/mock.rs b/frame/nomination-pools/benchmarking/src/mock.rs index cffb712ea2ae5..ebb3eea151f6b 100644 --- a/frame/nomination-pools/benchmarking/src/mock.rs +++ b/frame/nomination-pools/benchmarking/src/mock.rs @@ -123,6 +123,13 @@ impl pallet_staking::Config for Runtime { type OnStakerSlash = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = StakeTracker; +} + +impl pallet_stake_tracker::Config for Runtime { + type Currency = Balances; + type Staking = Staking; + type VoterList = VoterList; } parameter_types! { @@ -188,6 +195,7 @@ frame_support::construct_runtime!( Staking: pallet_staking::{Pallet, Call, Config, Storage, Event}, VoterList: pallet_bags_list::::{Pallet, Call, Storage, Event}, Pools: pallet_nomination_pools::{Pallet, Call, Storage, Event}, + StakeTracker: pallet_stake_tracker::{Pallet, Storage}, } ); diff --git a/frame/nomination-pools/src/mock.rs b/frame/nomination-pools/src/mock.rs index 6d83ef61de793..5737f2fadd2e5 100644 --- a/frame/nomination-pools/src/mock.rs +++ b/frame/nomination-pools/src/mock.rs @@ -47,6 +47,7 @@ impl StakingMock { impl sp_staking::StakingInterface for StakingMock { type Balance = Balance; type AccountId = AccountId; + type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; fn minimum_nominator_bond() -> Self::Balance { StakingMinBond::get() @@ -107,8 +108,7 @@ impl sp_staking::StakingInterface for StakingMock { Ok(()) } - #[cfg(feature = "runtime-benchmarks")] - fn nominations(_: Self::AccountId) -> Option> { + fn nominations(_: &Self::AccountId) -> Option> { Nominations::get() } @@ -116,7 +116,9 @@ impl sp_staking::StakingInterface for StakingMock { unimplemented!("method currently not used in testing") } - fn stake(who: &Self::AccountId) -> Result, DispatchError> { + fn stake( + who: &Self::AccountId, + ) -> Result, DispatchError> { match ( UnbondingBalanceMap::get().get(who).copied(), BondedBalanceMap::get().get(who).copied(), @@ -153,6 +155,10 @@ impl sp_staking::StakingInterface for StakingMock { fn set_current_era(_era: EraIndex) { unimplemented!("method currently not used in testing") } + + fn is_validator(_: &Self::AccountId) -> bool { + unimplemented!("method currently not used in testing") + } } impl frame_system::Config for Runtime { diff --git a/frame/nomination-pools/test-staking/src/mock.rs b/frame/nomination-pools/test-staking/src/mock.rs index 9726f5e6dad27..6c9311f9210ba 100644 --- a/frame/nomination-pools/test-staking/src/mock.rs +++ b/frame/nomination-pools/test-staking/src/mock.rs @@ -137,6 +137,7 @@ impl pallet_staking::Config for Runtime { type OnStakerSlash = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } parameter_types! { diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs index 668d88e0bf3d0..a6deb3ab07162 100644 --- a/frame/offences/benchmarking/src/mock.rs +++ b/frame/offences/benchmarking/src/mock.rs @@ -185,6 +185,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } impl pallet_im_online::Config for Test { diff --git a/frame/root-offences/src/mock.rs b/frame/root-offences/src/mock.rs index 828551e4d9c19..013d15d08d1e4 100644 --- a/frame/root-offences/src/mock.rs +++ b/frame/root-offences/src/mock.rs @@ -199,6 +199,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = OnStakerSlashMock; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } impl pallet_session::historical::Config for Test { diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs index b7671255f68fb..7546290214727 100644 --- a/frame/session/benchmarking/src/mock.rs +++ b/frame/session/benchmarking/src/mock.rs @@ -187,6 +187,7 @@ impl pallet_staking::Config for Test { type OnStakerSlash = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (); } impl crate::Config for Test {} diff --git a/frame/stake-tracker/Cargo.toml b/frame/stake-tracker/Cargo.toml new file mode 100644 index 0000000000000..6c648937a9b24 --- /dev/null +++ b/frame/stake-tracker/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "pallet-stake-tracker" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Unlicense" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME stake tracker pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } +log = { version = "0.4.17", default-features = false } +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" } + +sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } +sp-staking = { version = "4.0.0-dev", default-features = false, path = "../../primitives/staking" } + +frame-election-provider-support = { default-features = false, path = "../election-provider-support" } + +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } + +[dev-dependencies] +substrate-test-utils = { version = "4.0.0-dev", path = "../../test-utils" } +sp-tracing = { version = "6.0.0", path = "../../primitives/tracing" } +pallet-staking = { path = "../staking" } +pallet-balances = { path = "../balances" } +pallet-timestamp = { path = "../timestamp" } +sp-npos-elections = { version = "4.0.0-dev", path = "../../primitives/npos-elections" } +pallet-bags-list = { version = "4.0.0-dev", path = "../bags-list" } +frame-election-provider-support = { version = "4.0.0-dev", path = "../election-provider-support" } + +[features] +default = ["std"] +std = [ + "codec/std", + "log/std", + "scale-info/std", + + "frame-support/std", + "frame-system/std", + + "sp-io/std", + "sp-staking/std", + "sp-runtime/std", + "sp-std/std", + + "frame-election-provider-support/std", + + "frame-benchmarking/std", +] + +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-election-provider-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-staking/runtime-benchmarks", + "pallet-staking/runtime-benchmarks" +] + +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/stake-tracker/src/lib.rs b/frame/stake-tracker/src/lib.rs new file mode 100644 index 0000000000000..a12102dcd2b3f --- /dev/null +++ b/frame/stake-tracker/src/lib.rs @@ -0,0 +1,228 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 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. + +//! # Stake Tracker Pallet +//! +//! The Stake Tracker pallet is used to maintain sorted lists of [`frame_system::Config::AccountId`] +//! by listening to the events that Staking emits. +//! +//! - [`Config`] +//! - [`Pallet`] +//! +//! ## Overview +//! +//! The goal of Stake Tracker is to maintain [`SortedListProvider`] sorted list implementations +//! based on [`SortedListProvider::Score`]. This pallet implements [`OnStakingUpdate`] interface in +//! order to be able to listen to the events that Staking emits and propagate the changes to said +//! lists accordingly. It also exposes [`TrackedList`] that adds defensive checks to a subset of +//! [`SortedListProvider`] methods in order to spot unexpected list updates on the consumer side. +//! This wrapper should be used to pass the tracked entity to the consumer. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +pub(crate) mod mock; +#[cfg(test)] +mod tests; + +use frame_election_provider_support::{SortedListProvider, VoteWeight}; +use frame_support::{ + defensive, + traits::{Currency, CurrencyToVote, Defensive}, +}; +pub use pallet::*; +use sp_staking::{OnStakingUpdate, Stake, StakingInterface}; + +use sp_std::{boxed::Box, vec::Vec}; + +/// The balance type of this pallet. +pub type BalanceOf = <::Staking as StakingInterface>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use crate::*; + use frame_election_provider_support::{SortedListProvider, VoteWeight}; + use frame_support::pallet_prelude::*; + + use sp_staking::StakingInterface; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The same currency type that's used by Staking. + type Currency: Currency>; + + /// An interface to Staking. + type Staking: StakingInterface; + + /// A sorted list of nominators and validators, by their stake and self-stake respectively. + type VoterList: SortedListProvider; + } +} + +impl Pallet { + pub(crate) fn active_stake_of(who: &T::AccountId) -> BalanceOf { + T::Staking::stake(&who).map(|s| s.active).unwrap_or_default() + } + + pub(crate) fn to_vote(balance: BalanceOf) -> VoteWeight { + let total_issuance = T::Currency::total_issuance(); + ::CurrencyToVote::to_vote(balance, total_issuance) + } +} + +impl OnStakingUpdate> for Pallet { + fn on_stake_update(who: &T::AccountId, _: Option>>) { + if let Ok(current_stake) = T::Staking::stake(who) { + let current_active = current_stake.active; + + // If this is a nominator, update their position in the `VoterList`. + if let Some(_) = T::Staking::nominations(¤t_stake.stash) { + let _ = + T::VoterList::on_update(¤t_stake.stash, Self::to_vote(current_active)) + .defensive_proof("Nominator's position in VoterList updated; qed"); + } + + // If this is a validator, update their position in the `VoterList`. + if T::Staking::is_validator(¤t_stake.stash) { + let _ = + T::VoterList::on_update(¤t_stake.stash, Self::to_vote(current_active)) + .defensive_proof("Validator's position in VoterList updated; qed"); + } + } + } + fn on_nominator_add(who: &T::AccountId) { + let _ = T::VoterList::on_insert(who.clone(), Self::to_vote(Self::active_stake_of(who))) + .defensive_proof("Nominator inserted into VoterList; qed"); + } + + fn on_nominator_update(who: &T::AccountId, _prev_nominations: Vec) { + if !T::VoterList::contains(who) { + defensive!("Active nominator is in the VoterList; qed"); + } + } + + fn on_validator_add(who: &T::AccountId) { + let _ = T::VoterList::on_insert(who.clone(), Self::to_vote(Self::active_stake_of(who))) + .defensive_proof("Validator inserted into VoterList; qed"); + } + + fn on_validator_update(who: &T::AccountId) { + if !T::VoterList::contains(who) { + defensive!("Active validator is in the VoterList; qed"); + } + } + + fn on_validator_remove(who: &T::AccountId) { + let _ = + T::VoterList::on_remove(who).defensive_proof("Validator removed from VoterList; qed"); + } + + fn on_nominator_remove(who: &T::AccountId, _nominations: Vec) { + let _ = + T::VoterList::on_remove(who).defensive_proof("Nominator removed from VoterList; qed"); + } + + fn on_unstake(who: &T::AccountId) { + if T::VoterList::contains(who) { + defensive!("The staker has already been removed; qed"); + } + } +} + +/// A wrapper for a given `SortedListProvider` that introduces defensive checks for insert, update +/// and remove operations, suggesting that it's read-only, except for unsafe operations. +pub struct TrackedList(sp_std::marker::PhantomData<(AccountId, Inner)>); + +impl> SortedListProvider + for TrackedList +{ + type Error = Inner::Error; + type Score = Inner::Score; + fn iter() -> Box> { + Inner::iter() + } + + fn iter_from(start: &AccountId) -> Result>, Self::Error> { + Inner::iter_from(start) + } + + fn count() -> u32 { + Inner::count() + } + + fn contains(id: &AccountId) -> bool { + Inner::contains(id) + } + + fn get_score(id: &AccountId) -> Result { + Inner::get_score(id) + } + + #[cfg(feature = "try-runtime")] + fn try_state() -> Result<(), &'static str> { + Inner::try_state() + } + + fn on_insert(id: AccountId, score: Self::Score) -> Result<(), Self::Error> { + defensive!("TrackedList on_insert should never be called"); + Inner::on_insert(id, score) + } + + fn on_update(id: &AccountId, score: Self::Score) -> Result<(), Self::Error> { + defensive!("TrackedList on_update should never be called"); + Inner::on_update(id, score) + } + + fn on_increase(id: &AccountId, additional: Self::Score) -> Result<(), Self::Error> { + defensive!("TrackedList on_increase should never be called"); + Inner::on_increase(id, additional) + } + + fn on_decrease(id: &AccountId, decreased: Self::Score) -> Result<(), Self::Error> { + defensive!("TrackedList on_decrease should never be called"); + Inner::on_decrease(id, decreased) + } + + fn on_remove(id: &AccountId) -> Result<(), Self::Error> { + defensive!("TrackedList on_remove should never be called"); + Inner::on_remove(id) + } + + fn unsafe_regenerate( + all: impl IntoIterator, + score_of: Box Self::Score>, + ) -> u32 { + Inner::unsafe_regenerate(all, score_of) + } + + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + fn unsafe_clear() { + Inner::unsafe_clear() + } + + fn score_update_worst_case(who: &AccountId, is_increase: bool) -> Self::Score { + Inner::score_update_worst_case(who, is_increase) + } + } +} diff --git a/frame/stake-tracker/src/mock.rs b/frame/stake-tracker/src/mock.rs new file mode 100644 index 0000000000000..7a6473bc2596e --- /dev/null +++ b/frame/stake-tracker/src/mock.rs @@ -0,0 +1,292 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 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. + +use crate::{self as pallet_stake_tracker, *}; +use frame_election_provider_support::{ScoreProvider, VoteWeight}; +use frame_support::{parameter_types, weights::constants::RocksDbWeight}; +use sp_runtime::{ + testing::{Header, H256}, + traits::IdentityLookup, + DispatchError, DispatchResult, +}; +use sp_staking::{EraIndex, Stake, StakingInterface}; +use Currency; + +pub(crate) type AccountId = u64; +pub(crate) type AccountIndex = u64; +pub(crate) type BlockNumber = u64; +pub(crate) type Balance = u128; + +type Block = frame_system::mocking::MockBlock; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + VoterBagsList: pallet_bags_list::::{Pallet, Call, Storage, Event}, + StakeTracker: pallet_stake_tracker::{Pallet, Storage}, + } +); + +parameter_types! { + pub static ExistentialDeposit: Balance = 1; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = RocksDbWeight; + type RuntimeOrigin = RuntimeOrigin; + type Index = AccountIndex; + type BlockNumber = BlockNumber; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = frame_support::traits::ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = frame_support::traits::ConstU32<1024>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); +} + +impl pallet_stake_tracker::Config for Runtime { + type Currency = Balances; + type Staking = StakingMock; + type VoterList = VoterBagsList; +} +const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] = + [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; + +parameter_types! { + pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS; +} + +type VoterBagsListInstance = pallet_bags_list::Instance1; +impl pallet_bags_list::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + // Staking is the source of truth for voter bags list, since they are not kept up to date. + type ScoreProvider = StakingMock; + type BagThresholds = BagThresholds; + type Score = VoteWeight; +} + +pub struct StakingMock {} + +// We don't really care about this yet in the context of testing stake-tracker logic. +impl ScoreProvider for StakingMock { + type Score = VoteWeight; + + fn score(_id: &AccountId) -> Self::Score { + VoteWeight::default() + } + + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + fn set_score_of(_: &AccountId, _: Self::Score) { + // not use yet. + } + } +} + +parameter_types! { + pub static Nominators: Vec = vec![20, 21, 22, 23, 24]; + pub static Validators: Vec = vec![10, 11, 12, 13, 14]; +} + +pub(crate) fn stakers() -> Vec { + let mut stakers = Nominators::get(); + stakers.append(&mut Validators::get()); + stakers +} + +impl StakingInterface for StakingMock { + type Balance = Balance; + type AccountId = AccountId; + type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; + + fn minimum_nominator_bond() -> Self::Balance { + unreachable!(); + } + + fn minimum_validator_bond() -> Self::Balance { + unreachable!(); + } + + fn stash_by_ctrl(_: &Self::AccountId) -> Result { + unreachable!(); + } + + fn bonding_duration() -> EraIndex { + unreachable!(); + } + + fn current_era() -> EraIndex { + unreachable!(); + } + + fn stake( + who: &Self::AccountId, + ) -> Result, DispatchError> { + if !Nominators::get().contains(who) && !Validators::get().contains(who) { + return Err(DispatchError::Other("not bonded")) + } + let stake = Balances::total_balance(who); + Ok(Stake { + stash: *who, + active: stake.saturating_sub(ExistentialDeposit::get()), + total: stake, + }) + } + + fn bond(_: &Self::AccountId, _: Self::Balance, _: &Self::AccountId) -> DispatchResult { + unreachable!(); + } + + fn nominate(_: &Self::AccountId, _: Vec) -> DispatchResult { + unreachable!(); + } + + fn chill(_: &Self::AccountId) -> DispatchResult { + unreachable!(); + } + + fn bond_extra(_: &Self::AccountId, _: Self::Balance) -> DispatchResult { + unreachable!(); + } + + fn unbond(_: &Self::AccountId, _: Self::Balance) -> DispatchResult { + unreachable!(); + } + + fn withdraw_unbonded(_: Self::AccountId, _: u32) -> Result { + unreachable!(); + } + + fn desired_validator_count() -> u32 { + unreachable!(); + } + + fn election_ongoing() -> bool { + unreachable!(); + } + + fn force_unstake(_: Self::AccountId) -> DispatchResult { + unreachable!(); + } + + fn is_exposed_in_era(_: &Self::AccountId, _: &EraIndex) -> bool { + unreachable!(); + } + + fn is_validator(who: &Self::AccountId) -> bool { + Validators::get().contains(who) + } + + fn nominations(who: &Self::AccountId) -> Option> { + if Nominators::get().contains(who) { + Some(Vec::new()) + } else { + None + } + } + + #[cfg(feature = "runtime-benchmarks")] + fn add_era_stakers( + _: &EraIndex, + _: &Self::AccountId, + _: Vec<(Self::AccountId, Self::Balance)>, + ) { + unreachable!(); + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_current_era(_: EraIndex) { + unreachable!(); + } +} + +#[derive(Default)] +pub struct ExtBuilder {} + +impl ExtBuilder { + fn build(self) -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + + let mut storage = + frame_system::GenesisConfig::default().build_storage::().unwrap(); + + let _ = pallet_balances::GenesisConfig:: { + balances: vec![ + // Validator stashes, for simplicity we assume stash == controller as StakeTracker + // really does not care. + (10, 10), + (11, 20), + (12, 30), + (13, 40), + (14, 50), + // nominators + (20, 10), + (21, 20), + (22, 30), + (23, 40), + (24, 50), + ], + } + .assimilate_storage(&mut storage); + + let ext = sp_io::TestExternalities::from(storage); + + ext + } + + pub fn build_and_execute(self, test: impl FnOnce() -> ()) { + sp_tracing::try_init_simple(); + let mut ext = self.build(); + ext.execute_with(test); + } +} diff --git a/frame/stake-tracker/src/tests.rs b/frame/stake-tracker/src/tests.rs new file mode 100644 index 0000000000000..efd2229a61d41 --- /dev/null +++ b/frame/stake-tracker/src/tests.rs @@ -0,0 +1,312 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 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. + +use super::mock::*; +use crate as pallet_stake_tracker; +use frame_election_provider_support::SortedListProvider; +use frame_support::{assert_storage_noop, traits::fungible::Mutate}; +use sp_staking::OnStakingUpdate; + +type VoterList = ::VoterList; + +// It is the caller's problem to make sure each of events is emitted in the right context, therefore +// we test each event for all the stakers (validators + nominators). + +mod on_stake_update { + use super::*; + + #[test] + fn does_nothing_when_not_bonded() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + // user without stake + assert_storage_noop!(StakeTracker::on_stake_update(&30, None)); + }); + } + + #[test] + fn works() { + ExtBuilder::default().build_and_execute(|| { + let balance_before: Balance = 1000; + let balance_after: Balance = 10; + let validator_id = 10; + let nominator_id = 20; + assert_eq!(VoterList::count(), 0); + + // validator + Balances::set_balance(&validator_id, balance_before); + StakeTracker::on_validator_add(&validator_id); + assert_eq!( + VoterList::get_score(&validator_id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&validator_id)) + ); + + Balances::set_balance(&validator_id, balance_after); + StakeTracker::on_stake_update(&validator_id, None); + assert_eq!( + VoterList::get_score(&validator_id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&validator_id)) + ); + + // nominator + Balances::set_balance(&nominator_id, balance_before); + StakeTracker::on_nominator_add(&nominator_id); + assert_eq!( + VoterList::get_score(&nominator_id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&nominator_id)) + ); + + Balances::set_balance(&nominator_id, balance_after); + StakeTracker::on_stake_update(&nominator_id, None); + assert_eq!( + VoterList::get_score(&nominator_id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&nominator_id)) + ); + + assert_eq!(VoterList::count(), 2); + }); + } + + #[test] + #[should_panic(expected = "Nominator's position in VoterList updated; qed")] + fn defensive_when_not_in_list_nominator() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_stake_update(&20, None); + }); + } + + #[test] + #[should_panic(expected = "Validator's position in VoterList updated; qed")] + fn defensive_when_not_in_list_validator() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + + StakeTracker::on_stake_update(&10, None); + }); + } +} + +mod on_nominator_add { + use super::*; + + #[test] + fn works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + + // nominators + for id in Nominators::get() { + StakeTracker::on_nominator_add(&id); + assert_eq!( + VoterList::get_score(&id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&id)) + ); + } + + assert_eq!(VoterList::count(), Nominators::get().len() as u32); + }); + } + + #[test] + #[should_panic(expected = "Nominator inserted into VoterList; qed")] + fn defensive_when_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_nominator_add(&20); + StakeTracker::on_nominator_add(&20); + }); + } +} + +mod on_nominator_update { + use super::*; + + #[test] + #[should_panic(expected = "Active nominator is in the VoterList; qed")] + fn defensive_not_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_nominator_update(&20, Vec::new()) + }); + } + + #[test] + fn noop() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + let id = 20; + + StakeTracker::on_nominator_add(&id); + assert_storage_noop!(StakeTracker::on_nominator_update(&id, Vec::new())); + assert_eq!(VoterList::count(), 1); + }); + } +} + +mod on_validator_add { + use super::*; + + #[test] + fn works() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + + // validators + for id in Validators::get() { + StakeTracker::on_validator_add(&id); + assert_eq!( + VoterList::get_score(&id).unwrap(), + StakeTracker::to_vote(StakeTracker::active_stake_of(&id)) + ); + } + + assert_eq!(VoterList::count(), Validators::get().len() as u32); + }); + } + + #[test] + #[should_panic(expected = "Validator inserted into VoterList; qed")] + fn defensive_when_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + let id = 10; + StakeTracker::on_validator_add(&id); + StakeTracker::on_validator_add(&id); + }); + } +} + +mod on_validator_update { + use super::*; + + #[test] + fn noop() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + + let id = 10; + + StakeTracker::on_validator_add(&id); + assert_storage_noop!(StakeTracker::on_validator_update(&id)); + assert_eq!(VoterList::count(), 1); + }); + } + + #[test] + #[should_panic(expected = "Active validator is in the VoterList; qed")] + fn defensive_not_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_validator_update(&10) + }); + } +} + +mod on_validator_remove { + use super::*; + + #[test] + fn works_for_validator_and_nominator() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + + let validator_id = 10; + let nominator_id = 20; + + StakeTracker::on_validator_add(&validator_id); + StakeTracker::on_validator_remove(&validator_id); + + assert_eq!(VoterList::count(), 0); + + StakeTracker::on_nominator_add(&nominator_id); + StakeTracker::on_validator_remove(&nominator_id); + + assert_eq!(VoterList::count(), 0); + }); + } + + #[test] + #[should_panic(expected = "Validator removed from VoterList; qed")] + fn defensive_when_not_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_validator_remove(&10); + }); + } +} + +mod on_nominator_remove { + use super::*; + + #[test] + fn works_for_nominator_and_validator() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + + let validator_id = 10; + let nominator_id = 20; + + StakeTracker::on_nominator_add(&nominator_id); + StakeTracker::on_nominator_remove(&nominator_id, Vec::new()); + + assert_eq!(VoterList::count(), 0); + + StakeTracker::on_validator_add(&validator_id); + StakeTracker::on_nominator_remove(&validator_id, Vec::new()); + + assert_eq!(VoterList::count(), 0); + }); + } + + #[test] + #[should_panic(expected = "Nominator removed from VoterList; qed")] + fn defensive_when_not_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + StakeTracker::on_nominator_remove(&20, vec![]); + }); + } +} + +mod on_unstake { + use super::*; + + #[test] + // By the time this is called - staker has to already be removed from the list. Otherwise we hit + // the defensive path. + fn noop_when_not_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + + // any staker + for id in stakers() { + assert_storage_noop!(StakeTracker::on_unstake(&id)); + } + }); + } + + #[test] + #[should_panic(expected = "The staker has already been removed; qed")] + fn defensive_when_in_list() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(VoterList::count(), 0); + let _ = VoterList::on_insert(10, 100); + StakeTracker::on_unstake(&10); + }); + } +} diff --git a/frame/staking/Cargo.toml b/frame/staking/Cargo.toml index 79c0bb5c2a32d..0d1eef61d9b50 100644 --- a/frame/staking/Cargo.toml +++ b/frame/staking/Cargo.toml @@ -48,6 +48,7 @@ substrate-test-utils = { version = "4.0.0-dev", path = "../../test-utils" } frame-benchmarking = { version = "4.0.0-dev", path = "../benchmarking" } frame-election-provider-support = { version = "4.0.0-dev", path = "../election-provider-support" } rand_chacha = { version = "0.2" } +pallet-stake-tracker = { version = "4.0.0-dev", path = "../stake-tracker" } [features] default = ["std"] @@ -74,4 +75,7 @@ runtime-benchmarks = [ "rand_chacha", "sp-staking/runtime-benchmarks", ] -try-runtime = ["frame-support/try-runtime", "frame-election-provider-support/try-runtime"] +fuzz = [ + "frame-election-provider-support/fuzz" +] +try-runtime = ["frame-support/try-runtime", "frame-election-provider-support/try-runtime"] \ No newline at end of file diff --git a/frame/staking/src/migrations.rs b/frame/staking/src/migrations.rs index 23bcfa4398627..5c638b7504f35 100644 --- a/frame/staking/src/migrations.rs +++ b/frame/staking/src/migrations.rs @@ -17,40 +17,42 @@ //! Storage migrations for the Staking pallet. use super::*; -use frame_election_provider_support::SortedListProvider; use frame_support::{ dispatch::GetStorageVersion, pallet_prelude::ValueQuery, storage_alias, traits::OnRuntimeUpgrade, }; -/// Used for release versioning upto v12. -/// -/// Obsolete from v13. Keeping around to make encoding/decoding of old migration code easier. -#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] -enum ObsoleteReleases { - V1_0_0Ancient, - V2_0_0, - V3_0_0, - V4_0_0, - V5_0_0, // blockable validators. - V6_0_0, // removal of all storage associated with offchain phragmen. - V7_0_0, // keep track of number of nominators / validators in map - V8_0_0, // populate `VoterList`. - V9_0_0, // inject validators into `VoterList` as well. - V10_0_0, // remove `EarliestUnappliedSlash`. - V11_0_0, // Move pallet storage prefix, e.g. BagsList -> VoterBagsList - V12_0_0, // remove `HistoryDepth`. -} +mod obsolete { + use super::*; + /// Used for release versioning upto v12. + /// + /// Obsolete from v13. Keeping around to make encoding/decoding of old migration code easier. + #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub(super) enum Releases { + V1_0_0Ancient, + V2_0_0, + V3_0_0, + V4_0_0, + V5_0_0, // blockable validators. + V6_0_0, // removal of all storage associated with offchain phragmen. + V7_0_0, // keep track of number of nominators / validators in map + V8_0_0, // populate `VoterList`. + V9_0_0, // inject validators into `VoterList` as well. + V10_0_0, // remove `EarliestUnappliedSlash`. + V11_0_0, // Move pallet storage prefix, e.g. BagsList -> VoterBagsList + V12_0_0, // remove `HistoryDepth`. + } -impl Default for ObsoleteReleases { - fn default() -> Self { - ObsoleteReleases::V12_0_0 + impl Default for Releases { + fn default() -> Self { + Releases::V12_0_0 + } } -} -/// Alias to the old storage item used for release versioning. Obsolete since v13. -#[storage_alias] -type StorageVersion = StorageValue, ObsoleteReleases, ValueQuery>; + /// Alias to the old storage item used for release versioning. Obsolete since v13. + #[storage_alias] + pub(super) type StorageVersion = StorageValue, Releases, ValueQuery>; +} pub mod v13 { use super::*; @@ -60,7 +62,7 @@ pub mod v13 { #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, &'static str> { frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V12_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V12_0_0, "Required v12 before upgrading to v13" ); @@ -69,10 +71,10 @@ pub mod v13 { fn on_runtime_upgrade() -> Weight { let current = Pallet::::current_storage_version(); - let onchain = StorageVersion::::get(); + let onchain = obsolete::StorageVersion::::get(); - if current == 13 && onchain == ObsoleteReleases::V12_0_0 { - StorageVersion::::kill(); + if current == 13 && onchain == obsolete::Releases::V12_0_0 { + obsolete::StorageVersion::::kill(); current.put::>(); log!(info, "v13 applied successfully"); @@ -91,7 +93,7 @@ pub mod v13 { ); frame_support::ensure!( - !StorageVersion::::exists(), + !obsolete::StorageVersion::::exists(), "Storage version not migrated correctly" ); @@ -116,7 +118,7 @@ pub mod v12 { #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, &'static str> { frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V11_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V11_0_0, "Expected v11 before upgrading to v12" ); @@ -133,9 +135,9 @@ pub mod v12 { } fn on_runtime_upgrade() -> frame_support::weights::Weight { - if StorageVersion::::get() == ObsoleteReleases::V11_0_0 { + if obsolete::StorageVersion::::get() == obsolete::Releases::V11_0_0 { HistoryDepth::::kill(); - StorageVersion::::put(ObsoleteReleases::V12_0_0); + obsolete::StorageVersion::::put(obsolete::Releases::V12_0_0); log!(info, "v12 applied successfully"); T::DbWeight::get().reads_writes(1, 2) @@ -148,7 +150,7 @@ pub mod v12 { #[cfg(feature = "try-runtime")] fn post_upgrade(_state: Vec) -> Result<(), &'static str> { frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V12_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V12_0_0, "v12 not applied" ); Ok(()) @@ -172,7 +174,7 @@ pub mod v11 { #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, &'static str> { frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V10_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V10_0_0, "must upgrade linearly" ); let old_pallet_prefix = twox_128(N::get().as_bytes()); @@ -197,9 +199,9 @@ pub mod v11 { let old_pallet_name = N::get(); let new_pallet_name =

::name(); - if StorageVersion::::get() == ObsoleteReleases::V10_0_0 { + if obsolete::StorageVersion::::get() == obsolete::Releases::V10_0_0 { // bump version anyway, even if we don't need to move the prefix - StorageVersion::::put(ObsoleteReleases::V11_0_0); + obsolete::StorageVersion::::put(obsolete::Releases::V11_0_0); if new_pallet_name == old_pallet_name { log!( warn, @@ -219,7 +221,7 @@ pub mod v11 { #[cfg(feature = "try-runtime")] fn post_upgrade(_state: Vec) -> Result<(), &'static str> { frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V11_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V11_0_0, "wrong version after the upgrade" ); @@ -264,7 +266,7 @@ pub mod v10 { pub struct MigrateToV10(sp_std::marker::PhantomData); impl OnRuntimeUpgrade for MigrateToV10 { fn on_runtime_upgrade() -> frame_support::weights::Weight { - if StorageVersion::::get() == ObsoleteReleases::V9_0_0 { + if obsolete::StorageVersion::::get() == obsolete::Releases::V9_0_0 { let pending_slashes = UnappliedSlashes::::iter().take(512); for (era, slashes) in pending_slashes { for slash in slashes { @@ -276,7 +278,7 @@ pub mod v10 { } EarliestUnappliedSlash::::kill(); - StorageVersion::::put(ObsoleteReleases::V10_0_0); + obsolete::StorageVersion::::put(obsolete::Releases::V10_0_0); log!(info, "MigrateToV10 executed successfully"); T::DbWeight::get().reads_writes(1, 1) @@ -290,51 +292,62 @@ pub mod v10 { pub mod v9 { use super::*; + use frame_election_provider_support::SortedListProvider; #[cfg(feature = "try-runtime")] use frame_support::codec::{Decode, Encode}; #[cfg(feature = "try-runtime")] use sp_std::vec::Vec; + pub trait MigrationConfig { + type Config: Config; + type VoterList: frame_election_provider_support::SortedListProvider< + ::AccountId, + Score = u64, + >; + } + /// Migration implementation that injects all validators into sorted list. /// /// This is only useful for chains that started their `VoterList` just based on nominators. pub struct InjectValidatorsIntoVoterList(sp_std::marker::PhantomData); - impl OnRuntimeUpgrade for InjectValidatorsIntoVoterList { + impl OnRuntimeUpgrade for InjectValidatorsIntoVoterList { fn on_runtime_upgrade() -> Weight { - if StorageVersion::::get() == ObsoleteReleases::V8_0_0 { + if obsolete::StorageVersion::::get() == obsolete::Releases::V8_0_0 { let prev_count = T::VoterList::count(); - let weight_of_cached = Pallet::::weight_of_fn(); - for (v, _) in Validators::::iter() { + let weight_of_cached = Pallet::::weight_of_fn(); + for (v, _) in Validators::::iter() { let weight = weight_of_cached(&v); let _ = T::VoterList::on_insert(v.clone(), weight).map_err(|err| { - log!(warn, "failed to insert {:?} into VoterList: {:?}", v, err) + frame_support::log::warn!( + "failed to insert {:?} into VoterList: {:?}", + v, + err + ) }); } - log!( - info, + frame_support::log::info!( "injected a total of {} new voters, prev count: {} next count: {}, updating to version 9", - Validators::::count(), + Validators::::count(), prev_count, T::VoterList::count(), ); - StorageVersion::::put(ObsoleteReleases::V9_0_0); - T::BlockWeights::get().max_block + obsolete::StorageVersion::::put(obsolete::Releases::V9_0_0); + ::BlockWeights::get().max_block } else { - log!( - warn, + frame_support::log::warn!( "InjectValidatorsIntoVoterList being executed on the wrong storage \ - version, expected ObsoleteReleases::V8_0_0" + version, expected obsolete::Releases::V8_0_0" ); - T::DbWeight::get().reads(1) + ::DbWeight::get().reads(1) } } #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, &'static str> { frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V8_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V8_0_0, "must upgrade linearly" ); @@ -348,11 +361,11 @@ pub mod v9 { "the state parameter should be something that was generated by pre_upgrade", ); let post_count = T::VoterList::count(); - let validators = Validators::::count(); - assert!(post_count == prev_count + validators); + let validators = Validators::::count(); + assert_eq!(post_count, prev_count + validators); frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V9_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V9_0_0, "must upgrade " ); Ok(()) @@ -366,10 +379,18 @@ pub mod v8 { use frame_election_provider_support::SortedListProvider; use frame_support::traits::Get; + pub trait MigrationConfig { + type Config: Config; + type VoterList: frame_election_provider_support::SortedListProvider< + ::AccountId, + Score = u64, + >; + } + #[cfg(feature = "try-runtime")] pub fn pre_migrate() -> Result<(), &'static str> { frame_support::ensure!( - StorageVersion::::get() == ObsoleteReleases::V7_0_0, + obsolete::StorageVersion::::get() == obsolete::Releases::V7_0_0, "must upgrade linearly" ); @@ -378,25 +399,24 @@ pub mod v8 { } /// Migration to sorted `VoterList`. - pub fn migrate() -> Weight { - if StorageVersion::::get() == ObsoleteReleases::V7_0_0 { - crate::log!(info, "migrating staking to ObsoleteReleases::V8_0_0"); + pub fn migrate() -> Weight { + if obsolete::StorageVersion::::get() == obsolete::Releases::V7_0_0 { + frame_support::log::info!("migrating staking to obsolete::Releases::V8_0_0"); let migrated = T::VoterList::unsafe_regenerate( - Nominators::::iter().map(|(id, _)| id), - Pallet::::weight_of_fn(), + Nominators::::iter().map(|(id, _)| id), + Pallet::::weight_of_fn(), ); - StorageVersion::::put(ObsoleteReleases::V8_0_0); - crate::log!( - info, - "👜 completed staking migration to ObsoleteReleases::V8_0_0 with {} voters migrated", + obsolete::StorageVersion::::put(obsolete::Releases::V8_0_0); + frame_support::log::info!( + "👜 completed staking migration to obsolete::Releases::V8_0_0 with {} voters migrated", migrated, ); - T::BlockWeights::get().max_block + ::BlockWeights::get().max_block } else { - T::DbWeight::get().reads(1) + ::DbWeight::get().reads(1) } } @@ -428,20 +448,20 @@ pub mod v7 { ); assert!(Validators::::count().is_zero(), "Validators already set."); assert!(Nominators::::count().is_zero(), "Nominators already set."); - assert!(StorageVersion::::get() == ObsoleteReleases::V6_0_0); + assert!(obsolete::StorageVersion::::get() == obsolete::Releases::V6_0_0); Ok(()) } pub fn migrate() -> Weight { - log!(info, "Migrating staking to ObsoleteReleases::V7_0_0"); + log!(info, "Migrating staking to obsolete::Releases::V7_0_0"); let validator_count = Validators::::iter().count() as u32; let nominator_count = Nominators::::iter().count() as u32; CounterForValidators::::put(validator_count); CounterForNominators::::put(nominator_count); - StorageVersion::::put(ObsoleteReleases::V7_0_0); - log!(info, "Completed staking migration to ObsoleteReleases::V7_0_0"); + obsolete::StorageVersion::::put(obsolete::Releases::V7_0_0); + log!(info, "Completed staking migration to obsolete::Releases::V7_0_0"); T::DbWeight::get().reads_writes(validator_count.saturating_add(nominator_count).into(), 2) } @@ -483,7 +503,7 @@ pub mod v6 { /// Migrate storage to v6. pub fn migrate() -> Weight { - log!(info, "Migrating staking to ObsoleteReleases::V6_0_0"); + log!(info, "Migrating staking to obsolete::Releases::V6_0_0"); SnapshotValidators::::kill(); SnapshotNominators::::kill(); @@ -492,7 +512,7 @@ pub mod v6 { EraElectionStatus::::kill(); IsCurrentSessionFinal::::kill(); - StorageVersion::::put(ObsoleteReleases::V6_0_0); + obsolete::StorageVersion::::put(obsolete::Releases::V6_0_0); log!(info, "Done."); T::DbWeight::get().writes(6 + 1) diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index c2f559a9780eb..23f00749ae905 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -17,7 +17,14 @@ //! Test utilities -use crate::{self as pallet_staking, *}; +use crate::{ + self as pallet_staking, + mock::StakingEvent::{ + NominatorAdd, NominatorRemove, NominatorUpdate, StakeUpdate, Unstake, ValidatorAdd, + ValidatorRemove, ValidatorUpdate, + }, + *, +}; use frame_election_provider_support::{onchain, SequentialPhragmen, VoteWeight}; use frame_support::{ assert_ok, ord_parameter_types, parameter_types, @@ -35,7 +42,10 @@ use sp_runtime::{ testing::{Header, UintAuthorityId}, traits::{IdentityLookup, Zero}, }; -use sp_staking::offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}; +use sp_staking::{ + offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, + OnStakingUpdate, Stake, +}; pub const INIT_TIMESTAMP: u64 = 30_000; pub const BLOCK_TIME: u64 = 1000; @@ -99,6 +109,7 @@ frame_support::construct_runtime!( Session: pallet_session, Historical: pallet_session::historical, VoterBagsList: pallet_bags_list::, + StakeTracker: pallet_stake_tracker, } ); @@ -279,6 +290,57 @@ impl sp_staking::OnStakerSlash for OnStakerSlashM } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum StakingEvent { + StakeUpdate(AccountId, Option>), + NominatorAdd(AccountId), + NominatorUpdate(AccountId, Vec), + ValidatorAdd(AccountId), + ValidatorUpdate(AccountId), + ValidatorRemove(AccountId), + NominatorRemove(AccountId, Vec), + Unstake(AccountId), +} + +parameter_types! { + pub static EmittedEvents: Vec = Vec::new(); +} + +pub struct EventListenerMock; +impl OnStakingUpdate for EventListenerMock { + fn on_stake_update(who: &AccountId, prev_stake: Option>) { + EmittedEvents::mutate(|x| x.push(StakeUpdate(*who, prev_stake))) + } + + fn on_nominator_add(who: &AccountId) { + EmittedEvents::mutate(|x| x.push(NominatorAdd(*who))) + } + + fn on_nominator_update(who: &AccountId, prev_nominations: Vec) { + EmittedEvents::mutate(|x| x.push(NominatorUpdate(*who, prev_nominations))); + } + + fn on_validator_add(who: &AccountId) { + EmittedEvents::mutate(|x| x.push(ValidatorAdd(*who))); + } + + fn on_validator_update(who: &AccountId) { + EmittedEvents::mutate(|x| x.push(ValidatorUpdate(*who))); + } + + fn on_validator_remove(who: &AccountId) { + EmittedEvents::mutate(|x| x.push(ValidatorRemove(*who))); + } + + fn on_nominator_remove(who: &AccountId, nominations: Vec) { + EmittedEvents::mutate(|x| x.push(NominatorRemove(*who, nominations))); + } + + fn on_unstake(who: &AccountId) { + EmittedEvents::mutate(|x| x.push(Unstake(*who))); + } +} + impl crate::pallet::pallet::Config for Test { type MaxNominations = MaxNominations; type Currency = Balances; @@ -300,14 +362,20 @@ impl crate::pallet::pallet::Config for Test { type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; - // NOTE: consider a macro and use `UseNominatorsAndValidatorsMap` as well. - type VoterList = VoterBagsList; + type VoterList = pallet_stake_tracker::TrackedList; type TargetList = UseValidatorsMap; type MaxUnlockingChunks = MaxUnlockingChunks; type HistoryDepth = HistoryDepth; type OnStakerSlash = OnStakerSlashMock; type BenchmarkingConfig = TestBenchmarkingConfig; type WeightInfo = (); + type EventListeners = (StakeTracker, EventListenerMock); +} + +impl pallet_stake_tracker::Config for Test { + type Currency = Balances; + type Staking = Staking; + type VoterList = VoterBagsList; } pub(crate) type StakingCall = crate::Call; @@ -326,6 +394,7 @@ pub struct ExtBuilder { status: BTreeMap>, stakes: BTreeMap, stakers: Vec<(AccountId, AccountId, Balance, StakerStatus)>, + check_events: bool, } impl Default for ExtBuilder { @@ -343,6 +412,7 @@ impl Default for ExtBuilder { status: Default::default(), stakes: Default::default(), stakers: Default::default(), + check_events: false, } } } @@ -422,6 +492,10 @@ impl ExtBuilder { self.balance_factor = factor; self } + pub fn check_events(mut self, check: bool) -> Self { + self.check_events = check; + self + } fn build(self) -> sp_io::TestExternalities { sp_tracing::try_init_simple(); let mut storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); @@ -547,8 +621,19 @@ impl ExtBuilder { } pub fn build_and_execute(self, test: impl FnOnce() -> ()) { sp_tracing::try_init_simple(); + let check_events = self.check_events; let mut ext = self.build(); + ext.execute_with(|| { + // Clean up all the events produced on init. + EmittedEvents::take(); + }); ext.execute_with(test); + if check_events { + ext.execute_with(|| { + // Make sure we have checked all the events produced by the test. + assert!(EmittedEvents::get().is_empty(), "Encountered unchecked Staking events"); + }); + } ext.execute_with(|| { Staking::do_try_state(System::block_number()).unwrap(); }); diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs index 760345e8ddb28..d772bb04c733d 100644 --- a/frame/staking/src/pallet/impls.rs +++ b/frame/staking/src/pallet/impls.rs @@ -38,7 +38,7 @@ use sp_runtime::{ }; use sp_staking::{ offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, - EraIndex, SessionIndex, Stake, StakingInterface, + EraIndex, OnStakingUpdate, SessionIndex, Stake, StakingInterface, }; use sp_std::prelude::*; @@ -269,8 +269,14 @@ impl Pallet { /// /// This will also update the stash lock. pub(crate) fn update_ledger(controller: &T::AccountId, ledger: &StakingLedger) { + let prev_ledger = Self::ledger(controller).map(|l| Stake { + stash: l.stash, + total: l.total, + active: l.active, + }); T::Currency::set_lock(STAKING_ID, &ledger.stash, ledger.total, WithdrawReasons::all()); >::insert(controller, ledger); + T::EventListeners::on_stake_update(&ledger.stash, prev_ledger); } /// Chill a stash account. @@ -662,6 +668,7 @@ impl Pallet { Self::do_remove_nominator(stash); frame_system::Pallet::::dec_consumers(stash); + T::EventListeners::on_unstake(stash); Ok(()) } @@ -878,17 +885,17 @@ impl Pallet { /// to `Nominators` or `VoterList` outside of this function is almost certainly /// wrong. pub fn do_add_nominator(who: &T::AccountId, nominations: Nominations) { - if !Nominators::::contains_key(who) { - // maybe update sorted list. - let _ = T::VoterList::on_insert(who.clone(), Self::weight_of(who)) - .defensive_unwrap_or_default(); - } + let nominator_exists = Nominators::::contains_key(who); + // Get previous nominations before the nominator is updated. + let prev_nominations = Self::nominations(who); + Nominators::::insert(who, nominations); - debug_assert_eq!( - Nominators::::count() + Validators::::count(), - T::VoterList::count() - ); + if nominator_exists { + T::EventListeners::on_nominator_update(who, prev_nominations.unwrap_or_default()); + } else { + T::EventListeners::on_nominator_add(who); + } } /// This function will remove a nominator from the `Nominators` storage map, @@ -897,23 +904,14 @@ impl Pallet { /// Returns true if `who` was removed from `Nominators`, otherwise false. /// /// NOTE: you must ALWAYS use this function to remove a nominator from the system. Any access to - /// `Nominators` or `VoterList` outside of this function is almost certainly - /// wrong. + /// `Nominators` outside of this function is almost certainly wrong. pub fn do_remove_nominator(who: &T::AccountId) -> bool { - let outcome = if Nominators::::contains_key(who) { + if let Some(nominations) = Self::nominations(who) { Nominators::::remove(who); - let _ = T::VoterList::on_remove(who).defensive(); - true - } else { - false - }; - - debug_assert_eq!( - Nominators::::count() + Validators::::count(), - T::VoterList::count() - ); - - outcome + T::EventListeners::on_nominator_remove(who, nominations); + return true + } + false } /// This function will add a validator to the `Validators` storage map. @@ -924,17 +922,15 @@ impl Pallet { /// `Validators` or `VoterList` outside of this function is almost certainly /// wrong. pub fn do_add_validator(who: &T::AccountId, prefs: ValidatorPrefs) { - if !Validators::::contains_key(who) { - // maybe update sorted list. - let _ = T::VoterList::on_insert(who.clone(), Self::weight_of(who)) - .defensive_unwrap_or_default(); - } + let validator_exists = Validators::::contains_key(who); + Validators::::insert(who, prefs); - debug_assert_eq!( - Nominators::::count() + Validators::::count(), - T::VoterList::count() - ); + if validator_exists { + T::EventListeners::on_validator_update(who); + } else { + T::EventListeners::on_validator_add(who); + } } /// This function will remove a validator from the `Validators` storage map. @@ -942,23 +938,14 @@ impl Pallet { /// Returns true if `who` was removed from `Validators`, otherwise false. /// /// NOTE: you must ALWAYS use this function to remove a validator from the system. Any access to - /// `Validators` or `VoterList` outside of this function is almost certainly - /// wrong. + /// `Validators` outside of this function is almost certainly wrong. pub fn do_remove_validator(who: &T::AccountId) -> bool { - let outcome = if Validators::::contains_key(who) { + if Validators::::contains_key(who) { Validators::::remove(who); - let _ = T::VoterList::on_remove(who).defensive(); - true - } else { - false - }; - - debug_assert_eq!( - Nominators::::count() + Validators::::count(), - T::VoterList::count() - ); - - outcome + T::EventListeners::on_validator_remove(who); + return true + } + false } /// Register some amount of weight directly with the system pallet. @@ -1102,7 +1089,6 @@ impl ElectionDataProvider for Pallet { >::remove_all(); #[allow(deprecated)] >::remove_all(); - T::VoterList::unsafe_clear(); } @@ -1419,6 +1405,7 @@ impl ScoreProvider for Pallet { /// does not provide validators in sorted order. If you desire nominators in a sorted order take /// a look at [`pallet-bags-list`]. pub struct UseValidatorsMap(sp_std::marker::PhantomData); + impl SortedListProvider for UseValidatorsMap { type Score = BalanceOf; type Error = (); @@ -1427,6 +1414,7 @@ impl SortedListProvider for UseValidatorsMap { fn iter() -> Box> { Box::new(Validators::::iter().map(|(v, _)| v)) } + fn iter_from( start: &T::AccountId, ) -> Result>, Self::Error> { @@ -1437,27 +1425,34 @@ impl SortedListProvider for UseValidatorsMap { Err(()) } } + fn count() -> u32 { Validators::::count() } + fn contains(id: &T::AccountId) -> bool { Validators::::contains_key(id) } + fn on_insert(_: T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> { // nothing to do on insert. Ok(()) } + fn get_score(id: &T::AccountId) -> Result { Ok(Pallet::::weight_of(id).into()) } + fn on_update(_: &T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> { // nothing to do on update. Ok(()) } + fn on_remove(_: &T::AccountId) -> Result<(), Self::Error> { // nothing to do on remove. Ok(()) } + fn unsafe_regenerate( _: impl IntoIterator, _: Box Self::Score>, @@ -1465,19 +1460,21 @@ impl SortedListProvider for UseValidatorsMap { // nothing to do upon regenerate. 0 } + #[cfg(feature = "try-runtime")] fn try_state() -> Result<(), &'static str> { Ok(()) } - fn unsafe_clear() { - #[allow(deprecated)] - Validators::::remove_all(); - } + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + fn unsafe_clear() { + #[allow(deprecated)] + Validators::::remove_all(); + } - #[cfg(feature = "runtime-benchmarks")] - fn score_update_worst_case(_who: &T::AccountId, _is_increase: bool) -> Self::Score { - unimplemented!() + fn score_update_worst_case(_who: &T::AccountId, _is_increase: bool) -> Self::Score { + unimplemented!() + } } } @@ -1485,6 +1482,7 @@ impl SortedListProvider for UseValidatorsMap { /// does not provided nominators in sorted ordered. If you desire nominators in a sorted order take /// a look at [`pallet-bags-list]. pub struct UseNominatorsAndValidatorsMap(sp_std::marker::PhantomData); + impl SortedListProvider for UseNominatorsAndValidatorsMap { type Error = (); type Score = VoteWeight; @@ -1496,6 +1494,7 @@ impl SortedListProvider for UseNominatorsAndValidatorsM .chain(Nominators::::iter().map(|(n, _)| n)), ) } + fn iter_from( start: &T::AccountId, ) -> Result>, Self::Error> { @@ -1513,27 +1512,34 @@ impl SortedListProvider for UseNominatorsAndValidatorsM Err(()) } } + fn count() -> u32 { Nominators::::count().saturating_add(Validators::::count()) } + fn contains(id: &T::AccountId) -> bool { Nominators::::contains_key(id) || Validators::::contains_key(id) } + fn on_insert(_: T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> { // nothing to do on insert. Ok(()) } + fn get_score(id: &T::AccountId) -> Result { Ok(Pallet::::weight_of(id)) } + fn on_update(_: &T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> { // nothing to do on update. Ok(()) } + fn on_remove(_: &T::AccountId) -> Result<(), Self::Error> { // nothing to do on remove. Ok(()) } + fn unsafe_regenerate( _: impl IntoIterator, _: Box Self::Score>, @@ -1547,18 +1553,19 @@ impl SortedListProvider for UseNominatorsAndValidatorsM Ok(()) } - fn unsafe_clear() { - // NOTE: Caller must ensure this doesn't lead to too many storage accesses. This is a - // condition of SortedListProvider::unsafe_clear. - #[allow(deprecated)] - Nominators::::remove_all(); - #[allow(deprecated)] - Validators::::remove_all(); - } + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + fn unsafe_clear() { + // NOTE: Caller must ensure this doesn't lead to too many storage accesses. This is a + // condition of SortedListProvider::unsafe_clear. + #[allow(deprecated)] + Nominators::::remove_all(); + #[allow(deprecated)] + Validators::::remove_all(); + } - #[cfg(feature = "runtime-benchmarks")] - fn score_update_worst_case(_who: &T::AccountId, _is_increase: bool) -> Self::Score { - unimplemented!() + fn score_update_worst_case(_who: &T::AccountId, _is_increase: bool) -> Self::Score { + unimplemented!() + } } } @@ -1566,15 +1573,12 @@ impl SortedListProvider for UseNominatorsAndValidatorsM impl StakingInterface for Pallet { type AccountId = T::AccountId; type Balance = BalanceOf; + type CurrencyToVote = T::CurrencyToVote; fn minimum_nominator_bond() -> Self::Balance { MinNominatorBond::::get() } - fn minimum_validator_bond() -> Self::Balance { - MinValidatorBond::::get() - } - fn desired_validator_count() -> u32 { ValidatorCount::::get() } @@ -1588,6 +1592,10 @@ impl StakingInterface for Pallet { Self::force_unstake(RawOrigin::Root.into(), who.clone(), num_slashing_spans) } + fn minimum_validator_bond() -> Self::Balance { + MinValidatorBond::::get() + } + fn stash_by_ctrl(controller: &Self::AccountId) -> Result { Self::ledger(controller) .map(|l| l.stash) @@ -1608,7 +1616,9 @@ impl StakingInterface for Pallet { Self::current_era().unwrap_or(Zero::zero()) } - fn stake(who: &Self::AccountId) -> Result, DispatchError> { + fn stake( + who: &Self::AccountId, + ) -> Result, DispatchError> { Self::bonded(who) .and_then(|c| Self::ledger(c)) .map(|l| Stake { stash: l.stash, total: l.total, active: l.active }) @@ -1662,11 +1672,15 @@ impl StakingInterface for Pallet { Self::nominate(RawOrigin::Signed(ctrl).into(), targets) } - sp_staking::runtime_benchmarks_enabled! { - fn nominations(who: Self::AccountId) -> Option> { - Nominators::::get(who).map(|n| n.targets.into_inner()) - } + fn is_validator(who: &Self::AccountId) -> bool { + Validators::::contains_key(who) + } + + fn nominations(who: &Self::AccountId) -> Option> { + Nominators::::get(who).map(|n| n.targets.into_inner()) + } + sp_staking::runtime_benchmarks_enabled! { fn add_era_stakers( current_era: &EraIndex, stash: &T::AccountId, diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs index d8f1855da4bc0..cf878274a448f 100644 --- a/frame/staking/src/pallet/mod.rs +++ b/frame/staking/src/pallet/mod.rs @@ -17,14 +17,12 @@ //! Staking FRAME Pallet. -use frame_election_provider_support::{ - ElectionProvider, ElectionProviderBase, SortedListProvider, VoteWeight, -}; +use frame_election_provider_support::{ElectionProvider, ElectionProviderBase, VoteWeight}; use frame_support::{ dispatch::Codec, pallet_prelude::*, traits::{ - Currency, CurrencyToVote, Defensive, DefensiveResult, DefensiveSaturating, EnsureOrigin, + Currency, CurrencyToVote, DefensiveResult, DefensiveSaturating, EnsureOrigin, EstimateNextNewSession, Get, LockIdentifier, LockableCurrency, OnUnbalanced, TryCollect, UnixTime, }, @@ -58,7 +56,7 @@ pub(crate) const SPECULATIVE_NUM_SPANS: u32 = 32; #[frame_support::pallet] pub mod pallet { - use frame_election_provider_support::ElectionDataProvider; + use frame_election_provider_support::{ElectionDataProvider, SortedListProvider}; use crate::BenchmarkingConfig; @@ -248,6 +246,10 @@ pub mod pallet { /// VALIDATOR. type TargetList: SortedListProvider>; + /// Something that listens to staking updates and performs actions based on the data it + /// receives. + type EventListeners: sp_staking::OnStakingUpdate>; + /// The maximum number of `unlocking` chunks a [`StakingLedger`] can /// have. Effectively determines how many unique eras a staker may be /// unbonding in. @@ -773,6 +775,9 @@ pub mod pallet { CommissionTooLow, /// Some bound is not met. BoundNotMet, + /// Nominations are not decodable. A nominator is then stuck until it's fixed, because we + /// can't forgo the bookkeeping. + NotDecodableNominator, } #[pallet::hooks] @@ -939,11 +944,6 @@ pub mod pallet { // NOTE: ledger must be updated prior to calling `Self::weight_of`. Self::update_ledger(&controller, &ledger); - // update this staker in the sorted list, if they exist in it. - if T::VoterList::contains(&stash) { - let _ = - T::VoterList::on_update(&stash, Self::weight_of(&ledger.stash)).defensive(); - } Self::deposit_event(Event::::Bonded { stash, amount: extra }); } @@ -1043,12 +1043,6 @@ pub mod pallet { // NOTE: ledger must be updated prior to calling `Self::weight_of`. Self::update_ledger(&controller, &ledger); - // update this staker in the sorted list, if they exist in it. - if T::VoterList::contains(&ledger.stash) { - let _ = T::VoterList::on_update(&ledger.stash, Self::weight_of(&ledger.stash)) - .defensive(); - } - Self::deposit_event(Event::::Unbonded { stash: ledger.stash, amount: value }); } @@ -1147,8 +1141,18 @@ pub mod pallet { ensure!(ledger.active >= MinNominatorBond::::get(), Error::::InsufficientBond); let stash = &ledger.stash; + let is_nominator = Nominators::::contains_key(stash); + + // If the nominator is not decodable - throw an error. One of the reasons for that could + // be a decrease of `MaxNominations`, which should be accompanied by a migration that + // fixes those nominations. Otherwise the Staking pallet ends up in an inconsistent + // state, because we cannot do proper bookkeeping. + if is_nominator && Nominators::::get(stash).is_none() { + Err(Error::::NotDecodableNominator)? + } + // Only check limits if they are not already a nominator. - if !Nominators::::contains_key(stash) { + if !is_nominator { // If this error is reached, we need to adjust the `MinNominatorBond` and start // calling `chill_other`. Until then, we explicitly block new nominators to protect // the runtime. @@ -1517,10 +1521,6 @@ pub mod pallet { // NOTE: ledger must be updated prior to calling `Self::weight_of`. Self::update_ledger(&controller, &ledger); - if T::VoterList::contains(&ledger.stash) { - let _ = T::VoterList::on_update(&ledger.stash, Self::weight_of(&ledger.stash)) - .defensive(); - } let removed_chunks = 1u32 // for the case where the last iterated chunk is not removed .saturating_add(initial_unlocking) @@ -1691,10 +1691,6 @@ pub mod pallet { // In order for one user to chill another user, the following conditions must be met: // - // * `controller` belongs to a nominator who has become non-decodable, - // - // Or - // // * A `ChillThreshold` is set which defines how close to the max nominators or // validators we must reach before users can start chilling one-another. // * A `MaxNominatorCount` and `MaxValidatorCount` which is used to determine how close @@ -1705,9 +1701,10 @@ pub mod pallet { // // Otherwise, if caller is the same as the controller, this is just like `chill`. + // If the nominator is not decodable - a migration needs to be executed to fix the + // storage. We can't chill nominators without knowing their nominations. if Nominators::::contains_key(&stash) && Nominators::::get(&stash).is_none() { - Self::chill_stash(&stash); - return Ok(()) + return Err(Error::::NotDecodableNominator)? } if caller != controller { diff --git a/frame/staking/src/testing_utils.rs b/frame/staking/src/testing_utils.rs index 9bd231cce6c73..88525341229e8 100644 --- a/frame/staking/src/testing_utils.rs +++ b/frame/staking/src/testing_utils.rs @@ -27,7 +27,6 @@ use rand_chacha::{ }; use sp_io::hashing::blake2_256; -use frame_election_provider_support::SortedListProvider; use frame_support::{pallet_prelude::*, traits::Currency}; use sp_runtime::{traits::StaticLookup, Perbill}; use sp_std::prelude::*; @@ -43,8 +42,11 @@ pub fn clear_validators_and_nominators() { #[allow(deprecated)] Nominators::::remove_all(); - // NOTE: safe to call outside block production - T::VoterList::unsafe_clear(); + frame_election_provider_support::runtime_benchmarks_or_test_enabled! { + use frame_election_provider_support::SortedListProvider; + // NOTE: safe to call outside block production + T::VoterList::unsafe_clear(); + } } /// Grab a funded user. diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index d97eb3ef89cab..351816f2e5ff7 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -34,7 +34,7 @@ use sp_runtime::{ }; use sp_staking::{ offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, - SessionIndex, + SessionIndex, Stake, }; use sp_std::prelude::*; use substrate_test_utils::assert_eq_uvec; @@ -5137,27 +5137,25 @@ fn change_of_max_nominations() { assert!(Nominators::::get(60).is_some()); assert_eq!(Staking::electing_voters(None).unwrap().len(), 3 + 1); - // now one of them can revive themselves by re-nominating to a proper value. - assert_ok!(Staking::nominate(RuntimeOrigin::signed(71), vec![1])); - assert_eq!( - Nominators::::iter() - .map(|(k, n)| (k, n.targets.len())) - .collect::>(), - vec![(70, 1), (60, 1)] + // Impossible to re-nominate when not decodable. + assert_noop!( + Staking::nominate(RuntimeOrigin::signed(71), vec![1]), + Error::::NotDecodableNominator ); - // or they can be chilled by any account. assert!(Nominators::::contains_key(101)); assert!(Nominators::::get(101).is_none()); - assert_ok!(Staking::chill_other(RuntimeOrigin::signed(70), 100)); - assert!(!Nominators::::contains_key(101)); - assert!(Nominators::::get(101).is_none()); + + // Impossible to chill_other when not deodable. + assert_noop!( + Staking::chill_other(RuntimeOrigin::signed(70), 100), + Error::::NotDecodableNominator + ); }) } mod sorted_list_provider { use super::*; - use frame_election_provider_support::SortedListProvider; #[test] fn re_nominate_does_not_change_counters_or_list() { @@ -5825,3 +5823,174 @@ mod staking_interface { }); } } + +mod on_staking_update { + use super::*; + use crate::mock::StakingEvent::*; + + #[test] + fn on_validator_add() { + ExtBuilder::default().check_events(true).build_and_execute(|| { + assert_ok!(Staking::bond( + RuntimeOrigin::signed(3), + 4, + 1500, + RewardDestination::Controller + )); + assert_ok!(Staking::validate(RuntimeOrigin::signed(4), ValidatorPrefs::default())); + assert_eq!(EmittedEvents::take(), vec![StakeUpdate(3, None), ValidatorAdd(3)]); + }); + } + + #[test] + fn on_validator_update() { + ExtBuilder::default().check_events(true).build_and_execute(|| { + assert!(Validators::::contains_key(11)); + assert_ok!(Staking::validate(RuntimeOrigin::signed(10), ValidatorPrefs::default())); + assert_eq!(EmittedEvents::take(), vec![ValidatorUpdate(11)]); + }); + } + + #[test] + fn on_stake_update() { + ExtBuilder::default().check_events(true).build_and_execute(|| { + assert_ok!(Staking::bond( + RuntimeOrigin::signed(3), + 4, + 100, + RewardDestination::Controller + )); + + assert_ok!(Staking::bond_extra(RuntimeOrigin::signed(3), 500)); + assert_eq!( + EmittedEvents::take(), + vec![ + StakeUpdate(3, None), + StakeUpdate(3, Some(Stake { stash: 3, total: 100, active: 100 })) + ] + ); + }); + } + + #[test] + fn on_nominator_update() { + ExtBuilder::default().check_events(true).nominate(true).build_and_execute(|| { + assert!(Nominators::::contains_key(101)); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(100), vec![11])); + assert_eq!(EmittedEvents::take(), vec![NominatorUpdate(101, vec![11, 21])]); + }); + } + + #[test] + fn on_nominator_add() { + ExtBuilder::default().check_events(true).build_and_execute(|| { + assert_ok!(Staking::bond( + RuntimeOrigin::signed(1), + 2, + 1000, + RewardDestination::Controller + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(2), vec![11, 21, 31])); + assert_eq!(EmittedEvents::take(), vec![StakeUpdate(1, None), NominatorAdd(1)]); + }); + } + + #[test] + fn on_nominator_remove() { + ExtBuilder::default().check_events(true).nominate(true).build_and_execute(|| { + assert_ok!(Staking::force_unstake(RuntimeOrigin::root(), 101, 0)); + assert_eq!( + EmittedEvents::take(), + vec![NominatorRemove(101, vec![11, 21]), Unstake(101)] + ); + + assert_ok!(Staking::bond( + RuntimeOrigin::signed(1), + 2, + 10, + RewardDestination::Controller + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(2), vec![11])); + assert_eq!(EmittedEvents::take(), vec![StakeUpdate(1, None), NominatorAdd(1)]); + + assert_ok!(Staking::chill(RuntimeOrigin::signed(2))); + assert_ok!(Staking::unbond(RuntimeOrigin::signed(2), 10)); + assert_eq!( + EmittedEvents::take(), + [ + NominatorRemove(1, vec![11]), + StakeUpdate(1, Some(Stake { stash: 1, total: 10, active: 10 })) + ] + ); + }); + } + + #[test] + fn on_validator_remove() { + ExtBuilder::default().check_events(true).nominate(true).build_and_execute(|| { + assert_ok!(Staking::force_unstake(RuntimeOrigin::root(), 11, 0)); + assert_eq!(EmittedEvents::take(), vec![ValidatorRemove(11), Unstake(11)]); + + assert_ok!(Staking::bond( + RuntimeOrigin::signed(1), + 2, + 10, + RewardDestination::Controller + )); + assert_ok!(Staking::validate(RuntimeOrigin::signed(2), ValidatorPrefs::default())); + assert_eq!(EmittedEvents::take(), vec![StakeUpdate(1, None), ValidatorAdd(1)]); + + assert_ok!(Staking::chill(RuntimeOrigin::signed(2))); + assert_ok!(Staking::unbond(RuntimeOrigin::signed(2), 10)); + assert_eq!( + EmittedEvents::take(), + [ + ValidatorRemove(1), + StakeUpdate(1, Some(Stake { stash: 1, total: 10, active: 10 })) + ] + ); + }); + } + + #[test] + fn validator_to_nominator() { + ExtBuilder::default().check_events(true).build_and_execute(|| { + assert_ok!(Staking::bond( + RuntimeOrigin::signed(1), + 2, + 10, + RewardDestination::Controller + )); + assert_ok!(Staking::validate(RuntimeOrigin::signed(2), ValidatorPrefs::default())); + assert_eq!(EmittedEvents::take(), vec![StakeUpdate(1, None), ValidatorAdd(1)]); + + assert_ok!(Staking::nominate(RuntimeOrigin::signed(2), vec![11])); + assert_eq!(EmittedEvents::take(), vec![ValidatorRemove(1), NominatorAdd(1)]); + }); + } + + #[test] + fn nominator_to_validator() { + ExtBuilder::default().check_events(true).build_and_execute(|| { + assert_ok!(Staking::bond( + RuntimeOrigin::signed(1), + 2, + 10, + RewardDestination::Controller + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(2), vec![11])); + assert_eq!(EmittedEvents::take(), vec![StakeUpdate(1, None), NominatorAdd(1)]); + + assert_ok!(Staking::validate(RuntimeOrigin::signed(2), ValidatorPrefs::default())); + assert_eq!(EmittedEvents::take(), vec![NominatorRemove(1, vec![11]), ValidatorAdd(1)]); + }); + } + + #[test] + fn on_unstake() { + ExtBuilder::default().check_events(true).nominate(true).build_and_execute(|| { + assert_ok!(Staking::force_unstake(RuntimeOrigin::root(), 11, 0)); + assert_eq!(EmittedEvents::take(), vec![ValidatorRemove(11), Unstake(11)]); + }); + } +} diff --git a/frame/support/src/traits/voting.rs b/frame/support/src/traits/voting.rs index caec472785782..9cf9a13fb62bd 100644 --- a/frame/support/src/traits/voting.rs +++ b/frame/support/src/traits/voting.rs @@ -20,81 +20,11 @@ use crate::dispatch::{DispatchError, Parameter}; use codec::{HasCompact, MaxEncodedLen}; -use sp_arithmetic::{ - traits::{SaturatedConversion, UniqueSaturatedFrom, UniqueSaturatedInto}, - Perbill, -}; +use sp_arithmetic::Perbill; use sp_runtime::traits::Member; +pub use sp_staking::currency_to_vote::*; use sp_std::prelude::*; -/// A trait similar to `Convert` to convert values from `B` an abstract balance type -/// into u64 and back from u128. (This conversion is used in election and other places where complex -/// calculation over balance type is needed) -/// -/// Total issuance of the currency is passed in, but an implementation of this trait may or may not -/// use it. -/// -/// # WARNING -/// -/// the total issuance being passed in implies that the implementation must be aware of the fact -/// that its values can affect the outcome. This implies that if the vote value is dependent on the -/// total issuance, it should never ber written to storage for later re-use. -pub trait CurrencyToVote { - /// Convert balance to u64. - fn to_vote(value: B, issuance: B) -> u64; - - /// Convert u128 to balance. - fn to_currency(value: u128, issuance: B) -> B; -} - -/// An implementation of `CurrencyToVote` tailored for chain's that have a balance type of u128. -/// -/// The factor is the `(total_issuance / u64::MAX).max(1)`, represented as u64. Let's look at the -/// important cases: -/// -/// If the chain's total issuance is less than u64::MAX, this will always be 1, which means that -/// the factor will not have any effect. In this case, any account's balance is also less. Thus, -/// both of the conversions are basically an `as`; Any balance can fit in u64. -/// -/// If the chain's total issuance is more than 2*u64::MAX, then a factor might be multiplied and -/// divided upon conversion. -pub struct U128CurrencyToVote; - -impl U128CurrencyToVote { - fn factor(issuance: u128) -> u128 { - (issuance / u64::MAX as u128).max(1) - } -} - -impl CurrencyToVote for U128CurrencyToVote { - fn to_vote(value: u128, issuance: u128) -> u64 { - (value / Self::factor(issuance)).saturated_into() - } - - fn to_currency(value: u128, issuance: u128) -> u128 { - value.saturating_mul(Self::factor(issuance)) - } -} - -/// A naive implementation of `CurrencyConvert` that simply saturates all conversions. -/// -/// # Warning -/// -/// This is designed to be used mostly for testing. Use with care, and think about the consequences. -pub struct SaturatingCurrencyToVote; - -impl + UniqueSaturatedFrom> CurrencyToVote - for SaturatingCurrencyToVote -{ - fn to_vote(value: B, _: B) -> u64 { - value.unique_saturated_into() - } - - fn to_currency(value: u128, _: B) -> B { - B::unique_saturated_from(value) - } -} - pub trait VoteTally { fn new(_: Class) -> Self; fn ayes(&self, class: Class) -> Votes; diff --git a/primitives/staking/Cargo.toml b/primitives/staking/Cargo.toml index a8e5a543dbf75..37ef32e7d6e74 100644 --- a/primitives/staking/Cargo.toml +++ b/primitives/staking/Cargo.toml @@ -18,6 +18,7 @@ scale-info = { version = "2.1.1", default-features = false, features = ["derive" sp-core = { version = "7.0.0", default-features = false, path = "../core" } sp-runtime = { version = "7.0.0", default-features = false, path = "../runtime" } sp-std = { version = "5.0.0", default-features = false, path = "../std" } +impl-trait-for-tuples = "0.2.2" [features] default = ["std"] diff --git a/primitives/staking/src/currency_to_vote.rs b/primitives/staking/src/currency_to_vote.rs new file mode 100644 index 0000000000000..e4eac76219c3f --- /dev/null +++ b/primitives/staking/src/currency_to_vote.rs @@ -0,0 +1,89 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-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. + +use sp_runtime::{ + traits::{UniqueSaturatedFrom, UniqueSaturatedInto}, + SaturatedConversion, +}; + +/// A trait similar to `Convert` to convert values from `B` an abstract balance type +/// into u64 and back from u128. (This conversion is used in election and other places where complex +/// calculation over balance type is needed) +/// +/// Total issuance of the currency is passed in, but an implementation of this trait may or may not +/// use it. +/// +/// # WARNING +/// +/// the total issuance being passed in implies that the implementation must be aware of the fact +/// that its values can affect the outcome. This implies that if the vote value is dependent on the +/// total issuance, it should never ber written to storage for later re-use. +pub trait CurrencyToVote { + /// Convert balance to u64. + fn to_vote(value: B, issuance: B) -> u64; + + /// Convert u128 to balance. + fn to_currency(value: u128, issuance: B) -> B; +} + +/// An implementation of `CurrencyToVote` tailored for chain's that have a balance type of u128. +/// +/// The factor is the `(total_issuance / u64::MAX).max(1)`, represented as u64. Let's look at the +/// important cases: +/// +/// If the chain's total issuance is less than u64::MAX, this will always be 1, which means that +/// the factor will not have any effect. In this case, any account's balance is also less. Thus, +/// both of the conversions are basically an `as`; Any balance can fit in u64. +/// +/// If the chain's total issuance is more than 2*u64::MAX, then a factor might be multiplied and +/// divided upon conversion. +pub struct U128CurrencyToVote; + +impl U128CurrencyToVote { + fn factor(issuance: u128) -> u128 { + (issuance / u64::MAX as u128).max(1) + } +} + +impl CurrencyToVote for U128CurrencyToVote { + fn to_vote(value: u128, issuance: u128) -> u64 { + (value / Self::factor(issuance)).saturated_into() + } + + fn to_currency(value: u128, issuance: u128) -> u128 { + value.saturating_mul(Self::factor(issuance)) + } +} + +/// A naive implementation of `CurrencyConvert` that simply saturates all conversions. +/// +/// # Warning +/// +/// This is designed to be used mostly for testing. Use with care, and think about the consequences. +pub struct SaturatingCurrencyToVote; + +impl + UniqueSaturatedFrom> CurrencyToVote + for SaturatingCurrencyToVote +{ + fn to_vote(value: B, _: B) -> u64 { + value.unique_saturated_into() + } + + fn to_currency(value: u128, _: B) -> B { + B::unique_saturated_from(value) + } +} diff --git a/primitives/staking/src/lib.rs b/primitives/staking/src/lib.rs index a8d8e6a602c94..6101abac0cd46 100644 --- a/primitives/staking/src/lib.rs +++ b/primitives/staking/src/lib.rs @@ -20,11 +20,16 @@ //! A crate which contains primitives that are useful for implementation that uses staking //! approaches in general. Definitions related to sessions, slashing, etc go here. -use sp_runtime::{DispatchError, DispatchResult}; -use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; +use crate::currency_to_vote::CurrencyToVote; +use codec::{FullCodec, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{DispatchError, DispatchResult, Saturating}; +use sp_std::{collections::btree_map::BTreeMap, ops::Sub, vec::Vec}; pub mod offence; +pub mod currency_to_vote; + /// Simple index type with which we can count sessions. pub type SessionIndex = u32; @@ -57,9 +62,10 @@ impl OnStakerSlash for () { /// A struct that reflects stake that an account has in the staking system. Provides a set of /// methods to operate on it's properties. Aimed at making `StakingInterface` more concise. -pub struct Stake { +#[derive(Default, Clone, Debug, Eq, PartialEq)] +pub struct Stake { /// The stash account whose balance is actually locked and at stake. - pub stash: T::AccountId, + pub stash: AccountId, /// The total stake that `stash` has in the staking system. This includes the /// `active` stake, and any funds currently in the process of unbonding via /// [`StakingInterface::unbond`]. @@ -69,10 +75,53 @@ pub struct Stake { /// This is only guaranteed to reflect the amount locked by the staking system. If there are /// non-staking locks on the bonded pair's balance this amount is going to be larger in /// reality. - pub total: T::Balance, + pub total: Balance, /// The total amount of the stash's balance that will be at stake in any forthcoming /// rounds. - pub active: T::Balance, + pub active: Balance, +} + +/// A generic staking event listener. +/// +/// Note that the interface is designed in a way that the events are fired post-action, so any +/// pre-action data that is needed needs to be passed to interface methods. The rest of the data can +/// be retrieved by using `StakingInterface`. +#[impl_trait_for_tuples::impl_for_tuples(10)] +pub trait OnStakingUpdate { + /// Fired when the stake amount of someone updates. + /// + /// This is effectively any changes to the bond amount, such as bonding more funds, and + /// unbonding. + fn on_stake_update(who: &AccountId, prev_stake: Option>); + + /// Fired when someone sets their intention to nominate. + fn on_nominator_add(who: &AccountId); + + /// Fired when an existing nominator updates their nominations. + /// + /// Note that this is not fired when a nominator changes their stake. For that, + /// `on_stake_update` should be used, followed by querying whether `who` was a validator or a + /// nominator. + fn on_nominator_update(who: &AccountId, prev_nominations: Vec); + + /// Fired when someone sets their intention to validate. + /// + /// Note validator preference changes are not communicated, but could be added if needed. + fn on_validator_add(who: &AccountId); + + /// Fired when an existing validator updates their preferences. + /// + /// Note validator preference changes are not communicated, but could be added if needed. + fn on_validator_update(who: &AccountId); + + /// Fired when someone removes their intention to validate, either due to chill or nominating. + fn on_validator_remove(who: &AccountId); // only fire this event when this is an actual Validator + + /// Fired when someone removes their intention to nominate, either due to chill or validating. + fn on_nominator_remove(who: &AccountId, nominations: Vec); // only fire this if this is an actual Nominator + + /// fired when someone is fully unstaked. + fn on_unstake(who: &AccountId); // -> basically `kill_stash` } /// A generic representation of a staking implementation. @@ -81,10 +130,21 @@ pub struct Stake { /// implementations as well. pub trait StakingInterface { /// Balance type used by the staking system. - type Balance: PartialEq; - - /// AccountId type used by the staking system - type AccountId; + type Balance: Sub + + Ord + + PartialEq + + Default + + Copy + + MaxEncodedLen + + FullCodec + + TypeInfo + + Saturating; + + /// AccountId type used by the staking system. + type AccountId: Clone; + + /// Means of converting Currency to VoteWeight. + type CurrencyToVote: CurrencyToVote; /// The minimum amount required to bond in order to set nomination intentions. This does not /// necessarily mean the nomination will be counted in an election, but instead just enough to @@ -112,7 +172,8 @@ pub trait StakingInterface { fn current_era() -> EraIndex; /// Returns the stake of `who`. - fn stake(who: &Self::AccountId) -> Result, DispatchError>; + fn stake(who: &Self::AccountId) + -> Result, DispatchError>; fn total_stake(who: &Self::AccountId) -> Result { Self::stake(who).map(|s| s.total) @@ -176,9 +237,11 @@ pub trait StakingInterface { /// Checks whether an account `staker` has been exposed in an era. fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool; + /// Checks whether or not this is a validator account. + fn is_validator(who: &Self::AccountId) -> bool; + /// Get the nominations of a stash, if they are a nominator, `None` otherwise. - #[cfg(feature = "runtime-benchmarks")] - fn nominations(who: Self::AccountId) -> Option>; + fn nominations(who: &Self::AccountId) -> Option>; #[cfg(feature = "runtime-benchmarks")] fn add_era_stakers(