From bdd139989cb4dfa8cc6cc6b5ee470a711df32d25 Mon Sep 17 00:00:00 2001
From: kianenigma
Date: Sun, 31 Jul 2022 13:27:13 +0100
Subject: [PATCH 01/81] add failing test for itamar
---
frame/staking/src/tests.rs | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs
index d14d8c4a75f2e..646645fdf56a0 100644
--- a/frame/staking/src/tests.rs
+++ b/frame/staking/src/tests.rs
@@ -5102,6 +5102,21 @@ fn proportional_ledger_slash_works() {
assert_eq!(LedgerSlashPerEra::get().0, 0);
assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(6, 30), (7, 30)]));
+ // Given
+ ledger.unlocking = bounded_vec![c(4, 100), c(5, 100), c(6, 100), c(7, 100)];
+ ledger.total = 4 * 100;
+ ledger.active = 0;
+ // When the first 2 chunks don't overlap with the affected range of unlock eras.
+ assert_eq!(ledger.slash(15, 0, 3), 15);
+ // Then
+ assert_eq!(ledger.unlocking, vec![c(4, 100), c(5, 99), c(6, 100 - 7), c(7, 100 - 7)]);
+ // ISSUE: The sum of everything we round down is affecting chunk 5, which should have ideally
+ // remained unchanged.
+ assert_eq!(ledger.total, 4 * 100 - 15);
+ assert_eq!(LedgerSlashPerEra::get().0, 0);
+ assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(5, 99), (6, 93), (7, 93)]));
+ panic!();
+
// Given
ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
ledger.active = 500;
From 14486b6cdebc98f79b65f74ecf39bdff401edf85 Mon Sep 17 00:00:00 2001
From: kianenigma
Date: Sun, 31 Jul 2022 17:48:16 +0100
Subject: [PATCH 02/81] an ugly example of fast unstake
---
Cargo.lock | 20 +++
Cargo.toml | 1 +
frame/fast-unstake/Cargo.toml | 50 ++++++
frame/fast-unstake/README.md | 240 +++++++++++++++++++++++++
frame/fast-unstake/src/benchmarking.rs | 78 ++++++++
frame/fast-unstake/src/lib.rs | 128 +++++++++++++
frame/fast-unstake/src/tests.rs | 200 +++++++++++++++++++++
frame/fast-unstake/src/weights.rs | 101 +++++++++++
8 files changed, 818 insertions(+)
create mode 100644 frame/fast-unstake/Cargo.toml
create mode 100644 frame/fast-unstake/README.md
create mode 100644 frame/fast-unstake/src/benchmarking.rs
create mode 100644 frame/fast-unstake/src/lib.rs
create mode 100644 frame/fast-unstake/src/tests.rs
create mode 100644 frame/fast-unstake/src/weights.rs
diff --git a/Cargo.lock b/Cargo.lock
index 2b76f61df7912..6e93840cd13a9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5658,6 +5658,26 @@ dependencies = [
"sp-tasks",
]
+[[package]]
+name = "pallet-fast-unstake"
+version = "4.0.0-dev"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "log",
+ "pallet-balances",
+ "pallet-nomination-pools",
+ "pallet-staking",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-staking",
+ "sp-std",
+]
+
[[package]]
name = "pallet-gilt"
version = "4.0.0-dev"
diff --git a/Cargo.toml b/Cargo.toml
index 1f22343c002a8..9bdce012e8285 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -88,6 +88,7 @@ members = [
"frame/contracts/rpc/runtime-api",
"frame/conviction-voting",
"frame/democracy",
+ "frame/fast-unstake",
"frame/try-runtime",
"frame/election-provider-multi-phase",
"frame/election-provider-support",
diff --git a/frame/fast-unstake/Cargo.toml b/frame/fast-unstake/Cargo.toml
new file mode 100644
index 0000000000000..1b435559de6e1
--- /dev/null
+++ b/frame/fast-unstake/Cargo.toml
@@ -0,0 +1,50 @@
+[package]
+name = "pallet-fast-unstake"
+version = "4.0.0-dev"
+authors = ["Parity Technologies "]
+edition = "2021"
+license = "Unlicense"
+homepage = "https://substrate.io"
+repository = "https://github.com/paritytech/substrate/"
+description = "FRAME example 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-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" }
+frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
+frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
+pallet-balances = { version = "4.0.0-dev", default-features = false, path = "../balances" }
+sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" }
+sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" }
+sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" }
+sp-staking = { default-features = false, path = "../../primitives/staking" }
+pallet-staking = { default-features = false, path = "../staking" }
+pallet-nomination-pools = { default-features = false, path = "../nomination-pools" }
+
+[dev-dependencies]
+sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" }
+
+[features]
+default = ["std"]
+std = [
+ "codec/std",
+ "frame-benchmarking/std",
+ "frame-support/std",
+ "frame-system/std",
+ "log/std",
+ "pallet-balances/std",
+ "scale-info/std",
+ "sp-io/std",
+ "sp-staking/std",
+ "sp-runtime/std",
+ "sp-std/std",
+ "pallet-staking/std",
+]
+runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"]
+try-runtime = ["frame-support/try-runtime"]
diff --git a/frame/fast-unstake/README.md b/frame/fast-unstake/README.md
new file mode 100644
index 0000000000000..358829192f11d
--- /dev/null
+++ b/frame/fast-unstake/README.md
@@ -0,0 +1,240 @@
+
+# Basic Example Pallet
+
+
+The Example: A simple example of a FRAME pallet demonstrating
+concepts, APIs and structures common to most FRAME runtimes.
+
+Run `cargo doc --package pallet-example-basic --open` to view this pallet's documentation.
+
+**This pallet serves as an example and is not meant to be used in production.**
+
+### Documentation Guidelines:
+
+
+
+
+ - Documentation comments (i.e.
/// comment
) - should
+ accompany pallet functions and be restricted to the pallet interface,
+ not the internals of the pallet implementation. Only state inputs,
+ outputs, and a brief description that mentions whether calling it
+ requires root, but without repeating the source code details.
+ Capitalize the first word of each documentation comment and end it with
+ a full stop. See
+ Generic example of annotating source code with documentation comments
+ - Self-documenting code - Try to refactor code to be self-documenting.
+ - Code comments - Supplement complex code with a brief explanation, not every line of code.
+ - Identifiers - surround by backticks (i.e.
INHERENT_IDENTIFIER
, InherentType
,
+ u64
)
+ - Usage scenarios - should be simple doctests. The compiler should ensure they stay valid.
+ - Extended tutorials - should be moved to external files and refer to.
+
+ - Mandatory - include all of the sections/subsections where MUST is specified.
+ - Optional - optionally include sections/subsections where CAN is specified.
+
+
+### Documentation Template:
+
+Copy and paste this template from frame/examples/basic/src/lib.rs into file
+`frame//src/lib.rs` of your own custom pallet and complete it.
+
+// Add heading with custom pallet name
+
+\# Pallet
+
+// Add simple description
+
+// Include the following links that shows what trait needs to be implemented to use the pallet
+// and the supported dispatchables that are documented in the Call enum.
+
+- \[`::Config`](https://docs.rs/pallet-example-basic/latest/pallet_example_basic/trait.Config.html)
+- \[`Call`](https://docs.rs/pallet-example-basic/latest/pallet_example_basic/enum.Call.html)
+- \[`Module`](https://docs.rs/pallet-example-basic/latest/pallet_example_basic/struct.Module.html)
+
+\## Overview
+
+
+// Short description of pallet's purpose.
+// Links to Traits that should be implemented.
+// What this pallet is for.
+// What functionality the pallet provides.
+// When to use the pallet (use case examples).
+// How it is used.
+// Inputs it uses and the source of each input.
+// Outputs it produces.
+
+
+
+
+\## Terminology
+
+// Add terminology used in the custom pallet. Include concepts, storage items, or actions that you think
+// deserve to be noted to give context to the rest of the documentation or pallet usage. The author needs to
+// use some judgment about what is included. We don't want a list of every storage item nor types - the user
+// can go to the code for that. For example, "transfer fee" is obvious and should not be included, but
+// "free balance" and "reserved balance" should be noted to give context to the pallet.
+// Please do not link to outside resources. The reference docs should be the ultimate source of truth.
+
+
+
+\## Goals
+
+// Add goals that the custom pallet is designed to achieve.
+
+
+
+\### Scenarios
+
+
+
+\####
+
+// Describe requirements prior to interacting with the custom pallet.
+// Describe the process of interacting with the custom pallet for this scenario and public API functions used.
+
+\## Interface
+
+\### Supported Origins
+
+// What origins are used and supported in this pallet (root, signed, none)
+// i.e. root when \`ensure_root\`
used
+// i.e. none when \`ensure_none\`
used
+// i.e. signed when \`ensure_signed\`
used
+
+\`inherent\`
+
+
+
+
+\### Types
+
+// Type aliases. Include any associated types and where the user would typically define them.
+
+\`ExampleType\`
+
+
+
+// Reference documentation of aspects such as `storageItems` and `dispatchable` functions should only be
+// included in the https://docs.rs Rustdocs for Substrate and not repeated in the README file.
+
+\### Dispatchable Functions
+
+
+
+// A brief description of dispatchable functions and a link to the rustdoc with their actual documentation.
+
+// MUST have link to Call enum
+// MUST have origin information included in function doc
+// CAN have more info up to the user
+
+\### Public Functions
+
+
+
+// A link to the rustdoc and any notes about usage in the pallet, not for specific functions.
+// For example, in the Balances Pallet: "Note that when using the publicly exposed functions,
+// you (the runtime developer) are responsible for implementing any necessary checks
+// (e.g. that the sender is the signer) before calling a function that will affect storage."
+
+
+
+// It is up to the writer of the respective pallet (with respect to how much information to provide).
+
+\#### Public Inspection functions - Immutable (getters)
+
+// Insert a subheading for each getter function signature
+
+\##### \`example_getter_name()\`
+
+// What it returns
+// Why, when, and how often to call it
+// When it could panic or error
+// When safety issues to consider
+
+\#### Public Mutable functions (changing state)
+
+// Insert a subheading for each setter function signature
+
+\##### \`example_setter_name(origin, parameter_name: T::ExampleType)\`
+
+// What state it changes
+// Why, when, and how often to call it
+// When it could panic or error
+// When safety issues to consider
+// What parameter values are valid and why
+
+\### Storage Items
+
+// Explain any storage items included in this pallet
+
+\### Digest Items
+
+// Explain any digest items included in this pallet
+
+\### Inherent Data
+
+// Explain what inherent data (if any) is defined in the pallet and any other related types
+
+\### Events:
+
+// Insert events for this pallet if any
+
+\### Errors:
+
+// Explain what generates errors
+
+\## Usage
+
+// Insert 2-3 examples of usage and code snippets that show how to
+// use Pallet in a custom pallet.
+
+\### Prerequisites
+
+// Show how to include necessary imports for and derive
+// your pallet configuration trait with the `INSERT_CUSTOM_PALLET_NAME` trait.
+
+\```rust
+use ;
+
+pub trait Config: ::Config { }
+\```
+
+\### Simple Code Snippet
+
+// Show a simple example (e.g. how to query a public getter function of )
+
+\### Example from FRAME
+
+// Show a usage example in an actual runtime
+
+// See:
+// - Substrate TCR https://github.com/parity-samples/substrate-tcr
+// - Substrate Kitties https://shawntabrizi.github.io/substrate-collectables-workshop/#/
+
+\## Genesis Config
+
+
+
+\## Dependencies
+
+// Dependencies on other FRAME pallets and the genesis config should be mentioned,
+// but not the Rust Standard Library.
+// Genesis configuration modifications that may be made to incorporate this pallet
+// Interaction with other pallets
+
+
+
+\## Related Pallets
+
+// Interaction with other pallets in the form of a bullet point list
+
+\## References
+
+
+
+// Links to reference material, if applicable. For example, Phragmen, W3F research, etc.
+// that the implementation is based on.
+
+
+License: Unlicense
diff --git a/frame/fast-unstake/src/benchmarking.rs b/frame/fast-unstake/src/benchmarking.rs
new file mode 100644
index 0000000000000..d7b933577ead5
--- /dev/null
+++ b/frame/fast-unstake/src/benchmarking.rs
@@ -0,0 +1,78 @@
+// 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.
+
+//! Benchmarking for pallet-example-basic.
+
+#![cfg(feature = "runtime-benchmarks")]
+
+use crate::*;
+use frame_benchmarking::{benchmarks, whitelisted_caller};
+use frame_system::RawOrigin;
+
+// To actually run this benchmark on pallet-example-basic, we need to put this pallet into the
+// runtime and compile it with `runtime-benchmarks` feature. The detail procedures are
+// documented at:
+// https://docs.substrate.io/v3/runtime/benchmarking#how-to-benchmark
+//
+// The auto-generated weight estimate of this pallet is copied over to the `weights.rs` file.
+// The exact command of how the estimate generated is printed at the top of the file.
+
+// Details on using the benchmarks macro can be seen at:
+// https://paritytech.github.io/substrate/master/frame_benchmarking/trait.Benchmarking.html#tymethod.benchmarks
+benchmarks! {
+ // This will measure the execution time of `set_dummy` for b in [1..1000] range.
+ set_dummy_benchmark {
+ // This is the benchmark setup phase
+ let b in 1 .. 1000;
+ }: set_dummy(RawOrigin::Root, b.into()) // The execution phase is just running `set_dummy` extrinsic call
+ verify {
+ // This is the optional benchmark verification phase, asserting certain states.
+ assert_eq!(Pallet::::dummy(), Some(b.into()))
+ }
+
+ // This will measure the execution time of `accumulate_dummy` for b in [1..1000] range.
+ // The benchmark execution phase is shorthanded. When the name of the benchmark case is the same
+ // as the extrinsic call. `_(...)` is used to represent the extrinsic name.
+ // The benchmark verification phase is omitted.
+ accumulate_dummy {
+ let b in 1 .. 1000;
+ // The caller account is whitelisted for DB reads/write by the benchmarking macro.
+ let caller: T::AccountId = whitelisted_caller();
+ }: _(RawOrigin::Signed(caller), b.into())
+
+ // This will measure the execution time of sorting a vector.
+ sort_vector {
+ let x in 0 .. 10000;
+ let mut m = Vec::::new();
+ for i in (0..x).rev() {
+ m.push(i);
+ }
+ }: {
+ // The benchmark execution phase could also be a closure with custom code
+ m.sort_unstable();
+ }
+
+ // This line generates test cases for benchmarking, and could be run by:
+ // `cargo test -p pallet-example-basic --all-features`, you will see one line per case:
+ // `test benchmarking::bench_sort_vector ... ok`
+ // `test benchmarking::bench_accumulate_dummy ... ok`
+ // `test benchmarking::bench_set_dummy_benchmark ... ok` in the result.
+ //
+ // The line generates three steps per benchmark, with repeat=1 and the three steps are
+ // [low, mid, high] of the range.
+ impl_benchmark_test_suite!(Pallet, crate::tests::new_test_ext(), crate::tests::Test)
+}
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
new file mode 100644
index 0000000000000..cd30e09969818
--- /dev/null
+++ b/frame/fast-unstake/src/lib.rs
@@ -0,0 +1,128 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2017-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.
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[frame_support::pallet]
+pub mod pallet {
+ use frame_support::pallet_prelude::*;
+ use frame_system::pallet_prelude::*;
+ use sp_std::prelude::*;
+
+ use frame_support::traits::Currency;
+ use pallet_nomination_pools::PoolId;
+ use sp_staking::EraIndex;
+
+ type BalanceOf = <::Currency as Currency<
+ ::AccountId,
+ >>::Balance;
+
+ #[pallet::pallet]
+ pub struct Pallet(_);
+
+ #[pallet::config]
+ pub trait Config:
+ frame_system::Config + pallet_staking::Config + pallet_nomination_pools::Config
+ {
+ type SlashPerEra: Get>;
+ }
+
+ #[derive(Encode, Decode, Eq, PartialEq, Clone)]
+ pub struct Unstake {
+ stash: AccountId,
+ checked: Vec,
+ pool_id: PoolId,
+ }
+
+ #[pallet::storage]
+ #[pallet::unbounded]
+ pub type Head = StorageValue<_, Unstake, OptionQuery>;
+
+ #[pallet::storage]
+ pub type Queue = StorageMap<_, Twox64Concat, T::AccountId, PoolId>;
+
+ #[pallet::storage]
+ pub type ErasToCheckPerBlock = StorageValue<_, u32, ValueQuery>;
+
+ #[pallet::hooks]
+ impl Hooks for Pallet {
+ fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight {
+ 0
+ }
+ }
+
+ #[pallet::call]
+ impl Pallet {
+ #[pallet::weight(0)]
+ pub fn enqueue(origin: OriginFor, pool_id: PoolId) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+ }
+ }
+
+ impl Pallet {
+ fn process_head() {
+ let maybe_next = Head::::take().or_else(|| {
+ Queue::::drain().take(1).map(|(stash, pool_id)| Unstake { stash, pool_id, checked: Default::default() }).next()
+ });
+
+ let Unstake { stash, checked, pool_id } = match maybe_next {
+ None => return,
+ Some(x) => x,
+ };
+
+ let current_era = pallet_staking::CurrentEra::::get().unwrap_or_default();
+ let bonding_duration = ::BondingDuration::get();
+
+ let total_check_range = (current_era.saturating_sub(bonding_duration)..current_era)
+ .rev()
+ .collect::>();
+ let now_check_range = total_check_range
+ .iter()
+ .filter(|e| !checked.contains(e))
+ .take(ErasToCheckPerBlock::::get() as usize)
+ .collect::>();
+
+ if now_check_range.is_empty() {
+ // `stash` is not exposed in any era -- we can let go of them now.
+ let num_slashing_spans = 0; // TODO
+ let ctrl = pallet_staking::Bonded::::get(stash).unwrap();
+ let ledger = pallet_staking::Ledger::::get(ctrl).unwrap();
+ pallet_staking::Pallet::::force_unstake(Origin::Root, stash, num_slashing_spans)
+ .unwrap();
+ pallet_nomination_pools::Pallet::::join(Origin::Signed(stash), ledger.total, pool_id);
+ }
+
+ let is_exposed = now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
+
+ if is_exposed {
+ // this account was actually exposed in some era within the range -- slash them and
+ // remove them from the queue.
+ // TODO: slash
+ } else {
+ // Not exposed in these two eras.
+ checked.extend(now_check_range);
+ Head::::put(Unstake { stash, checked, pool_id });
+ }
+ }
+
+ fn is_exposed_in_era(who: &T::AccountId, era: &EraIndex) -> bool {
+ pallet_staking::ErasStakers::::iter_prefix(era)
+ .any(|(_, exposures)| exposures.others.iter().any(|i| i.who == *who))
+ }
+ }
+}
diff --git a/frame/fast-unstake/src/tests.rs b/frame/fast-unstake/src/tests.rs
new file mode 100644
index 0000000000000..0f659e12fb443
--- /dev/null
+++ b/frame/fast-unstake/src/tests.rs
@@ -0,0 +1,200 @@
+// 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.
+
+//! Tests for pallet-example-basic.
+
+use crate::*;
+use frame_support::{
+ assert_ok, parameter_types,
+ traits::{ConstU64, OnInitialize},
+ weights::{DispatchInfo, GetDispatchInfo},
+};
+use sp_core::H256;
+// The testing primitives are very useful for avoiding having to work with signatures
+// or public keys. `u64` is used as the `AccountId` and no `Signature`s are required.
+use sp_runtime::{
+ testing::Header,
+ traits::{BlakeTwo256, IdentityLookup},
+ BuildStorage,
+};
+// Reexport crate as its pallet name for construct_runtime.
+use crate as pallet_example_basic;
+
+type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic;
+type Block = frame_system::mocking::MockBlock;
+
+// For testing the pallet, we construct a mock runtime.
+frame_support::construct_runtime!(
+ pub enum Test where
+ Block = Block,
+ NodeBlock = Block,
+ UncheckedExtrinsic = UncheckedExtrinsic,
+ {
+ System: frame_system::{Pallet, Call, Config, Storage, Event},
+ Balances: pallet_balances::{Pallet, Call, Storage, Config, Event},
+ Example: pallet_example_basic::{Pallet, Call, Storage, Config, Event},
+ }
+);
+
+parameter_types! {
+ pub BlockWeights: frame_system::limits::BlockWeights =
+ frame_system::limits::BlockWeights::simple_max(1024);
+}
+impl frame_system::Config for Test {
+ type BaseCallFilter = frame_support::traits::Everything;
+ type BlockWeights = ();
+ type BlockLength = ();
+ type DbWeight = ();
+ type Origin = Origin;
+ type Index = u64;
+ type BlockNumber = u64;
+ type Hash = H256;
+ type Call = Call;
+ type Hashing = BlakeTwo256;
+ type AccountId = u64;
+ type Lookup = IdentityLookup;
+ type Header = Header;
+ type Event = Event;
+ type BlockHashCount = 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 Test {
+ type MaxLocks = ();
+ type MaxReserves = ();
+ type ReserveIdentifier = [u8; 8];
+ type Balance = u64;
+ type DustRemoval = ();
+ type Event = Event;
+ type ExistentialDeposit = ConstU64<1>;
+ type AccountStore = System;
+ type WeightInfo = ();
+}
+
+impl Config for Test {
+ type MagicNumber = ConstU64<1_000_000_000>;
+ type Event = Event;
+ type WeightInfo = ();
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+pub fn new_test_ext() -> sp_io::TestExternalities {
+ let t = GenesisConfig {
+ // We use default for brevity, but you can configure as desired if needed.
+ system: Default::default(),
+ balances: Default::default(),
+ example: pallet_example_basic::GenesisConfig {
+ dummy: 42,
+ // we configure the map with (key, value) pairs.
+ bar: vec![(1, 2), (2, 3)],
+ foo: 24,
+ },
+ }
+ .build_storage()
+ .unwrap();
+ t.into()
+}
+
+#[test]
+fn it_works_for_optional_value() {
+ new_test_ext().execute_with(|| {
+ // Check that GenesisBuilder works properly.
+ let val1 = 42;
+ let val2 = 27;
+ assert_eq!(Example::dummy(), Some(val1));
+
+ // Check that accumulate works when we have Some value in Dummy already.
+ assert_ok!(Example::accumulate_dummy(Origin::signed(1), val2));
+ assert_eq!(Example::dummy(), Some(val1 + val2));
+
+ // Check that accumulate works when we Dummy has None in it.
+ >::on_initialize(2);
+ assert_ok!(Example::accumulate_dummy(Origin::signed(1), val1));
+ assert_eq!(Example::dummy(), Some(val1 + val2 + val1));
+ });
+}
+
+#[test]
+fn it_works_for_default_value() {
+ new_test_ext().execute_with(|| {
+ assert_eq!(Example::foo(), 24);
+ assert_ok!(Example::accumulate_foo(Origin::signed(1), 1));
+ assert_eq!(Example::foo(), 25);
+ });
+}
+
+#[test]
+fn set_dummy_works() {
+ new_test_ext().execute_with(|| {
+ let test_val = 133;
+ assert_ok!(Example::set_dummy(Origin::root(), test_val.into()));
+ assert_eq!(Example::dummy(), Some(test_val));
+ });
+}
+
+#[test]
+fn signed_ext_watch_dummy_works() {
+ new_test_ext().execute_with(|| {
+ let call = pallet_example_basic::Call::set_dummy { new_value: 10 }.into();
+ let info = DispatchInfo::default();
+
+ assert_eq!(
+ WatchDummy::(PhantomData)
+ .validate(&1, &call, &info, 150)
+ .unwrap()
+ .priority,
+ u64::MAX,
+ );
+ assert_eq!(
+ WatchDummy::(PhantomData).validate(&1, &call, &info, 250),
+ InvalidTransaction::ExhaustsResources.into(),
+ );
+ })
+}
+
+#[test]
+fn counted_map_works() {
+ new_test_ext().execute_with(|| {
+ assert_eq!(CountedMap::::count(), 0);
+ CountedMap::::insert(3, 3);
+ assert_eq!(CountedMap::::count(), 1);
+ })
+}
+
+#[test]
+fn weights_work() {
+ // must have a defined weight.
+ let default_call = pallet_example_basic::Call::::accumulate_dummy { increase_by: 10 };
+ let info1 = default_call.get_dispatch_info();
+ // aka. `let info = as GetDispatchInfo>::get_dispatch_info(&default_call);`
+ assert!(info1.weight > 0);
+
+ // `set_dummy` is simpler than `accumulate_dummy`, and the weight
+ // should be less.
+ let custom_call = pallet_example_basic::Call::::set_dummy { new_value: 20 };
+ let info2 = custom_call.get_dispatch_info();
+ assert!(info1.weight > info2.weight);
+}
diff --git a/frame/fast-unstake/src/weights.rs b/frame/fast-unstake/src/weights.rs
new file mode 100644
index 0000000000000..5fc6434e396eb
--- /dev/null
+++ b/frame/fast-unstake/src/weights.rs
@@ -0,0 +1,101 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2021-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.
+
+//! Autogenerated weights for pallet_example_basic
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
+//! DATE: 2021-03-15, STEPS: `[100, ]`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128
+
+// Executed Command:
+// ./target/release/substrate
+// benchmark
+// --chain
+// dev
+// --execution
+// wasm
+// --wasm-execution
+// compiled
+// --pallet
+// pallet_example_basic
+// --extrinsic
+// *
+// --steps
+// 100
+// --repeat
+// 10
+// --raw
+// --output
+// ./
+// --template
+// ./.maintain/frame-weight-template.hbs
+
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use sp_std::marker::PhantomData;
+
+/// Weight functions needed for pallet_example_basic.
+pub trait WeightInfo {
+ fn set_dummy_benchmark(b: u32, ) -> Weight;
+ fn accumulate_dummy(b: u32, ) -> Weight;
+ fn sort_vector(x: u32, ) -> Weight;
+}
+
+/// Weights for pallet_example_basic using the Substrate node and recommended hardware.
+pub struct SubstrateWeight(PhantomData);
+impl WeightInfo for SubstrateWeight {
+ fn set_dummy_benchmark(b: u32, ) -> Weight {
+ (5_834_000 as Weight)
+ .saturating_add((24_000 as Weight).saturating_mul(b as Weight))
+ .saturating_add(T::DbWeight::get().writes(1 as Weight))
+ }
+ fn accumulate_dummy(b: u32, ) -> Weight {
+ (51_353_000 as Weight)
+ .saturating_add((14_000 as Weight).saturating_mul(b as Weight))
+ .saturating_add(T::DbWeight::get().reads(1 as Weight))
+ .saturating_add(T::DbWeight::get().writes(1 as Weight))
+ }
+ fn sort_vector(x: u32, ) -> Weight {
+ (2_569_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((4_000 as Weight).saturating_mul(x as Weight))
+ }
+}
+
+// For backwards compatibility and tests
+impl WeightInfo for () {
+ fn set_dummy_benchmark(b: u32, ) -> Weight {
+ (5_834_000 as Weight)
+ .saturating_add((24_000 as Weight).saturating_mul(b as Weight))
+ .saturating_add(RocksDbWeight::get().writes(1 as Weight))
+ }
+ fn accumulate_dummy(b: u32, ) -> Weight {
+ (51_353_000 as Weight)
+ .saturating_add((14_000 as Weight).saturating_mul(b as Weight))
+ .saturating_add(RocksDbWeight::get().reads(1 as Weight))
+ .saturating_add(RocksDbWeight::get().writes(1 as Weight))
+ }
+ fn sort_vector(x: u32, ) -> Weight {
+ (2_569_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((4_000 as Weight).saturating_mul(x as Weight))
+ }
+}
From 960c06726bae6fab5d3cfce330aa6db447cc608d Mon Sep 17 00:00:00 2001
From: kianenigma
Date: Sun, 31 Jul 2022 17:48:23 +0100
Subject: [PATCH 03/81] Revert "add failing test for itamar"
This reverts commit bdd139989cb4dfa8cc6cc6b5ee470a711df32d25.
---
frame/staking/src/tests.rs | 15 ---------------
1 file changed, 15 deletions(-)
diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs
index 646645fdf56a0..d14d8c4a75f2e 100644
--- a/frame/staking/src/tests.rs
+++ b/frame/staking/src/tests.rs
@@ -5102,21 +5102,6 @@ fn proportional_ledger_slash_works() {
assert_eq!(LedgerSlashPerEra::get().0, 0);
assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(6, 30), (7, 30)]));
- // Given
- ledger.unlocking = bounded_vec![c(4, 100), c(5, 100), c(6, 100), c(7, 100)];
- ledger.total = 4 * 100;
- ledger.active = 0;
- // When the first 2 chunks don't overlap with the affected range of unlock eras.
- assert_eq!(ledger.slash(15, 0, 3), 15);
- // Then
- assert_eq!(ledger.unlocking, vec![c(4, 100), c(5, 99), c(6, 100 - 7), c(7, 100 - 7)]);
- // ISSUE: The sum of everything we round down is affecting chunk 5, which should have ideally
- // remained unchanged.
- assert_eq!(ledger.total, 4 * 100 - 15);
- assert_eq!(LedgerSlashPerEra::get().0, 0);
- assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(5, 99), (6, 93), (7, 93)]));
- panic!();
-
// Given
ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
ledger.active = 500;
From 85fb600d402a7e49b3baef5876e44a4c69a9c4c7 Mon Sep 17 00:00:00 2001
From: kianenigma
Date: Tue, 2 Aug 2022 19:49:03 +0100
Subject: [PATCH 04/81] fast unstake wip
---
bin/node/runtime/src/lib.rs | 2 +-
frame/fast-unstake/src/lib.rs | 66 +++++++++++++++++++++++------------
frame/staking/src/lib.rs | 2 +-
3 files changed, 46 insertions(+), 24 deletions(-)
diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs
index 2ac1e6444f119..0b1bb07aef1b8 100644
--- a/bin/node/runtime/src/lib.rs
+++ b/bin/node/runtime/src/lib.rs
@@ -1403,7 +1403,7 @@ impl pallet_assets::Config for Runtime {
type Balance = u128;
type AssetId = u32;
type Currency = Balances;
- type ForceOrigin = EnsureRoot;
+ type ForceOrigin = EnsureSigned;
type AssetDeposit = AssetDeposit;
type AssetAccountDeposit = ConstU128;
type MetadataDepositBase = MetadataDepositBase;
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index cd30e09969818..417449d6fa640 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -15,7 +15,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-// Ensure we're `no_std` when compiling for Wasm.
#![cfg_attr(not(feature = "std"), no_std)]
#[frame_support::pallet]
@@ -27,6 +26,7 @@ pub mod pallet {
use frame_support::traits::Currency;
use pallet_nomination_pools::PoolId;
use sp_staking::EraIndex;
+ use sp_runtime::traits::Saturating;
type BalanceOf = <::Currency as Currency<
::AccountId,
@@ -37,12 +37,15 @@ pub mod pallet {
#[pallet::config]
pub trait Config:
- frame_system::Config + pallet_staking::Config + pallet_nomination_pools::Config
+ frame_system::Config
+ + pallet_staking::Config<
+ CurrencyBalance = ::CurrencyBalance,
+ > + pallet_nomination_pools::Config
{
type SlashPerEra: Get>;
}
- #[derive(Encode, Decode, Eq, PartialEq, Clone)]
+ #[derive(Encode, Decode, Eq, PartialEq, Clone, scale_info::TypeInfo)]
pub struct Unstake {
stash: AccountId,
checked: Vec,
@@ -70,21 +73,32 @@ pub mod pallet {
impl Pallet {
#[pallet::weight(0)]
pub fn enqueue(origin: OriginFor, pool_id: PoolId) -> DispatchResult {
+ // TODO: they must not already have any unbonding funds.
let who = ensure_signed(origin)?;
+ todo!();
}
}
impl Pallet {
fn process_head() {
+ let eras_to_check = ErasToCheckPerBlock::::get();
let maybe_next = Head::::take().or_else(|| {
- Queue::::drain().take(1).map(|(stash, pool_id)| Unstake { stash, pool_id, checked: Default::default() }).next()
+ Queue::::drain()
+ .take(1)
+ .map(|(stash, pool_id)| Unstake { stash, pool_id, checked: Default::default() })
+ .next()
});
- let Unstake { stash, checked, pool_id } = match maybe_next {
+ let Unstake { stash, mut checked, pool_id } = match maybe_next {
None => return,
Some(x) => x,
};
+ let slash_stash = |eras_checked: EraIndex| {
+ let slash_amount = T::SlashPerEra::get().saturating_mul(eras_checked.into());
+ let (_, imbalance) = ::Currency::slash(&stash, slash_amount);
+ };
+
let current_era = pallet_staking::CurrentEra::::get().unwrap_or_default();
let bonding_duration = ::BondingDuration::get();
@@ -94,30 +108,38 @@ pub mod pallet {
let now_check_range = total_check_range
.iter()
.filter(|e| !checked.contains(e))
- .take(ErasToCheckPerBlock::::get() as usize)
+ .take(eras_to_check as usize)
.collect::>();
- if now_check_range.is_empty() {
+ if now_check_range.is_empty() && eras_to_check > 0 {
// `stash` is not exposed in any era -- we can let go of them now.
let num_slashing_spans = 0; // TODO
- let ctrl = pallet_staking::Bonded::::get(stash).unwrap();
+ let ctrl = pallet_staking::Bonded::::get(&stash).unwrap();
let ledger = pallet_staking::Ledger::::get(ctrl).unwrap();
- pallet_staking::Pallet::::force_unstake(Origin::Root, stash, num_slashing_spans)
- .unwrap();
- pallet_nomination_pools::Pallet::::join(Origin::Signed(stash), ledger.total, pool_id);
- }
-
- let is_exposed = now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
-
- if is_exposed {
- // this account was actually exposed in some era within the range -- slash them and
- // remove them from the queue.
- // TODO: slash
+ pallet_staking::Pallet::::force_unstake(
+ frame_system::RawOrigin::Root.into(),
+ stash.clone(),
+ num_slashing_spans,
+ )
+ .unwrap();
+ pallet_nomination_pools::Pallet::::join(
+ frame_system::RawOrigin::Signed(stash.clone()).into(),
+ ledger.total,
+ pool_id,
+ ).unwrap();
} else {
- // Not exposed in these two eras.
- checked.extend(now_check_range);
- Head::::put(Unstake { stash, checked, pool_id });
+ let is_exposed = now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
+ if is_exposed {
+ // this account was actually exposed in some era within the range -- slash them and
+ // remove them from the queue.
+ // TODO: slash
+ } else {
+ // Not exposed in these two eras.
+ checked.extend(now_check_range);
+ Head::::put(Unstake { stash, checked, pool_id });
+ }
}
+
}
fn is_exposed_in_era(who: &T::AccountId, era: &EraIndex) -> bool {
diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs
index ab0ab685e6911..f58bcc3db71c1 100644
--- a/frame/staking/src/lib.rs
+++ b/frame/staking/src/lib.rs
@@ -552,7 +552,7 @@ impl StakingLedger {
///
/// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash was
/// applied.
- fn slash(
+ pub fn slash(
&mut self,
slash_amount: BalanceOf,
minimum_balance: BalanceOf,
From 87a712eb3d9034c7eb5bd493cffef22180920c59 Mon Sep 17 00:00:00 2001
From: kianenigma
Date: Fri, 19 Aug 2022 15:18:43 +0430
Subject: [PATCH 05/81] clean it up a bit
---
frame/fast-unstake/src/lib.rs | 60 +++++++++++++++++++++++++++++------
1 file changed, 50 insertions(+), 10 deletions(-)
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index 417449d6fa640..ad5a788820fe5 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -15,6 +15,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//! A pallet that's designed to ONLY do:
+//!
+//! If a nominator is not exposed at all in any `ErasStakers` (i.e. "has not backed any validators in the last 28 days"), then they can register themselves in this pallet, and move quickly into a nomination pool.
+
#![cfg_attr(not(feature = "std"), no_std)]
#[frame_support::pallet]
@@ -45,20 +49,31 @@ pub mod pallet {
type SlashPerEra: Get>;
}
+ /// One who wishes to be unstaked.
#[derive(Encode, Decode, Eq, PartialEq, Clone, scale_info::TypeInfo)]
pub struct Unstake {
+ /// Their stash account.
stash: AccountId,
+ /// The list of eras for which they have been checked.
checked: Vec,
+ /// The pool they wish to join.
pool_id: PoolId,
}
+ /// The current "head of the queue" being unstaked.
#[pallet::storage]
#[pallet::unbounded]
pub type Head = StorageValue<_, Unstake, OptionQuery>;
+ /// The map of all accounts wishing to be unstaked.
#[pallet::storage]
pub type Queue = StorageMap<_, Twox64Concat, T::AccountId, PoolId>;
+ /// Number of eras to check per block.
+ ///
+ /// If set to 0, this pallet does absolutely nothing.
+ ///
+ /// Based on the amount of weight available ot `on_idle`, up to this many eras of a single nominator might be checked.
#[pallet::storage]
pub type ErasToCheckPerBlock = StorageValue<_, u32, ValueQuery>;
@@ -71,29 +86,52 @@ pub mod pallet {
#[pallet::call]
impl Pallet {
+ /// enqueue oneself to be migrated from
#[pallet::weight(0)]
pub fn enqueue(origin: OriginFor, pool_id: PoolId) -> DispatchResult {
- // TODO: they must not already have any unbonding funds.
+ // TODO: they must not already have any unbonding funds, i.e. ledger.unlocking
+ // TODO: they should not be able to perform any actions in staking anymore once they are enqueued.. this might be a bit nasty. Should use a custom signed extension.
let who = ensure_signed(origin)?;
todo!();
}
}
impl Pallet {
- fn process_head() {
- let eras_to_check = ErasToCheckPerBlock::::get();
- let maybe_next = Head::::take().or_else(|| {
+ fn get_unstake_head() -> Option> {
+ Head::::take().or_else(|| {
Queue::::drain()
.take(1)
.map(|(stash, pool_id)| Unstake { stash, pool_id, checked: Default::default() })
.next()
- });
+ })
+ }
+ /// process up to `remaining_weight`.
+ ///
+ /// Returns the actual weight consumed.
+ fn process_head(remaining_weight: Weight) -> Weight {
+ let get_unstake_head_weight = T::DbWeight::get().reads(2);
+ if remaining_weight < get_unstake_head_weight {
+ // nothing can be done.
+ return 0;
+ }
+
- let Unstake { stash, mut checked, pool_id } = match maybe_next {
- None => return,
+ let Unstake { stash, mut checked, pool_id } = match Self::get_unstake_head() {
+ None => {
+ // There's no `Head` and nothing in the `Queue`, nothing to do here.
+ return get_unstake_head_weight;
+ },
Some(x) => x,
};
+ let weight_per_era_check = todo!("should come from our benchmarks");
+ let max_eras_to_check = remaining_weight.div(weight_per_era_check);
+ let final_eras_to_check = ErasToCheckPerBlock::::get().min(max_eras_to_check);
+
+ if final_eras_to_check.is_zero() {
+ return get_unstake_head_weight + T::DbWeight::get().reads(1)
+ }
+
let slash_stash = |eras_checked: EraIndex| {
let slash_amount = T::SlashPerEra::get().saturating_mul(eras_checked.into());
let (_, imbalance) = ::Currency::slash(&stash, slash_amount);
@@ -108,10 +146,10 @@ pub mod pallet {
let now_check_range = total_check_range
.iter()
.filter(|e| !checked.contains(e))
- .take(eras_to_check as usize)
+ .take(final_eras_to_check as usize)
.collect::>();
- if now_check_range.is_empty() && eras_to_check > 0 {
+ if now_check_range.is_empty() && final_eras_to_check > 0 {
// `stash` is not exposed in any era -- we can let go of them now.
let num_slashing_spans = 0; // TODO
let ctrl = pallet_staking::Bonded::::get(&stash).unwrap();
@@ -127,19 +165,21 @@ pub mod pallet {
ledger.total,
pool_id,
).unwrap();
+ 0 // TODO return weight, should be the weight of the code in this `if`
} else {
let is_exposed = now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
if is_exposed {
// this account was actually exposed in some era within the range -- slash them and
// remove them from the queue.
// TODO: slash
+ 0 // TODO: return weight, should be 'now_check_range.count() * weight_per_era_check + slash_weight'
} else {
// Not exposed in these two eras.
checked.extend(now_check_range);
Head::::put(Unstake { stash, checked, pool_id });
+ 0 // TODO: return weight, should be 'now_check_range.count() * weight_per_era_check'
}
}
-
}
fn is_exposed_in_era(who: &T::AccountId, era: &EraIndex) -> bool {
From 0449aab7af8a41e5e0aace69c49e8f9ae3c82b1b Mon Sep 17 00:00:00 2001
From: Ross Bulat
Date: Mon, 22 Aug 2022 10:51:15 +0100
Subject: [PATCH 06/81] some comments
---
frame/fast-unstake/src/lib.rs | 69 ++++++++++++++++++++++++++---------
1 file changed, 52 insertions(+), 17 deletions(-)
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index ad5a788820fe5..28339d14de638 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -29,8 +29,8 @@ pub mod pallet {
use frame_support::traits::Currency;
use pallet_nomination_pools::PoolId;
- use sp_staking::EraIndex;
use sp_runtime::traits::Saturating;
+ use sp_staking::EraIndex;
type BalanceOf = <::Currency as Currency<
::AccountId,
@@ -49,7 +49,7 @@ pub mod pallet {
type SlashPerEra: Get>;
}
- /// One who wishes to be unstaked.
+ /// One who wishes to be unstaked and join a pool.
#[derive(Encode, Decode, Eq, PartialEq, Clone, scale_info::TypeInfo)]
pub struct Unstake {
/// Their stash account.
@@ -61,11 +61,13 @@ pub mod pallet {
}
/// The current "head of the queue" being unstaked.
+ /// The leading `Unstake` item due to be processed for unstaking.
#[pallet::storage]
#[pallet::unbounded]
pub type Head = StorageValue<_, Unstake, OptionQuery>;
/// The map of all accounts wishing to be unstaked.
+ /// Points the `AccountId` wishing to unstake to the `PoolId` they wish to join thereafter.
#[pallet::storage]
pub type Queue = StorageMap<_, Twox64Concat, T::AccountId, PoolId>;
@@ -73,30 +75,50 @@ pub mod pallet {
///
/// If set to 0, this pallet does absolutely nothing.
///
- /// Based on the amount of weight available ot `on_idle`, up to this many eras of a single nominator might be checked.
+ /// Based on the amount of weight available at `on_idle`, up to this many eras of a single
+ /// nominator might be checked.
#[pallet::storage]
pub type ErasToCheckPerBlock = StorageValue<_, u32, ValueQuery>;
+ /// TODO (TODISCUSS?): Could we introduce another storage item to streamline the checking of
+ /// exposure in eras? Aim: to speed up `is_exposed_in_era()`.
+ /// Could introduce `HistoricalNominationsEras` storage item that was mentioned here:
+ /// https://github.com/paritytech/substrate/issues/8436#issuecomment-1212962043
+ /// Note: this would require us to append Eras when user `nominate`s (& rm Eras past
+ /// HISTORY_DEPTH) #[pallet::storage]
+ // pub type HistoricalNominatorEras = StorageMap<_, Twox64Concat, T::AccountId,
+ // Vec>;
+
#[pallet::hooks]
impl Hooks for Pallet {
fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight {
+ /// TODO: iterate remaining weight and process outstanding `Unstake` requests.
+ /// -> Start and Head
+ /// -> Process entries of Queue as long as remaining_weight allows
+ /// -> update Head
0
}
}
#[pallet::call]
impl Pallet {
- /// enqueue oneself to be migrated from
+ /// enqueue oneself to be migrated from.
#[pallet::weight(0)]
pub fn enqueue(origin: OriginFor, pool_id: PoolId) -> DispatchResult {
- // TODO: they must not already have any unbonding funds, i.e. ledger.unlocking
- // TODO: they should not be able to perform any actions in staking anymore once they are enqueued.. this might be a bit nasty. Should use a custom signed extension.
+ todo!("assert not already in Queue.");
+ todo!("assert must be `bonded` (have actively bonded funds) to unstake.");
+ todo!("they must not already have any unbonding funds, i.e. ledger.unlocking");
+
+ // TODO: they should not be able to perform any actions in staking anymore once they are
+ // enqueued. This might be a bit nasty. Should use a custom signed extension.
let who = ensure_signed(origin)?;
todo!();
}
}
impl Pallet {
+ /// Gets the first item of `Queue` or the `Head` Unstake entries if present.
+ /// Returns `None` if no entries present.
fn get_unstake_head() -> Option> {
Head::::take().or_else(|| {
Queue::::drain()
@@ -112,37 +134,42 @@ pub mod pallet {
let get_unstake_head_weight = T::DbWeight::get().reads(2);
if remaining_weight < get_unstake_head_weight {
// nothing can be done.
- return 0;
+ return 0
}
-
let Unstake { stash, mut checked, pool_id } = match Self::get_unstake_head() {
None => {
// There's no `Head` and nothing in the `Queue`, nothing to do here.
- return get_unstake_head_weight;
+ return get_unstake_head_weight
},
Some(x) => x,
};
+ // determine the amount of eras to check.
let weight_per_era_check = todo!("should come from our benchmarks");
let max_eras_to_check = remaining_weight.div(weight_per_era_check);
let final_eras_to_check = ErasToCheckPerBlock::::get().min(max_eras_to_check);
+ // return weight consumed if no eras to check (1 read).
if final_eras_to_check.is_zero() {
return get_unstake_head_weight + T::DbWeight::get().reads(1)
}
let slash_stash = |eras_checked: EraIndex| {
let slash_amount = T::SlashPerEra::get().saturating_mul(eras_checked.into());
- let (_, imbalance) = ::Currency::slash(&stash, slash_amount);
+ let (_, imbalance) =
+ ::Currency::slash(&stash, slash_amount);
};
let current_era = pallet_staking::CurrentEra::::get().unwrap_or_default();
let bonding_duration = ::BondingDuration::get();
+ // get the last available `bonding_duration` eras up to current era in reverse order.
let total_check_range = (current_era.saturating_sub(bonding_duration)..current_era)
.rev()
.collect::>();
+
+ // remove eras that do not exist in `checked`.
let now_check_range = total_check_range
.iter()
.filter(|e| !checked.contains(e))
@@ -164,24 +191,32 @@ pub mod pallet {
frame_system::RawOrigin::Signed(stash.clone()).into(),
ledger.total,
pool_id,
- ).unwrap();
- 0 // TODO return weight, should be the weight of the code in this `if`
+ )
+ .unwrap();
+ // TODO return weight, should be the weight of the code in this `if`.
+ // weight(nomination_pools.join) + weight(staking.force_unstake) + 2(read)
+ 0
} else {
- let is_exposed = now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
+ // eras remaining to be checked.
+ let is_exposed =
+ now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
if is_exposed {
- // this account was actually exposed in some era within the range -- slash them and
- // remove them from the queue.
+ // this account was actually exposed in some era within the range -- slash them
+ // and remove them from the queue.
// TODO: slash
- 0 // TODO: return weight, should be 'now_check_range.count() * weight_per_era_check + slash_weight'
+ 0 // TODO: return weight, should be 'now_check_range.count() *
+ // weight_per_era_check + slash_weight'
} else {
// Not exposed in these two eras.
checked.extend(now_check_range);
Head::::put(Unstake { stash, checked, pool_id });
- 0 // TODO: return weight, should be 'now_check_range.count() * weight_per_era_check'
+ 0 // TODO: return weight, should be 'now_check_range.count() *
+ // weight_per_era_check'
}
}
}
+ /// Checks whether an account `who` has been exposed in an era.
fn is_exposed_in_era(who: &T::AccountId, era: &EraIndex) -> bool {
pallet_staking::ErasStakers::::iter_prefix(era)
.any(|(_, exposures)| exposures.others.iter().any(|i| i.who == *who))
From be760571a08d54f11ab931a3696d2446f7efca84 Mon Sep 17 00:00:00 2001
From: Ross Bulat
Date: Mon, 22 Aug 2022 11:21:13 +0100
Subject: [PATCH 07/81] on_idle logic
---
frame/fast-unstake/src/lib.rs | 31 +++++++++++++++++++------------
1 file changed, 19 insertions(+), 12 deletions(-)
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index 28339d14de638..d056611e3d9c3 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -29,9 +29,11 @@ pub mod pallet {
use frame_support::traits::Currency;
use pallet_nomination_pools::PoolId;
- use sp_runtime::traits::Saturating;
+ use sp_runtime::traits::{Saturating, Zero};
use sp_staking::EraIndex;
+ use sp_std::{ops::Div, vec::Vec};
+
type BalanceOf = <::Currency as Currency<
::AccountId,
>>::Balance;
@@ -92,11 +94,18 @@ pub mod pallet {
#[pallet::hooks]
impl Hooks for Pallet {
fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight {
- /// TODO: iterate remaining weight and process outstanding `Unstake` requests.
- /// -> Start and Head
- /// -> Process entries of Queue as long as remaining_weight allows
- /// -> update Head
- 0
+ // We'll call `process_head` until 0 weight is returned
+ let mut remaining = remaining_weight;
+ loop {
+ // process head and
+ let last_consumed_weight = Self::process_head(remaining_weight);
+ // if nothing was done, break loop
+ if last_consumed_weight == Weight::from(0 as u64) {
+ break
+ }
+ remaining = remaining.saturating_sub(last_consumed_weight);
+ }
+ remaining
}
}
@@ -146,9 +155,9 @@ pub mod pallet {
};
// determine the amount of eras to check.
- let weight_per_era_check = todo!("should come from our benchmarks");
+ let weight_per_era_check: Weight = todo!("should come from our benchmarks");
let max_eras_to_check = remaining_weight.div(weight_per_era_check);
- let final_eras_to_check = ErasToCheckPerBlock::::get().min(max_eras_to_check);
+ let final_eras_to_check = ErasToCheckPerBlock::::get().min(max_eras_to_check as u32);
// return weight consumed if no eras to check (1 read).
if final_eras_to_check.is_zero() {
@@ -157,8 +166,7 @@ pub mod pallet {
let slash_stash = |eras_checked: EraIndex| {
let slash_amount = T::SlashPerEra::get().saturating_mul(eras_checked.into());
- let (_, imbalance) =
- ::Currency::slash(&stash, slash_amount);
+ let (_, imbalance) = ::Currency::slash(&stash, slash_amount);
};
let current_era = pallet_staking::CurrentEra::::get().unwrap_or_default();
@@ -198,8 +206,7 @@ pub mod pallet {
0
} else {
// eras remaining to be checked.
- let is_exposed =
- now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
+ let is_exposed = now_check_range.iter().any(|e| Self::is_exposed_in_era(&stash, *e));
if is_exposed {
// this account was actually exposed in some era within the range -- slash them
// and remove them from the queue.
From 6afd4ac3e80ebbcb4fffbd419f75cb3da17744ff Mon Sep 17 00:00:00 2001
From: Ross Bulat
Date: Mon, 22 Aug 2022 11:22:32 +0100
Subject: [PATCH 08/81] fix
---
frame/fast-unstake/src/lib.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index d056611e3d9c3..942f6bb7480ce 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -98,7 +98,7 @@ pub mod pallet {
let mut remaining = remaining_weight;
loop {
// process head and
- let last_consumed_weight = Self::process_head(remaining_weight);
+ let last_consumed_weight = Self::process_head(remaining);
// if nothing was done, break loop
if last_consumed_weight == Weight::from(0 as u64) {
break
From 91f047a3b61f9eda45a90f5bc20caaf0c17bc813 Mon Sep 17 00:00:00 2001
From: Ross Bulat
Date: Mon, 22 Aug 2022 11:27:24 +0100
Subject: [PATCH 09/81] comment
---
frame/fast-unstake/src/lib.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index 942f6bb7480ce..99bc3b0dee1c0 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -177,7 +177,7 @@ pub mod pallet {
.rev()
.collect::>();
- // remove eras that do not exist in `checked`.
+ // remove eras that exist in `checked`.
let now_check_range = total_check_range
.iter()
.filter(|e| !checked.contains(e))
From 548b5ccbe57fbb0f2de371e0688903740d5ae187 Mon Sep 17 00:00:00 2001
From: kianenigma
Date: Mon, 22 Aug 2022 18:28:05 +0430
Subject: [PATCH 10/81] new working version, checks all pass, looking good
---
Cargo.lock | 2 +-
.../election-provider-multi-phase/src/lib.rs | 11 +
frame/election-provider-support/src/lib.rs | 9 +-
.../election-provider-support/src/onchain.rs | 8 +
frame/fast-unstake/Cargo.toml | 21 +-
frame/fast-unstake/src/lib.rs | 429 +++++++++++++-----
frame/fast-unstake/src/tests.rs | 75 +--
frame/fast-unstake/src/weights.rs | 101 -----
frame/staking/src/pallet/mod.rs | 1 +
9 files changed, 379 insertions(+), 278 deletions(-)
delete mode 100644 frame/fast-unstake/src/weights.rs
diff --git a/Cargo.lock b/Cargo.lock
index 8f60f38f40c7a..886c584f4a113 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5686,10 +5686,10 @@ name = "pallet-fast-unstake"
version = "4.0.0-dev"
dependencies = [
"frame-benchmarking",
+ "frame-election-provider-support",
"frame-support",
"frame-system",
"log",
- "pallet-balances",
"pallet-nomination-pools",
"pallet-staking",
"parity-scale-codec",
diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs
index e1d3cb8ed5dee..e12e6f398d79b 100644
--- a/frame/election-provider-multi-phase/src/lib.rs
+++ b/frame/election-provider-multi-phase/src/lib.rs
@@ -318,6 +318,10 @@ impl ElectionProvider for NoFallback {
type DataProvider = T::DataProvider;
type Error = &'static str;
+ fn ongoing() -> bool {
+ false
+ }
+
fn elect() -> Result, Self::Error> {
// Do nothing, this will enable the emergency phase.
Err("NoFallback.")
@@ -1572,6 +1576,13 @@ impl ElectionProvider for Pallet {
type Error = ElectionError;
type DataProvider = T::DataProvider;
+ fn ongoing() -> bool {
+ match Self::current_phase() {
+ Phase::Off => false,
+ _ => true,
+ }
+ }
+
fn elect() -> Result, Self::Error> {
match Self::do_elect() {
Ok(supports) => {
diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs
index eee865d0b737b..f43f8db1700b9 100644
--- a/frame/election-provider-support/src/lib.rs
+++ b/frame/election-provider-support/src/lib.rs
@@ -136,7 +136,7 @@
//! type BlockNumber = BlockNumber;
//! type Error = &'static str;
//! type DataProvider = T::DataProvider;
-//!
+//! fn ongoing() -> bool { false }
//! fn elect() -> Result, Self::Error> {
//! Self::DataProvider::electable_targets(None)
//! .map_err(|_| "failed to elect")
@@ -370,6 +370,9 @@ pub trait ElectionProvider {
BlockNumber = Self::BlockNumber,
>;
+ /// Indicate if this election provider is currently ongoing an asynchronous election or not.
+ fn ongoing() -> bool;
+
/// Elect a new set of winners, without specifying any bounds on the amount of data fetched from
/// [`Self::DataProvider`]. An implementation could nonetheless impose its own custom limits.
///
@@ -420,6 +423,10 @@ where
fn elect() -> Result, Self::Error> {
Err(" cannot do anything.")
}
+
+ fn ongoing() -> bool {
+ false
+ }
}
/// A utility trait for something to implement `ElectionDataProvider` in a sensible way.
diff --git a/frame/election-provider-support/src/onchain.rs b/frame/election-provider-support/src/onchain.rs
index 62e76c3888822..c1d8ecb3b230b 100644
--- a/frame/election-provider-support/src/onchain.rs
+++ b/frame/election-provider-support/src/onchain.rs
@@ -138,6 +138,10 @@ impl ElectionProvider for UnboundedExecution {
type Error = Error;
type DataProvider = T::DataProvider;
+ fn ongoing() -> bool {
+ false
+ }
+
fn elect() -> Result, Self::Error> {
// This should not be called if not in `std` mode (and therefore neither in genesis nor in
// testing)
@@ -167,6 +171,10 @@ impl ElectionProvider for BoundedExecution {
type Error = Error;
type DataProvider = T::DataProvider;
+ fn ongoing() -> bool {
+ false
+ }
+
fn elect() -> Result, Self::Error> {
elect_with::(Some(T::VotersBound::get() as usize), Some(T::TargetsBound::get() as usize))
}
diff --git a/frame/fast-unstake/Cargo.toml b/frame/fast-unstake/Cargo.toml
index 1b435559de6e1..eb5971b5ee836 100644
--- a/frame/fast-unstake/Cargo.toml
+++ b/frame/fast-unstake/Cargo.toml
@@ -16,16 +16,20 @@ targets = ["x86_64-unknown-linux-gnu"]
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-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" }
+
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
-pallet-balances = { version = "4.0.0-dev", default-features = false, path = "../balances" }
+
sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" }
sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" }
sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" }
sp-staking = { default-features = false, path = "../../primitives/staking" }
+
pallet-staking = { default-features = false, path = "../staking" }
pallet-nomination-pools = { default-features = false, path = "../nomination-pools" }
+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]
sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" }
@@ -34,17 +38,22 @@ sp-core = { version = "6.0.0", default-features = false, path = "../../primitive
default = ["std"]
std = [
"codec/std",
- "frame-benchmarking/std",
- "frame-support/std",
- "frame-system/std",
"log/std",
- "pallet-balances/std",
"scale-info/std",
+
+ "frame-support/std",
+ "frame-system/std",
+
"sp-io/std",
"sp-staking/std",
"sp-runtime/std",
"sp-std/std",
+
"pallet-staking/std",
+ "pallet-nomination-pools/std",
+ "frame-election-provider-support/std",
+
+ "frame-benchmarking/std",
]
runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"]
try-runtime = ["frame-support/try-runtime"]
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index 942f6bb7480ce..8d87d0428e5bd 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -17,23 +17,57 @@
//! A pallet that's designed to ONLY do:
//!
-//! If a nominator is not exposed at all in any `ErasStakers` (i.e. "has not backed any validators in the last 28 days"), then they can register themselves in this pallet, and move quickly into a nomination pool.
+//! If a nominator is not exposed at all in any `ErasStakers` (i.e. "has not backed any validators
+//! in the last 28 days"), then they can register themselves in this pallet, and move quickly into
+//! a nomination pool.
+//!
+//! This pallet works of the basis of `on_idle`, meaning that it provides no guarantee about when it
+//! will succeed, if at all.
+//!
+//! Stakers who are certain about NOT being exposed can register themselves with
+//! [`Call::register_fast_unstake`]. This will chill, and fully unbond the staker, and place them in
+//! the queue to be checked.
+//!
+//! Once queued, but not being actively processed, stakers can withdraw their request via
+//! [`Call::deregister`].
+//!
+//! Once processed, if successful, no additional fees for the checking process is taken, and the
+//! staker is instantly unbonded. Optionally, if the have asked to join a pool, their *entire* stake
+//! is joined into their pool of choice.
+//!
+//! If unsuccessful, meaning that the staker was exposed sometime in the last 28 eras they will end
+//! up being slashed for the amount of wasted work they have inflicted on the chian.
#![cfg_attr(not(feature = "std"), no_std)]
+pub use pallet::*;
+
#[frame_support::pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
- use frame_system::pallet_prelude::*;
+ use frame_system::{pallet_prelude::*, RawOrigin};
use sp_std::prelude::*;
- use frame_support::traits::Currency;
+ use frame_support::traits::{Currency, IsSubType};
use pallet_nomination_pools::PoolId;
- use sp_runtime::traits::{Saturating, Zero};
+ use sp_runtime::{
+ traits::{Saturating, Zero},
+ transaction_validity::{InvalidTransaction, TransactionValidityError},
+ DispatchResult,
+ };
use sp_staking::EraIndex;
+ use frame_election_provider_support::ElectionProvider;
+ use pallet_nomination_pools::WeightInfo as _;
+ use pallet_staking::{Pallet as Staking, WeightInfo as _};
+
use sp_std::{ops::Div, vec::Vec};
+ pub trait WeightInfo {
+ fn weight_per_era_check() -> Weight;
+ fn do_slash() -> Weight;
+ }
+
type BalanceOf = <::Currency as Currency<
::AccountId,
>>::Balance;
@@ -48,30 +82,43 @@ pub mod pallet {
CurrencyBalance = ::CurrencyBalance,
> + pallet_nomination_pools::Config
{
+ /// The overarching event type.
+ type Event: From> + IsType<::Event>;
+
+ /// The amount of balance slashed per each era that was wastefully checked.
+ ///
+ /// A reasonable value could be `runtime_weight_to_fee(weight_per_era_check)`.
type SlashPerEra: Get>;
+
+ /// The origin that can control this pallet.
+ type ControlOrigin: frame_support::traits::EnsureOrigin;
+
+ /// The weight information of this pallet.
+ type WeightInfo: WeightInfo;
}
- /// One who wishes to be unstaked and join a pool.
+ /// An unstake request.
#[derive(Encode, Decode, Eq, PartialEq, Clone, scale_info::TypeInfo)]
- pub struct Unstake {
+ pub struct UnstakeRequest {
/// Their stash account.
stash: AccountId,
/// The list of eras for which they have been checked.
checked: Vec,
- /// The pool they wish to join.
- pool_id: PoolId,
+ /// The pool they wish to join, if any.
+ maybe_pool_id: Option,
}
/// The current "head of the queue" being unstaked.
- /// The leading `Unstake` item due to be processed for unstaking.
#[pallet::storage]
#[pallet::unbounded]
- pub type Head = StorageValue<_, Unstake, OptionQuery>;
+ pub type Head = StorageValue<_, UnstakeRequest, OptionQuery>;
/// The map of all accounts wishing to be unstaked.
- /// Points the `AccountId` wishing to unstake to the `PoolId` they wish to join thereafter.
+ ///
+ /// Points the `AccountId` wishing to unstake to the optional `PoolId` they wish to join
+ /// thereafter.
#[pallet::storage]
- pub type Queue = StorageMap<_, Twox64Concat, T::AccountId, PoolId>;
+ pub type Queue = StorageMap<_, Twox64Concat, T::AccountId, Option>;
/// Number of eras to check per block.
///
@@ -82,145 +129,253 @@ pub mod pallet {
#[pallet::storage]
pub type ErasToCheckPerBlock = StorageValue<_, u32, ValueQuery>;
- /// TODO (TODISCUSS?): Could we introduce another storage item to streamline the checking of
- /// exposure in eras? Aim: to speed up `is_exposed_in_era()`.
- /// Could introduce `HistoricalNominationsEras` storage item that was mentioned here:
- /// https://github.com/paritytech/substrate/issues/8436#issuecomment-1212962043
- /// Note: this would require us to append Eras when user `nominate`s (& rm Eras past
- /// HISTORY_DEPTH) #[pallet::storage]
- // pub type HistoricalNominatorEras = StorageMap<_, Twox64Concat, T::AccountId,
- // Vec>;
+ /// The events of this pallet.
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event {
+ /// A staker was unstaked.
+ Unstaked { stash: T::AccountId, maybe_pool_id: Option, result: DispatchResult },
+ /// A staker was slashed for requesting fast-unstake whilst being exposed.
+ Slashed { stash: T::AccountId, amount: BalanceOf },
+ /// A staker was partially checked for the given eras, but the process did not finish.
+ Checked { stash: T::AccountId, eras: Vec },
+ }
#[pallet::hooks]
impl Hooks for Pallet {
fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight {
- // We'll call `process_head` until 0 weight is returned
- let mut remaining = remaining_weight;
- loop {
- // process head and
- let last_consumed_weight = Self::process_head(remaining);
- // if nothing was done, break loop
- if last_consumed_weight == Weight::from(0 as u64) {
- break
- }
- remaining = remaining.saturating_sub(last_consumed_weight);
- }
- remaining
+ Self::process_head(remaining_weight)
}
}
#[pallet::call]
impl Pallet {
- /// enqueue oneself to be migrated from.
+ /// Register oneself for fast-unstake.
+ ///
+ /// The dispatch origin of this call must be signed by the controller account, similar to
+ /// `staking::unbond`.
+ ///
+ /// The stash associated with the origin must have no ongoing unlocking chunks. If
+ /// successful, this will fully unbond and chill the stash. Then, it will enqueue the stash
+ /// to be checked in further blocks.
+ ///
+ /// If by the time this is called, the stash is actually eligible for fast-unstake, then
+ /// they are guaranteed to remain eligible, because the call will chill them as well.
+ ///
+ /// If the check works, the entire staking data is removed, i.e. the stash is fully
+ /// unstaked, and they potentially join a pool with their entire bonded stake.
+ ///
+ /// If the check fails, the stash remains chilled and waiting for being unbonded as in with
+ /// the normal staking system, but they lose part of their unbonding chunks due to consuming
+ /// the chain's resources.
+ #[pallet::weight(0)]
+ pub fn register_fast_unstake(
+ origin: OriginFor,
+ maybe_pool_id: Option,
+ ) -> DispatchResult {
+ let ctrl = ensure_signed(origin)?;
+
+ let ledger = pallet_staking::Ledger::::get(&ctrl).ok_or("NotController")?;
+
+ ensure!(!Queue::::contains_key(&ledger.stash), "AlreadyQueued");
+ ensure!(
+ Head::::get().map_or(true, |UnstakeRequest { stash, .. }| stash != ledger.stash),
+ "AlreadyHead"
+ );
+ // second part of the && is defensive.
+ ensure!(ledger.active == ledger.total && ledger.unlocking.is_empty(), "NotFullyBonded");
+
+ // chill and fully unstake.
+ Staking::::chill(RawOrigin::Signed(ctrl.clone()).into())?;
+ Staking::::unbond(RawOrigin::Signed(ctrl).into(), ledger.total)?;
+
+ // enqueue them.
+ Queue::::insert(ledger.stash, maybe_pool_id);
+ Ok(())
+ }
+
+ /// Deregister oneself from the fast-unstake (and possibly joining a pool).
+ ///
+ /// This is useful id one is registered, they are still waiting, and they change their mind.
+ ///
+ /// Note that the associated stash is still fully unbonded and chilled as a consequence of
+ /// calling `register_fast_unstake`. This should probably be followed by a call to
+ /// `Staking::rebond`.
+ #[pallet::weight(0)]
+ pub fn deregister(origin: OriginFor) -> DispatchResult {
+ let ctrl = ensure_signed(origin)?;
+ let stash = pallet_staking::Ledger::::get(&ctrl)
+ .map(|l| l.stash)
+ .ok_or("NotController")?;
+ ensure!(Queue::::contains_key(&stash), "NotQueued");
+ ensure!(
+ Head::::get().map_or(true, |UnstakeRequest { stash, .. }| stash != stash),
+ "AlreadyHead"
+ );
+
+ Queue::::remove(stash);
+ Ok(())
+ }
+
+ /// Control the operation of this pallet.
+ ///
+ /// Dispatch origin must be signed by the [`Config::ControlOrigin`].
#[pallet::weight(0)]
- pub fn enqueue(origin: OriginFor, pool_id: PoolId) -> DispatchResult {
- todo!("assert not already in Queue.");
- todo!("assert must be `bonded` (have actively bonded funds) to unstake.");
- todo!("they must not already have any unbonding funds, i.e. ledger.unlocking");
-
- // TODO: they should not be able to perform any actions in staking anymore once they are
- // enqueued. This might be a bit nasty. Should use a custom signed extension.
- let who = ensure_signed(origin)?;
- todo!();
+ pub fn control(origin: OriginFor, eras_to_check: EraIndex) -> DispatchResult {
+ let _ = T::ControlOrigin::ensure_origin(origin)?;
+ ErasToCheckPerBlock::::put(eras_to_check);
+
+ Ok(())
}
}
impl Pallet {
- /// Gets the first item of `Queue` or the `Head` Unstake entries if present.
- /// Returns `None` if no entries present.
- fn get_unstake_head() -> Option> {
- Head::::take().or_else(|| {
- Queue::::drain()
- .take(1)
- .map(|(stash, pool_id)| Unstake { stash, pool_id, checked: Default::default() })
- .next()
- })
- }
/// process up to `remaining_weight`.
///
/// Returns the actual weight consumed.
fn process_head(remaining_weight: Weight) -> Weight {
let get_unstake_head_weight = T::DbWeight::get().reads(2);
if remaining_weight < get_unstake_head_weight {
+ // check that we have enough weight o read
// nothing can be done.
return 0
}
- let Unstake { stash, mut checked, pool_id } = match Self::get_unstake_head() {
+ if ::ElectionProvider::ongoing() {
+ // NOTE: we assume `ongoing` does not consume any weight.
+ // there is an ongoing election -- we better not do anything. Imagine someone is not
+ // exposed anywhere in the last era, and the snapshot for the election is already
+ // taken. In this time period, we don't want to accidentally unstake them.
+ return 0
+ }
+
+ let mut consumed_weight = 0;
+ let mut add_weight =
+ |amount: u64| consumed_weight = consumed_weight.saturating_add(amount);
+
+ let UnstakeRequest { stash, mut checked, maybe_pool_id } = match Head::::take()
+ .or_else(|| {
+ Queue::::drain()
+ .take(1)
+ .map(|(stash, maybe_pool_id)| UnstakeRequest {
+ stash,
+ maybe_pool_id,
+ checked: Default::default(),
+ })
+ .next()
+ }) {
None => {
// There's no `Head` and nothing in the `Queue`, nothing to do here.
return get_unstake_head_weight
},
- Some(x) => x,
+ Some(head) => {
+ add_weight(get_unstake_head_weight);
+ head
+ },
};
- // determine the amount of eras to check.
- let weight_per_era_check: Weight = todo!("should come from our benchmarks");
- let max_eras_to_check = remaining_weight.div(weight_per_era_check);
- let final_eras_to_check = ErasToCheckPerBlock::::get().min(max_eras_to_check as u32);
+ // determine the amount of eras to check. this is minimum of two criteria:
+ // `ErasToCheckPerBlock`, and how much weight is given to the on_idle hook. For the sake
+ // of simplicity, we assume we check at most one staker's eras per-block.
+ let final_eras_to_check = {
+ let weight_per_era_check: Weight =
+ ::WeightInfo::weight_per_era_check();
+ let eras_to_check_weight_limit = remaining_weight.div(weight_per_era_check);
+ add_weight(T::DbWeight::get().reads(1));
+ ErasToCheckPerBlock::::get().min(eras_to_check_weight_limit as u32)
+ };
- // return weight consumed if no eras to check (1 read).
+ // return weight consumed if no eras to check..
if final_eras_to_check.is_zero() {
- return get_unstake_head_weight + T::DbWeight::get().reads(1)
+ return consumed_weight
}
- let slash_stash = |eras_checked: EraIndex| {
- let slash_amount = T::SlashPerEra::get().saturating_mul(eras_checked.into());
- let (_, imbalance) = ::Currency::slash(&stash, slash_amount);
+ // the range that we're allowed to check in this round.
+ let current_era = pallet_staking::CurrentEra::::get().unwrap_or_default();
+ let eras_to_check = {
+ let bonding_duration = ::BondingDuration::get();
+ add_weight(T::DbWeight::get().reads(1));
+
+ // get the last available `bonding_duration` eras up to current era in reverse
+ // order.
+ let total_check_range = (current_era.saturating_sub(bonding_duration)..=
+ current_era)
+ .rev()
+ .collect::>();
+ debug_assert!(total_check_range.len() <= bonding_duration as usize);
+
+ // remove eras that have already been checked, take a maximum of
+ // final_eras_to_check.
+ total_check_range
+ .into_iter()
+ .filter(|e| !checked.contains(e))
+ .take(final_eras_to_check as usize)
+ .collect::>()
};
- let current_era = pallet_staking::CurrentEra::::get().unwrap_or_default();
- let bonding_duration = ::BondingDuration::get();
-
- // get the last available `bonding_duration` eras up to current era in reverse order.
- let total_check_range = (current_era.saturating_sub(bonding_duration)..current_era)
- .rev()
- .collect::>();
-
- // remove eras that do not exist in `checked`.
- let now_check_range = total_check_range
- .iter()
- .filter(|e| !checked.contains(e))
- .take(final_eras_to_check as usize)
- .collect::>();
-
- if now_check_range.is_empty() && final_eras_to_check > 0 {
- // `stash` is not exposed in any era -- we can let go of them now.
- let num_slashing_spans = 0; // TODO
+ if eras_to_check.is_empty() {
+ // `stash` is not exposed in any era now -- we can let go of them now.
+ let num_slashing_spans = Staking::::slashing_spans(&stash).iter().count() as u32;
let ctrl = pallet_staking::Bonded::::get(&stash).unwrap();
let ledger = pallet_staking::Ledger::::get(ctrl).unwrap();
- pallet_staking::Pallet::::force_unstake(
- frame_system::RawOrigin::Root.into(),
+
+ add_weight(