From 409ef451c8de69837c150ea9b3802e78ae9b0bfa Mon Sep 17 00:00:00 2001 From: Marcin Date: Mon, 4 Mar 2024 14:56:38 +0100 Subject: [PATCH] A0-4020: Pallet operations. (#1637) # Description An account can have an underflow of a `consumers` counter. Account categories that are impacted by this issue depend on a chain runtime, but specifically for AlephNode runtime are as follows: * `consumers` == 0, `reserved` > 0 * `consumers` == 1, `balances.Locks` contain an entry with `id` == `vesting` * `consumers` == 2, `balances.Locks` contain an entry with `id` == `staking` * `consumers` == 3, `balances.Locks` contain entries with `id` == `staking` and account id is in `session.nextKeys` This PR adds `pallet-operations` with a single extrinsic `fix_accounts_consumers_underflow` that checks if the account falls into one of the above categories, and increases its `consumers` counter in such scenario. ## Type of change Please delete options that are not relevant. - Bug fix (non-breaking change which fixes an issue) ## Testing There are 4 cases to test, and all of them are tested in the unit tests. 3 out of 4 cases were also tested on local chain with cloned state, except 4th scenario (validator). ## Companion PRs https://github.com/Cardinal-Cryptography/aleph-node/pull/1638 # TODO * weights adjustment for `fix_accounts_consumers_underflow` --- Cargo.lock | 24 +- Cargo.toml | 2 + bin/node/Cargo.toml | 2 +- bin/runtime/Cargo.toml | 4 +- bin/runtime/src/lib.rs | 10 +- pallets/operations/Cargo.toml | 48 +++ pallets/operations/LICENSE | 201 ++++++++++++ pallets/operations/README.md | 18 + pallets/operations/src/impls.rs | 76 +++++ pallets/operations/src/lib.rs | 84 +++++ pallets/operations/src/tests/mod.rs | 2 + pallets/operations/src/tests/setup.rs | 228 +++++++++++++ pallets/operations/src/tests/suite.rs | 452 ++++++++++++++++++++++++++ pallets/operations/src/traits.rs | 63 ++++ scripts/run_nodes.sh | 2 +- 15 files changed, 1210 insertions(+), 6 deletions(-) create mode 100644 pallets/operations/Cargo.toml create mode 100644 pallets/operations/LICENSE create mode 100644 pallets/operations/README.md create mode 100644 pallets/operations/src/impls.rs create mode 100644 pallets/operations/src/lib.rs create mode 100644 pallets/operations/src/tests/mod.rs create mode 100644 pallets/operations/src/tests/setup.rs create mode 100644 pallets/operations/src/tests/suite.rs create mode 100644 pallets/operations/src/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 913bd36250..7c301ae8ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,7 @@ dependencies = [ [[package]] name = "aleph-node" -version = "0.13.0" +version = "0.13.1" dependencies = [ "aleph-runtime", "finality-aleph", @@ -405,7 +405,7 @@ dependencies = [ [[package]] name = "aleph-runtime" -version = "0.13.0" +version = "0.13.1" dependencies = [ "baby-liminal-extension", "frame-benchmarking", @@ -428,6 +428,7 @@ dependencies = [ "pallet-multisig", "pallet-nomination-pools", "pallet-nomination-pools-runtime-api", + "pallet-operations", "pallet-proxy", "pallet-scheduler", "pallet-session", @@ -5947,6 +5948,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-operations" +version = "0.1.0" +dependencies = [ + "frame-election-provider-support", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-session", + "pallet-staking", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-proxy" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 116e3a289d..0fe369ff65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "pallets/aleph", "pallets/elections", "pallets/committee-management", + "pallets/operations", "pallets/support", "primitives", "rate-limiter", @@ -175,6 +176,7 @@ rate-limiter = { path = "rate-limiter" } pallet-aleph = { path = "pallets/aleph", default-features = false } pallet-committee-management = { path = "pallets/committee-management", default-features = false } pallet-elections = { path = "pallets/elections", default-features = false } +pallet-operations = { path = "pallets/operations", default-features = false } pallets-support = { path = "pallets/support", default-features = false } primitives = { path = "primitives", default-features = false } diff --git a/bin/node/Cargo.toml b/bin/node/Cargo.toml index b8a87e9dfe..192ba1855a 100644 --- a/bin/node/Cargo.toml +++ b/bin/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aleph-node" -version = "0.13.0" +version = "0.13.1" description = "Aleph node binary" build = "build.rs" license = "GPL-3.0-or-later" diff --git a/bin/runtime/Cargo.toml b/bin/runtime/Cargo.toml index fada11b32a..8cf4103c75 100644 --- a/bin/runtime/Cargo.toml +++ b/bin/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aleph-runtime" -version = "0.13.0" +version = "0.13.1" license = "GPL-3.0-or-later" authors.workspace = true edition.workspace = true @@ -63,6 +63,7 @@ frame-benchmarking = { workspace = true, optional = true } pallet-aleph = { workspace = true } pallet-committee-management= { workspace = true } pallet-elections = { workspace = true } +pallet-operations = { workspace = true } primitives = { workspace = true } pallet-proxy = { workspace = true } @@ -88,6 +89,7 @@ std = [ "pallet-authorship/std", "pallet-balances/std", "pallet-elections/std", + "pallet-operations/std", "pallet-identity/std", "pallet-insecure-randomness-collective-flip/std", "pallet-session/std", diff --git a/bin/runtime/src/lib.rs b/bin/runtime/src/lib.rs index 7fe23aaa87..783ded6025 100644 --- a/bin/runtime/src/lib.rs +++ b/bin/runtime/src/lib.rs @@ -97,7 +97,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("aleph-node"), impl_name: create_runtime_str!("aleph-node"), authoring_version: 1, - spec_version: 69, + spec_version: 70, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 18, @@ -376,6 +376,13 @@ impl pallet_elections::Config for Runtime { type BannedValidators = CommitteeManagement; } +impl pallet_operations::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AccountInfoProvider = System; + type BalancesProvider = Balances; + type NextKeysSessionProvider = Session; +} + impl pallet_committee_management::Config for Runtime { type RuntimeEvent = RuntimeEvent; type BanHandler = Elections; @@ -912,6 +919,7 @@ construct_runtime!( Identity: pallet_identity = 20, CommitteeManagement: pallet_committee_management = 21, Proxy: pallet_proxy = 22, + Operations: pallet_operations = 255, } ); diff --git a/pallets/operations/Cargo.toml b/pallets/operations/Cargo.toml new file mode 100644 index 0000000000..6808fe50a2 --- /dev/null +++ b/pallets/operations/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pallet-operations" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +parity-scale-codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +log = { workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-session = { workspace = true } +pallet-balances = { workspace = true } +sp-runtime = { workspace = true } +sp-core = { workspace = true } + +[dev-dependencies] +sp-io = { workspace = true } +pallet-staking = { workspace = true } +pallet-timestamp = { workspace = true } +frame-election-provider-support = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "scale-info/std", + "log/std", + + "frame-support/std", + "frame-system/std", + "pallet-session/std", + "pallet-balances/std", + "pallet-staking/std", + "pallet-timestamp/std", + "frame-election-provider-support/std", + "sp-runtime/std", + "sp-core/std", + +] + +try-runtime = [ + "frame-support/try-runtime", +] diff --git a/pallets/operations/LICENSE b/pallets/operations/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/pallets/operations/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/pallets/operations/README.md b/pallets/operations/README.md new file mode 100644 index 0000000000..6507aa7049 --- /dev/null +++ b/pallets/operations/README.md @@ -0,0 +1,18 @@ +# pallet-operations + +General ChainOps extrinsic that are used in chain maintenance activities. + +## fix_accounts_consumers_underflow + +An account can have an underflow of a `consumers` counter. +Account categories that are impacted by this issue depends on a chain runtime, +but specifically for AlephNode runtime are as follows: +* `consumers` == 0, `reserved` > 0 +* `consumers` == 1, `balances.Locks` contain an entry with `id` == `vesting` +* `consumers` == 2, `balances.Locks` contain an entry with `id` == `staking` +* `consumers` == 3, `balances.Locks` contain entries with `id` == `staking` + and account id is in `session.nextKeys` + +`fix_accounts_consumers_underflow` checks if the account falls into one of above +categories, and increase its `consumers` counter. + diff --git a/pallets/operations/src/impls.rs b/pallets/operations/src/impls.rs new file mode 100644 index 0000000000..f6745209c3 --- /dev/null +++ b/pallets/operations/src/impls.rs @@ -0,0 +1,76 @@ +#![allow(clippy::nonminimal_bool)] + +use frame_support::{ + dispatch::DispatchResultWithPostInfo, pallet_prelude::Get, traits::LockIdentifier, + WeakBoundedVec, +}; +use pallet_balances::BalanceLock; +use parity_scale_codec::Encode; +use sp_core::hexdisplay::HexDisplay; +use sp_runtime::DispatchError; + +use crate::{ + pallet::{Config, Event, Pallet}, + traits::{AccountInfoProvider, BalancesProvider, NextKeysSessionProvider}, + LOG_TARGET, STAKING_ID, VESTING_ID, +}; + +impl Pallet { + /// Checks if account has an underflow of `consumers` counter. In such case, it increments + /// it by one. + pub fn fix_underflow_consumer_counter(who: T::AccountId) -> DispatchResultWithPostInfo { + let mut weight = T::DbWeight::get().reads(1); + let consumers = T::AccountInfoProvider::get_consumers(&who); + + weight += T::DbWeight::get().reads(1); + if Self::no_consumers_some_reserved(&who, consumers) { + Self::increment_consumers(who)?; + weight += T::DbWeight::get().writes(1); + return Ok(Some(weight).into()); + } + + weight += T::DbWeight::get().reads(2); + if Self::staker_has_consumers_underflow(&who, consumers) { + Self::increment_consumers(who)?; + weight += T::DbWeight::get().writes(1); + return Ok(Some(weight).into()); + } + + log::debug!( + target: LOG_TARGET, + "Account {:?} has correct consumer counter, not incrementing", + HexDisplay::from(&who.encode()) + ); + Ok(Some(weight).into()) + } + + fn staker_has_consumers_underflow(who: &T::AccountId, consumers: u32) -> bool { + let locks = T::BalancesProvider::locks(who); + let has_vesting_lock = Self::has_lock(&locks, VESTING_ID); + let vester_has_consumers_underflow = consumers == 1 && has_vesting_lock; + let has_staking_lock = Self::has_lock(&locks, STAKING_ID); + let nominator_has_consumers_underflow = consumers == 2 && has_staking_lock; + let has_next_session_keys = T::NextKeysSessionProvider::has_next_session_keys(who); + let validator_has_consumers_underflow = + consumers == 3 && has_staking_lock && has_next_session_keys; + vester_has_consumers_underflow + || nominator_has_consumers_underflow + || validator_has_consumers_underflow + } + + fn no_consumers_some_reserved(who: &T::AccountId, consumers: u32) -> bool { + let is_reserved_not_zero = T::BalancesProvider::is_reserved_not_zero(who); + + consumers == 0 && is_reserved_not_zero + } + + fn has_lock(locks: &WeakBoundedVec, V>, id: LockIdentifier) -> bool { + locks.iter().any(|x| x.id == id) + } + + fn increment_consumers(who: T::AccountId) -> Result<(), DispatchError> { + frame_system::Pallet::::inc_consumers_without_limit(&who)?; + Self::deposit_event(Event::ConsumersUnderflowFixed { who }); + Ok(()) + } +} diff --git a/pallets/operations/src/lib.rs b/pallets/operations/src/lib.rs new file mode 100644 index 0000000000..8716428aac --- /dev/null +++ b/pallets/operations/src/lib.rs @@ -0,0 +1,84 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![doc = include_str!("../README.md")] + +extern crate core; + +mod impls; +mod traits; + +#[cfg(test)] +mod tests; + +use frame_support::traits::{LockIdentifier, StorageVersion}; + +const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); +pub const LOG_TARGET: &str = "pallet-operations"; +// harcoding as those consts are not public in substrate +pub const STAKING_ID: LockIdentifier = *b"staking "; +pub const VESTING_ID: LockIdentifier = *b"vesting "; + +pub use pallet::*; + +#[frame_support::pallet] +#[pallet_doc("../README.md")] +pub mod pallet { + use frame_support::{pallet_prelude::*, weights::constants::WEIGHT_REF_TIME_PER_MILLIS}; + use frame_system::{ensure_signed, pallet_prelude::OriginFor}; + + use crate::{ + traits::{AccountInfoProvider, BalancesProvider, NextKeysSessionProvider}, + STORAGE_VERSION, + }; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// Something that provides information about an account's consumers counter + type AccountInfoProvider: AccountInfoProvider; + type BalancesProvider: BalancesProvider; + type NextKeysSessionProvider: NextKeysSessionProvider; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// An account has fixed its consumers counter underflow + ConsumersUnderflowFixed { who: T::AccountId }, + } + + #[pallet::call] + impl Pallet { + /// An account can have an underflow of a `consumers` counter. + /// Account categories that are impacted by this issue depends on a chain runtime, + /// but specifically for AlephNode runtime are as follows: + /// * `consumers` == 0, `reserved` > 0 + /// * `consumers` == 1, `balances.Locks` contain an entry with `id` == `vesting` + /// * `consumers` == 2, `balances.Locks` contain an entry with `id` == `staking` + /// * `consumers` == 3, `balances.Locks` contain entries with `id` == `staking` + /// and account id is in `session.nextKeys` + /// + /// `fix_accounts_consumers_underflow` checks if the account falls into one of above + /// categories, and increase its `consumers` counter. + /// + /// - `origin`: Must be `Signed`. + /// - `who`: An account to be fixed + /// + #[pallet::call_index(0)] + #[pallet::weight( + Weight::from_parts(WEIGHT_REF_TIME_PER_MILLIS.saturating_mul(8), 0) + )] + pub fn fix_accounts_consumers_underflow( + origin: OriginFor, + who: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + Self::fix_underflow_consumer_counter(who)?; + Ok(().into()) + } + } +} diff --git a/pallets/operations/src/tests/mod.rs b/pallets/operations/src/tests/mod.rs new file mode 100644 index 0000000000..50a8dafee3 --- /dev/null +++ b/pallets/operations/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod setup; +mod suite; diff --git a/pallets/operations/src/tests/setup.rs b/pallets/operations/src/tests/setup.rs new file mode 100644 index 0000000000..ea1e2ba801 --- /dev/null +++ b/pallets/operations/src/tests/setup.rs @@ -0,0 +1,228 @@ +use frame_support::{ + construct_runtime, + pallet_prelude::ConstU32, + parameter_types, + traits::{ConstU64, OneSessionHandler}, + weights::{RuntimeDbWeight, Weight}, +}; +use frame_system::mocking::MockBlock; +use sp_runtime::{ + testing::{UintAuthorityId, H256}, + traits::{ConvertInto, IdentityLookup}, + BuildStorage, +}; + +use crate as pallet_operations; +pub(crate) type AccountId = u64; + +construct_runtime!( + pub struct TestRuntime { + System: frame_system, + Balances: pallet_balances, + Operations: pallet_operations, + Session: pallet_session, + Staking: pallet_staking, + } +); + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); + pub const TestDbWeight: RuntimeDbWeight = RuntimeDbWeight { + read: 25, + write: 100 + }; +} + +impl frame_system::Config for TestRuntime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Block = MockBlock; + type Hash = H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = TestDbWeight; + 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>; +} + +parameter_types! { + pub const Period: u64 = 1; + pub const Offset: u64 = 0; +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 1; +} + +impl pallet_balances::Config for TestRuntime { + type Balance = u128; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; + type RuntimeHoldReason = (); +} + +pub struct OtherSessionHandler; +impl OneSessionHandler for OtherSessionHandler { + type Key = UintAuthorityId; + + fn on_genesis_session<'a, I: 'a>(_: I) + where + I: Iterator, + AccountId: 'a, + { + } + + fn on_new_session<'a, I: 'a>(_: bool, _: I, _: I) + where + I: Iterator, + AccountId: 'a, + { + } + + fn on_disabled(_validator_index: u32) {} +} + +impl sp_runtime::BoundToRuntimeAppPublic for OtherSessionHandler { + type Public = UintAuthorityId; +} + +sp_runtime::impl_opaque_keys! { + pub struct TestSessionKeys { + pub other: OtherSessionHandler, + } +} + +impl pallet_session::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type ValidatorId = u64; + type ValidatorIdOf = ConvertInto; + type ShouldEndSession = pallet_session::PeriodicSessions; + type NextSessionRotation = pallet_session::PeriodicSessions; + type SessionManager = (); + type SessionHandler = (OtherSessionHandler,); + type Keys = TestSessionKeys; + type WeightInfo = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 3; +} + +impl pallet_timestamp::Config for TestRuntime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<5>; + type WeightInfo = (); +} + +parameter_types! { + pub static BondingDuration: u32 = 3; +} + +pub struct UniformEraPayout; + +impl pallet_staking::EraPayout for UniformEraPayout { + fn era_payout(_: u128, _: u128, _: u64) -> (u128, u128) { + (0, 0) + } +} + +impl pallet_staking::Config for TestRuntime { + type Currency = Balances; + type CurrencyBalance = u128; + type UnixTime = pallet_timestamp::Pallet; + type CurrencyToVote = (); + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; + type Slash = (); + type Reward = (); + type SessionsPerEra = (); + type SlashDeferDuration = (); + type AdminOrigin = frame_system::EnsureRoot; + type BondingDuration = BondingDuration; + type SessionInterface = (); + type EraPayout = UniformEraPayout; + type NextNewSession = (); + type MaxNominatorRewardedPerValidator = ConstU32<64>; + type OffendingValidatorsThreshold = (); + type ElectionProvider = + frame_election_provider_support::NoElection<(AccountId, u64, Staking, ())>; + type GenesisElectionProvider = Self::ElectionProvider; + type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; + type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; + type MaxUnlockingChunks = ConstU32<32>; + type HistoryDepth = ConstU32<84>; + type EventListeners = (); + type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type WeightInfo = (); +} + +impl pallet_operations::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type AccountInfoProvider = System; + type BalancesProvider = Balances; + type NextKeysSessionProvider = Session; +} + +pub fn new_test_ext(accounts_and_balances: &[(u64, bool, u128)]) -> sp_io::TestExternalities { + let mut t = as BuildStorage>::build_storage( + &frame_system::GenesisConfig::default(), + ) + .expect("Storage should be build."); + + let balances: Vec<_> = accounts_and_balances + .iter() + .map(|(id, _, balance)| (*id, *balance)) + .collect(); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_session::GenesisConfig:: { + keys: accounts_and_balances + .iter() + .filter(|(_, is_authority, _)| *is_authority) + .map(|(id, _, _)| { + ( + *id, + *id, + TestSessionKeys { + other: (*id).into(), + }, + ) + }) + .collect(), + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| frame_system::Pallet::::set_block_number(1)); + ext +} diff --git a/pallets/operations/src/tests/suite.rs b/pallets/operations/src/tests/suite.rs new file mode 100644 index 0000000000..8c6571894e --- /dev/null +++ b/pallets/operations/src/tests/suite.rs @@ -0,0 +1,452 @@ +use frame_support::{ + assert_ok, + traits::{Currency, LockableCurrency, ReservableCurrency, WithdrawReasons}, +}; +use pallet_staking::RewardDestination; + +use super::setup::*; +use crate::VESTING_ID; + +fn total_balance(account_id: u64) -> u128 { + pallet_balances::Pallet::::total_balance(&account_id) +} + +fn free_balance(account_id: u64) -> u128 { + pallet_balances::Pallet::::free_balance(account_id) +} + +fn reserved_balance(account_id: u64) -> u128 { + pallet_balances::Pallet::::reserved_balance(account_id) +} + +fn usable_balance(account_id: u64) -> u128 { + pallet_balances::Pallet::::usable_balance(account_id) +} + +fn ed() -> u128 { + ::ExistentialDeposit::get() +} + +fn providers(account_id: u64) -> u32 { + frame_system::Pallet::::providers(&account_id) +} + +fn consumers(account_id: u64) -> u32 { + frame_system::Pallet::::consumers(&account_id) +} + +fn pallet_operations_events() -> Vec> { + frame_system::Pallet::::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let RuntimeEvent::Operations(inner) = e { + Some(inner) + } else { + None + } + }) + .collect() +} + +#[test] +fn given_accounts_with_initial_balance_then_balances_data_and_counters_are_valid() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + assert_eq!(total_balance(authority_id), total_balance_authority); + assert_eq!(free_balance(authority_id), total_balance_authority); + assert_eq!(reserved_balance(authority_id), 0); + // there are > 0 consumers, so we can't transfer everything + assert_eq!(usable_balance(authority_id), total_balance_authority - ed()); + + assert_eq!(providers(authority_id), 1); + // +1 consumers due to session keys are set + assert_eq!(consumers(authority_id), 1); + + assert_eq!(total_balance(non_authority_id), total_balance_non_authority); + assert_eq!(free_balance(non_authority_id), total_balance_non_authority); + assert_eq!(reserved_balance(non_authority_id), 0); + // consumers == 0 so we can transfer everything + assert_eq!( + usable_balance(non_authority_id), + total_balance_non_authority + ); + assert_eq!(providers(non_authority_id), 1); + assert_eq!(consumers(non_authority_id), 0); + }); +} + +#[test] +fn given_accounts_with_initial_balance_when_reserving_then_balances_data_and_counters_are_valid() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let reserved_amount = 3_u128; + assert_ok!(pallet_balances::Pallet::::reserve( + &authority_id, + reserved_amount + )); + assert_eq!(total_balance(authority_id), total_balance_authority); + assert_eq!( + free_balance(authority_id), + total_balance_authority - reserved_amount + ); + assert_eq!(reserved_balance(authority_id), reserved_amount); + // since consumers > 0 + assert_eq!( + usable_balance(authority_id), + total_balance_authority - reserved_amount - ed() + ); + assert_eq!(providers(authority_id), 1); + // +1 consumers due to session keys are set + // +1 consumers since there is reserved balance + assert_eq!(consumers(authority_id), 2); + + assert_ok!(pallet_balances::Pallet::::reserve( + &non_authority_id, + reserved_amount + )); + assert_eq!(total_balance(non_authority_id), total_balance_non_authority); + assert_eq!( + free_balance(non_authority_id), + total_balance_non_authority - reserved_amount + ); + assert_eq!(reserved_balance(non_authority_id), reserved_amount); + // free - ed - reserved since consumers > 0 + assert_eq!( + usable_balance(non_authority_id), + total_balance_non_authority - reserved_amount - ed() + ); + assert_eq!(providers(non_authority_id), 1); + // +1 consumers since there is reserved balance + assert_eq!(consumers(authority_id), 2); + }); +} + +#[test] +fn given_account_with_initial_balance_when_bonding_then_balances_data_and_counters_are_valid() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let bonded = 123_u128; + assert_ok!(pallet_staking::Pallet::::bond( + RuntimeOrigin::signed(authority_id), + bonded, + RewardDestination::Controller + )); + assert_eq!(total_balance(authority_id), total_balance_authority); + assert_eq!(free_balance(authority_id), total_balance_authority); + assert_eq!(reserved_balance(authority_id), 0_u128); + assert_eq!( + usable_balance(authority_id), + total_balance_authority - bonded + ); + assert_eq!(providers(authority_id), 1); + // +1 consumers due to session keys are set + // +1 consumers since there is frozen balance + // +1 consumers since there is at least one lock + // +1 consumers from bond() + assert_eq!(consumers(authority_id), 4); + + assert_ok!(pallet_staking::Pallet::::bond( + RuntimeOrigin::signed(non_authority_id), + bonded, + RewardDestination::Controller + )); + assert_eq!(total_balance(non_authority_id), total_balance_non_authority); + assert_eq!(free_balance(non_authority_id), total_balance_non_authority); + assert_eq!(reserved_balance(non_authority_id), 0_u128); + // free - max(frozen, ed) + assert_eq!( + usable_balance(non_authority_id), + total_balance_non_authority - bonded + ); + assert_eq!(providers(non_authority_id), 1); + // +1 consumers since there is frozen balance + // +1 consumers since there is at least one lock + // +1 consumers from bond() + assert_eq!(consumers(non_authority_id), 3); + }); +} + +#[test] +fn given_accounts_with_initial_balance_when_fixing_consumers_then_accounts_do_not_change() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + assert_eq!(consumers(authority_id), 1); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + authority_id + ) + ); + assert_eq!(consumers(authority_id), 1); + + assert_eq!(consumers(non_authority_id), 0); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + non_authority_id + ) + ); + assert_eq!(consumers(non_authority_id), 0); + }); +} + +#[test] +fn given_accounts_with_reserved_balance_when_fixing_consumers_then_accounts_do_not_change() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let reserved_amount = 3_u128; + assert_ok!(pallet_balances::Pallet::::reserve( + &authority_id, + reserved_amount + )); + assert_eq!(consumers(authority_id), 2); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + authority_id + ) + ); + assert_eq!(consumers(authority_id), 2); + + assert_ok!(pallet_balances::Pallet::::reserve( + &non_authority_id, + reserved_amount + )); + assert_eq!(consumers(non_authority_id), 1); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + non_authority_id + ) + ); + assert_eq!(consumers(non_authority_id), 1); + }); +} + +#[test] +fn given_bonded_accounts_balance_when_fixing_consumers_then_accounts_do_not_change() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let bonded = 123_u128; + assert_ok!(pallet_staking::Pallet::::bond( + RuntimeOrigin::signed(authority_id), + bonded, + RewardDestination::Controller + )); + + assert_eq!(consumers(authority_id), 4); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + authority_id + ) + ); + assert_eq!(consumers(authority_id), 4); + + assert_ok!(pallet_staking::Pallet::::bond( + RuntimeOrigin::signed(non_authority_id), + bonded, + RewardDestination::Controller + )); + assert_eq!(consumers(non_authority_id), 3); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + non_authority_id + ) + ); + assert_eq!(consumers(non_authority_id), 3); + }); +} + +#[test] +fn given_account_zero_consumers_some_reserved_when_fixing_consumers_then_consumers_increase() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let reserved_amount = 3_u128; + assert_ok!(pallet_balances::Pallet::::reserve( + &non_authority_id, + reserved_amount + )); + frame_system::Pallet::::dec_consumers(&non_authority_id); + assert_eq!(consumers(non_authority_id), 0); + frame_system::Pallet::::reset_events(); + assert_eq!(pallet_operations_events().len(), 0); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + non_authority_id + ) + ); + assert_eq!( + pallet_operations_events(), + [crate::Event::ConsumersUnderflowFixed { + who: non_authority_id + }] + ); + assert_eq!(consumers(non_authority_id), 1); + }); +} + +#[test] +fn given_non_staking_account_with_vesting_lock_when_fixing_consumers_then_consumers_increase() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let locked = 3_u128; + pallet_balances::Pallet::::set_lock( + VESTING_ID, + &non_authority_id, + locked, + WithdrawReasons::all(), + ); + frame_system::Pallet::::dec_consumers(&non_authority_id); + assert_eq!(consumers(non_authority_id), 1); + frame_system::Pallet::::reset_events(); + assert_eq!(pallet_operations_events().len(), 0); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + non_authority_id + ) + ); + assert_eq!( + pallet_operations_events(), + [crate::Event::ConsumersUnderflowFixed { + who: non_authority_id + }] + ); + + assert_eq!(consumers(non_authority_id), 2); + }); +} + +#[test] +fn given_nominator_account_with_staking_lock_when_fixing_consumers_then_consumers_increase() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let bonded = 123_u128; + assert_ok!(pallet_staking::Pallet::::bond( + RuntimeOrigin::signed(non_authority_id), + bonded, + RewardDestination::Controller + )); + frame_system::Pallet::::dec_consumers(&non_authority_id); + assert_eq!(consumers(non_authority_id), 2); + frame_system::Pallet::::reset_events(); + assert_eq!(pallet_operations_events().len(), 0); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(authority_id), + non_authority_id + ) + ); + assert_eq!( + pallet_operations_events(), + [crate::Event::ConsumersUnderflowFixed { + who: non_authority_id + }] + ); + + assert_eq!(consumers(non_authority_id), 3); + }); +} + +#[test] +fn given_validator_account_with_staking_lock_when_fixing_consumers_then_consumers_increase() { + let authority_id = 1_u64; + let non_authority_id = 2_u64; + let total_balance_authority = 1000_u128; + let total_balance_non_authority = 999_u128; + new_test_ext(&[ + (authority_id, true, total_balance_authority), + (non_authority_id, false, total_balance_non_authority), + ]) + .execute_with(|| { + let bonded = 123_u128; + assert_ok!(pallet_staking::Pallet::::bond( + RuntimeOrigin::signed(authority_id), + bonded, + RewardDestination::Controller + )); + frame_system::Pallet::::dec_consumers(&authority_id); + assert_eq!(consumers(authority_id), 3); + frame_system::Pallet::::reset_events(); + assert_eq!(pallet_operations_events().len(), 0); + assert_ok!( + crate::Pallet::::fix_accounts_consumers_underflow( + RuntimeOrigin::signed(non_authority_id), + authority_id + ) + ); + assert_eq!( + pallet_operations_events(), + [crate::Event::ConsumersUnderflowFixed { who: authority_id }] + ); + + assert_eq!(consumers(authority_id), 4); + }); +} diff --git a/pallets/operations/src/traits.rs b/pallets/operations/src/traits.rs new file mode 100644 index 0000000000..6c8ea78b6b --- /dev/null +++ b/pallets/operations/src/traits.rs @@ -0,0 +1,63 @@ +use frame_support::{traits::StoredMap, WeakBoundedVec}; +use pallet_balances::BalanceLock; +use sp_runtime::traits::Zero; + +pub trait AccountInfoProvider { + type AccountId; + type RefCount; + + fn get_consumers(who: &Self::AccountId) -> Self::RefCount; +} + +impl AccountInfoProvider for frame_system::Pallet +where + T: frame_system::Config, +{ + type AccountId = T::AccountId; + type RefCount = frame_system::RefCount; + + fn get_consumers(who: &Self::AccountId) -> Self::RefCount { + frame_system::Pallet::::consumers(who) + } +} + +pub trait BalancesProvider { + type AccountId; + type Balance; + type MaxLocks; + + fn is_reserved_not_zero(who: &Self::AccountId) -> bool; + + fn locks(who: &Self::AccountId) -> WeakBoundedVec, Self::MaxLocks>; +} + +impl, I: 'static> BalancesProvider for pallet_balances::Pallet { + type AccountId = T::AccountId; + type Balance = T::Balance; + type MaxLocks = T::MaxLocks; + + fn is_reserved_not_zero(who: &Self::AccountId) -> bool { + !T::AccountStore::get(who).reserved.is_zero() + } + + fn locks(who: &Self::AccountId) -> WeakBoundedVec, Self::MaxLocks> { + pallet_balances::Locks::::get(who) + } +} + +pub trait NextKeysSessionProvider { + type AccountId; + + fn has_next_session_keys(who: &Self::AccountId) -> bool; +} + +impl NextKeysSessionProvider for pallet_session::Pallet +where + T: pallet_session::Config::AccountId>, +{ + type AccountId = T::AccountId; + + fn has_next_session_keys(who: &Self::AccountId) -> bool { + pallet_session::NextKeys::::get(who).is_some() + } +} diff --git a/scripts/run_nodes.sh b/scripts/run_nodes.sh index c403005788..95481e3a2d 100755 --- a/scripts/run_nodes.sh +++ b/scripts/run_nodes.sh @@ -69,7 +69,7 @@ Usage: set if you do not want to build testing aleph-node binary [--dont-delete-db] set to not delete database - [--dont-remove-abtf-backups] + [--dont-remove-abft-backups] set to not delete AlephBFT backups; by default they are removed since this script is intended to bootstrap chain by default, in which case you do not want to have them in 99% of scenarios