From 2c7960e8d2a8caaca8f67f01fe9f67c31ebee62f Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Sat, 26 Mar 2022 18:40:04 +0100 Subject: [PATCH 01/48] Add stepped curve for referenda --- frame/referenda/src/types.rs | 139 +++++++++++++++++------- primitives/arithmetic/src/per_things.rs | 17 +++ 2 files changed, 119 insertions(+), 37 deletions(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 622075100631b..60e39684ed45a 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -21,7 +21,7 @@ use super::*; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{traits::schedule::Anon, Parameter}; use scale_info::TypeInfo; -use sp_runtime::RuntimeDebug; +use sp_runtime::{RuntimeDebug, PerThing}; use sp_std::fmt::Debug; pub type BalanceOf = @@ -91,40 +91,6 @@ impl> InsertSorted for BoundedVec { } } -#[cfg(test)] -mod tests { - use super::*; - use frame_support::traits::ConstU32; - - #[test] - fn insert_sorted_works() { - let mut b: BoundedVec> = vec![20, 30, 40].try_into().unwrap(); - assert!(b.insert_sorted_by_key(10, |&x| x)); - assert_eq!(&b[..], &[10, 20, 30, 40][..]); - - assert!(b.insert_sorted_by_key(60, |&x| x)); - assert_eq!(&b[..], &[10, 20, 30, 40, 60][..]); - - assert!(b.insert_sorted_by_key(50, |&x| x)); - assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]); - - assert!(!b.insert_sorted_by_key(9, |&x| x)); - assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]); - - assert!(b.insert_sorted_by_key(11, |&x| x)); - assert_eq!(&b[..], &[11, 20, 30, 40, 50, 60][..]); - - assert!(b.insert_sorted_by_key(21, |&x| x)); - assert_eq!(&b[..], &[20, 21, 30, 40, 50, 60][..]); - - assert!(b.insert_sorted_by_key(61, |&x| x)); - assert_eq!(&b[..], &[21, 30, 40, 50, 60, 61][..]); - - assert!(b.insert_sorted_by_key(51, |&x| x)); - assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]); - } -} - #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct DecidingStatus { /// When this referendum began being "decided". If confirming, then the @@ -284,6 +250,11 @@ impl< pub enum Curve { /// Linear curve starting at `(0, begin)`, ending at `(period, begin - delta)`. LinearDecreasing { begin: Perbill, delta: Perbill }, + /// Stepped curve, beginning at `(0, begin)`, then remaining constant for `period`, at which + /// point it steps down to `(period, begin - step)`. It then remains constant for another + /// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues + /// but the `y` component has a lower limit of `end`. + SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill }, } impl Curve { @@ -291,12 +262,17 @@ impl Curve { pub(crate) fn threshold(&self, x: Perbill) -> Perbill { match self { Self::LinearDecreasing { begin, delta } => *begin - (*delta * x).min(*begin), + Self::SteppedDecreasing { begin, end, step, period } => + (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), } } /// Determine the smallest `x` value such that `passing` returns `true` when passed along with /// the given `y` value. /// + /// If `passing` never returns `true` for any value of `x` when paired with `y`, then + /// `Perbill::one` may be returned. + /// /// ```nocompile /// let c = Curve::LinearDecreasing { begin: Perbill::one(), delta: Perbill::one() }; /// // ^^^ Can be any curve. @@ -309,9 +285,15 @@ impl Curve { match self { Self::LinearDecreasing { begin, delta } => if delta.is_zero() { - return *delta + *delta } else { - return (*begin - y.min(*begin)).min(*delta) / *delta + (*begin - y.min(*begin)).min(*delta) / *delta + }, + Self::SteppedDecreasing { begin, end, step, period } => + if y < *end { + Perbill::one() + } else { + period.int_mul((*begin - y.min(*begin) + step.less_epsilon().unwrap_or(Zero::zero())).int_div(*step)) }, } } @@ -334,6 +316,89 @@ impl Debug for Curve { (*begin - *delta) * 100u32, ) }, + Self::SteppedDecreasing { begin, end, step, period } => { + write!( + f, + "Stepped[(0, {}%) -> (1, {}%) by ({}%, {}%)]", + *begin * 100u32, + *end * 100u32, + *period * 100u32, + *step * 100u32, + ) + }, } } } + +#[cfg(test)] +mod tests { + use super::*; + use sp_runtime::PerThing; + use frame_support::traits::ConstU32; + + #[test] + fn insert_sorted_works() { + let mut b: BoundedVec> = vec![20, 30, 40].try_into().unwrap(); + assert!(b.insert_sorted_by_key(10, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40][..]); + + assert!(b.insert_sorted_by_key(60, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40, 60][..]); + + assert!(b.insert_sorted_by_key(50, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]); + + assert!(!b.insert_sorted_by_key(9, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]); + + assert!(b.insert_sorted_by_key(11, |&x| x)); + assert_eq!(&b[..], &[11, 20, 30, 40, 50, 60][..]); + + assert!(b.insert_sorted_by_key(21, |&x| x)); + assert_eq!(&b[..], &[20, 21, 30, 40, 50, 60][..]); + + assert!(b.insert_sorted_by_key(61, |&x| x)); + assert_eq!(&b[..], &[21, 30, 40, 50, 60, 61][..]); + + assert!(b.insert_sorted_by_key(51, |&x| x)); + assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]); + } + + #[test] + fn stepped_decreasing_works() { + let percent = |x| Perbill::from_percent(x); + let c = Curve::SteppedDecreasing { + begin: percent(80), + end: percent(30), + step: percent(10), + period: percent(15), + }; + assert_eq!(c.threshold(percent(0)), percent(80)); + assert_eq!(c.threshold(percent(15).less_epsilon().unwrap()), percent(80)); + assert_eq!(c.threshold(percent(15)), percent(70)); + assert_eq!(c.threshold(percent(30).less_epsilon().unwrap()), percent(70)); + assert_eq!(c.threshold(percent(30)), percent(60)); + assert_eq!(c.threshold(percent(45).less_epsilon().unwrap()), percent(60)); + assert_eq!(c.threshold(percent(45)), percent(50)); + assert_eq!(c.threshold(percent(60).less_epsilon().unwrap()), percent(50)); + assert_eq!(c.threshold(percent(60)), percent(40)); + assert_eq!(c.threshold(percent(75).less_epsilon().unwrap()), percent(40)); + assert_eq!(c.threshold(percent(75)), percent(30)); + assert_eq!(c.threshold(percent(100)), percent(30)); + + assert_eq!(c.delay(percent(100)), percent(0)); + assert_eq!(c.delay(percent(80)), percent(0)); + assert_eq!(c.delay(percent(80).less_epsilon().unwrap()), percent(15)); + assert_eq!(c.delay(percent(70)), percent(15)); + assert_eq!(c.delay(percent(70).less_epsilon().unwrap()), percent(30)); + assert_eq!(c.delay(percent(60)), percent(30)); + assert_eq!(c.delay(percent(60).less_epsilon().unwrap()), percent(45)); + assert_eq!(c.delay(percent(50)), percent(45)); + assert_eq!(c.delay(percent(50).less_epsilon().unwrap()), percent(60)); + assert_eq!(c.delay(percent(40)), percent(60)); + assert_eq!(c.delay(percent(40).less_epsilon().unwrap()), percent(75)); + assert_eq!(c.delay(percent(30)), percent(75)); + assert_eq!(c.delay(percent(30).less_epsilon().unwrap()), percent(100)); + assert_eq!(c.delay(percent(0)), percent(100)); + } +} \ No newline at end of file diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index c3ccca56ca33f..5645c7d357ed7 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -90,6 +90,13 @@ pub trait PerThing: self.deconstruct() == Self::ACCURACY } + /// Return the next lower value to `self` or an error with the same value if `self` is already + /// zero. + fn less_epsilon(self) -> Result { + if self.is_zero() { return Err(self) } + Ok(Self::from_parts(self.deconstruct() - One::one())) + } + /// Build this type from a percent. Equivalent to `Self::from_parts(x * Self::ACCURACY / 100)` /// but more accurate and can cope with potential type overflows. fn from_percent(x: Self::Inner) -> Self { @@ -589,6 +596,16 @@ macro_rules! implement_per_thing { ::from_rational(p, q) } + /// Integer multiplication with another value, saturating at 1. + pub fn int_mul(self, b: $type) -> Self { + PerThing::from_parts(self.0.saturating_mul(b)) + } + + /// Integer division with another value, rounding down. + pub fn int_div(self, b: Self) -> $type { + self.0 / b.0 + } + /// See [`PerThing::mul_floor`]. pub fn mul_floor(self, b: N) -> N where From f756d986dcb347316c943a8cd38514bcd56ddcb6 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Mon, 28 Mar 2022 18:57:05 +0200 Subject: [PATCH 02/48] Treasury SpendOrigin --- frame/treasury/src/lib.rs | 44 +++++++++++++++++++++++++++++++++++++ frame/treasury/src/tests.rs | 20 +++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/frame/treasury/src/lib.rs b/frame/treasury/src/lib.rs index 81fca5243afa3..3355fe40fcb35 100644 --- a/frame/treasury/src/lib.rs +++ b/frame/treasury/src/lib.rs @@ -197,6 +197,11 @@ pub mod pallet { /// NOTE: This parameter is also used within the Bounties Pallet extension if enabled. #[pallet::constant] type MaxApprovals: Get; + + /// The origin required for approving spends from the treasury outside of the proposal + /// process. The `Success` value is the maximum amount that this origin is allowed to + /// spend at a time. + type SpendOrigin: EnsureOrigin>; } /// Number of proposals that have been made. @@ -274,6 +279,8 @@ pub mod pallet { Rollover { rollover_balance: BalanceOf }, /// Some funds have been deposited. Deposit { value: BalanceOf }, + /// A new spend proposal has been approved. + SpendApproved { proposal_index: ProposalIndex, amount: BalanceOf, beneficiary: T::AccountId }, } /// Old name generated by `decl_event`. @@ -289,6 +296,9 @@ pub mod pallet { InvalidIndex, /// Too many approvals in the queue. TooManyApprovals, + /// The spend origin is valid but the amount it is allowed to spend is lower than the + /// amount to be spent. + InsufficientPermission, } #[pallet::hooks] @@ -393,6 +403,40 @@ pub mod pallet { .map_err(|_| Error::::TooManyApprovals)?; Ok(()) } + + /// Propose and approve a spend of treasury funds. + /// + /// - `origin`: Must be `SpendOrigin` with the `Success` value being at least `amount`. + /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. + /// - `beneficiary`: The destination account for the transfer. + /// + /// NOTE: For record-keeping purposes, the proposer is deemed to be equivalent to the + /// beneficiary. + #[pallet::weight(T::WeightInfo::propose_spend())] + pub fn spend( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + beneficiary: ::Source, + ) -> DispatchResult { + let max_amount = T::SpendOrigin::ensure_origin(origin)?; + let beneficiary = T::Lookup::lookup(beneficiary)?; + + ensure!(amount <= max_amount, Error::::InsufficientPermission); + let proposal_index = Self::proposal_count(); + Approvals::::try_append(proposal_index) + .map_err(|_| Error::::TooManyApprovals)?; + let proposal = Proposal { + proposer: beneficiary.clone(), + value: amount, + beneficiary: beneficiary.clone(), + bond: Default::default(), + }; + Proposals::::insert(proposal_index, proposal); + ProposalCount::::put(proposal_index + 1); + + Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary }); + Ok(()) + } } } diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index 26189f5201498..feefed6cacbe7 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -106,6 +106,25 @@ parameter_types! { pub const BountyValueMinimum: u64 = 1; pub const MaxApprovals: u32 = 100; } +pub struct TestSpendOrigin; +impl frame_support::traits::EnsureOrigin for TestSpendOrigin { + type Success = u64; + fn try_origin(o: Origin) -> Result { + Result::, Origin>::from(o).and_then(|o| match o { + frame_system::RawOrigin::Root => Ok(u64::max_value()), + frame_system::RawOrigin::Signed(1) => Ok(5), + frame_system::RawOrigin::Signed(2) => Ok(10), + frame_system::RawOrigin::Signed(3) => Ok(20), + frame_system::RawOrigin::Signed(4) => Ok(50), + r => Err(Origin::from(r)), + }) + } + #[cfg(feature = "runtime-benchmarks")] + fn successful_origin() -> Origin { + Origin::root() + } +} + impl Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet; @@ -122,6 +141,7 @@ impl Config for Test { type WeightInfo = (); type SpendFunds = (); type MaxApprovals = MaxApprovals; + type SpendOrigin = TestSpendOrigin; } pub fn new_test_ext() -> sp_io::TestExternalities { From 788a85fe52e684b6cf31888c73be30eeb28baa3b Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Mon, 4 Apr 2022 15:04:35 +0200 Subject: [PATCH 03/48] Add tests --- frame/support/src/traits/dispatch.rs | 2 ++ frame/system/src/lib.rs | 25 ++++++++++++++++ frame/treasury/src/tests.rs | 44 ++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index 250a31ebfb17a..ae7956c47c01a 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -149,6 +149,8 @@ pub trait OriginTrait: Sized { /// The "OR gate" implementation of `EnsureOrigin`. /// /// Origin check will pass if `L` or `R` origin check passes. `L` is tested first. +/// +/// Successful origin is derived from the left side. pub struct EnsureOneOf(sp_std::marker::PhantomData<(L, R)>); impl, R: EnsureOrigin> diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index 9b30e7b452276..b6d6956834351 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -788,6 +788,31 @@ impl, O>> + From>, Acco } } +pub struct EnsureRootWithSuccess( + sp_std::marker::PhantomData<(AccountId, SuccessType, Success)> +); +impl< + O: Into, O>> + From>, + AccountId, + SuccessType, + Success: Get, +> + EnsureOrigin for EnsureRootWithSuccess +{ + type Success = SuccessType; + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Root => Ok(Success::get()), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn successful_origin() -> O { + O::from(RawOrigin::Root) + } +} + pub struct EnsureSigned(sp_std::marker::PhantomData); impl, O>> + From>, AccountId: Decode> EnsureOrigin for EnsureSigned diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index feefed6cacbe7..3aa2d120e661a 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -24,7 +24,7 @@ use std::cell::RefCell; use sp_core::H256; use sp_runtime::{ testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + traits::{BlakeTwo256, IdentityLookup, BadOrigin}, }; use frame_support::{ @@ -112,10 +112,10 @@ impl frame_support::traits::EnsureOrigin for TestSpendOrigin { fn try_origin(o: Origin) -> Result { Result::, Origin>::from(o).and_then(|o| match o { frame_system::RawOrigin::Root => Ok(u64::max_value()), - frame_system::RawOrigin::Signed(1) => Ok(5), - frame_system::RawOrigin::Signed(2) => Ok(10), - frame_system::RawOrigin::Signed(3) => Ok(20), - frame_system::RawOrigin::Signed(4) => Ok(50), + frame_system::RawOrigin::Signed(10) => Ok(5), + frame_system::RawOrigin::Signed(11) => Ok(10), + frame_system::RawOrigin::Signed(12) => Ok(20), + frame_system::RawOrigin::Signed(13) => Ok(50), r => Err(Origin::from(r)), }) } @@ -164,6 +164,40 @@ fn genesis_config_works() { }); } +#[test] +fn spend_origin_permissioning_works() { + new_test_ext().execute_with(|| { + // Check that accumulate works when we have Some value in Dummy already. + assert_noop!(Treasury::spend(Origin::signed(1), 1, 1), BadOrigin); + assert_noop!(Treasury::spend(Origin::signed(10), 6, 1), Error::::InsufficientPermission); + assert_noop!(Treasury::spend(Origin::signed(11), 11, 1), Error::::InsufficientPermission); + assert_noop!(Treasury::spend(Origin::signed(12), 21, 1), Error::::InsufficientPermission); + assert_noop!(Treasury::spend(Origin::signed(13), 51, 1), Error::::InsufficientPermission); + }); +} + +#[test] +fn spend_origin_works() { + new_test_ext().execute_with(|| { + // Check that accumulate works when we have Some value in Dummy already. + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_ok!(Treasury::spend(Origin::signed(10), 5, 6)); + assert_ok!(Treasury::spend(Origin::signed(10), 5, 6)); + assert_ok!(Treasury::spend(Origin::signed(10), 5, 6)); + assert_ok!(Treasury::spend(Origin::signed(10), 5, 6)); + assert_ok!(Treasury::spend(Origin::signed(11), 10, 6)); + assert_ok!(Treasury::spend(Origin::signed(12), 20, 6)); + assert_ok!(Treasury::spend(Origin::signed(13), 50, 6)); + + >::on_initialize(1); + assert_eq!(Balances::free_balance(6), 0); + + >::on_initialize(2); + assert_eq!(Balances::free_balance(6), 100); + assert_eq!(Treasury::pot(), 0); + }); +} + #[test] fn minting_works() { new_test_ext().execute_with(|| { From 9b967e70a4be89e6db0feb598f4bb5f44f49a0d1 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Mon, 4 Apr 2022 15:53:21 +0200 Subject: [PATCH 04/48] Better Origin Or-gating --- frame/support/src/lib.rs | 23 +++++- frame/support/src/traits.rs | 8 +- frame/support/src/traits/dispatch.rs | 109 +++++++++++++++++++++++---- frame/support/src/traits/misc.rs | 16 ++++ 4 files changed, 137 insertions(+), 19 deletions(-) diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index 714449eec7847..fa4ce938a4246 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -432,6 +432,13 @@ macro_rules! parameter_types { I::from(Self::get()) } } + + impl $crate::traits::TypedGet for $name { + type Type = $type; + fn get() -> $type { + Self::get() + } + } }; (IMPL $name:ident, $type:ty, $value:expr) => { impl $name { @@ -446,6 +453,13 @@ macro_rules! parameter_types { I::from(Self::get()) } } + + impl $crate::traits::TypedGet for $name { + type Type = $type; + fn get() -> $type { + Self::get() + } + } }; (IMPL_STORAGE $name:ident, $type:ty, $value:expr) => { impl $name { @@ -479,6 +493,13 @@ macro_rules! parameter_types { I::from(Self::get()) } } + + impl $crate::traits::TypedGet for $name { + type Type = $type; + fn get() -> $type { + Self::get() + } + } }; ( $( #[ $attr:meta ] )* @@ -1387,7 +1408,7 @@ pub mod pallet_prelude { }, traits::{ ConstU32, EnsureOrigin, Get, GetDefault, GetStorageVersion, Hooks, IsType, - PalletInfoAccess, StorageInfoTrait, + PalletInfoAccess, StorageInfoTrait, TypedGet, }, weights::{DispatchClass, Pays, Weight}, Blake2_128, Blake2_128Concat, Blake2_256, CloneNoBound, DebugNoBound, EqNoBound, Identity, diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 40afc0d337f43..2a5ee49b86f31 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -59,7 +59,7 @@ pub use misc::{ ConstU32, ConstU64, ConstU8, DefensiveSaturating, EnsureInherentsAreFirst, EqualPrivilegeOnly, EstimateCallFee, ExecuteBlock, ExtrinsicCall, Get, GetBacking, GetDefault, HandleLifetime, IsSubType, IsType, Len, OffchainWorker, OnKilledAccount, OnNewAccount, PreimageProvider, - PreimageRecipient, PrivilegeCmp, SameOrOther, Time, TryCollect, TryDrop, UnixTime, + PreimageRecipient, PrivilegeCmp, SameOrOther, Time, TryCollect, TryDrop, TypedGet, UnixTime, WrapperKeepOpaque, WrapperOpaque, }; #[doc(hidden)] @@ -94,9 +94,11 @@ pub use storage::{ mod dispatch; pub use dispatch::{ - AsEnsureOriginWithArg, EnsureOneOf, EnsureOrigin, EnsureOriginWithArg, OriginTrait, - UnfilteredDispatchable, + AsEnsureOriginWithArg, EitherOf, EitherOfDiverse, EnsureOrigin, EnsureOriginWithArg, + OriginTrait, UnfilteredDispatchable, }; +#[allow(deprecated)] +pub use dispatch::EnsureOneOf; mod voting; pub use voting::{ diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index ae7956c47c01a..bc7a0486eaf2a 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -146,15 +146,16 @@ pub trait OriginTrait: Sized { fn signed(by: Self::AccountId) -> Self; } -/// The "OR gate" implementation of `EnsureOrigin`. +/// "OR gate" implementation of `EnsureOrigin` allowing for different `Success` types for `L` +/// and `R`, with them combined using an `Either` type. /// /// Origin check will pass if `L` or `R` origin check passes. `L` is tested first. /// /// Successful origin is derived from the left side. -pub struct EnsureOneOf(sp_std::marker::PhantomData<(L, R)>); +pub struct EitherOfDiverse(sp_std::marker::PhantomData<(L, R)>); impl, R: EnsureOrigin> - EnsureOrigin for EnsureOneOf + EnsureOrigin for EitherOfDiverse { type Success = Either; fn try_origin(o: OuterOrigin) -> Result { @@ -168,17 +169,51 @@ impl, R: EnsureOrigin> } } +/// "OR gate" implementation of `EnsureOrigin` allowing for different `Success` types for `L` +/// and `R`, with them combined using an `Either` type. +/// +/// Origin check will pass if `L` or `R` origin check passes. `L` is tested first. +/// +/// Successful origin is derived from the left side. +#[deprecated = "Use `EitherOfDiverse` instead"] +pub type EnsureOneOf = EitherOfDiverse; + +/// "OR gate" implementation of `EnsureOrigin`, `Success` type for both `L` and `R` must +/// be equal. +/// +/// Origin check will pass if `L` or `R` origin check passes. `L` is tested first. +/// +/// Successful origin is derived from the left side. +pub struct EitherOf(sp_std::marker::PhantomData<(L, R)>); + +impl, R: EnsureOrigin> + EnsureOrigin for EitherOf +{ + type Success = L::Success; + fn try_origin(o: OuterOrigin) -> Result { + L::try_origin(o) + .or_else(|o| R::try_origin(o)) + } + + #[cfg(feature = "runtime-benchmarks")] + fn successful_origin() -> OuterOrigin { + L::successful_origin() + } +} + #[cfg(test)] mod tests { + use std::marker::PhantomData; + use crate::traits::{TypedGet, ConstBool, ConstU8}; use super::*; - struct EnsureSuccess; - struct EnsureFail; + struct EnsureSuccess(PhantomData); + struct EnsureFail(PhantomData); - impl EnsureOrigin<()> for EnsureSuccess { - type Success = (); + impl EnsureOrigin<()> for EnsureSuccess { + type Success = V::Type; fn try_origin(_: ()) -> Result { - Ok(()) + Ok(V::get()) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> () { @@ -186,8 +221,8 @@ mod tests { } } - impl EnsureOrigin<()> for EnsureFail { - type Success = (); + impl EnsureOrigin<()> for EnsureFail { + type Success = T; fn try_origin(_: ()) -> Result { Err(()) } @@ -198,10 +233,54 @@ mod tests { } #[test] - fn ensure_one_of_test() { - assert!(>::try_origin(()).is_ok()); - assert!(>::try_origin(()).is_ok()); - assert!(>::try_origin(()).is_ok()); - assert!(>::try_origin(()).is_err()); + fn either_of_diverse_works() { + assert_eq!( + EitherOfDiverse::< + EnsureSuccess>, + EnsureSuccess>, + >::try_origin(()).unwrap().left(), + Some(true) + ); + assert_eq!( + EitherOfDiverse::< + EnsureSuccess>, + EnsureFail, + >::try_origin(()).unwrap().left(), + Some(true) + ); + assert_eq!( + EitherOfDiverse::< + EnsureFail, + EnsureSuccess>, + >::try_origin(()).unwrap().right(), + Some(0u8) + ); + assert!(EitherOfDiverse::, EnsureFail>::try_origin(()).is_err()); + } + + #[test] + fn either_of_works() { + assert_eq!( + EitherOf::< + EnsureSuccess>, + EnsureSuccess>, + >::try_origin(()).unwrap(), + true + ); + assert_eq!( + EitherOf::< + EnsureSuccess>, + EnsureFail, + >::try_origin(()).unwrap(), + true + ); + assert_eq!( + EitherOf::< + EnsureFail, + EnsureSuccess>, + >::try_origin(()).unwrap(), + false + ); + assert!(EitherOf::, EnsureFail>::try_origin(()).is_err()); } } diff --git a/frame/support/src/traits/misc.rs b/frame/support/src/traits/misc.rs index 2a0d6f0523e71..c3f470c4ba430 100644 --- a/frame/support/src/traits/misc.rs +++ b/frame/support/src/traits/misc.rs @@ -345,6 +345,16 @@ where } } +/// A trait for querying a single value from a type defined in the trait. +/// +/// It is not required that the value is constant. +pub trait TypedGet { + /// The type which is returned. + type Type; + /// Return the current value. + fn get() -> Self::Type; +} + /// A trait for querying a single value from a type. /// /// It is not required that the value is constant. @@ -381,6 +391,12 @@ macro_rules! impl_const_get { Some(T) } } + impl TypedGet for $name { + type Type = $t; + fn get() -> $t { + T + } + } }; } From b84e2992555a92cb274419c990b8d709fbbace43 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Sun, 10 Apr 2022 22:11:49 +0100 Subject: [PATCH 05/48] Reciprocal curve --- frame/referenda/src/types.rs | 161 +++++++++++++++++++++++- primitives/arithmetic/src/per_things.rs | 11 ++ 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 60e39684ed45a..a9985da918397 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -255,6 +255,8 @@ pub enum Curve { /// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues /// but the `y` component has a lower limit of `end`. SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill }, + /// A simple recipocal (`K/x + O`) curve: `factor` is `K` and `offset` is `O`. + Reciprocal { factor: Perbill, offset: Perbill }, } impl Curve { @@ -264,6 +266,12 @@ impl Curve { Self::LinearDecreasing { begin, delta } => *begin - (*delta * x).min(*begin), Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), + Self::Reciprocal { factor, offset } => { + let x_offset = factor.saturating_div(offset.left_from_one()); + // Actual curve is y = factor / (x + x_offset) + offset + // we want to avoid saturating prior to the division. + Perbill::from_rational(factor.deconstruct(), x.deconstruct() + x_offset.deconstruct()).saturating_add(*offset) + } } } @@ -295,6 +303,22 @@ impl Curve { } else { period.int_mul((*begin - y.min(*begin) + step.less_epsilon().unwrap_or(Zero::zero())).int_div(*step)) }, + Self::Reciprocal { factor, offset } => { + let x_offset = factor.saturating_div(offset.left_from_one()); + // Actual curve is y = factor / (x + x_offset) + offset + // Ergo curve is x = factor / (y - offset) - x_offset + // To avoid pre-saturation problems, we move the x_offset term to happen prior to + // the division. + // So: + // yo := y - offset + // x = (factor - x_offset * yo) / yo + if y < *offset { + Perbill::one() + } else { + let yo = y - *offset; + factor.saturating_sub(x_offset * yo).saturating_div(yo) + } + } } } @@ -326,6 +350,14 @@ impl Debug for Curve { *step * 100u32, ) }, + Self::Reciprocal { factor, offset } => { + write!( + f, + "Reciprocal[factor of {}%, offset of {}%]", + *factor * 100u32, + *offset * 100u32, + ) + } } } } @@ -336,6 +368,10 @@ mod tests { use sp_runtime::PerThing; use frame_support::traits::ConstU32; + fn percent(x: u32) -> Perbill { + Perbill::from_percent(x) + } + #[test] fn insert_sorted_works() { let mut b: BoundedVec> = vec![20, 30, 40].try_into().unwrap(); @@ -364,9 +400,132 @@ mod tests { assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]); } + #[test] + fn basic_reciprocal_works() { + let c: Curve = Curve::Reciprocal { + factor: percent(5), + offset: percent(0), + }; + + for i in 0..1000u32 { + let x = Perbill::from_rational(i, 1000u32); + + let threshold_at_x = c.threshold(x); + let min_delay_for_threshold = c.delay(threshold_at_x); + assert!(min_delay_for_threshold >= x); + + let delay_for_x = c.delay(x); + let min_threshold_for_delay = c.threshold(delay_for_x); + assert!(min_threshold_for_delay >= x); + } + + assert_eq!(c.threshold(percent(0)) * 100u32, 100); + assert_eq!(c.threshold(percent(1)) * 100u32, 83); + assert_eq!(c.threshold(percent(2)) * 100u32, 71); + assert_eq!(c.threshold(percent(3)) * 100u32, 62); + assert_eq!(c.threshold(percent(4)) * 100u32, 56); + assert_eq!(c.threshold(percent(5)) * 100u32, 50); + assert_eq!(c.threshold(percent(6)) * 100u32, 45); + assert_eq!(c.threshold(percent(7)) * 100u32, 42); + assert_eq!(c.threshold(percent(8)) * 100u32, 38); + assert_eq!(c.threshold(percent(9)) * 100u32, 36); + assert_eq!(c.threshold(percent(10)) * 100u32, 33); + assert_eq!(c.threshold(percent(15)) * 100u32, 25); + assert_eq!(c.threshold(percent(20)) * 100u32, 20); + assert_eq!(c.threshold(percent(25)) * 100u32, 17); + assert_eq!(c.threshold(percent(30)) * 100u32, 14); + assert_eq!(c.threshold(percent(35)) * 100u32, 12); + assert_eq!(c.threshold(percent(40)) * 100u32, 11); + assert_eq!(c.threshold(percent(45)) * 100u32, 10); + assert_eq!(c.threshold(percent(50)) * 100u32, 9); + assert_eq!(c.threshold(percent(55)) * 100u32, 8); + assert_eq!(c.threshold(percent(60)) * 100u32, 8); + assert_eq!(c.threshold(percent(65)) * 100u32, 7); + assert_eq!(c.threshold(percent(70)) * 100u32, 7); + assert_eq!(c.threshold(percent(75)) * 100u32, 6); + assert_eq!(c.threshold(percent(80)) * 100u32, 6); + assert_eq!(c.threshold(percent(85)) * 100u32, 6); + assert_eq!(c.threshold(percent(90)) * 100u32, 5); + assert_eq!(c.threshold(percent(95)) * 100u32, 5); + assert_eq!(c.threshold(percent(100)) * 100u32, 5); + + assert_eq!(c.delay(percent(0)) * 100u32, 100); + assert_eq!(c.delay(percent(1)) * 100u32, 100); + assert_eq!(c.delay(percent(2)) * 100u32, 100); + assert_eq!(c.delay(percent(3)) * 100u32, 100); + assert_eq!(c.delay(percent(4)) * 100u32, 100); + assert_eq!(c.delay(percent(5)) * 100u32, 95); + assert_eq!(c.delay(percent(6)) * 100u32, 78); + assert_eq!(c.delay(percent(7)) * 100u32, 66); + assert_eq!(c.delay(percent(8)) * 100u32, 57); + assert_eq!(c.delay(percent(9)) * 100u32, 51); + assert_eq!(c.delay(percent(10)) * 100u32, 45); + assert_eq!(c.delay(percent(15)) * 100u32, 28); + assert_eq!(c.delay(percent(20)) * 100u32, 20); + assert_eq!(c.delay(percent(25)) * 100u32, 15); + assert_eq!(c.delay(percent(30)) * 100u32, 12); + assert_eq!(c.delay(percent(35)) * 100u32, 9); + assert_eq!(c.delay(percent(40)) * 100u32, 7); + assert_eq!(c.delay(percent(45)) * 100u32, 6); + assert_eq!(c.delay(percent(50)) * 100u32, 5); + assert_eq!(c.delay(percent(55)) * 100u32, 4); + assert_eq!(c.delay(percent(60)) * 100u32, 3); + assert_eq!(c.delay(percent(65)) * 100u32, 3); + assert_eq!(c.delay(percent(70)) * 100u32, 2); + assert_eq!(c.delay(percent(75)) * 100u32, 2); + assert_eq!(c.delay(percent(80)) * 100u32, 1); + assert_eq!(c.delay(percent(85)) * 100u32, 1); + assert_eq!(c.delay(percent(90)) * 100u32, 1); + assert_eq!(c.delay(percent(95)) * 100u32, 0); + assert_eq!(c.delay(percent(100)) * 100u32, 0); + } + + #[test] + fn offset_reciprocal_works() { + let c: Curve = Curve::Reciprocal { + factor: percent(10), + offset: percent(50), + }; + + for i in 0..1000u32 { + let x = Perbill::from_rational(i, 1000u32); + + let threshold_at_x = c.threshold(x); + let min_delay_for_threshold = c.delay(threshold_at_x); + assert!(min_delay_for_threshold >= x); + + let delay_for_x = c.delay(x); + let min_threshold_for_delay = c.threshold(delay_for_x); + assert!(min_threshold_for_delay >= x); + } + + assert_eq!(c.threshold(percent(0)) * 100u32, 100); + assert_eq!(c.threshold(percent(10)) * 100u32, 83); + assert_eq!(c.threshold(percent(20)) * 100u32, 75); + assert_eq!(c.threshold(percent(30)) * 100u32, 70); + assert_eq!(c.threshold(percent(40)) * 100u32, 67); + assert_eq!(c.threshold(percent(50)) * 100u32, 64); + assert_eq!(c.threshold(percent(60)) * 100u32, 62); + assert_eq!(c.threshold(percent(70)) * 100u32, 61); + assert_eq!(c.threshold(percent(80)) * 100u32, 60); + assert_eq!(c.threshold(percent(90)) * 100u32, 59); + assert_eq!(c.threshold(percent(100)) * 100u32, 58); + + assert_eq!(c.delay(percent(0)) * 100u32, 100); + assert_eq!(c.delay(percent(10)) * 100u32, 100); + assert_eq!(c.delay(percent(20)) * 100u32, 100); + assert_eq!(c.delay(percent(30)) * 100u32, 100); + assert_eq!(c.delay(percent(40)) * 100u32, 100); + assert_eq!(c.delay(percent(50)) * 100u32, 100); + assert_eq!(c.delay(percent(60)) * 100u32, 80); + assert_eq!(c.delay(percent(70)) * 100u32, 30); + assert_eq!(c.delay(percent(80)) * 100u32, 13); + assert_eq!(c.delay(percent(90)) * 100u32, 5); + assert_eq!(c.delay(percent(100)) * 100u32, 0); + } + #[test] fn stepped_decreasing_works() { - let percent = |x| Perbill::from_percent(x); let c = Curve::SteppedDecreasing { begin: percent(80), end: percent(30), diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index 12c8c9a6d700d..9155159053e96 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -660,6 +660,17 @@ macro_rules! implement_per_thing { { PerThing::saturating_reciprocal_mul_ceil(self, b) } + + /// Saturating division. Compute `self / rhs`, saturating at one if `rhs < self`. + pub fn saturating_div(self, rhs: Self) -> Self { + let p = self.0; + let q = rhs.0; + if p < q { + Self::from_rational(p, q) + } else { + Self::one() + } + } } impl Saturating for $name { From c8e8a5ceb93b4a9a296ddee6d816cfb7505751e0 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Mon, 11 Apr 2022 13:27:01 +0100 Subject: [PATCH 06/48] Tests for reciprical and rounding in PerThings --- Cargo.lock | 1 + frame/referenda/Cargo.toml | 2 + frame/referenda/src/types.rs | 93 +++++--- primitives/arithmetic/src/lib.rs | 2 +- primitives/arithmetic/src/per_things.rs | 288 ++++++++++++++++++++---- 5 files changed, 306 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23f11860ca6f8..51b8c8176a074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6320,6 +6320,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", + "sp-arithmetic", "sp-core", "sp-io", "sp-runtime", diff --git a/frame/referenda/Cargo.toml b/frame/referenda/Cargo.toml index d85503a741f43..5f6d319b1269c 100644 --- a/frame/referenda/Cargo.toml +++ b/frame/referenda/Cargo.toml @@ -21,6 +21,7 @@ scale-info = { version = "2.0.1", default-features = false, features = ["derive" sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } 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-arithmetic = { version = "5.0.0", default-features = false, path = "../../primitives/arithmetic" } frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } @@ -44,6 +45,7 @@ std = [ "frame-benchmarking/std", "frame-support/std", "sp-runtime/std", + "sp-arithmetic/std", "frame-system/std", ] runtime-benchmarks = [ diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index a9985da918397..28e70a0b458f0 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -22,6 +22,7 @@ use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{traits::schedule::Anon, Parameter}; use scale_info::TypeInfo; use sp_runtime::{RuntimeDebug, PerThing}; +use sp_arithmetic::Rounding::*; use sp_std::fmt::Debug; pub type BalanceOf = @@ -267,10 +268,10 @@ impl Curve { Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), Self::Reciprocal { factor, offset } => { - let x_offset = factor.saturating_div(offset.left_from_one()); + let x_offset = factor.saturating_div(offset.left_from_one(), Down); // Actual curve is y = factor / (x + x_offset) + offset // we want to avoid saturating prior to the division. - Perbill::from_rational(factor.deconstruct(), x.deconstruct() + x_offset.deconstruct()).saturating_add(*offset) + Perbill::from_rational_with_rounding(factor.deconstruct(), x.deconstruct() + x_offset.deconstruct(), Down).unwrap_or_else(|_| Perbill::one()).saturating_add(*offset) } } } @@ -301,10 +302,10 @@ impl Curve { if y < *end { Perbill::one() } else { - period.int_mul((*begin - y.min(*begin) + step.less_epsilon().unwrap_or(Zero::zero())).int_div(*step)) + period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step)) }, Self::Reciprocal { factor, offset } => { - let x_offset = factor.saturating_div(offset.left_from_one()); + let x_offset = factor.saturating_div(offset.left_from_one(), Down); // Actual curve is y = factor / (x + x_offset) + offset // Ergo curve is x = factor / (y - offset) - x_offset // To avoid pre-saturation problems, we move the x_offset term to happen prior to @@ -316,7 +317,10 @@ impl Curve { Perbill::one() } else { let yo = y - *offset; - factor.saturating_sub(x_offset * yo).saturating_div(yo) + if *factor < x_offset * yo { + println!("SATURATING in delay"); + } + factor.saturating_sub(x_offset * yo).saturating_div(yo, Up) } } } @@ -407,16 +411,12 @@ mod tests { offset: percent(0), }; - for i in 0..1000u32 { - let x = Perbill::from_rational(i, 1000u32); - - let threshold_at_x = c.threshold(x); - let min_delay_for_threshold = c.delay(threshold_at_x); - assert!(min_delay_for_threshold >= x); - - let delay_for_x = c.delay(x); - let min_threshold_for_delay = c.threshold(delay_for_x); - assert!(min_threshold_for_delay >= x); + for i in 0..9_696_969u32 { + let query = Perbill::from_rational(i, 9_696_969); + // Determine the nearest point in time when the query will be above threshold. + let delay_needed = c.delay(query); + // Ensure that it actually does pass at that time, or that it will never pass. + assert!(delay_needed.is_one() || c.passing(delay_needed, query)); } assert_eq!(c.threshold(percent(0)) * 100u32, 100); @@ -487,16 +487,12 @@ mod tests { offset: percent(50), }; - for i in 0..1000u32 { - let x = Perbill::from_rational(i, 1000u32); - - let threshold_at_x = c.threshold(x); - let min_delay_for_threshold = c.delay(threshold_at_x); - assert!(min_delay_for_threshold >= x); - - let delay_for_x = c.delay(x); - let min_threshold_for_delay = c.threshold(delay_for_x); - assert!(min_threshold_for_delay >= x); + for i in 0..9_696_969u32 { + let query = Perbill::from_rational(i, 9_696_969); + // Determine the nearest point in time when the query will be above threshold. + let delay_needed = c.delay(query); + // Ensure that it actually does pass at that time, or that it will never pass. + assert!(delay_needed.is_one() || c.passing(delay_needed, query)); } assert_eq!(c.threshold(percent(0)) * 100u32, 100); @@ -524,6 +520,22 @@ mod tests { assert_eq!(c.delay(percent(100)) * 100u32, 0); } + #[test] + fn realistic_offset_reciprocal_works() { + let c: Curve = Curve::Reciprocal { + factor: Perbill::from_rational(35u32, 10_000u32), + offset: percent(10), + }; + + for i in 0..9_696_969u32 { + let query = Perbill::from_rational(i, 9_696_969); + // Determine the nearest point in time when the query will be above threshold. + let delay_needed = c.delay(query); + // Ensure that it actually does pass at that time, or that it will never pass. + assert!(delay_needed.is_one() || c.passing(delay_needed, query)); + } + } + #[test] fn stepped_decreasing_works() { let c = Curve::SteppedDecreasing { @@ -532,32 +544,41 @@ mod tests { step: percent(10), period: percent(15), }; + + for i in 0..9_696_969u32 { + let query = Perbill::from_rational(i, 9_696_969); + // Determine the nearest point in time when the query will be above threshold. + let delay_needed = c.delay(query); + // Ensure that it actually does pass at that time, or that it will never pass. + assert!(delay_needed.is_one() || c.passing(delay_needed, query)); + } + assert_eq!(c.threshold(percent(0)), percent(80)); - assert_eq!(c.threshold(percent(15).less_epsilon().unwrap()), percent(80)); + assert_eq!(c.threshold(percent(15).less_epsilon()), percent(80)); assert_eq!(c.threshold(percent(15)), percent(70)); - assert_eq!(c.threshold(percent(30).less_epsilon().unwrap()), percent(70)); + assert_eq!(c.threshold(percent(30).less_epsilon()), percent(70)); assert_eq!(c.threshold(percent(30)), percent(60)); - assert_eq!(c.threshold(percent(45).less_epsilon().unwrap()), percent(60)); + assert_eq!(c.threshold(percent(45).less_epsilon()), percent(60)); assert_eq!(c.threshold(percent(45)), percent(50)); - assert_eq!(c.threshold(percent(60).less_epsilon().unwrap()), percent(50)); + assert_eq!(c.threshold(percent(60).less_epsilon()), percent(50)); assert_eq!(c.threshold(percent(60)), percent(40)); - assert_eq!(c.threshold(percent(75).less_epsilon().unwrap()), percent(40)); + assert_eq!(c.threshold(percent(75).less_epsilon()), percent(40)); assert_eq!(c.threshold(percent(75)), percent(30)); assert_eq!(c.threshold(percent(100)), percent(30)); assert_eq!(c.delay(percent(100)), percent(0)); assert_eq!(c.delay(percent(80)), percent(0)); - assert_eq!(c.delay(percent(80).less_epsilon().unwrap()), percent(15)); + assert_eq!(c.delay(percent(80).less_epsilon()), percent(15)); assert_eq!(c.delay(percent(70)), percent(15)); - assert_eq!(c.delay(percent(70).less_epsilon().unwrap()), percent(30)); + assert_eq!(c.delay(percent(70).less_epsilon()), percent(30)); assert_eq!(c.delay(percent(60)), percent(30)); - assert_eq!(c.delay(percent(60).less_epsilon().unwrap()), percent(45)); + assert_eq!(c.delay(percent(60).less_epsilon()), percent(45)); assert_eq!(c.delay(percent(50)), percent(45)); - assert_eq!(c.delay(percent(50).less_epsilon().unwrap()), percent(60)); + assert_eq!(c.delay(percent(50).less_epsilon()), percent(60)); assert_eq!(c.delay(percent(40)), percent(60)); - assert_eq!(c.delay(percent(40).less_epsilon().unwrap()), percent(75)); + assert_eq!(c.delay(percent(40).less_epsilon()), percent(75)); assert_eq!(c.delay(percent(30)), percent(75)); - assert_eq!(c.delay(percent(30).less_epsilon().unwrap()), percent(100)); + assert_eq!(c.delay(percent(30).less_epsilon()), percent(100)); assert_eq!(c.delay(percent(0)), percent(100)); } } \ No newline at end of file diff --git a/primitives/arithmetic/src/lib.rs b/primitives/arithmetic/src/lib.rs index 729da123757c7..a6162daa6df87 100644 --- a/primitives/arithmetic/src/lib.rs +++ b/primitives/arithmetic/src/lib.rs @@ -41,7 +41,7 @@ pub mod rational; pub mod traits; pub use fixed_point::{FixedI128, FixedI64, FixedPointNumber, FixedPointOperand, FixedU128}; -pub use per_things::{InnerOf, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, UpperOf}; +pub use per_things::{InnerOf, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, UpperOf, Rounding}; pub use rational::{Rational128, RationalInfinite}; use sp_std::{cmp::Ordering, fmt::Debug, prelude::*}; diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index 9155159053e96..6f24ee1acf1cf 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -89,13 +89,32 @@ pub trait PerThing: self.deconstruct() == Self::ACCURACY } + /// Return the next lower value to `self` or `self` if it is already zero. + fn less_epsilon(self) -> Self { + if self.is_zero() { return self } + Self::from_parts(self.deconstruct() - One::one()) + } + /// Return the next lower value to `self` or an error with the same value if `self` is already /// zero. - fn less_epsilon(self) -> Result { + fn try_less_epsilon(self) -> Result { if self.is_zero() { return Err(self) } Ok(Self::from_parts(self.deconstruct() - One::one())) } + /// Return the next higher value to `self` or `self` if it is already one. + fn plus_epsilon(self) -> Self { + if self.is_one() { return self } + Self::from_parts(self.deconstruct() + One::one()) + } + + /// Return the next higher value to `self` or an error with the same value if `self` is already + /// one. + fn try_plus_epsilon(self) -> Result { + if self.is_one() { return Err(self) } + Ok(Self::from_parts(self.deconstruct() + One::one())) + } + /// Build this type from a percent. Equivalent to `Self::from_parts(x * Self::ACCURACY / 100)` /// but more accurate and can cope with potential type overflows. fn from_percent(x: Self::Inner) -> Self { @@ -282,9 +301,9 @@ pub trait PerThing: /// # fn main () { /// // 989/100 is technically closer to 99%. /// assert_eq!( - /// Percent::from_rational(989u64, 1000), - /// Percent::from_parts(98), - /// ); + /// Percent::from_rational(989u64, 1000), + /// Percent::from_parts(98), + /// ); /// # } /// ``` fn from_rational(p: N, q: N) -> Self @@ -296,9 +315,77 @@ pub trait PerThing: + ops::Div + ops::Rem + ops::Add - + Unsigned, + + ops::AddAssign + + Unsigned + + Zero + + One, + Self::Inner: Into + { + Self::from_rational_with_rounding(p, q, Rounding::Down).unwrap_or_else(|_| Self::one()) + } + + /// Approximate the fraction `p/q` into a per-thing fraction. + /// + /// The computation of this approximation is performed in the generic type `N`. Given + /// `M` as the data type that can hold the maximum value of this per-thing (e.g. `u32` for + /// `Perbill`), this can only work if `N == M` or `N: From + TryInto`. + /// + /// In the case of an overflow (or divide by zero), an `Err` is returned. + /// + /// Rounding is determined by the parameter `rounding`, i.e. + /// + /// ```rust + /// # use sp_arithmetic::{Percent, PerThing, Rounding::*}; + /// # fn main () { + /// // 989/100 is technically closer to 99%. + /// assert_eq!( + /// Percent::from_rational_with_rounding(989u64, 1000, Down).unwrap(), + /// Percent::from_parts(98), + /// ); + /// assert_eq!( + /// Percent::from_rational_with_rounding(984u64, 1000, Nearest).unwrap(), + /// Percent::from_parts(98), + /// ); + /// assert_eq!( + /// Percent::from_rational_with_rounding(985u64, 1000, Nearest).unwrap(), + /// Percent::from_parts(99), + /// ); + /// assert_eq!( + /// Percent::from_rational_with_rounding(981u64, 1000, Up).unwrap(), + /// Percent::from_parts(99), + /// ); + /// assert_eq!( + /// Percent::from_rational_with_rounding(1001u64, 1000, Up), + /// Err(()), + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # use sp_arithmetic::{Percent, PerThing, Rounding::*}; + /// # fn main () { + /// assert_eq!( + /// Percent::from_rational_with_rounding(981u64, 1000, Up).unwrap(), + /// Percent::from_parts(99), + /// ); + /// # } + /// ``` + fn from_rational_with_rounding(p: N, q: N, rounding: Rounding) -> Result + where + N: Clone + + Ord + + TryInto + + TryInto + + ops::Div + + ops::Rem + + ops::Add + + ops::AddAssign + + Unsigned + + Zero + + One, Self::Inner: Into; + /// Same as `Self::from_rational`. #[deprecated = "Use from_rational instead"] fn from_rational_approximation(p: N, q: N) -> Self @@ -310,6 +397,7 @@ pub trait PerThing: + ops::Div + ops::Rem + ops::Add + + ops::AddAssign + Unsigned + Zero + One, @@ -323,7 +411,8 @@ pub trait PerThing: /// /// `PerThing`s are unsigned so `Up` means towards infinity and `Down` means towards zero. /// `Nearest` will round an exact half down. -enum Rounding { +#[derive(sp_std::fmt::Debug)] +pub enum Rounding { Up, Down, Nearest, @@ -430,7 +519,7 @@ macro_rules! implement_per_thing { /// #[doc = $title] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] - #[derive(Encode, Copy, Clone, PartialEq, Eq, codec::MaxEncodedLen, PartialOrd, Ord, sp_std::fmt::Debug, scale_info::TypeInfo)] + #[derive(Encode, Copy, Clone, PartialEq, Eq, codec::MaxEncodedLen, PartialOrd, Ord, scale_info::TypeInfo)] pub struct $name($type); /// Implementation makes any compact encoding of `PerThing::Inner` valid, @@ -452,6 +541,55 @@ macro_rules! implement_per_thing { } } + #[cfg(feature = "std")] + impl sp_std::fmt::Debug for $name { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + if $max == <$type>::max_value() { + // Not a power of ten: show as N/D and approx % + let pc = (self.0 as f64) / (self.0 as f64) * 100f64; + write!(fmt, "{:.2}% ({}/{})", pc, self.0, $max) + } else { + // A power of ten: calculate exact percent + let units = self.0 / ($max / 100); + let rest = self.0 % ($max / 100); + write!(fmt, "{}", units)?; + if rest > 0 { + write!(fmt, ".")?; + let mut m = $max / 100; + while rest % m > 0 { + m /= 10; + write!(fmt, "{:01}", rest / m % 10)?; + } + } + write!(fmt, "%") + } + } + } + + #[cfg(not(feature = "std"))] + impl sp_std::fmt::Debug for $name { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + if $max == <$type>::max_value() { + // Not a power of ten: show as N/D and approx % + write!(fmt, "{}/{}", self.0, $max) + } else { + // A power of ten: calculate exact percent + let units = n / ($max / 100); + let rest = n % ($max / 100); + write!(fmt, "{}", units)?; + if rest > 0 { + write!(fmt, ".", units)?; + let mut m = $max; + while rest % m > 0 { + m /= 10; + write!(fmt, "{:01}", rest / m % 10)?; + } + } + write!(fmt, "%") + } + } + } + impl PerThing for $name { type Inner = $type; type Upper = $upper_type; @@ -470,53 +608,78 @@ macro_rules! implement_per_thing { Self::from_parts((x.max(0.).min(1.) * $max as f64) as Self::Inner) } - fn from_rational(p: N, q: N) -> Self + fn from_rational_with_rounding(p: N, q: N, r: Rounding) -> Result where - N: Clone + Ord + TryInto + TryInto - + ops::Div + ops::Rem + ops::Add + Unsigned - + Zero + One, - Self::Inner: Into, + N: Clone + + Ord + + TryInto + + TryInto + + ops::Div + + ops::Rem + + ops::Add + + ops::AddAssign + + Unsigned + + Zero + + One, + Self::Inner: Into { let div_ceil = |x: N, f: N| -> N { let mut o = x.clone() / f.clone(); - let r = x.rem(f.clone()); - if r > N::zero() { - o = o + N::one(); + let r = x % f; + if !r.is_zero() { + o += N::one(); + } + o + }; + let div_rounded = |n: N, d: N| -> N { + let mut o = n.clone() / d.clone(); + if match r { + Rounding::Up => { !((n % d).is_zero()) }, + Rounding::Nearest => { let rem = n % d.clone(); rem.clone() + rem >= d }, + Rounding::Down => false, + } { + o += N::one() } o }; // q cannot be zero. - let q: N = q.max((1 as Self::Inner).into()); + if q.is_zero() { return Err(()) } // p should not be bigger than q. - let p: N = p.min(q.clone()); + if p > q { return Err(()) } - let factor: N = div_ceil(q.clone(), $max.into()).max((1 as Self::Inner).into()); + let factor: N = div_ceil(q.clone(), $max.into()).max(One::one()); // q cannot overflow: (q / (q/$max)) < $max. p < q hence p also cannot overflow. - let q_reduce: $type = (q.clone() / factor.clone()) + let q_reduce: $type = div_rounded(q, factor.clone()) .try_into() .map_err(|_| "Failed to convert") .expect( - "q / ceil(q/$max) < $max. Macro prevents any type being created that \ + "`q / ceil(q/$max) < $max`; macro prevents any type being created that \ does not satisfy this; qed" ); - let p_reduce: $type = (p / factor) + let p_reduce: $type = div_rounded(p, factor) .try_into() .map_err(|_| "Failed to convert") .expect( - "q / ceil(q/$max) < $max. Macro prevents any type being created that \ + "`p / ceil(p/$max) < $max`; macro prevents any type being created that \ does not satisfy this; qed" ); - // `p_reduced` and `q_reduced` are withing Self::Inner. Mul by another $max will - // always fit in $upper_type. This is guaranteed by the macro tests. - let part = - p_reduce as $upper_type - * <$upper_type>::from($max) - / q_reduce as $upper_type; + // `p_reduced` and `q_reduced` are within `Self::Inner`. Multiplication by another + // `$max` will always fit in `$upper_type`. This is guaranteed by the macro tests. + let n = p_reduce as $upper_type * <$upper_type>::from($max); + let d = q_reduce as $upper_type; + let mut part = n / d; + if match r { + Rounding::Up => { !((n % d).is_zero()) }, + Rounding::Nearest => { let r = n % d; r + r >= d }, + Rounding::Down => false, + } { + part += 1 as $upper_type + } - $name(part as Self::Inner) + Ok($name(part as Self::Inner)) } } @@ -577,20 +740,38 @@ macro_rules! implement_per_thing { /// See [`PerThing::from_rational`]. #[deprecated = "Use `PerThing::from_rational` instead"] pub fn from_rational_approximation(p: N, q: N) -> Self - where N: Clone + Ord + TryInto<$type> + - TryInto<$upper_type> + ops::Div + ops::Rem + - ops::Add + Unsigned, - $type: Into, + where + N: Clone + + Ord + + TryInto<$type> + + TryInto<$upper_type> + + ops::Div + + ops::Rem + + ops::Add + + ops::AddAssign + + Unsigned + + Zero + + One, + $type: Into { ::from_rational(p, q) } /// See [`PerThing::from_rational`]. pub fn from_rational(p: N, q: N) -> Self - where N: Clone + Ord + TryInto<$type> + - TryInto<$upper_type> + ops::Div + ops::Rem + - ops::Add + Unsigned, - $type: Into, + where + N: Clone + + Ord + + TryInto<$type> + + TryInto<$upper_type> + + ops::Div + + ops::Rem + + ops::Add + + ops::AddAssign + + Unsigned + + Zero + + One, + $type: Into { ::from_rational(p, q) } @@ -662,14 +843,35 @@ macro_rules! implement_per_thing { } /// Saturating division. Compute `self / rhs`, saturating at one if `rhs < self`. - pub fn saturating_div(self, rhs: Self) -> Self { + /// + /// The `rounding` method must be specified. e.g.: + /// + /// ```rust + /// # use sp_arithmetic::{Percent, PerThing, Rounding::*}; + /// # fn main () { + /// let pc = |x| Percent::from_percent(x); + /// assert_eq!( + /// pc(2).saturating_div(pc(3), Down), + /// pc(66), + /// ); + /// assert_eq!( + /// pc(1).saturating_div(pc(3), Nearest), + /// pc(33), + /// ); + /// assert_eq!( + /// pc(2).saturating_div(pc(3), Nearest), + /// pc(67), + /// ); + /// assert_eq!( + /// pc(1).saturating_div(pc(3), Up), + /// pc(34), + /// ); + /// # } + /// ``` + pub fn saturating_div(self, rhs: Self, r: Rounding) -> Self { let p = self.0; let q = rhs.0; - if p < q { - Self::from_rational(p, q) - } else { - Self::one() - } + Self::from_rational_with_rounding(p, q, r).unwrap_or_else(|_| Self::one()) } } From a1a4619c74322ebd5e5aa705b0cb28d9b07d4cd4 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 12 Apr 2022 16:24:43 +0100 Subject: [PATCH 07/48] Tweaks and new quad curve --- Cargo.lock | 1 + frame/referenda/src/types.rs | 243 ++++++++++++++++++-- primitives/arithmetic/Cargo.toml | 1 + primitives/arithmetic/src/fixed_point.rs | 82 ++++++- primitives/arithmetic/src/helpers_128bit.rs | 228 +++++++++++++++++- primitives/arithmetic/src/traits.rs | 2 + 6 files changed, 534 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51b8c8176a074..a89163b8365c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9748,6 +9748,7 @@ dependencies = [ "rand 0.7.3", "scale-info", "serde", + "sp-core", "sp-debug-derive", "sp-std", "static_assertions", diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 28e70a0b458f0..2d10c2d000446 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -21,7 +21,7 @@ use super::*; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{traits::schedule::Anon, Parameter}; use scale_info::TypeInfo; -use sp_runtime::{RuntimeDebug, PerThing}; +use sp_runtime::{RuntimeDebug, PerThing, FixedI64, FixedPointNumber}; use sp_arithmetic::Rounding::*; use sp_std::fmt::Debug; @@ -256,22 +256,170 @@ pub enum Curve { /// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues /// but the `y` component has a lower limit of `end`. SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill }, - /// A simple recipocal (`K/x + O`) curve: `factor` is `K` and `offset` is `O`. + /// A simple reciprocal (`K/x + O`) curve: `factor` is `K` and `offset` is `O`. + SimpleReciprocal { factor: Perbill, offset: Perbill }, + /// A reciprocal (`K/(x + K/(1-O)) + O`) curve: `factor` is `K` and `offset` is `O`. This + /// guarantees the point (0, 1) without resorting to truncation. Reciprocal { factor: Perbill, offset: Perbill }, + /// A recipocal (`K/(x+S)-T`) curve: `factor` is `K` and `x_offset` is `S`, `y_offset` is `T`. + TranslatedReciprocal { factor: FixedI64, x_offset: FixedI64, y_offset: FixedI64 }, +} + +fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 { + let two = FixedI64::saturating_from_integer(2); + let four = FixedI64::saturating_from_integer(4); + (-b + (b*b - four*a*c).sqrt().unwrap_or(two)) / (two*a) } impl Curve { + #[cfg(feature = "std")] + pub fn basic_reciprocal_from_point( + delay: N, + period: N, + level: Perbill, + ) -> Curve { + Self::basic_reciprocal_from_point_and_floor(delay, period, level, Perbill::zero()) + } + + #[cfg(feature = "std")] + pub fn basic_reciprocal_from_point_and_floor( + delay: N, + period: N, + level: Perbill, + floor: Perbill, + ) -> Curve { + let delay = Perbill::from_rational(delay, period); + let offset = floor; + let ymo = level.saturating_sub(offset); + let bottom = ymo.saturating_div(offset.left_from_one(), Down).left_from_one(); + let factor = (delay * ymo).saturating_div(bottom, Down); + Curve::Reciprocal { factor, offset } + } + #[cfg(feature = "std")] + pub fn reciprocal_from_point( + delay: N, + period: N, + level: Perbill, + ) -> Curve { + Self::reciprocal_from_point_and_floor(delay, period, level, Perbill::zero()) + } + + #[cfg(feature = "std")] + pub fn reciprocal_from_point_and_floor( + delay: N, + period: N, + level: Perbill, + floor: Perbill, + ) -> Curve { + let delay = Perbill::from_rational(delay, period); + let floor_fixed = FixedI64::from(floor); + let mut bounds = (FixedI64::zero(), FixedI64::one()); + let two = FixedI64::saturating_from_integer(2); + let epsilon = FixedI64::from_inner(1); + while bounds.1 - bounds.0 > epsilon { + let factor = (bounds.0 + bounds.1) / two; + let c = Self::reciprocal_from_factor_and_floor(factor, floor_fixed); + if c.threshold(delay) > level { + bounds = (bounds.0, factor) + } else { + bounds = (factor, bounds.1) + } + } + let c0 = Self::reciprocal_from_factor_and_floor(bounds.0, floor_fixed); + let c1 = Self::reciprocal_from_factor_and_floor(bounds.1, floor_fixed); + let c0_level = c0.threshold(delay); + let c1_level = c1.threshold(delay); + if c0_level.max(level) - c0_level.min(level) < c1_level.max(level) - c1_level.min(level) { + c0 + } else { + c1 + } + } + + #[cfg(feature = "std")] + fn reciprocal_from_factor_and_floor(factor: FixedI64, floor: FixedI64) -> Self { + let x_offset = pos_quad_solution(FixedI64::one() - floor, FixedI64::one() - floor, -factor); + let y_offset = floor - factor / (FixedI64::one() + x_offset); + Curve::TranslatedReciprocal { factor, x_offset, y_offset } + } + + /// Print some info on the curve. + #[cfg(feature = "std")] + pub fn info(&self, days: u32, name: impl std::fmt::Display) { + let hours = days * 24; + println!("Curve {name} := {:?}:", self); + println!(" t + 0h: {:?}", self.threshold(Perbill::zero())); + println!(" t + 1h: {:?}", self.threshold(Perbill::from_rational(1, hours))); + println!(" t + 2h: {:?}", self.threshold(Perbill::from_rational(2, hours))); + println!(" t + 3h: {:?}", self.threshold(Perbill::from_rational(3, hours))); + println!(" t + 6h: {:?}", self.threshold(Perbill::from_rational(6, hours))); + println!(" t + 12h: {:?}", self.threshold(Perbill::from_rational(12, hours))); + println!(" t + 24h: {:?}", self.threshold(Perbill::from_rational(24, hours))); + let mut l = 0; + for &(n, d) in [(1, 12), (1, 8), (1, 4), (1, 2), (3, 4), (1, 1)].iter() { + let t = days * n / d; + if t != l { + println!(" t + {t}d: {:?}", self.threshold(Perbill::from_rational(t, days))); + l = t; + } + } + let t = |p: Perbill| -> std::string::String { + if p.is_one() { + "never".into() + } else { + let minutes = p * (hours * 60); + if minutes < 60 { + format!("{} minutes", minutes) + } else if minutes < 8 * 60 && minutes % 60 != 0 { + format!("{} hours {} minutes", minutes / 60, minutes % 60) + } else if minutes < 72 * 60 { + format!("{} hours", minutes / 60) + } else if minutes / 60 % 24 == 0 { + format!("{} days", minutes / 60 / 24) + } else { + format!("{} days {} hours", minutes / 60 / 24, minutes / 60 % 24) + } + } + }; + if self.delay(Perbill::from_percent(49)) < Perbill::one() { + println!(" 30% threshold: {}", t(self.delay(Perbill::from_percent(30)))); + println!(" 10% threshold: {}", t(self.delay(Perbill::from_percent(10)))); + println!(" 3% threshold: {}", t(self.delay(Perbill::from_percent(3)))); + println!(" 1% threshold: {}", t(self.delay(Perbill::from_percent(1)))); + println!(" 0.1% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 1_000)))); + println!(" 0.01% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 10_000)))); + } else { + println!(" 99.9% threshold: {}", t(self.delay(Perbill::from_rational(999u32, 1_000)))); + println!(" 99% threshold: {}", t(self.delay(Perbill::from_percent(99)))); + println!(" 95% threshold: {}", t(self.delay(Perbill::from_percent(95)))); + println!(" 90% threshold: {}", t(self.delay(Perbill::from_percent(90)))); + println!(" 75% threshold: {}", t(self.delay(Perbill::from_percent(75)))); + println!(" 60% threshold: {}", t(self.delay(Perbill::from_percent(60)))); + } + } + /// Determine the `y` value for the given `x` value. pub(crate) fn threshold(&self, x: Perbill) -> Perbill { match self { Self::LinearDecreasing { begin, delta } => *begin - (*delta * x).min(*begin), Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), + Self::SimpleReciprocal { factor, offset } => { + // Actual curve is y = factor / (x + x_offset) + offset + // we want to avoid saturating prior to the division. + Perbill::from_rational(factor.deconstruct(), x.deconstruct()).saturating_add(*offset) + } Self::Reciprocal { factor, offset } => { let x_offset = factor.saturating_div(offset.left_from_one(), Down); // Actual curve is y = factor / (x + x_offset) + offset // we want to avoid saturating prior to the division. - Perbill::from_rational_with_rounding(factor.deconstruct(), x.deconstruct() + x_offset.deconstruct(), Down).unwrap_or_else(|_| Perbill::one()).saturating_add(*offset) + Perbill::from_rational(factor.deconstruct(), x.deconstruct() + x_offset.deconstruct()) + .saturating_add(*offset) + } + Self::TranslatedReciprocal { factor, x_offset, y_offset } => { + factor.checked_rounding_div(FixedI64::from(x) + *x_offset, Down) + .map(|yp| (yp + *y_offset).into_clamped_perthing()) + .unwrap_or_else(Perbill::one) } } } @@ -304,11 +452,20 @@ impl Curve { } else { period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step)) }, + Self::SimpleReciprocal { factor, offset } => { + // Actual curve is y = factor / x + offset + // Ergo curve is x = factor / (y - offset) + if y < *offset { + Perbill::one() + } else { + factor.saturating_div(y - *offset, Up) + } + }, Self::Reciprocal { factor, offset } => { let x_offset = factor.saturating_div(offset.left_from_one(), Down); - // Actual curve is y = factor / (x + x_offset) + offset + // Actual curve is y = factor / (x + x_offset) + y_offset // Ergo curve is x = factor / (y - offset) - x_offset - // To avoid pre-saturation problems, we move the x_offset term to happen prior to + // To avoid pre-saturation problems, we move the `x_offset` term to happen prior to // the division. // So: // yo := y - offset @@ -317,12 +474,15 @@ impl Curve { Perbill::one() } else { let yo = y - *offset; - if *factor < x_offset * yo { - println!("SATURATING in delay"); - } factor.saturating_sub(x_offset * yo).saturating_div(yo, Up) } - } + }, + Self::TranslatedReciprocal { factor, x_offset, y_offset } => { + let y = FixedI64::from(y); + let maybe_term = factor.checked_rounding_div(y - *y_offset, Up); + maybe_term.and_then(|term| (term - *x_offset).try_into_perthing().ok()) + .unwrap_or_else(Perbill::one) + }, } } @@ -339,27 +499,44 @@ impl Debug for Curve { Self::LinearDecreasing { begin, delta } => { write!( f, - "Linear[(0%, {}%) -> (100%, {}%)]", - *begin * 100u32, - (*begin - *delta) * 100u32, + "Linear[(0%, {:?}) -> (100%, {:?})]", + begin, + *begin - *delta, ) }, Self::SteppedDecreasing { begin, end, step, period } => { write!( f, - "Stepped[(0, {}%) -> (1, {}%) by ({}%, {}%)]", - *begin * 100u32, - *end * 100u32, - *period * 100u32, - *step * 100u32, + "Stepped[(0%, {:?}) -> (100%, {:?}) by ({:?}, {:?})]", + begin, + end, + period, + step, + ) + }, + Self::SimpleReciprocal { factor, offset } => { + write!( + f, + "SimpleReciprocal[factor of {:?}, offset of {:?}]", + factor, + offset, ) }, Self::Reciprocal { factor, offset } => { write!( f, - "Reciprocal[factor of {}%, offset of {}%]", - *factor * 100u32, - *offset * 100u32, + "Reciprocal[factor of {:?}, offset of {:?}]", + factor, + offset, + ) + }, + Self::TranslatedReciprocal { factor, x_offset, y_offset } => { + write!( + f, + "TranslatedReciprocal[factor of {:?}, x_offset of {:?}, y_offset of {:?}]", + factor, + x_offset, + y_offset, ) } } @@ -376,6 +553,14 @@ mod tests { Perbill::from_percent(x) } + #[test] + #[should_panic] + fn check_curves() { +// Curve::reciprocal_from_point(7, 28u32, 0.1).info(28u32, "Tip"); + Curve::reciprocal_from_point_and_floor(1u32, 28, percent(65), percent(50)).info(28u32, "Tip"); + assert!(false); + } + #[test] fn insert_sorted_works() { let mut b: BoundedVec> = vec![20, 30, 40].try_into().unwrap(); @@ -404,6 +589,24 @@ mod tests { assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]); } + #[test] + fn translated_reciprocal_works() { + let c: Curve = Curve::TranslatedReciprocal { + factor: FixedI64::from_float(0.03125), + x_offset: FixedI64::from_float(0.0363306838226), + y_offset: FixedI64::from_float(0.139845532427), + }; + c.info(28u32, "Test"); + + for i in 0..9_696_969u32 { + let query = Perbill::from_rational(i, 9_696_969); + // Determine the nearest point in time when the query will be above threshold. + let delay_needed = c.delay(query); + // Ensure that it actually does pass at that time, or that it will never pass. + assert!(delay_needed.is_one() || c.passing(delay_needed, query)); + } + } + #[test] fn basic_reciprocal_works() { let c: Curve = Curve::Reciprocal { diff --git a/primitives/arithmetic/Cargo.toml b/primitives/arithmetic/Cargo.toml index 26ee7677363c9..f663765141eb5 100644 --- a/primitives/arithmetic/Cargo.toml +++ b/primitives/arithmetic/Cargo.toml @@ -31,6 +31,7 @@ sp-debug-derive = { version = "4.0.0", default-features = false, path = "../debu rand = "0.7.2" criterion = "0.3" primitive-types = "0.11.1" +sp-core = { version = "6.0.0", features = ["full_crypto"], path = "../core" } [features] default = ["std"] diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index 3ecfb60ee0f52..713a0cd844d41 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -18,12 +18,12 @@ //! Decimal Fixed Point implementations for Substrate runtime. use crate::{ - helpers_128bit::multiply_by_rational, + helpers_128bit::{multiply_by_rational, multiply_by_rational_with_rounding}, traits::{ Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedSub, One, SaturatedConversion, Saturating, UniqueSaturatedInto, Zero, }, - PerThing, + PerThing, Rounding, }; use codec::{CompactAs, Decode, Encode}; use sp_std::{ @@ -419,6 +419,72 @@ macro_rules! implement_fixed { pub fn to_float(self) -> f64 { self.0 as f64 / ::DIV as f64 } + + /// Attempt to convert into a `PerThing`. This will succeed iff `self` is at least zero + /// and at most one. If it is out of bounds, it will result in an error returning the + /// clamped value. + pub fn try_into_perthing(self) -> Result { + if self < Self::zero() { + Err(P::zero()) + } else if self > Self::one() { + Err(P::one()) + } else { + Ok(P::from_rational(self.0 as u128, $div)) + } + } + + /// Attempt to convert into a `PerThing`. This will always succeed resulting in a + /// clamped value if `self` is less than zero or greater than one. + pub fn into_clamped_perthing(self) -> P { + if self < Self::zero() { + P::zero() + } else if self > Self::one() { + P::one() + } else { + P::from_rational(self.0 as u128, $div) + } + } + + /// A version of div with customisable rounding. + pub fn checked_rounding_div(self, other: Self, rounding: Rounding) -> Option { + if other.0 == 0 { + return None + } + + let lhs: I129 = self.0.into(); + let rhs: I129 = other.0.into(); + let negative = lhs.negative != rhs.negative; + + multiply_by_rational_with_rounding(lhs.value, Self::DIV as u128, rhs.value, rounding) + .and_then(|value| from_i129(I129 { value, negative })) + .map(Self) + } + + /// Compute the square root, rounding as desired. If it overflows or is negative, then + /// `None` is returned. + pub fn sqrt(self) -> Option { + use integer_sqrt::IntegerSquareRoot; + let v: u128 = self.0.try_into().ok()?; + if v == 0 { + return Some(Self(0)) + } + + // Want x' = sqrt(x) where x = n/D and x' = n'/D (D is fixed) + // Our prefered way is: + // sqrt(n/D) = sqrt(nD / D^2) = sqrt(nD)/sqrt(D^2) = sqrt(nD)/D + // ergo n' = sqrt(nD) + // but this requires nD to fit into our type. + // if nD doesn't fit then we can fall back on: + // sqrt(nD) = sqrt(n)*sqrt(D) + // computing them individually and taking the product at the end. we will lose some + // precision though. + let r = if let Some(vd) = v.checked_mul($div) { + vd.integer_sqrt() + } else { + v.integer_sqrt().checked_mul($div.integer_sqrt())? + }; + Some(Self(r.try_into().ok()?)) + } } impl Saturating for $name { @@ -850,6 +916,18 @@ macro_rules! implement_fixed { } } + #[test] + fn op_sqrt_works() { + for i in 1..1_000i64 { + let x = $name::saturating_from_rational(i, 1_000i64); + dbg!(i, x, x * x, (x * x).sqrt()); + assert_eq!((x * x).sqrt(), Some(x)); + let x = $name::saturating_from_rational(i, 1i64); + dbg!(i, x, x * x, (x * x).sqrt()); + assert_eq!((x * x).sqrt(), Some(x)); + } + } + #[test] fn op_div_works() { let a = $name::saturating_from_integer(42); diff --git a/primitives/arithmetic/src/helpers_128bit.rs b/primitives/arithmetic/src/helpers_128bit.rs index 735b11287cbe4..c6ce50fb5f39d 100644 --- a/primitives/arithmetic/src/helpers_128bit.rs +++ b/primitives/arithmetic/src/helpers_128bit.rs @@ -20,7 +20,8 @@ //! assumptions of a bigger type (u128) being available, or simply create a per-thing and use the //! multiplication implementation provided there. -use crate::biguint; +use crate::{biguint, Rounding}; +use sp_std::convert::TryInto; use num_traits::Zero; use sp_std::{ cmp::{max, min}, @@ -117,3 +118,228 @@ pub fn multiply_by_rational(mut a: u128, mut b: u128, mut c: u128) -> Result u128 { + a & ((1<<64)-1) + } + + /// Returns the most significant 64 bits of a + fn high_64(a: u128) -> u128 { + a >> 64 + } + + /// Returns 2^128 - a (two's complement) + fn neg128(a: u128) -> u128 { + (!a).wrapping_add(1) + } + + /// Returns 2^128 / a + fn div128(a: u128) -> u128 { + (neg128(a)/a).wrapping_add(1) + } + + /// Returns 2^128 % a + fn mod128(a: u128) -> u128 { + neg128(a) % a + } + + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct Double128 { + high: u128, + low: u128, + } + + impl TryFrom for u128 { + type Error = (); + fn try_from(x: Double128) -> Result { + match x.high { + 0 => Ok(x.low), + _ => Err(()), + } + } + } + + impl Zero for Double128 { + fn zero() -> Self { + Self { + high: 0, + low: 0, + } + } + fn is_zero(&self) -> bool { + self.high == 0 && self.low == 0 + } + } + + impl sp_std::ops::Add for Double128 { + type Output = Self; + fn add(self, b: Self) -> Self { + let (low, overflow) = self.low.overflowing_add(b.low); + let carry = overflow as u128; // 1 if true, 0 if false. + let high = self.high.wrapping_add(b.high).wrapping_add(carry as u128); + Double128 { high, low } + } + } + + impl sp_std::ops::AddAssign for Double128 { + fn add_assign(&mut self, b: Self) { + *self = *self + b; + } + } + + impl sp_std::ops::Div for Double128 { + type Output = (Self, u128); + fn div(mut self, rhs: u128) -> (Self, u128) { + if rhs == 1 { + return (self, 0); + } + + // (self === a; rhs === b) + // Calculate a / b + // = (a_high << 128 + a_low) / b + // let (q, r) = (div128(b), mod128(b)); + // = (a_low * (q * b + r)) + a_high) / b + // = (a_low * q * b + a_low * r + a_high)/b + // = (a_low * r + a_high) / b + a_low * q + let (q, r) = (div128(rhs), mod128(rhs)); + + // x = current result + // a = next number + let mut x = Double128::zero(); + while self.high != 0 { + // x += a.low * q + x += Double128::product_of(self.high, q); + // a = a.low * r + a.high + self = Double128::product_of(self.high, r) + self.low_part(); + } + + (x + Double128::from_low(self.low / rhs), self.low % rhs) + } + } + + impl Double128 { + /// Return a `Double128` value representing the `scaled_value << 64`. + /// + /// This means the lower half of the `high` component will be equal to the upper 64-bits of + /// `scaled_value` (in the lower positions) and the upper half of the `low` component will + /// be equal to the lower 64-bits of `scaled_value`. + pub fn left_shift_64(scaled_value: u128) -> Self { + Self { + high: scaled_value >> 64, + low: scaled_value << 64, + } + } + + /// Construct a value from the upper 128 bits only, with the lower being zeroed. + pub fn from_low(low: u128) -> Self { + Self { high: 0, low } + } + + /// Returns the same value ignoring anything in the high 128-bits. + pub fn low_part(self) -> Self { + Self { high: 0, .. self } + } + + /// Returns a*b (in 256 bits) + pub fn product_of(a: u128, b: u128) -> Self { + // Split a and b into hi and lo 64-bit parts + let (a_low, a_high) = (low_64(a), high_64(a)); + let (b_low, b_high) = (low_64(b), high_64(b)); + // a = (a_low + a_high << 64); b = (b_low + b_high << 64); + // ergo a*b = (a_low + a_high << 64)(b_low + b_high << 64) + // = a_low * b_low + // + a_low * b_high << 64 + // + a_high << 64 * b_low + // + a_high << 64 * b_high << 64 + // assuming: + // f = a_low * b_low + // o = a_low * b_high + // i = a_high * b_low + // l = a_high * b_high + // then: + // a*b = (o+i) << 64 + f + l << 128 + let (f, o, i, l) = (a_low * b_low, a_low * b_high, a_high * b_low, a_high * b_high); + let fl = Self { high: l, low: f }; + let i = Self::left_shift_64(i); + let o = Self::left_shift_64(o); + fl + i + o + } + } +} + +/// Returns `a * b / c` and `(a * b) % c` (wrapping to 128 bits) or `None` in the case of +/// overflow. +pub fn multiply_by_rational_with_rounding(a: u128, b: u128, c: u128, r: Rounding) -> Option { + use double128::Double128; + if c == 0 { + panic!("attempt to divide by zero") + } + let (result, remainder) = Double128::product_of(a, b) / c; + let mut result: u128 = result.try_into().ok()?; + if match r { + Rounding::Up => remainder > 0, + Rounding::Nearest => remainder >= c / 2 + c % 2, + Rounding::Down => false, + } { + result = result.checked_add(1)?; + } + Some(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use Rounding::*; + use multiply_by_rational_with_rounding as mulrat; + use codec::{Encode, Decode}; + + const MAX: u128 = u128::max_value(); + + #[test] + fn rational_multiply_basic_rounding_works() { + assert_eq!(mulrat(1, 1, 1, Up), Some(1)); + assert_eq!(mulrat(3, 1, 3, Up), Some(1)); + assert_eq!(mulrat(1, 2, 3, Down), Some(0)); + assert_eq!(mulrat(1, 1, 3, Up), Some(1)); + assert_eq!(mulrat(1, 2, 3, Nearest), Some(1)); + assert_eq!(mulrat(1, 1, 3, Nearest), Some(0)); + } + + #[test] + fn rational_multiply_big_number_works() { + assert_eq!(mulrat(MAX, MAX-1, MAX, Down), Some(MAX-1)); + assert_eq!(mulrat(MAX, 1, MAX, Down), Some(1)); + assert_eq!(mulrat(MAX, MAX-1, MAX, Up), Some(MAX-1)); + assert_eq!(mulrat(MAX, 1, MAX, Up), Some(1)); + assert_eq!(mulrat(1, MAX-1, MAX, Down), Some(0)); + assert_eq!(mulrat(1, 1, MAX, Up), Some(1)); + assert_eq!(mulrat(1, MAX/2, MAX, Nearest), Some(0)); + assert_eq!(mulrat(1, MAX/2+1, MAX, Nearest), Some(1)); + } + + fn random_u128(seed: u32) -> u128 { + u128::decode(&mut &seed.using_encoded(sp_core::hashing::twox_128)[..]).unwrap_or(0) + } + + #[test] + fn op_checked_rounded_div_works() { + for i in 0..100_000u32 { + let a = random_u128(i); + let b = random_u128(i + 1 << 30); + let c = random_u128(i + 1 << 31); + let x = mulrat(a, b, c, Nearest); + let y = multiply_by_rational(a, b, c).ok(); + assert_eq!(x.is_some(), y.is_some()); + let x = x.unwrap_or(0); + let y = y.unwrap_or(0); + let d = x.max(y) - x.min(y); + assert_eq!(d, 0); + } + } +} \ No newline at end of file diff --git a/primitives/arithmetic/src/traits.rs b/primitives/arithmetic/src/traits.rs index 748aaed2a7cf5..466d5696c7136 100644 --- a/primitives/arithmetic/src/traits.rs +++ b/primitives/arithmetic/src/traits.rs @@ -58,6 +58,7 @@ pub trait BaseArithmetic: + Bounded + HasCompact + Sized + + Clone + TryFrom + TryInto + TryFrom @@ -113,6 +114,7 @@ impl< + Bounded + HasCompact + Sized + + Clone + TryFrom + TryInto + TryFrom From 57100580b5497c5e8347a8755e288f8742b8deb3 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 12 Apr 2022 19:23:20 +0100 Subject: [PATCH 08/48] Const derivation of reciprocal curve parameters --- frame/referenda/src/types.rs | 396 +++++--------------- primitives/arithmetic/src/fixed_point.rs | 208 +++++++++- primitives/arithmetic/src/helpers_128bit.rs | 164 +++++--- 3 files changed, 405 insertions(+), 363 deletions(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 2d10c2d000446..5137e9b9996a6 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -21,7 +21,7 @@ use super::*; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{traits::schedule::Anon, Parameter}; use scale_info::TypeInfo; -use sp_runtime::{RuntimeDebug, PerThing, FixedI64, FixedPointNumber}; +use sp_runtime::{RuntimeDebug, PerThing, FixedI64}; use sp_arithmetic::Rounding::*; use sp_std::fmt::Debug; @@ -256,95 +256,67 @@ pub enum Curve { /// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues /// but the `y` component has a lower limit of `end`. SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill }, - /// A simple reciprocal (`K/x + O`) curve: `factor` is `K` and `offset` is `O`. - SimpleReciprocal { factor: Perbill, offset: Perbill }, - /// A reciprocal (`K/(x + K/(1-O)) + O`) curve: `factor` is `K` and `offset` is `O`. This - /// guarantees the point (0, 1) without resorting to truncation. - Reciprocal { factor: Perbill, offset: Perbill }, /// A recipocal (`K/(x+S)-T`) curve: `factor` is `K` and `x_offset` is `S`, `y_offset` is `T`. - TranslatedReciprocal { factor: FixedI64, x_offset: FixedI64, y_offset: FixedI64 }, + Reciprocal { factor: FixedI64, x_offset: FixedI64, y_offset: FixedI64 }, } -fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 { - let two = FixedI64::saturating_from_integer(2); - let four = FixedI64::saturating_from_integer(4); - (-b + (b*b - four*a*c).sqrt().unwrap_or(two)) / (two*a) +const fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 { + const TWO: FixedI64 = FixedI64::from_u32(2); + const FOUR: FixedI64 = FixedI64::from_u32(4); + b.neg().add(b.mul(b).sub(FOUR.mul(a).mul(c)).sqrt()).div(TWO.mul(a)) } impl Curve { - #[cfg(feature = "std")] - pub fn basic_reciprocal_from_point( - delay: N, - period: N, - level: Perbill, + pub const fn reciprocal_from_point( + delay: u128, + period: u128, + level: FixedI64, ) -> Curve { - Self::basic_reciprocal_from_point_and_floor(delay, period, level, Perbill::zero()) + Self::reciprocal_from_point_and_floor(delay, period, level, FixedI64::from_u32(0)) } - #[cfg(feature = "std")] - pub fn basic_reciprocal_from_point_and_floor( - delay: N, - period: N, - level: Perbill, - floor: Perbill, - ) -> Curve { - let delay = Perbill::from_rational(delay, period); - let offset = floor; - let ymo = level.saturating_sub(offset); - let bottom = ymo.saturating_div(offset.left_from_one(), Down).left_from_one(); - let factor = (delay * ymo).saturating_div(bottom, Down); - Curve::Reciprocal { factor, offset } - } - #[cfg(feature = "std")] - pub fn reciprocal_from_point( - delay: N, - period: N, - level: Perbill, + pub const fn reciprocal_from_point_and_floor( + delay: u128, + period: u128, + level: FixedI64, + floor: FixedI64, ) -> Curve { - Self::reciprocal_from_point_and_floor(delay, period, level, Perbill::zero()) - } - - #[cfg(feature = "std")] - pub fn reciprocal_from_point_and_floor( - delay: N, - period: N, - level: Perbill, - floor: Perbill, - ) -> Curve { - let delay = Perbill::from_rational(delay, period); - let floor_fixed = FixedI64::from(floor); - let mut bounds = (FixedI64::zero(), FixedI64::one()); - let two = FixedI64::saturating_from_integer(2); - let epsilon = FixedI64::from_inner(1); - while bounds.1 - bounds.0 > epsilon { - let factor = (bounds.0 + bounds.1) / two; - let c = Self::reciprocal_from_factor_and_floor(factor, floor_fixed); - if c.threshold(delay) > level { - bounds = (bounds.0, factor) + let delay = FixedI64::from_rational(delay, period).into_perbill(); + let mut bounds = (( + FixedI64::from_u32(0), + Self::reciprocal_from_factor_and_floor(FixedI64::from_u32(0), floor), + FixedI64::from_inner(i64::max_value()), + ), ( + FixedI64::from_u32(1), + Self::reciprocal_from_factor_and_floor(FixedI64::from_u32(1), floor), + FixedI64::from_inner(i64::max_value()), + )); + const TWO: FixedI64 = FixedI64::from_u32(2); + while (bounds.1).0.sub((bounds.0).0).into_inner() > 1 { + let factor = (bounds.0).0.add((bounds.1).0).div(TWO); + let curve = Self::reciprocal_from_factor_and_floor(factor, floor); + let curve_level = FixedI64::from_perbill(curve.const_threshold(delay)); + if curve_level.into_inner() > level.into_inner() { + bounds = (bounds.0, (factor, curve, curve_level.sub(level))); } else { - bounds = (factor, bounds.1) + bounds = ((factor, curve, level.sub(curve_level)), bounds.1); } } - let c0 = Self::reciprocal_from_factor_and_floor(bounds.0, floor_fixed); - let c1 = Self::reciprocal_from_factor_and_floor(bounds.1, floor_fixed); - let c0_level = c0.threshold(delay); - let c1_level = c1.threshold(delay); - if c0_level.max(level) - c0_level.min(level) < c1_level.max(level) - c1_level.min(level) { - c0 + if (bounds.0).2.into_inner() < (bounds.1).2.into_inner() { + (bounds.0).1 } else { - c1 + (bounds.1).1 } } - #[cfg(feature = "std")] - fn reciprocal_from_factor_and_floor(factor: FixedI64, floor: FixedI64) -> Self { - let x_offset = pos_quad_solution(FixedI64::one() - floor, FixedI64::one() - floor, -factor); - let y_offset = floor - factor / (FixedI64::one() + x_offset); - Curve::TranslatedReciprocal { factor, x_offset, y_offset } + const fn reciprocal_from_factor_and_floor(factor: FixedI64, floor: FixedI64) -> Self { + let one_minus_floor = FixedI64::from_u32(1).sub(floor); + let x_offset = pos_quad_solution(one_minus_floor, one_minus_floor, factor.neg()); + let y_offset = floor.sub(factor.div(FixedI64::from_u32(1).add(x_offset))); + Curve::Reciprocal { factor, x_offset, y_offset } } /// Print some info on the curve. - #[cfg(feature = "std")] pub fn info(&self, days: u32, name: impl std::fmt::Display) { let hours = days * 24; println!("Curve {name} := {:?}:", self); @@ -404,19 +376,7 @@ impl Curve { Self::LinearDecreasing { begin, delta } => *begin - (*delta * x).min(*begin), Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), - Self::SimpleReciprocal { factor, offset } => { - // Actual curve is y = factor / (x + x_offset) + offset - // we want to avoid saturating prior to the division. - Perbill::from_rational(factor.deconstruct(), x.deconstruct()).saturating_add(*offset) - } - Self::Reciprocal { factor, offset } => { - let x_offset = factor.saturating_div(offset.left_from_one(), Down); - // Actual curve is y = factor / (x + x_offset) + offset - // we want to avoid saturating prior to the division. - Perbill::from_rational(factor.deconstruct(), x.deconstruct() + x_offset.deconstruct()) - .saturating_add(*offset) - } - Self::TranslatedReciprocal { factor, x_offset, y_offset } => { + Self::Reciprocal { factor, x_offset, y_offset } => { factor.checked_rounding_div(FixedI64::from(x) + *x_offset, Down) .map(|yp| (yp + *y_offset).into_clamped_perthing()) .unwrap_or_else(Perbill::one) @@ -424,6 +384,22 @@ impl Curve { } } + /// Determine the `y` value for the given `x` value. + /// + /// This is a partial implementation designed only for use in const functions. + #[cfg(feature = "std")] + const fn const_threshold(&self, x: Perbill) -> Perbill { + match self { + Self::Reciprocal { factor, x_offset, y_offset } => { + match factor.checked_rounding_div(FixedI64::from_perbill(x).add(*x_offset), Down) { + Some(yp) => (yp.add(*y_offset)).into_perbill(), + None => Perbill::one(), + } + } + _ => panic!("const_threshold cannot be used on this curve"), + } + } + /// Determine the smallest `x` value such that `passing` returns `true` when passed along with /// the given `y` value. /// @@ -452,32 +428,7 @@ impl Curve { } else { period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step)) }, - Self::SimpleReciprocal { factor, offset } => { - // Actual curve is y = factor / x + offset - // Ergo curve is x = factor / (y - offset) - if y < *offset { - Perbill::one() - } else { - factor.saturating_div(y - *offset, Up) - } - }, - Self::Reciprocal { factor, offset } => { - let x_offset = factor.saturating_div(offset.left_from_one(), Down); - // Actual curve is y = factor / (x + x_offset) + y_offset - // Ergo curve is x = factor / (y - offset) - x_offset - // To avoid pre-saturation problems, we move the `x_offset` term to happen prior to - // the division. - // So: - // yo := y - offset - // x = (factor - x_offset * yo) / yo - if y < *offset { - Perbill::one() - } else { - let yo = y - *offset; - factor.saturating_sub(x_offset * yo).saturating_div(yo, Up) - } - }, - Self::TranslatedReciprocal { factor, x_offset, y_offset } => { + Self::Reciprocal { factor, x_offset, y_offset } => { let y = FixedI64::from(y); let maybe_term = factor.checked_rounding_div(y - *y_offset, Up); maybe_term.and_then(|term| (term - *x_offset).try_into_perthing().ok()) @@ -514,26 +465,10 @@ impl Debug for Curve { step, ) }, - Self::SimpleReciprocal { factor, offset } => { + Self::Reciprocal { factor, x_offset, y_offset } => { write!( f, - "SimpleReciprocal[factor of {:?}, offset of {:?}]", - factor, - offset, - ) - }, - Self::Reciprocal { factor, offset } => { - write!( - f, - "Reciprocal[factor of {:?}, offset of {:?}]", - factor, - offset, - ) - }, - Self::TranslatedReciprocal { factor, x_offset, y_offset } => { - write!( - f, - "TranslatedReciprocal[factor of {:?}, x_offset of {:?}, y_offset of {:?}]", + "Reciprocal[factor of {:?}, x_offset of {:?}, y_offset of {:?}]", factor, x_offset, y_offset, @@ -549,15 +484,16 @@ mod tests { use sp_runtime::PerThing; use frame_support::traits::ConstU32; - fn percent(x: u32) -> Perbill { - Perbill::from_percent(x) + const fn percent(x: u128) -> FixedI64 { + FixedI64::from_rational(x, 100) } + const TIP_CURVE: Curve = Curve::reciprocal_from_point_and_floor(1, 28, percent(65), percent(50)); + #[test] #[should_panic] fn check_curves() { -// Curve::reciprocal_from_point(7, 28u32, 0.1).info(28u32, "Tip"); - Curve::reciprocal_from_point_and_floor(1u32, 28, percent(65), percent(50)).info(28u32, "Tip"); + TIP_CURVE.info(28u32, "Tip"); assert!(false); } @@ -591,7 +527,7 @@ mod tests { #[test] fn translated_reciprocal_works() { - let c: Curve = Curve::TranslatedReciprocal { + let c: Curve = Curve::Reciprocal { factor: FixedI64::from_float(0.03125), x_offset: FixedI64::from_float(0.0363306838226), y_offset: FixedI64::from_float(0.139845532427), @@ -608,144 +544,16 @@ mod tests { } #[test] - fn basic_reciprocal_works() { - let c: Curve = Curve::Reciprocal { - factor: percent(5), - offset: percent(0), - }; - - for i in 0..9_696_969u32 { - let query = Perbill::from_rational(i, 9_696_969); - // Determine the nearest point in time when the query will be above threshold. - let delay_needed = c.delay(query); - // Ensure that it actually does pass at that time, or that it will never pass. - assert!(delay_needed.is_one() || c.passing(delay_needed, query)); - } - - assert_eq!(c.threshold(percent(0)) * 100u32, 100); - assert_eq!(c.threshold(percent(1)) * 100u32, 83); - assert_eq!(c.threshold(percent(2)) * 100u32, 71); - assert_eq!(c.threshold(percent(3)) * 100u32, 62); - assert_eq!(c.threshold(percent(4)) * 100u32, 56); - assert_eq!(c.threshold(percent(5)) * 100u32, 50); - assert_eq!(c.threshold(percent(6)) * 100u32, 45); - assert_eq!(c.threshold(percent(7)) * 100u32, 42); - assert_eq!(c.threshold(percent(8)) * 100u32, 38); - assert_eq!(c.threshold(percent(9)) * 100u32, 36); - assert_eq!(c.threshold(percent(10)) * 100u32, 33); - assert_eq!(c.threshold(percent(15)) * 100u32, 25); - assert_eq!(c.threshold(percent(20)) * 100u32, 20); - assert_eq!(c.threshold(percent(25)) * 100u32, 17); - assert_eq!(c.threshold(percent(30)) * 100u32, 14); - assert_eq!(c.threshold(percent(35)) * 100u32, 12); - assert_eq!(c.threshold(percent(40)) * 100u32, 11); - assert_eq!(c.threshold(percent(45)) * 100u32, 10); - assert_eq!(c.threshold(percent(50)) * 100u32, 9); - assert_eq!(c.threshold(percent(55)) * 100u32, 8); - assert_eq!(c.threshold(percent(60)) * 100u32, 8); - assert_eq!(c.threshold(percent(65)) * 100u32, 7); - assert_eq!(c.threshold(percent(70)) * 100u32, 7); - assert_eq!(c.threshold(percent(75)) * 100u32, 6); - assert_eq!(c.threshold(percent(80)) * 100u32, 6); - assert_eq!(c.threshold(percent(85)) * 100u32, 6); - assert_eq!(c.threshold(percent(90)) * 100u32, 5); - assert_eq!(c.threshold(percent(95)) * 100u32, 5); - assert_eq!(c.threshold(percent(100)) * 100u32, 5); - - assert_eq!(c.delay(percent(0)) * 100u32, 100); - assert_eq!(c.delay(percent(1)) * 100u32, 100); - assert_eq!(c.delay(percent(2)) * 100u32, 100); - assert_eq!(c.delay(percent(3)) * 100u32, 100); - assert_eq!(c.delay(percent(4)) * 100u32, 100); - assert_eq!(c.delay(percent(5)) * 100u32, 95); - assert_eq!(c.delay(percent(6)) * 100u32, 78); - assert_eq!(c.delay(percent(7)) * 100u32, 66); - assert_eq!(c.delay(percent(8)) * 100u32, 57); - assert_eq!(c.delay(percent(9)) * 100u32, 51); - assert_eq!(c.delay(percent(10)) * 100u32, 45); - assert_eq!(c.delay(percent(15)) * 100u32, 28); - assert_eq!(c.delay(percent(20)) * 100u32, 20); - assert_eq!(c.delay(percent(25)) * 100u32, 15); - assert_eq!(c.delay(percent(30)) * 100u32, 12); - assert_eq!(c.delay(percent(35)) * 100u32, 9); - assert_eq!(c.delay(percent(40)) * 100u32, 7); - assert_eq!(c.delay(percent(45)) * 100u32, 6); - assert_eq!(c.delay(percent(50)) * 100u32, 5); - assert_eq!(c.delay(percent(55)) * 100u32, 4); - assert_eq!(c.delay(percent(60)) * 100u32, 3); - assert_eq!(c.delay(percent(65)) * 100u32, 3); - assert_eq!(c.delay(percent(70)) * 100u32, 2); - assert_eq!(c.delay(percent(75)) * 100u32, 2); - assert_eq!(c.delay(percent(80)) * 100u32, 1); - assert_eq!(c.delay(percent(85)) * 100u32, 1); - assert_eq!(c.delay(percent(90)) * 100u32, 1); - assert_eq!(c.delay(percent(95)) * 100u32, 0); - assert_eq!(c.delay(percent(100)) * 100u32, 0); - } - - #[test] - fn offset_reciprocal_works() { - let c: Curve = Curve::Reciprocal { - factor: percent(10), - offset: percent(50), - }; - - for i in 0..9_696_969u32 { - let query = Perbill::from_rational(i, 9_696_969); - // Determine the nearest point in time when the query will be above threshold. - let delay_needed = c.delay(query); - // Ensure that it actually does pass at that time, or that it will never pass. - assert!(delay_needed.is_one() || c.passing(delay_needed, query)); - } - - assert_eq!(c.threshold(percent(0)) * 100u32, 100); - assert_eq!(c.threshold(percent(10)) * 100u32, 83); - assert_eq!(c.threshold(percent(20)) * 100u32, 75); - assert_eq!(c.threshold(percent(30)) * 100u32, 70); - assert_eq!(c.threshold(percent(40)) * 100u32, 67); - assert_eq!(c.threshold(percent(50)) * 100u32, 64); - assert_eq!(c.threshold(percent(60)) * 100u32, 62); - assert_eq!(c.threshold(percent(70)) * 100u32, 61); - assert_eq!(c.threshold(percent(80)) * 100u32, 60); - assert_eq!(c.threshold(percent(90)) * 100u32, 59); - assert_eq!(c.threshold(percent(100)) * 100u32, 58); - - assert_eq!(c.delay(percent(0)) * 100u32, 100); - assert_eq!(c.delay(percent(10)) * 100u32, 100); - assert_eq!(c.delay(percent(20)) * 100u32, 100); - assert_eq!(c.delay(percent(30)) * 100u32, 100); - assert_eq!(c.delay(percent(40)) * 100u32, 100); - assert_eq!(c.delay(percent(50)) * 100u32, 100); - assert_eq!(c.delay(percent(60)) * 100u32, 80); - assert_eq!(c.delay(percent(70)) * 100u32, 30); - assert_eq!(c.delay(percent(80)) * 100u32, 13); - assert_eq!(c.delay(percent(90)) * 100u32, 5); - assert_eq!(c.delay(percent(100)) * 100u32, 0); - } - - #[test] - fn realistic_offset_reciprocal_works() { - let c: Curve = Curve::Reciprocal { - factor: Perbill::from_rational(35u32, 10_000u32), - offset: percent(10), - }; - - for i in 0..9_696_969u32 { - let query = Perbill::from_rational(i, 9_696_969); - // Determine the nearest point in time when the query will be above threshold. - let delay_needed = c.delay(query); - // Ensure that it actually does pass at that time, or that it will never pass. - assert!(delay_needed.is_one() || c.passing(delay_needed, query)); + fn stepped_decreasing_works() { + fn pc(x: u32) -> Perbill { + Perbill::from_percent(x) } - } - #[test] - fn stepped_decreasing_works() { let c = Curve::SteppedDecreasing { - begin: percent(80), - end: percent(30), - step: percent(10), - period: percent(15), + begin: pc(80), + end: pc(30), + step: pc(10), + period: pc(15), }; for i in 0..9_696_969u32 { @@ -756,32 +564,32 @@ mod tests { assert!(delay_needed.is_one() || c.passing(delay_needed, query)); } - assert_eq!(c.threshold(percent(0)), percent(80)); - assert_eq!(c.threshold(percent(15).less_epsilon()), percent(80)); - assert_eq!(c.threshold(percent(15)), percent(70)); - assert_eq!(c.threshold(percent(30).less_epsilon()), percent(70)); - assert_eq!(c.threshold(percent(30)), percent(60)); - assert_eq!(c.threshold(percent(45).less_epsilon()), percent(60)); - assert_eq!(c.threshold(percent(45)), percent(50)); - assert_eq!(c.threshold(percent(60).less_epsilon()), percent(50)); - assert_eq!(c.threshold(percent(60)), percent(40)); - assert_eq!(c.threshold(percent(75).less_epsilon()), percent(40)); - assert_eq!(c.threshold(percent(75)), percent(30)); - assert_eq!(c.threshold(percent(100)), percent(30)); - - assert_eq!(c.delay(percent(100)), percent(0)); - assert_eq!(c.delay(percent(80)), percent(0)); - assert_eq!(c.delay(percent(80).less_epsilon()), percent(15)); - assert_eq!(c.delay(percent(70)), percent(15)); - assert_eq!(c.delay(percent(70).less_epsilon()), percent(30)); - assert_eq!(c.delay(percent(60)), percent(30)); - assert_eq!(c.delay(percent(60).less_epsilon()), percent(45)); - assert_eq!(c.delay(percent(50)), percent(45)); - assert_eq!(c.delay(percent(50).less_epsilon()), percent(60)); - assert_eq!(c.delay(percent(40)), percent(60)); - assert_eq!(c.delay(percent(40).less_epsilon()), percent(75)); - assert_eq!(c.delay(percent(30)), percent(75)); - assert_eq!(c.delay(percent(30).less_epsilon()), percent(100)); - assert_eq!(c.delay(percent(0)), percent(100)); + assert_eq!(c.threshold(pc(0)), pc(80)); + assert_eq!(c.threshold(pc(15).less_epsilon()), pc(80)); + assert_eq!(c.threshold(pc(15)), pc(70)); + assert_eq!(c.threshold(pc(30).less_epsilon()), pc(70)); + assert_eq!(c.threshold(pc(30)), pc(60)); + assert_eq!(c.threshold(pc(45).less_epsilon()), pc(60)); + assert_eq!(c.threshold(pc(45)), pc(50)); + assert_eq!(c.threshold(pc(60).less_epsilon()), pc(50)); + assert_eq!(c.threshold(pc(60)), pc(40)); + assert_eq!(c.threshold(pc(75).less_epsilon()), pc(40)); + assert_eq!(c.threshold(pc(75)), pc(30)); + assert_eq!(c.threshold(pc(100)), pc(30)); + + assert_eq!(c.delay(pc(100)), pc(0)); + assert_eq!(c.delay(pc(80)), pc(0)); + assert_eq!(c.delay(pc(80).less_epsilon()), pc(15)); + assert_eq!(c.delay(pc(70)), pc(15)); + assert_eq!(c.delay(pc(70).less_epsilon()), pc(30)); + assert_eq!(c.delay(pc(60)), pc(30)); + assert_eq!(c.delay(pc(60).less_epsilon()), pc(45)); + assert_eq!(c.delay(pc(50)), pc(45)); + assert_eq!(c.delay(pc(50).less_epsilon()), pc(60)); + assert_eq!(c.delay(pc(40)), pc(60)); + assert_eq!(c.delay(pc(40).less_epsilon()), pc(75)); + assert_eq!(c.delay(pc(30)), pc(75)); + assert_eq!(c.delay(pc(30).less_epsilon()), pc(100)); + assert_eq!(c.delay(pc(0)), pc(100)); } } \ No newline at end of file diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index 713a0cd844d41..71945aed30b56 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -18,12 +18,15 @@ //! Decimal Fixed Point implementations for Substrate runtime. use crate::{ - helpers_128bit::{multiply_by_rational, multiply_by_rational_with_rounding}, + helpers_128bit::{ + multiply_by_rational, multiply_by_rational_with_rounding, sqrt, checked_mul, + saturating_add, + }, traits::{ Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedSub, One, SaturatedConversion, Saturating, UniqueSaturatedInto, Zero, }, - PerThing, Rounding, + PerThing, Perbill, Rounding, }; use codec::{CompactAs, Decode, Encode}; use sp_std::{ @@ -410,11 +413,50 @@ macro_rules! implement_fixed { Self(inner) } + /// const version of `FixedPointNumber::into_inner`. + pub const fn into_inner(self) -> $inner_type { + self.0 + } + + /// Creates self from a `u32`. + pub const fn from_u32(n: u32) -> Self { + Self::from_inner((n as $inner_type) * $div) + } + #[cfg(any(feature = "std", test))] pub fn from_float(x: f64) -> Self { Self((x * (::DIV as f64)) as $inner_type) } + pub const fn from_perbill(n: Perbill) -> Self { + Self::from_rational(n.deconstruct() as u128, 1_000_000_000) + } + + pub const fn into_perbill(self) -> Perbill { + if self.0 <= 0 { + Perbill::zero() + } else if self.0 >= $div { + Perbill::one() + } else { + match multiply_by_rational_with_rounding( + self.0 as u128, + 1_000_000_000, + Self::DIV as u128, + Rounding::Nearest, + ) { + Some(value) => { + if value > (u32::max_value() as u128) { + panic!("prior logic ensures 0 Perbill::zero(), + } + } + } + #[cfg(any(feature = "std", test))] pub fn to_float(self) -> f64 { self.0 as f64 / ::DIV as f64 @@ -445,29 +487,48 @@ macro_rules! implement_fixed { } } + pub const fn neg(self) -> Self { + Self(0 - self.0) + } + /// A version of div with customisable rounding. - pub fn checked_rounding_div(self, other: Self, rounding: Rounding) -> Option { + pub const fn checked_rounding_div(self, other: Self, rounding: Rounding) -> Option { if other.0 == 0 { return None } - let lhs: I129 = self.0.into(); - let rhs: I129 = other.0.into(); + let lhs = self.into_i129(); + let rhs = other.into_i129(); let negative = lhs.negative != rhs.negative; - multiply_by_rational_with_rounding(lhs.value, Self::DIV as u128, rhs.value, rounding) - .and_then(|value| from_i129(I129 { value, negative })) - .map(Self) + match multiply_by_rational_with_rounding( + lhs.value, + Self::DIV as u128, + rhs.value, + rounding, + ) { + Some(value) => Self::from_i129(I129 { value, negative }), + None => None, + } + } + + pub const fn sqrt(self) -> Self { + match self.try_sqrt() { + Some(v) => v, + None => panic!("sqrt overflow or negative input"), + } } /// Compute the square root, rounding as desired. If it overflows or is negative, then /// `None` is returned. - pub fn sqrt(self) -> Option { - use integer_sqrt::IntegerSquareRoot; - let v: u128 = self.0.try_into().ok()?; - if v == 0 { + pub const fn try_sqrt(self) -> Option { + if self.0 == 0 { return Some(Self(0)) } + if self.0 < 1 { + return None + } + let v = self.0 as u128; // Want x' = sqrt(x) where x = n/D and x' = n'/D (D is fixed) // Our prefered way is: @@ -478,12 +539,127 @@ macro_rules! implement_fixed { // sqrt(nD) = sqrt(n)*sqrt(D) // computing them individually and taking the product at the end. we will lose some // precision though. - let r = if let Some(vd) = v.checked_mul($div) { - vd.integer_sqrt() + let maybe_vd = checked_mul(v, $div); + let r = if let Some(vd) = maybe_vd { + sqrt(vd) + } else { + sqrt(v) * sqrt($div) + }; + Some(Self(r as $inner_type)) + } + + pub const fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + + pub const fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + + pub const fn mul(self, rhs: Self) -> Self { + match $name::const_checked_mul(self, rhs) { + Some(v) => v, + None => panic!("attempt to multiply with overflow"), + } + } + + pub const fn div(self, rhs: Self) -> Self { + match $name::const_checked_div(self, rhs) { + Some(v) => v, + None => panic!("attempt to divide with overflow or NaN"), + } + } + + const fn into_i129(self) -> I129 { + #[allow(unused_comparisons)] + if self.0 < 0 { + let value = match self.0.checked_neg() { + Some(n) => n as u128, + None => saturating_add(<$inner_type>::max_value() as u128, 1), + }; + I129 { value, negative: true } + } else { + I129 { value: self.0 as u128, negative: false } + } + } + + const fn from_i129(n: I129) -> Option { + let max_plus_one = saturating_add(<$inner_type>::max_value() as u128, 1); + #[allow(unused_comparisons)] + let inner = if n.negative && <$inner_type>::min_value() < 0 && n.value == max_plus_one { + <$inner_type>::min_value() } else { - v.integer_sqrt().checked_mul($div.integer_sqrt())? + let unsigned_inner = n.value as $inner_type; + if unsigned_inner as u128 != n.value { + return None + }; + if n.negative { + match unsigned_inner.checked_neg() { + Some(v) => v, + None => return None, + } + } else { + unsigned_inner + } }; - Some(Self(r.try_into().ok()?)) + Some(Self(inner)) + } + + /// Const function for getting an (approximate) value from a rational. + /// + /// It is designed to be used in const expressions. This will panic if the input is bad. + pub const fn from_rational(a: u128, b: u128) -> Self { + if b == 0 { + panic!("attempt to divide by zero in from_rational") + } + match multiply_by_rational_with_rounding( + Self::DIV as u128, + a, + b, + Rounding::Nearest, + ) { + Some(value) => match Self::from_i129(I129 { value, negative: false }) { + Some(x) => x, + None => panic!("overflow in from_rational"), + }, + None => panic!("overflow in from_rational"), + } + } + + pub const fn const_checked_mul(self, other: Self) -> Option { + let lhs = self.into_i129(); + let rhs = other.into_i129(); + let negative = lhs.negative != rhs.negative; + + match multiply_by_rational_with_rounding( + lhs.value, + rhs.value, + Self::DIV as u128, + Rounding::Nearest, + ) { + Some(value) => Self::from_i129(I129 { value, negative }), + None => None, + } + } + + pub const fn const_checked_div(self, other: Self) -> Option { + if other.0 == 0 { + return None + } + + let lhs = self.into_i129(); + let rhs = other.into_i129(); + let negative = lhs.negative != rhs.negative; + + match multiply_by_rational_with_rounding( + lhs.value, + Self::DIV as u128, + rhs.value, + Rounding::Nearest, + ) { + Some(value) => Self::from_i129(I129 { value, negative }), + None => None, + } } } diff --git a/primitives/arithmetic/src/helpers_128bit.rs b/primitives/arithmetic/src/helpers_128bit.rs index c6ce50fb5f39d..51f6aed04ad19 100644 --- a/primitives/arithmetic/src/helpers_128bit.rs +++ b/primitives/arithmetic/src/helpers_128bit.rs @@ -1,6 +1,7 @@ // This file is part of Substrate. // Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// Some code is modified from Derek Dreery's IntegerSquareRoot impl. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); @@ -125,27 +126,27 @@ mod double128 { use sp_std::convert::TryFrom; /// Returns the least significant 64 bits of a - fn low_64(a: u128) -> u128 { + const fn low_64(a: u128) -> u128 { a & ((1<<64)-1) } /// Returns the most significant 64 bits of a - fn high_64(a: u128) -> u128 { + const fn high_64(a: u128) -> u128 { a >> 64 } /// Returns 2^128 - a (two's complement) - fn neg128(a: u128) -> u128 { + const fn neg128(a: u128) -> u128 { (!a).wrapping_add(1) } /// Returns 2^128 / a - fn div128(a: u128) -> u128 { + const fn div128(a: u128) -> u128 { (neg128(a)/a).wrapping_add(1) } /// Returns 2^128 % a - fn mod128(a: u128) -> u128 { + const fn mod128(a: u128) -> u128 { neg128(a) % a } @@ -158,78 +159,64 @@ mod double128 { impl TryFrom for u128 { type Error = (); fn try_from(x: Double128) -> Result { - match x.high { - 0 => Ok(x.low), - _ => Err(()), - } + x.try_into_u128() } } impl Zero for Double128 { fn zero() -> Self { - Self { - high: 0, - low: 0, - } + Double128::zero() } fn is_zero(&self) -> bool { - self.high == 0 && self.low == 0 + Double128::is_zero(&self) } } impl sp_std::ops::Add for Double128 { type Output = Self; - fn add(self, b: Self) -> Self { - let (low, overflow) = self.low.overflowing_add(b.low); - let carry = overflow as u128; // 1 if true, 0 if false. - let high = self.high.wrapping_add(b.high).wrapping_add(carry as u128); - Double128 { high, low } + fn add(self, rhs: Self) -> Self { + Double128::add(self, rhs) } } impl sp_std::ops::AddAssign for Double128 { - fn add_assign(&mut self, b: Self) { - *self = *self + b; + fn add_assign(&mut self, rhs: Self) { + *self = self.add(rhs); } } impl sp_std::ops::Div for Double128 { type Output = (Self, u128); - fn div(mut self, rhs: u128) -> (Self, u128) { - if rhs == 1 { - return (self, 0); - } + fn div(self, rhs: u128) -> (Self, u128) { + Double128::div(self, rhs) + } + } - // (self === a; rhs === b) - // Calculate a / b - // = (a_high << 128 + a_low) / b - // let (q, r) = (div128(b), mod128(b)); - // = (a_low * (q * b + r)) + a_high) / b - // = (a_low * q * b + a_low * r + a_high)/b - // = (a_low * r + a_high) / b + a_low * q - let (q, r) = (div128(rhs), mod128(rhs)); + impl Double128 { + pub const fn try_into_u128(self) -> Result { + match self.high { + 0 => Ok(self.low), + _ => Err(()), + } + } - // x = current result - // a = next number - let mut x = Double128::zero(); - while self.high != 0 { - // x += a.low * q - x += Double128::product_of(self.high, q); - // a = a.low * r + a.high - self = Double128::product_of(self.high, r) + self.low_part(); + pub const fn zero() -> Self { + Self { + high: 0, + low: 0, } + } - (x + Double128::from_low(self.low / rhs), self.low % rhs) + pub const fn is_zero(&self) -> bool { + self.high == 0 && self.low == 0 } - } - impl Double128 { /// Return a `Double128` value representing the `scaled_value << 64`. /// /// This means the lower half of the `high` component will be equal to the upper 64-bits of /// `scaled_value` (in the lower positions) and the upper half of the `low` component will /// be equal to the lower 64-bits of `scaled_value`. - pub fn left_shift_64(scaled_value: u128) -> Self { + pub const fn left_shift_64(scaled_value: u128) -> Self { Self { high: scaled_value >> 64, low: scaled_value << 64, @@ -237,17 +224,17 @@ mod double128 { } /// Construct a value from the upper 128 bits only, with the lower being zeroed. - pub fn from_low(low: u128) -> Self { + pub const fn from_low(low: u128) -> Self { Self { high: 0, low } } /// Returns the same value ignoring anything in the high 128-bits. - pub fn low_part(self) -> Self { + pub const fn low_part(self) -> Self { Self { high: 0, .. self } } /// Returns a*b (in 256 bits) - pub fn product_of(a: u128, b: u128) -> Self { + pub const fn product_of(a: u128, b: u128) -> Self { // Split a and b into hi and lo 64-bit parts let (a_low, a_high) = (low_64(a), high_64(a)); let (b_low, b_high) = (low_64(b), high_64(b)); @@ -268,26 +255,97 @@ mod double128 { let fl = Self { high: l, low: f }; let i = Self::left_shift_64(i); let o = Self::left_shift_64(o); - fl + i + o + fl.add(i).add(o) + } + + pub const fn add(self, b: Self) -> Self { + let (low, overflow) = self.low.overflowing_add(b.low); + let carry = overflow as u128; // 1 if true, 0 if false. + let high = self.high.wrapping_add(b.high).wrapping_add(carry as u128); + Double128 { high, low } + } + + pub const fn div(mut self, rhs: u128) -> (Self, u128) { + if rhs == 1 { + return (self, 0); + } + + // (self === a; rhs === b) + // Calculate a / b + // = (a_high << 128 + a_low) / b + // let (q, r) = (div128(b), mod128(b)); + // = (a_low * (q * b + r)) + a_high) / b + // = (a_low * q * b + a_low * r + a_high)/b + // = (a_low * r + a_high) / b + a_low * q + let (q, r) = (div128(rhs), mod128(rhs)); + + // x = current result + // a = next number + let mut x = Self::zero(); + while self.high != 0 { + // x += a.low * q + x = x.add(Self::product_of(self.high, q)); + // a = a.low * r + a.high + self = Self::product_of(self.high, r).add(self.low_part()); + } + + (x.add(Self::from_low(self.low / rhs)), self.low % rhs) + } + } +} + +pub const fn checked_mul(a: u128, b: u128) -> Option { + a.checked_mul(b) +} + +pub const fn checked_neg(a: u128) -> Option { + a.checked_neg() +} + +pub const fn saturating_add(a: u128, b: u128) -> u128 { + a.saturating_add(b) +} + +pub const fn sqrt(mut n: u128) -> u128 { + // Modified from https://github.com/derekdreery/integer-sqrt-rs (Apache/MIT). + if n == 0 { return 0 } + + // Compute bit, the largest power of 4 <= n + let max_shift: u32 = 0u128.leading_zeros() - 1; + let shift: u32 = (max_shift - n.leading_zeros()) & !1; + let mut bit = 1u128 << shift; + + // Algorithm based on the implementation in: + // https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2) + // Note that result/bit are logically unsigned (even if T is signed). + let mut result = 0u128; + while bit != 0 { + if n >= result + bit { + n -= result + bit; + result = (result >> 1) + bit; + } else { + result = result >> 1; } + bit = bit >> 2; } + result } /// Returns `a * b / c` and `(a * b) % c` (wrapping to 128 bits) or `None` in the case of /// overflow. -pub fn multiply_by_rational_with_rounding(a: u128, b: u128, c: u128, r: Rounding) -> Option { +pub const fn multiply_by_rational_with_rounding(a: u128, b: u128, c: u128, r: Rounding) -> Option { use double128::Double128; if c == 0 { panic!("attempt to divide by zero") } - let (result, remainder) = Double128::product_of(a, b) / c; - let mut result: u128 = result.try_into().ok()?; + let (result, remainder) = Double128::product_of(a, b).div(c); + let mut result: u128 = match result.try_into_u128() { Ok(v) => v, Err(_) => return None }; if match r { Rounding::Up => remainder > 0, Rounding::Nearest => remainder >= c / 2 + c % 2, Rounding::Down => false, } { - result = result.checked_add(1)?; + result = match result.checked_add(1) { Some(v) => v, None => return None }; } Some(result) } From 6bde6aae8c8757c696ace4baea339293002b736c Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 12 Apr 2022 19:31:41 +0100 Subject: [PATCH 09/48] Remove some unneeded code --- frame/referenda/src/types.rs | 20 ++--- primitives/arithmetic/src/fixed_point.rs | 11 +-- primitives/arithmetic/src/helpers_128bit.rs | 88 ++++----------------- 3 files changed, 27 insertions(+), 92 deletions(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 5137e9b9996a6..312773039ffb7 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -267,15 +267,7 @@ const fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 { } impl Curve { - pub const fn reciprocal_from_point( - delay: u128, - period: u128, - level: FixedI64, - ) -> Curve { - Self::reciprocal_from_point_and_floor(delay, period, level, FixedI64::from_u32(0)) - } - - pub const fn reciprocal_from_point_and_floor( + pub const fn make_reciprocal( delay: u128, period: u128, level: FixedI64, @@ -284,17 +276,17 @@ impl Curve { let delay = FixedI64::from_rational(delay, period).into_perbill(); let mut bounds = (( FixedI64::from_u32(0), - Self::reciprocal_from_factor_and_floor(FixedI64::from_u32(0), floor), + Self::reciprocal_from_parts(FixedI64::from_u32(0), floor), FixedI64::from_inner(i64::max_value()), ), ( FixedI64::from_u32(1), - Self::reciprocal_from_factor_and_floor(FixedI64::from_u32(1), floor), + Self::reciprocal_from_parts(FixedI64::from_u32(1), floor), FixedI64::from_inner(i64::max_value()), )); const TWO: FixedI64 = FixedI64::from_u32(2); while (bounds.1).0.sub((bounds.0).0).into_inner() > 1 { let factor = (bounds.0).0.add((bounds.1).0).div(TWO); - let curve = Self::reciprocal_from_factor_and_floor(factor, floor); + let curve = Self::reciprocal_from_parts(factor, floor); let curve_level = FixedI64::from_perbill(curve.const_threshold(delay)); if curve_level.into_inner() > level.into_inner() { bounds = (bounds.0, (factor, curve, curve_level.sub(level))); @@ -309,7 +301,7 @@ impl Curve { } } - const fn reciprocal_from_factor_and_floor(factor: FixedI64, floor: FixedI64) -> Self { + const fn reciprocal_from_parts(factor: FixedI64, floor: FixedI64) -> Self { let one_minus_floor = FixedI64::from_u32(1).sub(floor); let x_offset = pos_quad_solution(one_minus_floor, one_minus_floor, factor.neg()); let y_offset = floor.sub(factor.div(FixedI64::from_u32(1).add(x_offset))); @@ -488,7 +480,7 @@ mod tests { FixedI64::from_rational(x, 100) } - const TIP_CURVE: Curve = Curve::reciprocal_from_point_and_floor(1, 28, percent(65), percent(50)); + const TIP_CURVE: Curve = Curve::make_reciprocal(1, 28, percent(65), percent(50)); #[test] #[should_panic] diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index 71945aed30b56..a437974a3b7fb 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -18,10 +18,7 @@ //! Decimal Fixed Point implementations for Substrate runtime. use crate::{ - helpers_128bit::{ - multiply_by_rational, multiply_by_rational_with_rounding, sqrt, checked_mul, - saturating_add, - }, + helpers_128bit::{multiply_by_rational, multiply_by_rational_with_rounding, sqrt}, traits::{ Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedSub, One, SaturatedConversion, Saturating, UniqueSaturatedInto, Zero, @@ -539,7 +536,7 @@ macro_rules! implement_fixed { // sqrt(nD) = sqrt(n)*sqrt(D) // computing them individually and taking the product at the end. we will lose some // precision though. - let maybe_vd = checked_mul(v, $div); + let maybe_vd = u128::checked_mul(v, $div); let r = if let Some(vd) = maybe_vd { sqrt(vd) } else { @@ -575,7 +572,7 @@ macro_rules! implement_fixed { if self.0 < 0 { let value = match self.0.checked_neg() { Some(n) => n as u128, - None => saturating_add(<$inner_type>::max_value() as u128, 1), + None => u128::saturating_add(<$inner_type>::max_value() as u128, 1), }; I129 { value, negative: true } } else { @@ -584,7 +581,7 @@ macro_rules! implement_fixed { } const fn from_i129(n: I129) -> Option { - let max_plus_one = saturating_add(<$inner_type>::max_value() as u128, 1); + let max_plus_one = u128::saturating_add(<$inner_type>::max_value() as u128, 1); #[allow(unused_comparisons)] let inner = if n.negative && <$inner_type>::min_value() < 0 && n.value == max_plus_one { <$inner_type>::min_value() diff --git a/primitives/arithmetic/src/helpers_128bit.rs b/primitives/arithmetic/src/helpers_128bit.rs index 51f6aed04ad19..f70564e3c1812 100644 --- a/primitives/arithmetic/src/helpers_128bit.rs +++ b/primitives/arithmetic/src/helpers_128bit.rs @@ -122,8 +122,6 @@ pub fn multiply_by_rational(mut a: u128, mut b: u128, mut c: u128) -> Result u128 { @@ -156,42 +154,6 @@ mod double128 { low: u128, } - impl TryFrom for u128 { - type Error = (); - fn try_from(x: Double128) -> Result { - x.try_into_u128() - } - } - - impl Zero for Double128 { - fn zero() -> Self { - Double128::zero() - } - fn is_zero(&self) -> bool { - Double128::is_zero(&self) - } - } - - impl sp_std::ops::Add for Double128 { - type Output = Self; - fn add(self, rhs: Self) -> Self { - Double128::add(self, rhs) - } - } - - impl sp_std::ops::AddAssign for Double128 { - fn add_assign(&mut self, rhs: Self) { - *self = self.add(rhs); - } - } - - impl sp_std::ops::Div for Double128 { - type Output = (Self, u128); - fn div(self, rhs: u128) -> (Self, u128) { - Double128::div(self, rhs) - } - } - impl Double128 { pub const fn try_into_u128(self) -> Result { match self.high { @@ -207,10 +169,6 @@ mod double128 { } } - pub const fn is_zero(&self) -> bool { - self.high == 0 && self.low == 0 - } - /// Return a `Double128` value representing the `scaled_value << 64`. /// /// This means the lower half of the `high` component will be equal to the upper 64-bits of @@ -294,16 +252,23 @@ mod double128 { } } -pub const fn checked_mul(a: u128, b: u128) -> Option { - a.checked_mul(b) -} - -pub const fn checked_neg(a: u128) -> Option { - a.checked_neg() -} - -pub const fn saturating_add(a: u128, b: u128) -> u128 { - a.saturating_add(b) +/// Returns `a * b / c` and `(a * b) % c` (wrapping to 128 bits) or `None` in the case of +/// overflow. +pub const fn multiply_by_rational_with_rounding(a: u128, b: u128, c: u128, r: Rounding) -> Option { + use double128::Double128; + if c == 0 { + panic!("attempt to divide by zero") + } + let (result, remainder) = Double128::product_of(a, b).div(c); + let mut result: u128 = match result.try_into_u128() { Ok(v) => v, Err(_) => return None }; + if match r { + Rounding::Up => remainder > 0, + Rounding::Nearest => remainder >= c / 2 + c % 2, + Rounding::Down => false, + } { + result = match result.checked_add(1) { Some(v) => v, None => return None }; + } + Some(result) } pub const fn sqrt(mut n: u128) -> u128 { @@ -331,25 +296,6 @@ pub const fn sqrt(mut n: u128) -> u128 { result } -/// Returns `a * b / c` and `(a * b) % c` (wrapping to 128 bits) or `None` in the case of -/// overflow. -pub const fn multiply_by_rational_with_rounding(a: u128, b: u128, c: u128, r: Rounding) -> Option { - use double128::Double128; - if c == 0 { - panic!("attempt to divide by zero") - } - let (result, remainder) = Double128::product_of(a, b).div(c); - let mut result: u128 = match result.try_into_u128() { Ok(v) => v, Err(_) => return None }; - if match r { - Rounding::Up => remainder > 0, - Rounding::Nearest => remainder >= c / 2 + c % 2, - Rounding::Down => false, - } { - result = match result.checked_add(1) { Some(v) => v, None => return None }; - } - Some(result) -} - #[cfg(test)] mod tests { use super::*; From abb8a8f478c2b34e86beaecaaf6492e904d56137 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 12 Apr 2022 20:32:57 +0100 Subject: [PATCH 10/48] Actually useful linear curve --- frame/referenda/src/mock.rs | 16 ++++----- frame/referenda/src/tests.rs | 4 +-- frame/referenda/src/types.rs | 40 ++++++++++++++------- primitives/arithmetic/src/helpers_128bit.rs | 2 +- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/frame/referenda/src/mock.rs b/frame/referenda/src/mock.rs index fdd14fdadf04d..c4bbc1c2b0cff 100644 --- a/frame/referenda/src/mock.rs +++ b/frame/referenda/src/mock.rs @@ -172,12 +172,12 @@ impl TracksInfo for TestTracksInfo { confirm_period: 2, min_enactment_period: 4, min_approval: Curve::LinearDecreasing { - begin: Perbill::from_percent(100), - delta: Perbill::from_percent(50), + length: Perbill::from_percent(100), + floor: Perbill::from_percent(50), }, min_turnout: Curve::LinearDecreasing { - begin: Perbill::from_percent(100), - delta: Perbill::from_percent(100), + length: Perbill::from_percent(100), + floor: Perbill::from_percent(100), }, }, ), @@ -192,12 +192,12 @@ impl TracksInfo for TestTracksInfo { confirm_period: 1, min_enactment_period: 2, min_approval: Curve::LinearDecreasing { - begin: Perbill::from_percent(55), - delta: Perbill::from_percent(5), + length: Perbill::from_percent(55), + floor: Perbill::from_percent(5), }, min_turnout: Curve::LinearDecreasing { - begin: Perbill::from_percent(10), - delta: Perbill::from_percent(10), + length: Perbill::from_percent(10), + floor: Perbill::from_percent(10), }, }, ), diff --git a/frame/referenda/src/tests.rs b/frame/referenda/src/tests.rs index 96edd4ce879ce..3b174eac61e1d 100644 --- a/frame/referenda/src/tests.rs +++ b/frame/referenda/src/tests.rs @@ -504,12 +504,12 @@ fn set_balance_proposal_is_correctly_filtered_out() { #[test] fn curve_handles_all_inputs() { - let test_curve = Curve::LinearDecreasing { begin: Perbill::zero(), delta: Perbill::zero() }; + let test_curve = Curve::LinearDecreasing { length: Perbill::zero(), floor: Perbill::zero() }; let delay = test_curve.delay(Perbill::zero()); assert_eq!(delay, Perbill::zero()); - let test_curve = Curve::LinearDecreasing { begin: Perbill::zero(), delta: Perbill::one() }; + let test_curve = Curve::LinearDecreasing { length: Perbill::zero(), floor: Perbill::one() }; let threshold = test_curve.threshold(Perbill::one()); assert_eq!(threshold, Perbill::zero()); diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 312773039ffb7..e2e63923ac575 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -249,8 +249,9 @@ impl< #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] #[cfg_attr(not(feature = "std"), derive(RuntimeDebug))] pub enum Curve { - /// Linear curve starting at `(0, begin)`, ending at `(period, begin - delta)`. - LinearDecreasing { begin: Perbill, delta: Perbill }, + /// Linear curve starting at `(0, 0)`, proceeding linearly to `(length, floor)`, then + /// remaining at `floor` until the end of the period. + LinearDecreasing { length: Perbill, floor: Perbill }, /// Stepped curve, beginning at `(0, begin)`, then remaining constant for `period`, at which /// point it steps down to `(period, begin - step)`. It then remains constant for another /// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues @@ -267,6 +268,15 @@ const fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 { } impl Curve { + pub const fn make_linear( + length: u128, + period: u128, + floor: FixedI64, + ) -> Curve { + let length = FixedI64::from_rational(length, period).into_perbill(); + let floor = floor.into_perbill(); + Curve::LinearDecreasing { length, floor } + } pub const fn make_reciprocal( delay: u128, period: u128, @@ -365,7 +375,8 @@ impl Curve { /// Determine the `y` value for the given `x` value. pub(crate) fn threshold(&self, x: Perbill) -> Perbill { match self { - Self::LinearDecreasing { begin, delta } => *begin - (*delta * x).min(*begin), + Self::LinearDecreasing { length, floor } => + (x.min(*length).saturating_div(*length, Down) * *floor).left_from_one(), Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), Self::Reciprocal { factor, x_offset, y_offset } => { @@ -408,11 +419,11 @@ impl Curve { /// ``` pub fn delay(&self, y: Perbill) -> Perbill { match self { - Self::LinearDecreasing { begin, delta } => - if delta.is_zero() { - *delta + Self::LinearDecreasing { length, floor } => + if y < *floor { + Perbill::one() } else { - (*begin - y.min(*begin)).min(*delta) / *delta + y.left_from_one().saturating_div(*floor, Up) * *length }, Self::SteppedDecreasing { begin, end, step, period } => if y < *end { @@ -439,12 +450,13 @@ impl Curve { impl Debug for Curve { fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result { match self { - Self::LinearDecreasing { begin, delta } => { + Self::LinearDecreasing { length, floor } => { write!( f, - "Linear[(0%, {:?}) -> (100%, {:?})]", - begin, - *begin - *delta, + "Linear[(0%, 100%) -> ({:?}, {:?}) -> (100%, {:?})]", + length, + floor, + floor, ) }, Self::SteppedDecreasing { begin, end, step, period } => { @@ -480,12 +492,14 @@ mod tests { FixedI64::from_rational(x, 100) } - const TIP_CURVE: Curve = Curve::make_reciprocal(1, 28, percent(65), percent(50)); + const TIP_QUO: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0)); + const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50)); #[test] #[should_panic] fn check_curves() { - TIP_CURVE.info(28u32, "Tip"); + TIP_QUO.info(28u32, "Tip Quorum"); + TIP_APP.info(28u32, "Tip Approval"); assert!(false); } diff --git a/primitives/arithmetic/src/helpers_128bit.rs b/primitives/arithmetic/src/helpers_128bit.rs index f70564e3c1812..f63816bc9d372 100644 --- a/primitives/arithmetic/src/helpers_128bit.rs +++ b/primitives/arithmetic/src/helpers_128bit.rs @@ -1,7 +1,7 @@ // This file is part of Substrate. // Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. -// Some code is modified from Derek Dreery's IntegerSquareRoot impl. +// Some code is based upon Derek Dreery's IntegerSquareRoot impl, used under license. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); From 9cecb072af77b92852525611dbaff37a6db7f26b Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 12 Apr 2022 20:38:59 +0100 Subject: [PATCH 11/48] Fixes --- frame/referenda/src/types.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index e2e63923ac575..c7de88fd94c9d 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -376,7 +376,7 @@ impl Curve { pub(crate) fn threshold(&self, x: Perbill) -> Perbill { match self { Self::LinearDecreasing { length, floor } => - (x.min(*length).saturating_div(*length, Down) * *floor).left_from_one(), + (x.min(*length).saturating_div(*length, Down) * floor.left_from_one()).left_from_one(), Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), Self::Reciprocal { factor, x_offset, y_offset } => { @@ -423,7 +423,7 @@ impl Curve { if y < *floor { Perbill::one() } else { - y.left_from_one().saturating_div(*floor, Up) * *length + y.left_from_one().saturating_div(floor.left_from_one(), Up) * *length }, Self::SteppedDecreasing { begin, end, step, period } => if y < *end { @@ -494,12 +494,16 @@ mod tests { const TIP_QUO: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0)); const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50)); + const ROOT_QUO: Curve = Curve::make_linear(28, 28, percent(0)); + const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50)); #[test] #[should_panic] fn check_curves() { TIP_QUO.info(28u32, "Tip Quorum"); TIP_APP.info(28u32, "Tip Approval"); + ROOT_QUO.info(28u32, "Root Quorum"); + ROOT_APP.info(28u32, "Root Approval"); assert!(false); } From 31da17626fb4d7b46cae3f1f0fdf8dd8c7c6a7a1 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 12 Apr 2022 21:01:39 +0100 Subject: [PATCH 12/48] Provisional curves --- frame/referenda/src/types.rs | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index c7de88fd94c9d..25ec9761b7afe 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -492,18 +492,46 @@ mod tests { FixedI64::from_rational(x, 100) } - const TIP_QUO: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0)); const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50)); - const ROOT_QUO: Curve = Curve::make_linear(28, 28, percent(0)); + const TIP_QUO: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0)); const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50)); + const ROOT_QUO: Curve = Curve::make_linear(28, 28, percent(0)); + const WHITE_APP: Curve = Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50)); + const WHITE_QUO: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10)); + const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50)); + const SMALL_QUO: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0)); + const MID_APP: Curve = Curve::make_linear(17, 28, percent(50)); + const MID_QUO: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); + const BIG_APP: Curve = Curve::make_linear(23, 28, percent(50)); + const BIG_QUO: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0)); + const HUGE_APP: Curve = Curve::make_linear(28, 28, percent(50)); + const HUGE_QUO: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0)); + const PARAM_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50)); + const PARAM_QUO: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0)); + const ADMIN_APP: Curve = Curve::make_linear(17, 28, percent(50)); + const ADMIN_QUO: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); #[test] #[should_panic] fn check_curves() { - TIP_QUO.info(28u32, "Tip Quorum"); TIP_APP.info(28u32, "Tip Approval"); - ROOT_QUO.info(28u32, "Root Quorum"); + TIP_QUO.info(28u32, "Tip Quorum"); ROOT_APP.info(28u32, "Root Approval"); + ROOT_QUO.info(28u32, "Root Quorum"); + WHITE_APP.info(28u32, "Whitelist Approval"); + WHITE_QUO.info(28u32, "Whitelist Quorum"); + SMALL_APP.info(28u32, "Small Spend Approval"); + SMALL_QUO.info(28u32, "Small Spend Quorum"); + MID_APP.info(28u32, "Mid Spend Approval"); + MID_QUO.info(28u32, "Mid Spend Quorum"); + BIG_APP.info(28u32, "Big Spend Approval"); + BIG_QUO.info(28u32, "Big Spend Quorum"); + HUGE_APP.info(28u32, "Huge Spend Approval"); + HUGE_QUO.info(28u32, "Huge Spend Quorum"); + PARAM_APP.info(28u32, "Mid-tier Parameter Change Approval"); + PARAM_QUO.info(28u32, "Mid-tier Parameter Change Quorum"); + ADMIN_APP.info(28u32, "Admin (e.g. Cancel Slash) Approval"); + ADMIN_QUO.info(28u32, "Admin (e.g. Cancel Slash) Quorum"); assert!(false); } From e6c0f02a95430a429422fadd42290a5256c34480 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 13 Apr 2022 13:30:04 +0100 Subject: [PATCH 13/48] Rejig 'turnout' as 'support' --- bin/node/runtime/src/lib.rs | 2 +- frame/conviction-voting/src/types.rs | 61 +++++++++++++++++----------- frame/referenda/src/benchmarking.rs | 4 +- frame/referenda/src/lib.rs | 22 ++++++---- frame/referenda/src/mock.rs | 16 ++++---- frame/referenda/src/types.rs | 42 +++++++++---------- frame/support/src/traits/voting.rs | 4 +- 7 files changed, 85 insertions(+), 66 deletions(-) diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 565f151ce2a08..8633a40f3c1f4 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -739,7 +739,7 @@ impl pallet_referenda::TracksInfo for TracksInfo { begin: Perbill::from_percent(100), delta: Perbill::from_percent(50), }, - min_turnout: pallet_referenda::Curve::LinearDecreasing { + min_support: pallet_referenda::Curve::LinearDecreasing { begin: Perbill::from_percent(100), delta: Perbill::from_percent(100), }, diff --git a/frame/conviction-voting/src/types.rs b/frame/conviction-voting/src/types.rs index e2b5844ddd5df..55fc869302138 100644 --- a/frame/conviction-voting/src/types.rs +++ b/frame/conviction-voting/src/types.rs @@ -54,8 +54,8 @@ pub struct Tally< pub ayes: Votes, /// The number of nay votes, expressed in terms of post-conviction lock-vote. pub nays: Votes, - /// The amount of funds currently expressing its opinion. Pre-conviction. - pub turnout: Votes, + /// The basic number of aye votes, expressed pre-conviction. + pub support: Votes, /// Dummy. dummy: PhantomData, } @@ -77,8 +77,8 @@ impl< self.ayes } - fn turnout(&self) -> Perbill { - Perbill::from_rational(self.turnout, Total::get()) + fn support(&self) -> Perbill { + Perbill::from_rational(self.support, Total::get()) } fn approval(&self) -> Perbill { @@ -87,14 +87,15 @@ impl< #[cfg(feature = "runtime-benchmarks")] fn unanimity() -> Self { - Self { ayes: Total::get(), nays: Zero::zero(), turnout: Total::get(), dummy: PhantomData } + Self { ayes: Total::get(), nays: Zero::zero(), support: Total::get(), dummy: PhantomData } } #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(turnout: Perbill, approval: Perbill) -> Self { - let turnout = turnout.mul_ceil(Total::get()); - let ayes = approval.mul_ceil(turnout); - Self { ayes, nays: turnout - ayes, turnout, dummy: PhantomData } + fn from_requirements(support: Perbill, approval: Perbill) -> Self { + let support = support.mul_ceil(Total::get()); + let ayes = approval.mul_ceil(support); + // TODO + Self { ayes, nays: support - ayes, support, dummy: PhantomData } } } @@ -117,13 +118,18 @@ impl< Self { ayes: if vote.aye { votes } else { Zero::zero() }, nays: if vote.aye { Zero::zero() } else { votes }, - turnout: capital, + support: capital, dummy: PhantomData, } } - pub fn from_parts(ayes: Votes, nays: Votes, turnout: Votes) -> Self { - Self { ayes, nays, turnout, dummy: PhantomData } + pub fn from_parts(ayes_with_conviction: Votes, nays_with_conviction: Votes, ayes: Votes) -> Self { + Self { + ayes: ayes_with_conviction, + nays: nays_with_conviction, + support: ayes, + dummy: PhantomData, + } } /// Add an account's vote into the tally. @@ -131,16 +137,18 @@ impl< match vote { AccountVote::Standard { vote, balance } => { let Delegations { votes, capital } = vote.conviction.votes(balance); - self.turnout = self.turnout.checked_add(&capital)?; match vote.aye { - true => self.ayes = self.ayes.checked_add(&votes)?, + true => { + self.support = self.support.checked_add(&capital)?; + self.ayes = self.ayes.checked_add(&votes)? + }, false => self.nays = self.nays.checked_add(&votes)?, } }, AccountVote::Split { aye, nay } => { let aye = Conviction::None.votes(aye); let nay = Conviction::None.votes(nay); - self.turnout = self.turnout.checked_add(&aye.capital)?.checked_add(&nay.capital)?; + self.support = self.support.checked_add(&aye.capital)?; self.ayes = self.ayes.checked_add(&aye.votes)?; self.nays = self.nays.checked_add(&nay.votes)?; }, @@ -153,16 +161,19 @@ impl< match vote { AccountVote::Standard { vote, balance } => { let Delegations { votes, capital } = vote.conviction.votes(balance); - self.turnout = self.turnout.checked_sub(&capital)?; match vote.aye { - true => self.ayes = self.ayes.checked_sub(&votes)?, + true => { + self.support = self.support.checked_sub(&capital)?; + self.ayes = self.ayes.checked_sub(&votes)? + }, false => self.nays = self.nays.checked_sub(&votes)?, } }, + // TODO: abstain. AccountVote::Split { aye, nay } => { let aye = Conviction::None.votes(aye); let nay = Conviction::None.votes(nay); - self.turnout = self.turnout.checked_sub(&aye.capital)?.checked_sub(&nay.capital)?; + self.support = self.support.checked_sub(&aye.capital)?; self.ayes = self.ayes.checked_sub(&aye.votes)?; self.nays = self.nays.checked_sub(&nay.votes)?; }, @@ -172,18 +183,22 @@ impl< /// Increment some amount of votes. pub fn increase(&mut self, approve: bool, delegations: Delegations) { - self.turnout = self.turnout.saturating_add(delegations.capital); match approve { - true => self.ayes = self.ayes.saturating_add(delegations.votes), + true => { + self.support = self.support.saturating_add(delegations.capital); + self.ayes = self.ayes.saturating_add(delegations.votes); + }, false => self.nays = self.nays.saturating_add(delegations.votes), } } /// Decrement some amount of votes. pub fn reduce(&mut self, approve: bool, delegations: Delegations) { - self.turnout = self.turnout.saturating_sub(delegations.capital); match approve { - true => self.ayes = self.ayes.saturating_sub(delegations.votes), + true => { + self.support = self.support.saturating_sub(delegations.capital); + self.ayes = self.ayes.saturating_sub(delegations.votes); + }, false => self.nays = self.nays.saturating_sub(delegations.votes), } } @@ -196,7 +211,7 @@ impl< pub struct Delegations { /// The number of votes (this is post-conviction). pub votes: Balance, - /// The amount of raw capital, used for the turnout. + /// The amount of raw capital, used for the support. pub capital: Balance, } diff --git a/frame/referenda/src/benchmarking.rs b/frame/referenda/src/benchmarking.rs index 08612e0614aeb..3308c8a6ef258 100644 --- a/frame/referenda/src/benchmarking.rs +++ b/frame/referenda/src/benchmarking.rs @@ -104,11 +104,11 @@ fn info(index: ReferendumIndex) -> &'static TrackInfoOf { } fn make_passing_after(index: ReferendumIndex, period_portion: Perbill) { - let turnout = info::(index).min_turnout.threshold(period_portion); + let support = info::(index).min_support.threshold(period_portion); let approval = info::(index).min_approval.threshold(period_portion); Referenda::::access_poll(index, |status| { if let PollStatus::Ongoing(tally, ..) = status { - *tally = T::Tally::from_requirements(turnout, approval); + *tally = T::Tally::from_requirements(support, approval); } }); } diff --git a/frame/referenda/src/lib.rs b/frame/referenda/src/lib.rs index 067775fd336d0..c28a6cc350469 100644 --- a/frame/referenda/src/lib.rs +++ b/frame/referenda/src/lib.rs @@ -41,7 +41,7 @@ //! In order to become concluded, one of three things must happen: //! - The referendum should remain in an unbroken _Passing_ state for a period of time. This //! is known as the _Confirmation Period_ and is determined by the track. A referendum is considered -//! _Passing_ when there is a sufficiently high turnout and approval, given the amount of time it +//! _Passing_ when there is a sufficiently high support and approval, given the amount of time it //! has been being decided. Generally the threshold for what counts as being "sufficiently high" //! will reduce over time. The curves setting these thresholds are determined by the track. In this //! case, the referendum is considered _Approved_ and the proposal is scheduled for dispatch. @@ -54,6 +54,10 @@ //! //! Once a referendum is concluded, the decision deposit may be refunded. //! +//! ## Terms +//! - *Support*: The number of aye-votes, pre-conviction, as a proportion of the total number of +//! pre-conviction votes able to be cast in the population. +//! //! - [`Config`] //! - [`Call`] @@ -723,7 +727,7 @@ impl, I: 'static> Pallet { &status.tally, Zero::zero(), track.decision_period, - &track.min_turnout, + &track.min_support, &track.min_approval, ); status.in_queue = false; @@ -930,7 +934,7 @@ impl, I: 'static> Pallet { &status.tally, now.saturating_sub(deciding.since), track.decision_period, - &track.min_turnout, + &track.min_support, &track.min_approval, ); branch = if is_passing { @@ -1014,10 +1018,10 @@ impl, I: 'static> Pallet { deciding.confirming.unwrap_or_else(|| { // Set alarm to the point where the current voting would make it pass. let approval = tally.approval(); - let turnout = tally.turnout(); + let support = tally.support(); let until_approval = track.min_approval.delay(approval); - let until_turnout = track.min_turnout.delay(turnout); - let offset = until_turnout.max(until_approval); + let until_support = track.min_support.delay(support); + let offset = until_support.max(until_approval); deciding.since.saturating_add(offset * track.decision_period) }) } @@ -1062,16 +1066,16 @@ impl, I: 'static> Pallet { } /// Determine whether the given `tally` would result in a referendum passing at `elapsed` blocks - /// into a total decision `period`, given the two curves for `turnout_needed` and + /// into a total decision `period`, given the two curves for `support_needed` and /// `approval_needed`. fn is_passing( tally: &T::Tally, elapsed: T::BlockNumber, period: T::BlockNumber, - turnout_needed: &Curve, + support_needed: &Curve, approval_needed: &Curve, ) -> bool { let x = Perbill::from_rational(elapsed.min(period), period); - turnout_needed.passing(x, tally.turnout()) && approval_needed.passing(x, tally.approval()) + support_needed.passing(x, tally.support()) && approval_needed.passing(x, tally.approval()) } } diff --git a/frame/referenda/src/mock.rs b/frame/referenda/src/mock.rs index c4bbc1c2b0cff..16e91c72317a7 100644 --- a/frame/referenda/src/mock.rs +++ b/frame/referenda/src/mock.rs @@ -175,7 +175,7 @@ impl TracksInfo for TestTracksInfo { length: Perbill::from_percent(100), floor: Perbill::from_percent(50), }, - min_turnout: Curve::LinearDecreasing { + min_support: Curve::LinearDecreasing { length: Perbill::from_percent(100), floor: Perbill::from_percent(100), }, @@ -195,7 +195,7 @@ impl TracksInfo for TestTracksInfo { length: Perbill::from_percent(55), floor: Perbill::from_percent(5), }, - min_turnout: Curve::LinearDecreasing { + min_support: Curve::LinearDecreasing { length: Perbill::from_percent(10), floor: Perbill::from_percent(10), }, @@ -264,8 +264,8 @@ impl VoteTally for Tally { self.ayes } - fn turnout(&self) -> Perbill { - Perbill::from_percent(self.ayes + self.nays) + fn support(&self) -> Perbill { + Perbill::from_percent(self.ayes) } fn approval(&self) -> Perbill { @@ -278,10 +278,10 @@ impl VoteTally for Tally { } #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(turnout: Perbill, approval: Perbill) -> Self { - let turnout = turnout.mul_ceil(100u32); - let ayes = approval.mul_ceil(turnout); - Self { ayes, nays: turnout - ayes } + fn from_requirements(support: Perbill, approval: Perbill) -> Self { + let ayes = support.mul_ceil(100u32); + let nays = (ayes * 1_000_000_000u64 / approval.deconstruct() as u64) - ayes; + Self { ayes, nays } } } diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 25ec9761b7afe..736b350ee2940 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -128,9 +128,9 @@ pub struct TrackInfo { /// Minimum aye votes as percentage of overall conviction-weighted votes needed for /// approval as a function of time into decision period. pub min_approval: Curve, - /// Minimum turnout as percentage of overall population that is needed for - /// approval as a function of time into decision period. - pub min_turnout: Curve, + /// Minimum pre-conviction aye-votes ("support") as percentage of overall population that is + /// needed for approval as a function of time into decision period. + pub min_support: Curve, } /// Information on the voting tracks. @@ -493,45 +493,45 @@ mod tests { } const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50)); - const TIP_QUO: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0)); + const TIP_SUP: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0)); const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50)); - const ROOT_QUO: Curve = Curve::make_linear(28, 28, percent(0)); + const ROOT_SUP: Curve = Curve::make_linear(28, 28, percent(0)); const WHITE_APP: Curve = Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50)); - const WHITE_QUO: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10)); + const WHITE_SUP: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10)); const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50)); - const SMALL_QUO: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0)); + const SMALL_SUP: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0)); const MID_APP: Curve = Curve::make_linear(17, 28, percent(50)); - const MID_QUO: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); + const MID_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); const BIG_APP: Curve = Curve::make_linear(23, 28, percent(50)); - const BIG_QUO: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0)); + const BIG_SUP: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0)); const HUGE_APP: Curve = Curve::make_linear(28, 28, percent(50)); - const HUGE_QUO: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0)); + const HUGE_SUP: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0)); const PARAM_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50)); - const PARAM_QUO: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0)); + const PARAM_SUP: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0)); const ADMIN_APP: Curve = Curve::make_linear(17, 28, percent(50)); - const ADMIN_QUO: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); + const ADMIN_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); #[test] #[should_panic] fn check_curves() { TIP_APP.info(28u32, "Tip Approval"); - TIP_QUO.info(28u32, "Tip Quorum"); + TIP_SUP.info(28u32, "Tip Support"); ROOT_APP.info(28u32, "Root Approval"); - ROOT_QUO.info(28u32, "Root Quorum"); + ROOT_SUP.info(28u32, "Root Support"); WHITE_APP.info(28u32, "Whitelist Approval"); - WHITE_QUO.info(28u32, "Whitelist Quorum"); + WHITE_SUP.info(28u32, "Whitelist Support"); SMALL_APP.info(28u32, "Small Spend Approval"); - SMALL_QUO.info(28u32, "Small Spend Quorum"); + SMALL_SUP.info(28u32, "Small Spend Support"); MID_APP.info(28u32, "Mid Spend Approval"); - MID_QUO.info(28u32, "Mid Spend Quorum"); + MID_SUP.info(28u32, "Mid Spend Support"); BIG_APP.info(28u32, "Big Spend Approval"); - BIG_QUO.info(28u32, "Big Spend Quorum"); + BIG_SUP.info(28u32, "Big Spend Support"); HUGE_APP.info(28u32, "Huge Spend Approval"); - HUGE_QUO.info(28u32, "Huge Spend Quorum"); + HUGE_SUP.info(28u32, "Huge Spend Support"); PARAM_APP.info(28u32, "Mid-tier Parameter Change Approval"); - PARAM_QUO.info(28u32, "Mid-tier Parameter Change Quorum"); + PARAM_SUP.info(28u32, "Mid-tier Parameter Change Support"); ADMIN_APP.info(28u32, "Admin (e.g. Cancel Slash) Approval"); - ADMIN_QUO.info(28u32, "Admin (e.g. Cancel Slash) Quorum"); + ADMIN_SUP.info(28u32, "Admin (e.g. Cancel Slash) Support"); assert!(false); } diff --git a/frame/support/src/traits/voting.rs b/frame/support/src/traits/voting.rs index 978c5ce4f6a01..f495ba248453a 100644 --- a/frame/support/src/traits/voting.rs +++ b/frame/support/src/traits/voting.rs @@ -97,12 +97,12 @@ impl + UniqueSaturatedFrom> CurrencyToVote pub trait VoteTally { fn ayes(&self) -> Votes; - fn turnout(&self) -> Perbill; + fn support(&self) -> Perbill; fn approval(&self) -> Perbill; #[cfg(feature = "runtime-benchmarks")] fn unanimity() -> Self; #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(turnout: Perbill, approval: Perbill) -> Self; + fn from_requirements(support: Perbill, approval: Perbill) -> Self; } pub enum PollStatus { From ed050cacc66525f20ee946b9ec8bd65aa6659825 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 13 Apr 2022 14:25:17 +0100 Subject: [PATCH 14/48] Use TypedGet --- frame/system/src/lib.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index 60c27b98b5d9e..e2d7a11e53d4c 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -86,7 +86,7 @@ use frame_support::{ storage, traits::{ ConstU32, Contains, EnsureOrigin, Get, HandleLifetime, OnKilledAccount, OnNewAccount, - OriginTrait, PalletInfo, SortedMembers, StoredMap, + OriginTrait, PalletInfo, SortedMembers, StoredMap, TypedGet, }, weights::{ extract_actual_weight, DispatchClass, DispatchInfo, PerDispatchClass, RuntimeDbWeight, @@ -791,18 +791,17 @@ impl, O>> + From>, Acco } } -pub struct EnsureRootWithSuccess( - sp_std::marker::PhantomData<(AccountId, SuccessType, Success)> +pub struct EnsureRootWithSuccess( + sp_std::marker::PhantomData<(AccountId, Success)> ); impl< O: Into, O>> + From>, AccountId, - SuccessType, - Success: Get, + Success: TypedGet, > - EnsureOrigin for EnsureRootWithSuccess + EnsureOrigin for EnsureRootWithSuccess { - type Success = SuccessType; + type Success = Success::Type; fn try_origin(o: O) -> Result { o.into().and_then(|o| match o { RawOrigin::Root => Ok(Success::get()), From 386977adc6558000166dade7b7525fd2821a31ee Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 13 Apr 2022 14:26:15 +0100 Subject: [PATCH 15/48] Fixes --- primitives/arithmetic/src/per_things.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index 6f24ee1acf1cf..e7b5a97d63f21 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -578,7 +578,7 @@ macro_rules! implement_per_thing { let rest = n % ($max / 100); write!(fmt, "{}", units)?; if rest > 0 { - write!(fmt, ".", units)?; + write!(fmt, ".")?; let mut m = $max; while rest % m > 0 { m /= 10; From 2cd8a46fc06c0143d49d5745d933de4fd00b7568 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 13 Apr 2022 16:18:47 +0100 Subject: [PATCH 16/48] Enable curve's ceil to be configured --- frame/referenda/src/mock.rs | 14 +++-- frame/referenda/src/tests.rs | 10 ++-- frame/referenda/src/types.rs | 74 ++++++++++++++----------- primitives/arithmetic/src/per_things.rs | 8 +-- 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/frame/referenda/src/mock.rs b/frame/referenda/src/mock.rs index 16e91c72317a7..44bf7dc6372fa 100644 --- a/frame/referenda/src/mock.rs +++ b/frame/referenda/src/mock.rs @@ -174,10 +174,12 @@ impl TracksInfo for TestTracksInfo { min_approval: Curve::LinearDecreasing { length: Perbill::from_percent(100), floor: Perbill::from_percent(50), + ceil: Perbill::from_percent(100), }, min_support: Curve::LinearDecreasing { length: Perbill::from_percent(100), - floor: Perbill::from_percent(100), + floor: Perbill::from_percent(0), + ceil: Perbill::from_percent(100), }, }, ), @@ -192,12 +194,14 @@ impl TracksInfo for TestTracksInfo { confirm_period: 1, min_enactment_period: 2, min_approval: Curve::LinearDecreasing { - length: Perbill::from_percent(55), - floor: Perbill::from_percent(5), + length: Perbill::from_percent(100), + floor: Perbill::from_percent(95), + ceil: Perbill::from_percent(100), }, min_support: Curve::LinearDecreasing { - length: Perbill::from_percent(10), - floor: Perbill::from_percent(10), + length: Perbill::from_percent(100), + floor: Perbill::from_percent(90), + ceil: Perbill::from_percent(100), }, }, ), diff --git a/frame/referenda/src/tests.rs b/frame/referenda/src/tests.rs index 3b174eac61e1d..8134e024dda39 100644 --- a/frame/referenda/src/tests.rs +++ b/frame/referenda/src/tests.rs @@ -504,12 +504,14 @@ fn set_balance_proposal_is_correctly_filtered_out() { #[test] fn curve_handles_all_inputs() { - let test_curve = Curve::LinearDecreasing { length: Perbill::zero(), floor: Perbill::zero() }; + let test_curve = Curve::LinearDecreasing { + length: Perbill::one(), + floor: Perbill::zero(), + ceil: Perbill::from_percent(100), + }; let delay = test_curve.delay(Perbill::zero()); - assert_eq!(delay, Perbill::zero()); - - let test_curve = Curve::LinearDecreasing { length: Perbill::zero(), floor: Perbill::one() }; + assert_eq!(delay, Perbill::one()); let threshold = test_curve.threshold(Perbill::one()); assert_eq!(threshold, Perbill::zero()); diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 736b350ee2940..ed4db26203f24 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -249,9 +249,9 @@ impl< #[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] #[cfg_attr(not(feature = "std"), derive(RuntimeDebug))] pub enum Curve { - /// Linear curve starting at `(0, 0)`, proceeding linearly to `(length, floor)`, then + /// Linear curve starting at `(0, ceil)`, proceeding linearly to `(length, floor)`, then /// remaining at `floor` until the end of the period. - LinearDecreasing { length: Perbill, floor: Perbill }, + LinearDecreasing { length: Perbill, floor: Perbill, ceil: Perbill }, /// Stepped curve, beginning at `(0, begin)`, then remaining constant for `period`, at which /// point it steps down to `(period, begin - step)`. It then remains constant for another /// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues @@ -272,31 +272,34 @@ impl Curve { length: u128, period: u128, floor: FixedI64, + ceil: FixedI64, ) -> Curve { let length = FixedI64::from_rational(length, period).into_perbill(); let floor = floor.into_perbill(); - Curve::LinearDecreasing { length, floor } + let ceil = ceil.into_perbill(); + Curve::LinearDecreasing { length, floor, ceil } } pub const fn make_reciprocal( delay: u128, period: u128, level: FixedI64, floor: FixedI64, + ceil: FixedI64, ) -> Curve { let delay = FixedI64::from_rational(delay, period).into_perbill(); let mut bounds = (( FixedI64::from_u32(0), - Self::reciprocal_from_parts(FixedI64::from_u32(0), floor), + Self::reciprocal_from_parts(FixedI64::from_u32(0), floor, ceil), FixedI64::from_inner(i64::max_value()), ), ( FixedI64::from_u32(1), - Self::reciprocal_from_parts(FixedI64::from_u32(1), floor), + Self::reciprocal_from_parts(FixedI64::from_u32(1), floor, ceil), FixedI64::from_inner(i64::max_value()), )); const TWO: FixedI64 = FixedI64::from_u32(2); while (bounds.1).0.sub((bounds.0).0).into_inner() > 1 { let factor = (bounds.0).0.add((bounds.1).0).div(TWO); - let curve = Self::reciprocal_from_parts(factor, floor); + let curve = Self::reciprocal_from_parts(factor, floor, ceil); let curve_level = FixedI64::from_perbill(curve.const_threshold(delay)); if curve_level.into_inner() > level.into_inner() { bounds = (bounds.0, (factor, curve, curve_level.sub(level))); @@ -311,14 +314,15 @@ impl Curve { } } - const fn reciprocal_from_parts(factor: FixedI64, floor: FixedI64) -> Self { - let one_minus_floor = FixedI64::from_u32(1).sub(floor); + const fn reciprocal_from_parts(factor: FixedI64, floor: FixedI64, ceil: FixedI64) -> Self { + let one_minus_floor = ceil.sub(floor); let x_offset = pos_quad_solution(one_minus_floor, one_minus_floor, factor.neg()); let y_offset = floor.sub(factor.div(FixedI64::from_u32(1).add(x_offset))); Curve::Reciprocal { factor, x_offset, y_offset } } /// Print some info on the curve. + #[cfg(feature = "std")] pub fn info(&self, days: u32, name: impl std::fmt::Display) { let hours = days * 24; println!("Curve {name} := {:?}:", self); @@ -375,8 +379,8 @@ impl Curve { /// Determine the `y` value for the given `x` value. pub(crate) fn threshold(&self, x: Perbill) -> Perbill { match self { - Self::LinearDecreasing { length, floor } => - (x.min(*length).saturating_div(*length, Down) * floor.left_from_one()).left_from_one(), + Self::LinearDecreasing { length, floor, ceil } => + *ceil - (x.min(*length).saturating_div(*length, Down) * (*ceil - *floor)), Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), Self::Reciprocal { factor, x_offset, y_offset } => { @@ -390,7 +394,6 @@ impl Curve { /// Determine the `y` value for the given `x` value. /// /// This is a partial implementation designed only for use in const functions. - #[cfg(feature = "std")] const fn const_threshold(&self, x: Perbill) -> Perbill { match self { Self::Reciprocal { factor, x_offset, y_offset } => { @@ -419,11 +422,13 @@ impl Curve { /// ``` pub fn delay(&self, y: Perbill) -> Perbill { match self { - Self::LinearDecreasing { length, floor } => + Self::LinearDecreasing { length, floor, ceil } => if y < *floor { Perbill::one() + } else if y > *ceil { + Perbill::zero() } else { - y.left_from_one().saturating_div(floor.left_from_one(), Up) * *length + (*ceil - y).saturating_div(*ceil - *floor, Up) * *length }, Self::SteppedDecreasing { begin, end, step, period } => if y < *end { @@ -450,10 +455,11 @@ impl Curve { impl Debug for Curve { fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result { match self { - Self::LinearDecreasing { length, floor } => { + Self::LinearDecreasing { length, floor, ceil } => { write!( f, - "Linear[(0%, 100%) -> ({:?}, {:?}) -> (100%, {:?})]", + "Linear[(0%, {:?}) -> ({:?}, {:?}) -> (100%, {:?})]", + ceil, length, floor, floor, @@ -492,24 +498,26 @@ mod tests { FixedI64::from_rational(x, 100) } - const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50)); - const TIP_SUP: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0)); - const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50)); - const ROOT_SUP: Curve = Curve::make_linear(28, 28, percent(0)); - const WHITE_APP: Curve = Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50)); - const WHITE_SUP: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10)); - const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50)); - const SMALL_SUP: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0)); - const MID_APP: Curve = Curve::make_linear(17, 28, percent(50)); - const MID_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); - const BIG_APP: Curve = Curve::make_linear(23, 28, percent(50)); - const BIG_SUP: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0)); - const HUGE_APP: Curve = Curve::make_linear(28, 28, percent(50)); - const HUGE_SUP: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0)); - const PARAM_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50)); - const PARAM_SUP: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0)); - const ADMIN_APP: Curve = Curve::make_linear(17, 28, percent(50)); - const ADMIN_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0)); + const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100)); + const TIP_SUP: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0), percent(50)); + const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100)); + const ROOT_SUP: Curve = Curve::make_linear(28, 28, percent(0), percent(50)); + const WHITE_APP: Curve = Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50), percent(100)); + const WHITE_SUP: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10), percent(50)); + const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100)); + const SMALL_SUP: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0), percent(50)); + const MID_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100)); + const MID_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50)); + const BIG_APP: Curve = Curve::make_linear(23, 28, percent(50), percent(100)); + const BIG_SUP: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0), percent(50)); + const HUGE_APP: Curve = Curve::make_linear(28, 28, percent(50), percent(100)); + const HUGE_SUP: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0), percent(50)); + const PARAM_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100)); + const PARAM_SUP: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0), percent(50)); + const ADMIN_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100)); + const ADMIN_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50)); + + // TODO: ceil for linear. #[test] #[should_panic] diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index e7b5a97d63f21..3bf3ae7c50ce1 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -568,18 +568,18 @@ macro_rules! implement_per_thing { #[cfg(not(feature = "std"))] impl sp_std::fmt::Debug for $name { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, fmt: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { if $max == <$type>::max_value() { // Not a power of ten: show as N/D and approx % write!(fmt, "{}/{}", self.0, $max) } else { // A power of ten: calculate exact percent - let units = n / ($max / 100); - let rest = n % ($max / 100); + let units = self.0 / ($max / 100); + let rest = self.0 % ($max / 100); write!(fmt, "{}", units)?; if rest > 0 { write!(fmt, ".")?; - let mut m = $max; + let mut m = $max / 100; while rest % m > 0 { m /= 10; write!(fmt, "{:01}", rest / m % 10)?; From 485fd23797771370ef923f7e2af7d381ac1a3e85 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 11 May 2022 15:13:21 +0100 Subject: [PATCH 17/48] Formatting --- frame/conviction-voting/src/types.rs | 6 +- frame/referenda/src/types.rs | 80 +++++++++------------ frame/support/src/lib.rs | 2 +- frame/support/src/traits.rs | 4 +- frame/support/src/traits/dispatch.rs | 38 +++++----- frame/system/src/lib.rs | 11 ++- frame/treasury/src/lib.rs | 10 ++- frame/treasury/src/tests.rs | 22 ++++-- primitives/arithmetic/src/fixed_point.rs | 26 ++++--- primitives/arithmetic/src/helpers_128bit.rs | 59 ++++++++------- primitives/arithmetic/src/lib.rs | 4 +- primitives/arithmetic/src/per_things.rs | 19 +++-- 12 files changed, 149 insertions(+), 132 deletions(-) diff --git a/frame/conviction-voting/src/types.rs b/frame/conviction-voting/src/types.rs index 55fc869302138..7615b1bd65745 100644 --- a/frame/conviction-voting/src/types.rs +++ b/frame/conviction-voting/src/types.rs @@ -123,7 +123,11 @@ impl< } } - pub fn from_parts(ayes_with_conviction: Votes, nays_with_conviction: Votes, ayes: Votes) -> Self { + pub fn from_parts( + ayes_with_conviction: Votes, + nays_with_conviction: Votes, + ayes: Votes, + ) -> Self { Self { ayes: ayes_with_conviction, nays: nays_with_conviction, diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index cd28c6246c127..d11493e481bd1 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -21,8 +21,8 @@ use super::*; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{traits::schedule::Anon, Parameter}; use scale_info::TypeInfo; -use sp_runtime::{RuntimeDebug, PerThing, FixedI64}; use sp_arithmetic::Rounding::*; +use sp_runtime::{FixedI64, PerThing, RuntimeDebug}; use sp_std::fmt::Debug; pub type BalanceOf = @@ -268,12 +268,7 @@ const fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 { } impl Curve { - pub const fn make_linear( - length: u128, - period: u128, - floor: FixedI64, - ceil: FixedI64, - ) -> Curve { + pub const fn make_linear(length: u128, period: u128, floor: FixedI64, ceil: FixedI64) -> Curve { let length = FixedI64::from_rational(length, period).into_perbill(); let floor = floor.into_perbill(); let ceil = ceil.into_perbill(); @@ -287,15 +282,18 @@ impl Curve { ceil: FixedI64, ) -> Curve { let delay = FixedI64::from_rational(delay, period).into_perbill(); - let mut bounds = (( - FixedI64::from_u32(0), - Self::reciprocal_from_parts(FixedI64::from_u32(0), floor, ceil), - FixedI64::from_inner(i64::max_value()), - ), ( - FixedI64::from_u32(1), - Self::reciprocal_from_parts(FixedI64::from_u32(1), floor, ceil), - FixedI64::from_inner(i64::max_value()), - )); + let mut bounds = ( + ( + FixedI64::from_u32(0), + Self::reciprocal_from_parts(FixedI64::from_u32(0), floor, ceil), + FixedI64::from_inner(i64::max_value()), + ), + ( + FixedI64::from_u32(1), + Self::reciprocal_from_parts(FixedI64::from_u32(1), floor, ceil), + FixedI64::from_inner(i64::max_value()), + ), + ); const TWO: FixedI64 = FixedI64::from_u32(2); while (bounds.1).0.sub((bounds.0).0).into_inner() > 1 { let factor = (bounds.0).0.add((bounds.1).0).div(TWO); @@ -367,7 +365,10 @@ impl Curve { println!(" 0.1% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 1_000)))); println!(" 0.01% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 10_000)))); } else { - println!(" 99.9% threshold: {}", t(self.delay(Perbill::from_rational(999u32, 1_000)))); + println!( + " 99.9% threshold: {}", + t(self.delay(Perbill::from_rational(999u32, 1_000))) + ); println!(" 99% threshold: {}", t(self.delay(Perbill::from_percent(99)))); println!(" 95% threshold: {}", t(self.delay(Perbill::from_percent(95)))); println!(" 90% threshold: {}", t(self.delay(Perbill::from_percent(90)))); @@ -383,11 +384,10 @@ impl Curve { *ceil - (x.min(*length).saturating_div(*length, Down) * (*ceil - *floor)), Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), - Self::Reciprocal { factor, x_offset, y_offset } => { - factor.checked_rounding_div(FixedI64::from(x) + *x_offset, Down) - .map(|yp| (yp + *y_offset).into_clamped_perthing()) - .unwrap_or_else(Perbill::one) - } + Self::Reciprocal { factor, x_offset, y_offset } => factor + .checked_rounding_div(FixedI64::from(x) + *x_offset, Down) + .map(|yp| (yp + *y_offset).into_clamped_perthing()) + .unwrap_or_else(Perbill::one), } } @@ -401,7 +401,7 @@ impl Curve { Some(yp) => (yp.add(*y_offset)).into_perbill(), None => Perbill::one(), } - } + }, _ => panic!("const_threshold cannot be used on this curve"), } } @@ -439,7 +439,8 @@ impl Curve { Self::Reciprocal { factor, x_offset, y_offset } => { let y = FixedI64::from(y); let maybe_term = factor.checked_rounding_div(y - *y_offset, Up); - maybe_term.and_then(|term| (term - *x_offset).try_into_perthing().ok()) + maybe_term + .and_then(|term| (term - *x_offset).try_into_perthing().ok()) .unwrap_or_else(Perbill::one) }, } @@ -459,31 +460,23 @@ impl Debug for Curve { write!( f, "Linear[(0%, {:?}) -> ({:?}, {:?}) -> (100%, {:?})]", - ceil, - length, - floor, - floor, + ceil, length, floor, floor, ) }, Self::SteppedDecreasing { begin, end, step, period } => { write!( f, "Stepped[(0%, {:?}) -> (100%, {:?}) by ({:?}, {:?})]", - begin, - end, - period, - step, + begin, end, period, step, ) }, Self::Reciprocal { factor, x_offset, y_offset } => { write!( f, "Reciprocal[factor of {:?}, x_offset of {:?}, y_offset of {:?}]", - factor, - x_offset, - y_offset, + factor, x_offset, y_offset, ) - } + }, } } } @@ -491,8 +484,8 @@ impl Debug for Curve { #[cfg(test)] mod tests { use super::*; - use sp_runtime::PerThing; use frame_support::traits::ConstU32; + use sp_runtime::PerThing; const fn percent(x: u128) -> FixedI64 { FixedI64::from_rational(x, 100) @@ -502,7 +495,8 @@ mod tests { const TIP_SUP: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0), percent(50)); const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100)); const ROOT_SUP: Curve = Curve::make_linear(28, 28, percent(0), percent(50)); - const WHITE_APP: Curve = Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50), percent(100)); + const WHITE_APP: Curve = + Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50), percent(100)); const WHITE_SUP: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10), percent(50)); const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100)); const SMALL_SUP: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0), percent(50)); @@ -595,12 +589,8 @@ mod tests { Perbill::from_percent(x) } - let c = Curve::SteppedDecreasing { - begin: pc(80), - end: pc(30), - step: pc(10), - period: pc(15), - }; + let c = + Curve::SteppedDecreasing { begin: pc(80), end: pc(30), step: pc(10), period: pc(15) }; for i in 0..9_696_969u32 { let query = Perbill::from_rational(i, 9_696_969); @@ -638,4 +628,4 @@ mod tests { assert_eq!(c.delay(pc(30).less_epsilon()), pc(100)); assert_eq!(c.delay(pc(0)), pc(100)); } -} \ No newline at end of file +} diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index 7946133909f67..174fb071236ca 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -515,7 +515,7 @@ macro_rules! parameter_types { I::from(Self::get()) } } - + impl $crate::traits::TypedGet for $name { type Type = $type; fn get() -> $type { diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index b786ea1059cd6..7c0a26d90aa59 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -93,12 +93,12 @@ pub use storage::{ }; mod dispatch; +#[allow(deprecated)] +pub use dispatch::EnsureOneOf; pub use dispatch::{ AsEnsureOriginWithArg, EitherOf, EitherOfDiverse, EnsureOrigin, EnsureOriginWithArg, OriginTrait, UnfilteredDispatchable, }; -#[allow(deprecated)] -pub use dispatch::EnsureOneOf; mod voting; pub use voting::{ diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index 04f1b9a7ccc6a..b59744e1c715e 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -186,13 +186,15 @@ pub type EnsureOneOf = EitherOfDiverse; /// Successful origin is derived from the left side. pub struct EitherOf(sp_std::marker::PhantomData<(L, R)>); -impl, R: EnsureOrigin> - EnsureOrigin for EitherOf +impl< + OuterOrigin, + L: EnsureOrigin, + R: EnsureOrigin, + > EnsureOrigin for EitherOf { type Success = L::Success; fn try_origin(o: OuterOrigin) -> Result { - L::try_origin(o) - .or_else(|o| R::try_origin(o)) + L::try_origin(o).or_else(|o| R::try_origin(o)) } #[cfg(feature = "runtime-benchmarks")] @@ -203,9 +205,9 @@ impl, R: EnsureOrigin(PhantomData); struct EnsureFail(PhantomData); @@ -242,17 +244,15 @@ mod tests { Some(true) ); assert_eq!( - EitherOfDiverse::< - EnsureSuccess>, - EnsureFail, - >::try_origin(()).unwrap().left(), + EitherOfDiverse::>, EnsureFail>::try_origin(()) + .unwrap() + .left(), Some(true) ); assert_eq!( - EitherOfDiverse::< - EnsureFail, - EnsureSuccess>, - >::try_origin(()).unwrap().right(), + EitherOfDiverse::, EnsureSuccess>>::try_origin(()) + .unwrap() + .right(), Some(0u8) ); assert!(EitherOfDiverse::, EnsureFail>::try_origin(()).is_err()); @@ -268,17 +268,11 @@ mod tests { true ); assert_eq!( - EitherOf::< - EnsureSuccess>, - EnsureFail, - >::try_origin(()).unwrap(), + EitherOf::>, EnsureFail>::try_origin(()).unwrap(), true ); assert_eq!( - EitherOf::< - EnsureFail, - EnsureSuccess>, - >::try_origin(()).unwrap(), + EitherOf::, EnsureSuccess>>::try_origin(()).unwrap(), false ); assert!(EitherOf::, EnsureFail>::try_origin(()).is_err()); diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index d0376e3932bf0..530bd30f4c771 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -788,14 +788,13 @@ impl, O>> + From>, Acco } pub struct EnsureRootWithSuccess( - sp_std::marker::PhantomData<(AccountId, Success)> + sp_std::marker::PhantomData<(AccountId, Success)>, ); impl< - O: Into, O>> + From>, - AccountId, - Success: TypedGet, -> - EnsureOrigin for EnsureRootWithSuccess + O: Into, O>> + From>, + AccountId, + Success: TypedGet, + > EnsureOrigin for EnsureRootWithSuccess { type Success = Success::Type; fn try_origin(o: O) -> Result { diff --git a/frame/treasury/src/lib.rs b/frame/treasury/src/lib.rs index 541ec2f74895a..c250ff3be9791 100644 --- a/frame/treasury/src/lib.rs +++ b/frame/treasury/src/lib.rs @@ -202,7 +202,7 @@ pub mod pallet { /// The origin required for approving spends from the treasury outside of the proposal /// process. The `Success` value is the maximum amount that this origin is allowed to /// spend at a time. - type SpendOrigin: EnsureOrigin>; + type SpendOrigin: EnsureOrigin>; } /// Number of proposals that have been made. @@ -281,7 +281,11 @@ pub mod pallet { /// Some funds have been deposited. Deposit { value: BalanceOf }, /// A new spend proposal has been approved. - SpendApproved { proposal_index: ProposalIndex, amount: BalanceOf, beneficiary: T::AccountId }, + SpendApproved { + proposal_index: ProposalIndex, + amount: BalanceOf, + beneficiary: T::AccountId, + }, } /// Error for the treasury pallet. @@ -434,7 +438,7 @@ pub mod pallet { ProposalCount::::put(proposal_index + 1); Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary }); - + Ok(()) } diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index c3464eb393b28..40172e9f2b455 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -24,7 +24,7 @@ use std::cell::RefCell; use sp_core::H256; use sp_runtime::{ testing::Header, - traits::{BlakeTwo256, IdentityLookup, BadOrigin}, + traits::{BadOrigin, BlakeTwo256, IdentityLookup}, }; use frame_support::{ @@ -165,10 +165,22 @@ fn spend_origin_permissioning_works() { new_test_ext().execute_with(|| { // Check that accumulate works when we have Some value in Dummy already. assert_noop!(Treasury::spend(Origin::signed(1), 1, 1), BadOrigin); - assert_noop!(Treasury::spend(Origin::signed(10), 6, 1), Error::::InsufficientPermission); - assert_noop!(Treasury::spend(Origin::signed(11), 11, 1), Error::::InsufficientPermission); - assert_noop!(Treasury::spend(Origin::signed(12), 21, 1), Error::::InsufficientPermission); - assert_noop!(Treasury::spend(Origin::signed(13), 51, 1), Error::::InsufficientPermission); + assert_noop!( + Treasury::spend(Origin::signed(10), 6, 1), + Error::::InsufficientPermission + ); + assert_noop!( + Treasury::spend(Origin::signed(11), 11, 1), + Error::::InsufficientPermission + ); + assert_noop!( + Treasury::spend(Origin::signed(12), 21, 1), + Error::::InsufficientPermission + ); + assert_noop!( + Treasury::spend(Origin::signed(13), 51, 1), + Error::::InsufficientPermission + ); }); } diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index f3f96ae16c13a..58348f51784c8 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -444,9 +444,11 @@ macro_rules! implement_fixed { ) { Some(value) => { if value > (u32::max_value() as u128) { - panic!("prior logic ensures 0 Option { + pub const fn checked_rounding_div( + self, + other: Self, + rounding: Rounding, + ) -> Option { if other.0 == 0 { return None } @@ -538,11 +544,7 @@ macro_rules! implement_fixed { // computing them individually and taking the product at the end. we will lose some // precision though. let maybe_vd = u128::checked_mul(v, $div); - let r = if let Some(vd) = maybe_vd { - sqrt(vd) - } else { - sqrt(v) * sqrt($div) - }; + let r = if let Some(vd) = maybe_vd { sqrt(vd) } else { sqrt(v) * sqrt($div) }; Some(Self(r as $inner_type)) } @@ -610,12 +612,8 @@ macro_rules! implement_fixed { if b == 0 { panic!("attempt to divide by zero in from_rational") } - match multiply_by_rational_with_rounding( - Self::DIV as u128, - a, - b, - Rounding::Nearest, - ) { + match multiply_by_rational_with_rounding(Self::DIV as u128, a, b, Rounding::Nearest) + { Some(value) => match Self::from_i129(I129 { value, negative: false }) { Some(x) => x, None => panic!("overflow in from_rational"), diff --git a/primitives/arithmetic/src/helpers_128bit.rs b/primitives/arithmetic/src/helpers_128bit.rs index f63816bc9d372..4528fb7a63d3d 100644 --- a/primitives/arithmetic/src/helpers_128bit.rs +++ b/primitives/arithmetic/src/helpers_128bit.rs @@ -22,10 +22,10 @@ //! multiplication implementation provided there. use crate::{biguint, Rounding}; -use sp_std::convert::TryInto; use num_traits::Zero; use sp_std::{ cmp::{max, min}, + convert::TryInto, mem, }; @@ -125,7 +125,7 @@ mod double128 { /// Returns the least significant 64 bits of a const fn low_64(a: u128) -> u128 { - a & ((1<<64)-1) + a & ((1 << 64) - 1) } /// Returns the most significant 64 bits of a @@ -140,7 +140,7 @@ mod double128 { /// Returns 2^128 / a const fn div128(a: u128) -> u128 { - (neg128(a)/a).wrapping_add(1) + (neg128(a) / a).wrapping_add(1) } /// Returns 2^128 % a @@ -163,10 +163,7 @@ mod double128 { } pub const fn zero() -> Self { - Self { - high: 0, - low: 0, - } + Self { high: 0, low: 0 } } /// Return a `Double128` value representing the `scaled_value << 64`. @@ -175,10 +172,7 @@ mod double128 { /// `scaled_value` (in the lower positions) and the upper half of the `low` component will /// be equal to the lower 64-bits of `scaled_value`. pub const fn left_shift_64(scaled_value: u128) -> Self { - Self { - high: scaled_value >> 64, - low: scaled_value << 64, - } + Self { high: scaled_value >> 64, low: scaled_value << 64 } } /// Construct a value from the upper 128 bits only, with the lower being zeroed. @@ -188,7 +182,7 @@ mod double128 { /// Returns the same value ignoring anything in the high 128-bits. pub const fn low_part(self) -> Self { - Self { high: 0, .. self } + Self { high: 0, ..self } } /// Returns a*b (in 256 bits) @@ -218,14 +212,14 @@ mod double128 { pub const fn add(self, b: Self) -> Self { let (low, overflow) = self.low.overflowing_add(b.low); - let carry = overflow as u128; // 1 if true, 0 if false. + let carry = overflow as u128; // 1 if true, 0 if false. let high = self.high.wrapping_add(b.high).wrapping_add(carry as u128); Double128 { high, low } } pub const fn div(mut self, rhs: u128) -> (Self, u128) { if rhs == 1 { - return (self, 0); + return (self, 0) } // (self === a; rhs === b) @@ -254,26 +248,39 @@ mod double128 { /// Returns `a * b / c` and `(a * b) % c` (wrapping to 128 bits) or `None` in the case of /// overflow. -pub const fn multiply_by_rational_with_rounding(a: u128, b: u128, c: u128, r: Rounding) -> Option { +pub const fn multiply_by_rational_with_rounding( + a: u128, + b: u128, + c: u128, + r: Rounding, +) -> Option { use double128::Double128; if c == 0 { panic!("attempt to divide by zero") } let (result, remainder) = Double128::product_of(a, b).div(c); - let mut result: u128 = match result.try_into_u128() { Ok(v) => v, Err(_) => return None }; + let mut result: u128 = match result.try_into_u128() { + Ok(v) => v, + Err(_) => return None, + }; if match r { Rounding::Up => remainder > 0, Rounding::Nearest => remainder >= c / 2 + c % 2, Rounding::Down => false, } { - result = match result.checked_add(1) { Some(v) => v, None => return None }; + result = match result.checked_add(1) { + Some(v) => v, + None => return None, + }; } Some(result) } pub const fn sqrt(mut n: u128) -> u128 { // Modified from https://github.com/derekdreery/integer-sqrt-rs (Apache/MIT). - if n == 0 { return 0 } + if n == 0 { + return 0 + } // Compute bit, the largest power of 4 <= n let max_shift: u32 = 0u128.leading_zeros() - 1; @@ -299,9 +306,9 @@ pub const fn sqrt(mut n: u128) -> u128 { #[cfg(test)] mod tests { use super::*; - use Rounding::*; + use codec::{Decode, Encode}; use multiply_by_rational_with_rounding as mulrat; - use codec::{Encode, Decode}; + use Rounding::*; const MAX: u128 = u128::max_value(); @@ -317,14 +324,14 @@ mod tests { #[test] fn rational_multiply_big_number_works() { - assert_eq!(mulrat(MAX, MAX-1, MAX, Down), Some(MAX-1)); + assert_eq!(mulrat(MAX, MAX - 1, MAX, Down), Some(MAX - 1)); assert_eq!(mulrat(MAX, 1, MAX, Down), Some(1)); - assert_eq!(mulrat(MAX, MAX-1, MAX, Up), Some(MAX-1)); + assert_eq!(mulrat(MAX, MAX - 1, MAX, Up), Some(MAX - 1)); assert_eq!(mulrat(MAX, 1, MAX, Up), Some(1)); - assert_eq!(mulrat(1, MAX-1, MAX, Down), Some(0)); + assert_eq!(mulrat(1, MAX - 1, MAX, Down), Some(0)); assert_eq!(mulrat(1, 1, MAX, Up), Some(1)); - assert_eq!(mulrat(1, MAX/2, MAX, Nearest), Some(0)); - assert_eq!(mulrat(1, MAX/2+1, MAX, Nearest), Some(1)); + assert_eq!(mulrat(1, MAX / 2, MAX, Nearest), Some(0)); + assert_eq!(mulrat(1, MAX / 2 + 1, MAX, Nearest), Some(1)); } fn random_u128(seed: u32) -> u128 { @@ -346,4 +353,4 @@ mod tests { assert_eq!(d, 0); } } -} \ No newline at end of file +} diff --git a/primitives/arithmetic/src/lib.rs b/primitives/arithmetic/src/lib.rs index ea40bd017b27e..a4bdfe46d13a1 100644 --- a/primitives/arithmetic/src/lib.rs +++ b/primitives/arithmetic/src/lib.rs @@ -41,7 +41,9 @@ pub mod rational; pub mod traits; pub use fixed_point::{FixedI128, FixedI64, FixedPointNumber, FixedPointOperand, FixedU128}; -pub use per_things::{InnerOf, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, UpperOf, Rounding}; +pub use per_things::{ + InnerOf, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, Rounding, UpperOf, +}; pub use rational::{Rational128, RationalInfinite}; use sp_std::{cmp::Ordering, fmt::Debug, prelude::*}; diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index ed293f706ea90..08be884e4b321 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -91,27 +91,35 @@ pub trait PerThing: /// Return the next lower value to `self` or `self` if it is already zero. fn less_epsilon(self) -> Self { - if self.is_zero() { return self } + if self.is_zero() { + return self + } Self::from_parts(self.deconstruct() - One::one()) } /// Return the next lower value to `self` or an error with the same value if `self` is already /// zero. fn try_less_epsilon(self) -> Result { - if self.is_zero() { return Err(self) } + if self.is_zero() { + return Err(self) + } Ok(Self::from_parts(self.deconstruct() - One::one())) } /// Return the next higher value to `self` or `self` if it is already one. fn plus_epsilon(self) -> Self { - if self.is_one() { return self } + if self.is_one() { + return self + } Self::from_parts(self.deconstruct() + One::one()) } /// Return the next higher value to `self` or an error with the same value if `self` is already /// one. fn try_plus_epsilon(self) -> Result { - if self.is_one() { return Err(self) } + if self.is_one() { + return Err(self) + } Ok(Self::from_parts(self.deconstruct() + One::one())) } @@ -319,7 +327,7 @@ pub trait PerThing: + Unsigned + Zero + One, - Self::Inner: Into + Self::Inner: Into, { Self::from_rational_with_rounding(p, q, Rounding::Down).unwrap_or_else(|_| Self::one()) } @@ -385,7 +393,6 @@ pub trait PerThing: + One, Self::Inner: Into; - /// Same as `Self::from_rational`. #[deprecated = "Use from_rational instead"] fn from_rational_approximation(p: N, q: N) -> Self From f5d6f4565e0102a22e103c17bd451da3c2c8706d Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 11 May 2022 15:38:55 +0100 Subject: [PATCH 18/48] Fixes --- Cargo.lock | 4 ++-- primitives/arithmetic/src/fixed_point.rs | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 183e62f80ed95..eebab17fb3db6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3410,9 +3410,9 @@ checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" [[package]] name = "libgit2-sys" -version = "0.13.3+1.4.2" +version = "0.13.2+1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c24d36c3ac9b9996a2418d6bf428cc0bc5d1a814a84303fc60986088c5ed60de" +checksum = "3a42de9a51a5c12e00fc0e4ca6bc2ea43582fc6418488e8f615e905d886f258b" dependencies = [ "cc", "libc", diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index 58348f51784c8..04a48bc7b0601 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -1092,11 +1092,9 @@ macro_rules! implement_fixed { fn op_sqrt_works() { for i in 1..1_000i64 { let x = $name::saturating_from_rational(i, 1_000i64); - dbg!(i, x, x * x, (x * x).sqrt()); - assert_eq!((x * x).sqrt(), Some(x)); + assert_eq!((x * x).try_sqrt(), Some(x)); let x = $name::saturating_from_rational(i, 1i64); - dbg!(i, x, x * x, (x * x).sqrt()); - assert_eq!((x * x).sqrt(), Some(x)); + assert_eq!((x * x).try_sqrt(), Some(x)); } } From 7ce42e8b2f47daaf94ff0f758f43d360d67d7c24 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 11 May 2022 15:40:03 +0100 Subject: [PATCH 19/48] Fixes --- frame/treasury/src/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index 40172e9f2b455..d0386c3efd81d 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -437,7 +437,7 @@ fn max_approvals_limited() { Balances::make_free_balance_be(&Treasury::account_id(), u64::MAX); Balances::make_free_balance_be(&0, u64::MAX); - for _ in 0..MaxApprovals::get() { + for _ in 0..::MaxApprovals::get() { assert_ok!(Treasury::propose_spend(Origin::signed(0), 100, 3)); assert_ok!(Treasury::approve_proposal(Origin::root(), 0)); } From b25bc5f777eac1d767dd8691a688cce543db3cfc Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 11 May 2022 15:42:24 +0100 Subject: [PATCH 20/48] Fixes --- frame/referenda/src/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index d11493e481bd1..0f9ae16ca7767 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -323,7 +323,7 @@ impl Curve { #[cfg(feature = "std")] pub fn info(&self, days: u32, name: impl std::fmt::Display) { let hours = days * 24; - println!("Curve {name} := {:?}:", self); + println!("Curve {} := {:?}:", name, self); println!(" t + 0h: {:?}", self.threshold(Perbill::zero())); println!(" t + 1h: {:?}", self.threshold(Perbill::from_rational(1, hours))); println!(" t + 2h: {:?}", self.threshold(Perbill::from_rational(2, hours))); @@ -335,7 +335,7 @@ impl Curve { for &(n, d) in [(1, 12), (1, 8), (1, 4), (1, 2), (3, 4), (1, 1)].iter() { let t = days * n / d; if t != l { - println!(" t + {t}d: {:?}", self.threshold(Perbill::from_rational(t, days))); + println!(" t + {}d: {:?}", t, self.threshold(Perbill::from_rational(t, days))); l = t; } } From 45208e0a63cf7c7b1a59d9e88bcef0b9973799df Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 11 May 2022 21:52:47 +0100 Subject: [PATCH 21/48] Remove EnsureOneOf --- bin/node/runtime/src/lib.rs | 12 ++++++------ docs/Upgrading-2.0-to-3.0.md | 2 +- frame/identity/src/tests.rs | 6 +++--- frame/scheduler/src/mock.rs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index c350ae0194e78..82112a7b52428 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -29,7 +29,7 @@ use frame_support::{ pallet_prelude::Get, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, Currency, EnsureOneOf, + AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse, EqualPrivilegeOnly, Everything, Imbalance, InstanceFilter, KeyOwnerProofSystem, LockIdentifier, Nothing, OnUnbalanced, U128CurrencyToVote, }, @@ -548,7 +548,7 @@ impl pallet_staking::Config for Runtime { type BondingDuration = BondingDuration; type SlashDeferDuration = SlashDeferDuration; /// A super-majority of the council can cancel the slash. - type SlashCancelOrigin = EnsureOneOf< + type SlashCancelOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionAtLeast, >; @@ -861,7 +861,7 @@ impl pallet_democracy::Config for Runtime { pallet_collective::EnsureProportionAtLeast; // To cancel a proposal before it has been passed, the technical committee must be unanimous or // Root must agree. - type CancelProposalOrigin = EnsureOneOf< + type CancelProposalOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionAtLeast, >; @@ -951,7 +951,7 @@ impl pallet_collective::Config for Runtime { type WeightInfo = pallet_collective::weights::SubstrateWeight; } -type EnsureRootOrHalfCouncil = EnsureOneOf< +type EnsureRootOrHalfCouncil = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionMoreThan, >; @@ -985,11 +985,11 @@ parameter_types! { impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; - type ApproveOrigin = EnsureOneOf< + type ApproveOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionAtLeast, >; - type RejectOrigin = EnsureOneOf< + type RejectOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionMoreThan, >; diff --git a/docs/Upgrading-2.0-to-3.0.md b/docs/Upgrading-2.0-to-3.0.md index 017467ede2d7e..f750c6dd5865b 100644 --- a/docs/Upgrading-2.0-to-3.0.md +++ b/docs/Upgrading-2.0-to-3.0.md @@ -290,7 +290,7 @@ Democracy brings three new settings with this release, all to allow for better i type CancellationOrigin = pallet_collective::EnsureProportionAtLeast<_2, _3, AccountId, CouncilCollective>; + // To cancel a proposal before it has been passed, the technical committee must be unanimous or + // Root must agree. -+ type CancelProposalOrigin = EnsureOneOf< ++ type CancelProposalOrigin = EitherOfDiverse< + AccountId, + EnsureRoot, + pallet_collective::EnsureProportionAtLeast<_1, _1, AccountId, TechnicalCollective>, diff --git a/frame/identity/src/tests.rs b/frame/identity/src/tests.rs index 8cb0563ebeaa1..6066f176a6106 100644 --- a/frame/identity/src/tests.rs +++ b/frame/identity/src/tests.rs @@ -23,7 +23,7 @@ use crate as pallet_identity; use codec::{Decode, Encode}; use frame_support::{ assert_noop, assert_ok, ord_parameter_types, parameter_types, - traits::{ConstU32, ConstU64, EnsureOneOf}, + traits::{ConstU32, ConstU64, EitherOfDiverse}, BoundedVec, }; use frame_system::{EnsureRoot, EnsureSignedBy}; @@ -100,8 +100,8 @@ ord_parameter_types! { pub const One: u64 = 1; pub const Two: u64 = 2; } -type EnsureOneOrRoot = EnsureOneOf, EnsureSignedBy>; -type EnsureTwoOrRoot = EnsureOneOf, EnsureSignedBy>; +type EnsureOneOrRoot = EitherOfDiverse, EnsureSignedBy>; +type EnsureTwoOrRoot = EitherOfDiverse, EnsureSignedBy>; impl pallet_identity::Config for Test { type Event = Event; type Currency = Balances; diff --git a/frame/scheduler/src/mock.rs b/frame/scheduler/src/mock.rs index ecd04c3e48b52..008105dc737ea 100644 --- a/frame/scheduler/src/mock.rs +++ b/frame/scheduler/src/mock.rs @@ -23,7 +23,7 @@ use crate as scheduler; use frame_support::{ ord_parameter_types, parameter_types, traits::{ - ConstU32, ConstU64, Contains, EnsureOneOf, EqualPrivilegeOnly, OnFinalize, OnInitialize, + ConstU32, ConstU64, Contains, EitherOfDiverse, EqualPrivilegeOnly, OnFinalize, OnInitialize, }, weights::constants::RocksDbWeight, }; @@ -174,7 +174,7 @@ impl Config for Test { type PalletsOrigin = OriginCaller; type Call = Call; type MaximumWeight = MaximumSchedulerWeight; - type ScheduleOrigin = EnsureOneOf, EnsureSignedBy>; + type ScheduleOrigin = EitherOfDiverse, EnsureSignedBy>; type MaxScheduledPerBlock = ConstU32<10>; type WeightInfo = (); type OriginPrivilegeCmp = EqualPrivilegeOnly; From 35764fb832a774097da5ef7902e3ba3c116098de Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 11 May 2022 22:50:12 +0100 Subject: [PATCH 22/48] Fixes --- frame/conviction-voting/src/types.rs | 5 +++++ frame/referenda/src/benchmarking.rs | 3 ++- frame/referenda/src/mock.rs | 13 +++++++++++-- frame/support/src/traits/voting.rs | 2 ++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/frame/conviction-voting/src/types.rs b/frame/conviction-voting/src/types.rs index 7615b1bd65745..b8204a51623ed 100644 --- a/frame/conviction-voting/src/types.rs +++ b/frame/conviction-voting/src/types.rs @@ -90,6 +90,11 @@ impl< Self { ayes: Total::get(), nays: Zero::zero(), support: Total::get(), dummy: PhantomData } } + #[cfg(feature = "runtime-benchmarks")] + fn rejection() -> Self { + Self { ayes: Zero::zero(), nays: Total::get(), support: Total::get(), dummy: PhantomData } + } + #[cfg(feature = "runtime-benchmarks")] fn from_requirements(support: Perbill, approval: Perbill) -> Self { let support = support.mul_ceil(Total::get()); diff --git a/frame/referenda/src/benchmarking.rs b/frame/referenda/src/benchmarking.rs index 9d46549cb91db..3a9bbddf57c58 100644 --- a/frame/referenda/src/benchmarking.rs +++ b/frame/referenda/src/benchmarking.rs @@ -121,7 +121,7 @@ fn make_passing(index: ReferendumIndex) { fn make_failing(index: ReferendumIndex) { Referenda::::access_poll(index, |status| { if let PollStatus::Ongoing(tally, ..) = status { - *tally = T::Tally::default(); + *tally = T::Tally::rejection(); } }); } @@ -501,6 +501,7 @@ benchmarks! { let (_caller, index) = create_referendum::(); place_deposit::(index); skip_prepare_period::(index); + make_failing::(index); nudge::(index); skip_decision_period::(index); }: nudge_referendum(RawOrigin::Root, index) diff --git a/frame/referenda/src/mock.rs b/frame/referenda/src/mock.rs index 92a07d1bced8a..d4676b4e9c3cc 100644 --- a/frame/referenda/src/mock.rs +++ b/frame/referenda/src/mock.rs @@ -261,7 +261,11 @@ impl VoteTally for Tally { } fn approval(&self) -> Perbill { - Perbill::from_rational(self.ayes, self.ayes + self.nays) + if self.ayes + self.nays > 0 { + Perbill::from_rational(self.ayes, self.ayes + self.nays) + } else { + Perbill::zero() + } } #[cfg(feature = "runtime-benchmarks")] @@ -269,10 +273,15 @@ impl VoteTally for Tally { Self { ayes: 100, nays: 0 } } + #[cfg(feature = "runtime-benchmarks")] + fn rejection() -> Self { + Self { ayes: 0, nays: 100 } + } + #[cfg(feature = "runtime-benchmarks")] fn from_requirements(support: Perbill, approval: Perbill) -> Self { let ayes = support.mul_ceil(100u32); - let nays = (ayes * 1_000_000_000u64 / approval.deconstruct() as u64) - ayes; + let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes; Self { ayes, nays } } } diff --git a/frame/support/src/traits/voting.rs b/frame/support/src/traits/voting.rs index f495ba248453a..293170cc09cdd 100644 --- a/frame/support/src/traits/voting.rs +++ b/frame/support/src/traits/voting.rs @@ -102,6 +102,8 @@ pub trait VoteTally { #[cfg(feature = "runtime-benchmarks")] fn unanimity() -> Self; #[cfg(feature = "runtime-benchmarks")] + fn rejection() -> Self; + #[cfg(feature = "runtime-benchmarks")] fn from_requirements(support: Perbill, approval: Perbill) -> Self; } From 3b53be614827632c00fd84abbd9feca5096abd5f Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 11 May 2022 23:00:21 +0100 Subject: [PATCH 23/48] Fixes --- frame/conviction-voting/src/types.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/frame/conviction-voting/src/types.rs b/frame/conviction-voting/src/types.rs index b8204a51623ed..8f5afc8505c91 100644 --- a/frame/conviction-voting/src/types.rs +++ b/frame/conviction-voting/src/types.rs @@ -99,7 +99,6 @@ impl< fn from_requirements(support: Perbill, approval: Perbill) -> Self { let support = support.mul_ceil(Total::get()); let ayes = approval.mul_ceil(support); - // TODO Self { ayes, nays: support - ayes, support, dummy: PhantomData } } } @@ -178,7 +177,6 @@ impl< false => self.nays = self.nays.checked_sub(&votes)?, } }, - // TODO: abstain. AccountVote::Split { aye, nay } => { let aye = Conviction::None.votes(aye); let nay = Conviction::None.votes(nay); From 940f4b3e283a758992451ea6858ac844fc340d1a Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 12 May 2022 13:13:22 +0100 Subject: [PATCH 24/48] Fixes --- bin/node/runtime/src/lib.rs | 12 +++++++----- frame/bounties/src/tests.rs | 2 +- frame/child-bounties/src/tests.rs | 2 +- frame/staking/src/benchmarking.rs | 4 ++-- frame/staking/src/tests.rs | 6 +++--- frame/support/src/traits.rs | 2 +- frame/support/src/traits/dispatch.rs | 12 ++++++++++++ frame/tips/src/tests.rs | 2 +- 8 files changed, 28 insertions(+), 14 deletions(-) diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 8d7ca18646aa5..230ed28e36497 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -773,12 +773,14 @@ impl pallet_referenda::TracksInfo for TracksInfo { confirm_period: 2, min_enactment_period: 4, min_approval: pallet_referenda::Curve::LinearDecreasing { - begin: Perbill::from_percent(100), - delta: Perbill::from_percent(50), + length: Perbill::from_percent(100), + floor: Perbill::from_percent(50), + ceil: Perbill::from_percent(100), }, min_support: pallet_referenda::Curve::LinearDecreasing { - begin: Perbill::from_percent(100), - delta: Perbill::from_percent(100), + length: Perbill::from_percent(100), + floor: Perbill::from_percent(0), + ceil: Perbill::from_percent(100), }, }, )]; @@ -1004,7 +1006,7 @@ impl pallet_treasury::Config for Runtime { type SpendFunds = Bounties; type WeightInfo = pallet_treasury::weights::SubstrateWeight; type MaxApprovals = MaxApprovals; - type SpendOrigin = (); + type SpendOrigin = frame_support::traits::NeverEnsureOrigin; } parameter_types! { diff --git a/frame/bounties/src/tests.rs b/frame/bounties/src/tests.rs index 534dec6c881dd..0904e3a2901bb 100644 --- a/frame/bounties/src/tests.rs +++ b/frame/bounties/src/tests.rs @@ -125,7 +125,7 @@ impl pallet_treasury::Config for Test { type WeightInfo = (); type SpendFunds = Bounties; type MaxApprovals = ConstU32<100>; - type SpendOrigin = (); + type SpendOrigin = frame_support::traits::NeverEnsureOrigin; } parameter_types! { diff --git a/frame/child-bounties/src/tests.rs b/frame/child-bounties/src/tests.rs index ee7c89f66a0e2..2584445071471 100644 --- a/frame/child-bounties/src/tests.rs +++ b/frame/child-bounties/src/tests.rs @@ -130,7 +130,7 @@ impl pallet_treasury::Config for Test { type WeightInfo = (); type SpendFunds = Bounties; type MaxApprovals = ConstU32<100>; - type SpendOrigin = (); + type SpendOrigin = frame_support::traits::NeverEnsureOrigin; } parameter_types! { // This will be 50% of the bounty fee. diff --git a/frame/staking/src/benchmarking.rs b/frame/staking/src/benchmarking.rs index eb9129ac4b436..12de0ff9cc665 100644 --- a/frame/staking/src/benchmarking.rs +++ b/frame/staking/src/benchmarking.rs @@ -990,7 +990,7 @@ mod tests { let (validator_stash, nominators) = create_validator_with_nominators::( n, - ::MaxNominatorRewardedPerValidator::get(), + <::MaxNominatorRewardedPerValidator as Get<_>>::get(), false, RewardDestination::Staked, ) @@ -1015,7 +1015,7 @@ mod tests { let (validator_stash, _nominators) = create_validator_with_nominators::( n, - ::MaxNominatorRewardedPerValidator::get(), + <::MaxNominatorRewardedPerValidator as Get<_>>::get(), false, RewardDestination::Staked, ) diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index 21d4714985c6b..8d768a9dc6dc7 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -3314,7 +3314,7 @@ fn six_session_delay() { #[test] fn test_max_nominator_rewarded_per_validator_and_cant_steal_someone_else_reward() { ExtBuilder::default().build_and_execute(|| { - for i in 0..=::MaxNominatorRewardedPerValidator::get() { + for i in 0..=<::MaxNominatorRewardedPerValidator as Get<_>>::get() { let stash = 10_000 + i as AccountId; let controller = 20_000 + i as AccountId; let balance = 10_000 + i as Balance; @@ -3337,7 +3337,7 @@ fn test_max_nominator_rewarded_per_validator_and_cant_steal_someone_else_reward( mock::make_all_reward_payment(1); // Assert only nominators from 1 to Max are rewarded - for i in 0..=::MaxNominatorRewardedPerValidator::get() { + for i in 0..=<::MaxNominatorRewardedPerValidator as Get<_>>::get() { let stash = 10_000 + i as AccountId; let balance = 10_000 + i as Balance; if stash == 10_000 { @@ -3575,7 +3575,7 @@ fn payout_stakers_handles_weight_refund() { // Note: this test relies on the assumption that `payout_stakers_alive_staked` is solely used by // `payout_stakers` to calculate the weight of each payout op. ExtBuilder::default().has_stakers(false).build_and_execute(|| { - let max_nom_rewarded = ::MaxNominatorRewardedPerValidator::get(); + let max_nom_rewarded = <::MaxNominatorRewardedPerValidator as Get<_>>::get(); // Make sure the configured value is meaningful for our use. assert!(max_nom_rewarded >= 4); let half_max_nom_rewarded = max_nom_rewarded / 2; diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 7c0a26d90aa59..cef6e69c06317 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -97,7 +97,7 @@ mod dispatch; pub use dispatch::EnsureOneOf; pub use dispatch::{ AsEnsureOriginWithArg, EitherOf, EitherOfDiverse, EnsureOrigin, EnsureOriginWithArg, - OriginTrait, UnfilteredDispatchable, + OriginTrait, UnfilteredDispatchable, NeverEnsureOrigin, }; mod voting; diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index b59744e1c715e..51a217b53a4a8 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -43,6 +43,18 @@ pub trait EnsureOrigin { fn successful_origin() -> OuterOrigin; } +pub struct NeverEnsureOrigin(sp_std::marker::PhantomData); +impl EnsureOrigin for NeverEnsureOrigin { + type Success = Success; + fn try_origin(o: OO) -> Result { + Err(o) + } + #[cfg(feature = "runtime-benchmarks")] + fn successful_origin() -> OO { + panic!("No `successful_origin` possible for `NeverEnsureOrigin`") + } +} + /// Some sort of check on the origin is performed by this object. pub trait EnsureOriginWithArg { /// A return type. diff --git a/frame/tips/src/tests.rs b/frame/tips/src/tests.rs index 85896515ea6ef..2a26fdf6ed3a3 100644 --- a/frame/tips/src/tests.rs +++ b/frame/tips/src/tests.rs @@ -144,7 +144,7 @@ impl pallet_treasury::Config for Test { type WeightInfo = (); type SpendFunds = (); type MaxApprovals = ConstU32<100>; - type SpendOrigin = (); + type SpendOrigin = frame_support::traits::NeverEnsureOrigin; } parameter_types! { pub const TipFindersFee: Percent = Percent::from_percent(20); From a091e9d94fec85063e7ca63dcd5d4e3e09d7ab74 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 12 May 2022 13:13:28 +0100 Subject: [PATCH 25/48] Formatting --- frame/staking/src/tests.rs | 3 ++- frame/support/src/traits.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index 8d768a9dc6dc7..0006f8dcec413 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -3575,7 +3575,8 @@ fn payout_stakers_handles_weight_refund() { // Note: this test relies on the assumption that `payout_stakers_alive_staked` is solely used by // `payout_stakers` to calculate the weight of each payout op. ExtBuilder::default().has_stakers(false).build_and_execute(|| { - let max_nom_rewarded = <::MaxNominatorRewardedPerValidator as Get<_>>::get(); + let max_nom_rewarded = + <::MaxNominatorRewardedPerValidator as Get<_>>::get(); // Make sure the configured value is meaningful for our use. assert!(max_nom_rewarded >= 4); let half_max_nom_rewarded = max_nom_rewarded / 2; diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index cef6e69c06317..a94c7bc3600be 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -97,7 +97,7 @@ mod dispatch; pub use dispatch::EnsureOneOf; pub use dispatch::{ AsEnsureOriginWithArg, EitherOf, EitherOfDiverse, EnsureOrigin, EnsureOriginWithArg, - OriginTrait, UnfilteredDispatchable, NeverEnsureOrigin, + NeverEnsureOrigin, OriginTrait, UnfilteredDispatchable, }; mod voting; From 73bbf7ce11b03078ae0c94631545cc254000d74c Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 12 May 2022 14:08:19 +0100 Subject: [PATCH 26/48] Fixes --- frame/conviction-voting/src/tests.rs | 50 ++++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/frame/conviction-voting/src/tests.rs b/frame/conviction-voting/src/tests.rs index 6a8bad5d8944e..6e9b76e3d3c0b 100644 --- a/frame/conviction-voting/src/tests.rs +++ b/frame/conviction-voting/src/tests.rs @@ -271,19 +271,19 @@ fn basic_voting_works() { assert_ok!(Voting::vote(Origin::signed(1), 3, aye(2, 5))); assert_eq!(tally(3), Tally::from_parts(10, 0, 2)); assert_ok!(Voting::vote(Origin::signed(1), 3, nay(2, 5))); - assert_eq!(tally(3), Tally::from_parts(0, 10, 2)); + assert_eq!(tally(3), Tally::from_parts(0, 10, 0)); assert_eq!(Balances::usable_balance(1), 8); assert_ok!(Voting::vote(Origin::signed(1), 3, aye(5, 1))); assert_eq!(tally(3), Tally::from_parts(5, 0, 5)); assert_ok!(Voting::vote(Origin::signed(1), 3, nay(5, 1))); - assert_eq!(tally(3), Tally::from_parts(0, 5, 5)); + assert_eq!(tally(3), Tally::from_parts(0, 5, 0)); assert_eq!(Balances::usable_balance(1), 5); assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0))); assert_eq!(tally(3), Tally::from_parts(1, 0, 10)); assert_ok!(Voting::vote(Origin::signed(1), 3, nay(10, 0))); - assert_eq!(tally(3), Tally::from_parts(0, 1, 10)); + assert_eq!(tally(3), Tally::from_parts(0, 1, 0)); assert_eq!(Balances::usable_balance(1), 0); assert_ok!(Voting::remove_vote(Origin::signed(1), None, 3)); @@ -300,19 +300,19 @@ fn voting_balance_gets_locked() { assert_ok!(Voting::vote(Origin::signed(1), 3, aye(2, 5))); assert_eq!(tally(3), Tally::from_parts(10, 0, 2)); assert_ok!(Voting::vote(Origin::signed(1), 3, nay(2, 5))); - assert_eq!(tally(3), Tally::from_parts(0, 10, 2)); + assert_eq!(tally(3), Tally::from_parts(0, 10, 0)); assert_eq!(Balances::usable_balance(1), 8); assert_ok!(Voting::vote(Origin::signed(1), 3, aye(5, 1))); assert_eq!(tally(3), Tally::from_parts(5, 0, 5)); assert_ok!(Voting::vote(Origin::signed(1), 3, nay(5, 1))); - assert_eq!(tally(3), Tally::from_parts(0, 5, 5)); + assert_eq!(tally(3), Tally::from_parts(0, 5, 0)); assert_eq!(Balances::usable_balance(1), 5); assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0))); assert_eq!(tally(3), Tally::from_parts(1, 0, 10)); assert_ok!(Voting::vote(Origin::signed(1), 3, nay(10, 0))); - assert_eq!(tally(3), Tally::from_parts(0, 1, 10)); + assert_eq!(tally(3), Tally::from_parts(0, 1, 0)); assert_eq!(Balances::usable_balance(1), 0); assert_ok!(Voting::remove_vote(Origin::signed(1), None, 3)); @@ -403,9 +403,9 @@ fn classwise_delegation_works() { assert_eq!( Polls::get(), vec![ - (0, Ongoing(Tally::from_parts(6, 2, 35), 0)), - (1, Ongoing(Tally::from_parts(6, 2, 35), 1)), - (2, Ongoing(Tally::from_parts(6, 2, 35), 2)), + (0, Ongoing(Tally::from_parts(6, 2, 15), 0)), + (1, Ongoing(Tally::from_parts(6, 2, 15), 1)), + (2, Ongoing(Tally::from_parts(6, 2, 15), 2)), (3, Ongoing(Tally::from_parts(0, 0, 0), 2)), ] .into_iter() @@ -417,10 +417,10 @@ fn classwise_delegation_works() { assert_eq!( Polls::get(), vec![ - (0, Ongoing(Tally::from_parts(6, 2, 35), 0)), - (1, Ongoing(Tally::from_parts(6, 2, 35), 1)), - (2, Ongoing(Tally::from_parts(6, 2, 35), 2)), - (3, Ongoing(Tally::from_parts(0, 6, 15), 2)), + (0, Ongoing(Tally::from_parts(6, 2, 15), 0)), + (1, Ongoing(Tally::from_parts(6, 2, 15), 1)), + (2, Ongoing(Tally::from_parts(6, 2, 15), 2)), + (3, Ongoing(Tally::from_parts(0, 6, 0), 2)), ] .into_iter() .collect() @@ -432,10 +432,10 @@ fn classwise_delegation_works() { assert_eq!( Polls::get(), vec![ - (0, Ongoing(Tally::from_parts(6, 2, 35), 0)), - (1, Ongoing(Tally::from_parts(6, 2, 35), 1)), - (2, Ongoing(Tally::from_parts(1, 7, 35), 2)), - (3, Ongoing(Tally::from_parts(0, 1, 10), 2)), + (0, Ongoing(Tally::from_parts(6, 2, 15), 0)), + (1, Ongoing(Tally::from_parts(6, 2, 15), 1)), + (2, Ongoing(Tally::from_parts(1, 7, 10), 2)), + (3, Ongoing(Tally::from_parts(0, 1, 0), 2)), ] .into_iter() .collect() @@ -451,10 +451,10 @@ fn classwise_delegation_works() { assert_eq!( Polls::get(), vec![ - (0, Ongoing(Tally::from_parts(4, 2, 33), 0)), - (1, Ongoing(Tally::from_parts(4, 2, 33), 1)), - (2, Ongoing(Tally::from_parts(4, 2, 33), 2)), - (3, Ongoing(Tally::from_parts(0, 4, 13), 2)), + (0, Ongoing(Tally::from_parts(4, 2, 13), 0)), + (1, Ongoing(Tally::from_parts(4, 2, 13), 1)), + (2, Ongoing(Tally::from_parts(4, 2, 13), 2)), + (3, Ongoing(Tally::from_parts(0, 4, 0), 2)), ] .into_iter() .collect() @@ -483,10 +483,10 @@ fn classwise_delegation_works() { assert_eq!( Polls::get(), vec![ - (0, Ongoing(Tally::from_parts(7, 2, 36), 0)), - (1, Ongoing(Tally::from_parts(8, 2, 37), 1)), - (2, Ongoing(Tally::from_parts(9, 2, 38), 2)), - (3, Ongoing(Tally::from_parts(0, 9, 18), 2)), + (0, Ongoing(Tally::from_parts(7, 2, 16), 0)), + (1, Ongoing(Tally::from_parts(8, 2, 17), 1)), + (2, Ongoing(Tally::from_parts(9, 2, 18), 2)), + (3, Ongoing(Tally::from_parts(0, 9, 0), 2)), ] .into_iter() .collect() From 8019e332d2b5691cd5b066b202e7478eab9dacf0 Mon Sep 17 00:00:00 2001 From: Gavin Wood Date: Wed, 18 May 2022 15:03:46 +0200 Subject: [PATCH 27/48] Update frame/support/src/traits/dispatch.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> --- frame/support/src/traits/dispatch.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index 51a217b53a4a8..ce038d8839ae8 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -43,6 +43,7 @@ pub trait EnsureOrigin { fn successful_origin() -> OuterOrigin; } +/// `EnsureOrigin` implementation that always fails. pub struct NeverEnsureOrigin(sp_std::marker::PhantomData); impl EnsureOrigin for NeverEnsureOrigin { type Success = Success; From 7596264f2afa2751460da12a85b95ff37401fe37 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 15:04:24 +0200 Subject: [PATCH 28/48] Grumbles --- Cargo.lock | 17 +++++++++++++++++ Cargo.toml | 1 + frame/treasury/src/tests.rs | 1 - 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4802d67fed48e..b2656b319e1d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6026,6 +6026,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-ranked-collective" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-recovery" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 41739fe6f1ebc..74e7aae7949c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,7 @@ members = [ "frame/nomination-pools", "frame/nomination-pools/benchmarking", "frame/randomness-collective-flip", + "frame/ranked-collective", "frame/recovery", "frame/referenda", "frame/remark", diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index d0386c3efd81d..a21296d1b39ec 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -163,7 +163,6 @@ fn genesis_config_works() { #[test] fn spend_origin_permissioning_works() { new_test_ext().execute_with(|| { - // Check that accumulate works when we have Some value in Dummy already. assert_noop!(Treasury::spend(Origin::signed(1), 1, 1), BadOrigin); assert_noop!( Treasury::spend(Origin::signed(10), 6, 1), From 40ca11e0ce989f3afafa487015ef81a8fecbbee7 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 15:42:30 +0200 Subject: [PATCH 29/48] Formatting --- frame/support/src/lib.rs | 2 +- frame/support/src/traits/dispatch.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index 58655e19bfc3d..a506706aa947c 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -1338,7 +1338,7 @@ pub mod pallet_prelude { }, traits::{ ConstU32, EnsureOrigin, Get, GetDefault, GetStorageVersion, Hooks, IsType, - PalletInfoAccess, StorageInfoTrait, TypedGet, StorageVersion, + PalletInfoAccess, StorageInfoTrait, StorageVersion, TypedGet, }, weights::{DispatchClass, Pays, Weight}, Blake2_128, Blake2_128Concat, Blake2_256, CloneNoBound, DebugNoBound, EqNoBound, Identity, diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index ce038d8839ae8..255fa83ca7d67 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -43,7 +43,7 @@ pub trait EnsureOrigin { fn successful_origin() -> OuterOrigin; } -/// `EnsureOrigin` implementation that always fails. +/// `EnsureOrigin` implementation that always fails. pub struct NeverEnsureOrigin(sp_std::marker::PhantomData); impl EnsureOrigin for NeverEnsureOrigin { type Success = Success; From 18c6438daff149e7f847797f51f14cbe74839fc3 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 15:52:57 +0200 Subject: [PATCH 30/48] Fixes --- Cargo.lock | 17 ----------------- Cargo.toml | 1 - 2 files changed, 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbd14bfc9d798..d7bfbc2d3b40a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5980,23 +5980,6 @@ dependencies = [ "sp-std", ] -[[package]] -name = "pallet-ranked-collective" -version = "4.0.0-dev" -dependencies = [ - "frame-benchmarking", - "frame-support", - "frame-system", - "log", - "parity-scale-codec", - "scale-info", - "sp-arithmetic", - "sp-core", - "sp-io", - "sp-runtime", - "sp-std", -] - [[package]] name = "pallet-recovery" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 74e7aae7949c5..41739fe6f1ebc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,7 +115,6 @@ members = [ "frame/nomination-pools", "frame/nomination-pools/benchmarking", "frame/randomness-collective-flip", - "frame/ranked-collective", "frame/recovery", "frame/referenda", "frame/remark", From d04ebd0ac1f94d1fbe2e35e50ba773e775a5e520 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 12:18:52 +0100 Subject: [PATCH 31/48] APIs of VoteTally should include class --- frame/conviction-voting/src/tests.rs | 40 ++++++++++++++-------------- frame/conviction-voting/src/types.rs | 36 ++++++++++++------------- frame/referenda/src/benchmarking.rs | 12 ++++----- frame/referenda/src/lib.rs | 22 ++++++++------- frame/referenda/src/mock.rs | 20 ++++++++------ frame/support/src/traits/voting.rs | 16 +++++------ 6 files changed, 77 insertions(+), 69 deletions(-) diff --git a/frame/conviction-voting/src/tests.rs b/frame/conviction-voting/src/tests.rs index 6e9b76e3d3c0b..9eb7f679efca3 100644 --- a/frame/conviction-voting/src/tests.rs +++ b/frame/conviction-voting/src/tests.rs @@ -21,7 +21,7 @@ use std::collections::BTreeMap; use frame_support::{ assert_noop, assert_ok, parameter_types, - traits::{ConstU32, ConstU64, Contains, Polling}, + traits::{ConstU32, ConstU64, Contains, Polling, VoteTally}, }; use sp_core::H256; use sp_runtime::{ @@ -166,7 +166,7 @@ impl Polling> for TestPolls { fn create_ongoing(class: Self::Class) -> Result { let mut polls = Polls::get(); let i = polls.keys().rev().next().map_or(0, |x| x + 1); - polls.insert(i, Ongoing(Tally::default(), class)); + polls.insert(i, Ongoing(Tally::new(0), class)); Polls::set(polls); Ok(i) } @@ -376,10 +376,10 @@ fn classwise_delegation_works() { new_test_ext().execute_with(|| { Polls::set( vec![ - (0, Ongoing(Tally::default(), 0)), - (1, Ongoing(Tally::default(), 1)), - (2, Ongoing(Tally::default(), 2)), - (3, Ongoing(Tally::default(), 2)), + (0, Ongoing(Tally::new(0), 0)), + (1, Ongoing(Tally::new(0), 1)), + (2, Ongoing(Tally::new(0), 2)), + (3, Ongoing(Tally::new(0), 2)), ] .into_iter() .collect(), @@ -497,7 +497,7 @@ fn classwise_delegation_works() { #[test] fn redelegation_after_vote_ending_should_keep_lock() { new_test_ext().execute_with(|| { - Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect()); + Polls::set(vec![(0, Ongoing(Tally::new(0), 0))].into_iter().collect()); assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5)); assert_ok!(Voting::vote(Origin::signed(2), 0, aye(10, 1))); Polls::set(vec![(0, Completed(1, true))].into_iter().collect()); @@ -515,9 +515,9 @@ fn lock_amalgamation_valid_with_multiple_removed_votes() { new_test_ext().execute_with(|| { Polls::set( vec![ - (0, Ongoing(Tally::default(), 0)), - (1, Ongoing(Tally::default(), 0)), - (2, Ongoing(Tally::default(), 0)), + (0, Ongoing(Tally::new(0), 0)), + (1, Ongoing(Tally::new(0), 0)), + (2, Ongoing(Tally::new(0), 0)), ] .into_iter() .collect(), @@ -587,7 +587,7 @@ fn lock_amalgamation_valid_with_multiple_delegations() { #[test] fn lock_amalgamation_valid_with_move_roundtrip_to_delegation() { new_test_ext().execute_with(|| { - Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect()); + Polls::set(vec![(0, Ongoing(Tally::new(0), 0))].into_iter().collect()); assert_ok!(Voting::vote(Origin::signed(1), 0, aye(5, 1))); Polls::set(vec![(0, Completed(1, true))].into_iter().collect()); assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0)); @@ -599,7 +599,7 @@ fn lock_amalgamation_valid_with_move_roundtrip_to_delegation() { assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); assert_eq!(Balances::usable_balance(1), 0); - Polls::set(vec![(1, Ongoing(Tally::default(), 0))].into_iter().collect()); + Polls::set(vec![(1, Ongoing(Tally::new(0), 0))].into_iter().collect()); assert_ok!(Voting::vote(Origin::signed(1), 1, aye(5, 2))); Polls::set(vec![(1, Completed(1, true))].into_iter().collect()); assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 1)); @@ -627,7 +627,7 @@ fn lock_amalgamation_valid_with_move_roundtrip_to_casting() { assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); assert_eq!(Balances::usable_balance(1), 5); - Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect()); + Polls::set(vec![(0, Ongoing(Tally::new(0), 0))].into_iter().collect()); assert_ok!(Voting::vote(Origin::signed(1), 0, aye(10, 1))); Polls::set(vec![(0, Completed(1, true))].into_iter().collect()); assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0)); @@ -688,9 +688,9 @@ fn lock_aggregation_over_different_classes_with_casting_works() { new_test_ext().execute_with(|| { Polls::set( vec![ - (0, Ongoing(Tally::default(), 0)), - (1, Ongoing(Tally::default(), 1)), - (2, Ongoing(Tally::default(), 2)), + (0, Ongoing(Tally::new(0), 0)), + (1, Ongoing(Tally::new(0), 1)), + (2, Ongoing(Tally::new(0), 2)), ] .into_iter() .collect(), @@ -747,10 +747,10 @@ fn errors_with_vote_work() { assert_ok!(Voting::undelegate(Origin::signed(1), 0)); Polls::set( vec![ - (0, Ongoing(Tally::default(), 0)), - (1, Ongoing(Tally::default(), 0)), - (2, Ongoing(Tally::default(), 0)), - (3, Ongoing(Tally::default(), 0)), + (0, Ongoing(Tally::new(0), 0)), + (1, Ongoing(Tally::new(0), 0)), + (2, Ongoing(Tally::new(0), 0)), + (3, Ongoing(Tally::new(0), 0)), ] .into_iter() .collect(), diff --git a/frame/conviction-voting/src/types.rs b/frame/conviction-voting/src/types.rs index 8f5afc8505c91..5ed059ccd65f2 100644 --- a/frame/conviction-voting/src/types.rs +++ b/frame/conviction-voting/src/types.rs @@ -17,11 +17,10 @@ //! Miscellaneous additional datatypes. -use sp_std::marker::PhantomData; - +use sp_std::{marker::PhantomData, fmt::Debug}; use codec::{Codec, Decode, Encode, MaxEncodedLen}; use frame_support::{ - traits::VoteTally, CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, + traits::VoteTally, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use scale_info::TypeInfo; @@ -36,7 +35,6 @@ use crate::{AccountVote, Conviction, Vote}; /// Info regarding an ongoing referendum. #[derive( CloneNoBound, - DefaultNoBound, PartialEqNoBound, EqNoBound, RuntimeDebugNoBound, @@ -46,10 +44,7 @@ use crate::{AccountVote, Conviction, Vote}; MaxEncodedLen, )] #[scale_info(skip_type_params(Total))] -pub struct Tally< - Votes: Clone + Default + PartialEq + Eq + sp_std::fmt::Debug + TypeInfo + Codec, - Total, -> { +pub struct Tally { /// The number of aye votes, expressed in terms of post-conviction lock-vote. pub ayes: Votes, /// The number of nay votes, expressed in terms of post-conviction lock-vote. @@ -65,38 +60,43 @@ impl< + Default + PartialEq + Eq - + sp_std::fmt::Debug + + Debug + Copy + AtLeast32BitUnsigned + TypeInfo + Codec, Total: Get, - > VoteTally for Tally + Class, + > VoteTally for Tally { - fn ayes(&self) -> Votes { + fn new(_: Class) -> Self { + Self { ayes: Zero::zero(), nays: Zero::zero(), support: Zero::zero(), dummy: PhantomData } + } + + fn ayes(&self, _: Class) -> Votes { self.ayes } - fn support(&self) -> Perbill { + fn support(&self, _: Class) -> Perbill { Perbill::from_rational(self.support, Total::get()) } - fn approval(&self) -> Perbill { + fn approval(&self, _: Class) -> Perbill { Perbill::from_rational(self.ayes, self.ayes.saturating_add(self.nays)) } #[cfg(feature = "runtime-benchmarks")] - fn unanimity() -> Self { + fn unanimity(_: Class) -> Self { Self { ayes: Total::get(), nays: Zero::zero(), support: Total::get(), dummy: PhantomData } } #[cfg(feature = "runtime-benchmarks")] - fn rejection() -> Self { + fn rejection(_: Class) -> Self { Self { ayes: Zero::zero(), nays: Total::get(), support: Total::get(), dummy: PhantomData } } #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(support: Perbill, approval: Perbill) -> Self { + fn from_requirements(support: Perbill, approval: Perbill, _: Class) -> Self { let support = support.mul_ceil(Total::get()); let ayes = approval.mul_ceil(support); Self { ayes, nays: support - ayes, support, dummy: PhantomData } @@ -108,7 +108,7 @@ impl< + Default + PartialEq + Eq - + sp_std::fmt::Debug + + Debug + Copy + AtLeast32BitUnsigned + TypeInfo @@ -117,7 +117,7 @@ impl< > Tally { /// Create a new tally. - pub fn new(vote: Vote, balance: Votes) -> Self { + pub fn from_vote(vote: Vote, balance: Votes) -> Self { let Delegations { votes, capital } = vote.conviction.votes(balance); Self { ayes: if vote.aye { votes } else { Zero::zero() }, diff --git a/frame/referenda/src/benchmarking.rs b/frame/referenda/src/benchmarking.rs index 3a9bbddf57c58..e4cbf2ca52e7c 100644 --- a/frame/referenda/src/benchmarking.rs +++ b/frame/referenda/src/benchmarking.rs @@ -104,24 +104,24 @@ fn make_passing_after(index: ReferendumIndex, period_portion: Perbill let support = info::(index).min_support.threshold(period_portion); let approval = info::(index).min_approval.threshold(period_portion); Referenda::::access_poll(index, |status| { - if let PollStatus::Ongoing(tally, ..) = status { - *tally = T::Tally::from_requirements(support, approval); + if let PollStatus::Ongoing(tally, class) = status { + *tally = T::Tally::from_requirements(support, approval, class); } }); } fn make_passing(index: ReferendumIndex) { Referenda::::access_poll(index, |status| { - if let PollStatus::Ongoing(tally, ..) = status { - *tally = T::Tally::unanimity(); + if let PollStatus::Ongoing(tally, class) = status { + *tally = T::Tally::unanimity(class); } }); } fn make_failing(index: ReferendumIndex) { Referenda::::access_poll(index, |status| { - if let PollStatus::Ongoing(tally, ..) = status { - *tally = T::Tally::rejection(); + if let PollStatus::Ongoing(tally, class) = status { + *tally = T::Tally::rejection(class); } }); } diff --git a/frame/referenda/src/lib.rs b/frame/referenda/src/lib.rs index 28ffcce8dccc3..7fa7035b55572 100644 --- a/frame/referenda/src/lib.rs +++ b/frame/referenda/src/lib.rs @@ -152,7 +152,7 @@ pub mod pallet { /// The counting type for votes. Usually just balance. type Votes: AtLeast32BitUnsigned + Copy + Parameter + Member; /// The tallying type. - type Tally: VoteTally + Default + Clone + Codec + Eq + Debug + TypeInfo; + type Tally: VoteTally> + Clone + Codec + Eq + Debug + TypeInfo; // Constants /// The minimum amount to be used as a deposit for a public referendum proposal. @@ -373,7 +373,7 @@ pub mod pallet { submission_deposit, decision_deposit: None, deciding: None, - tally: Default::default(), + tally: TallyOf::::new(track), in_queue: false, alarm: Self::set_alarm(nudge_call, now.saturating_add(T::UndecidingTimeout::get())), }; @@ -729,6 +729,7 @@ impl, I: 'static> Pallet { track.decision_period, &track.min_support, &track.min_approval, + status.track, ); status.in_queue = false; Self::deposit_event(Event::::DecisionStarted { @@ -744,7 +745,7 @@ impl, I: 'static> Pallet { None }; let deciding_status = DecidingStatus { since: now, confirming }; - let alarm = Self::decision_time(&deciding_status, &status.tally, track); + let alarm = Self::decision_time(&deciding_status, &status.tally, status.track, track); status.deciding = Some(deciding_status); let branch = if is_passing { BeginDecidingBranch::Passing } else { BeginDecidingBranch::Failing }; @@ -769,7 +770,7 @@ impl, I: 'static> Pallet { (r.0, r.1.into()) } else { // Add to queue. - let item = (index, status.tally.ayes()); + let item = (index, status.tally.ayes(status.track)); status.in_queue = true; TrackQueue::::mutate(status.track, |q| q.insert_sorted_by_key(item, |x| x.1)); (None, ServiceBranch::Queued) @@ -876,7 +877,7 @@ impl, I: 'static> Pallet { // Are we already queued for deciding? if status.in_queue { // Does our position in the queue need updating? - let ayes = status.tally.ayes(); + let ayes = status.tally.ayes(status.track); let mut queue = TrackQueue::::get(status.track); let maybe_old_pos = queue.iter().position(|(x, _)| *x == index); let new_pos = queue.binary_search_by_key(&ayes, |x| x.1).unwrap_or_else(|x| x); @@ -936,6 +937,7 @@ impl, I: 'static> Pallet { track.decision_period, &track.min_support, &track.min_approval, + status.track, ); branch = if is_passing { match deciding.confirming { @@ -1000,7 +1002,7 @@ impl, I: 'static> Pallet { ServiceBranch::ContinueNotConfirming } }; - alarm = Self::decision_time(deciding, &status.tally, track); + alarm = Self::decision_time(deciding, &status.tally, status.track, track); }, } @@ -1013,12 +1015,13 @@ impl, I: 'static> Pallet { fn decision_time( deciding: &DecidingStatusOf, tally: &T::Tally, + track_id: TrackIdOf, track: &TrackInfoOf, ) -> T::BlockNumber { deciding.confirming.unwrap_or_else(|| { // Set alarm to the point where the current voting would make it pass. - let approval = tally.approval(); - let support = tally.support(); + let approval = tally.approval(track_id); + let support = tally.support(track_id); let until_approval = track.min_approval.delay(approval); let until_support = track.min_support.delay(support); let offset = until_support.max(until_approval); @@ -1074,8 +1077,9 @@ impl, I: 'static> Pallet { period: T::BlockNumber, support_needed: &Curve, approval_needed: &Curve, + id: TrackIdOf, ) -> bool { let x = Perbill::from_rational(elapsed.min(period), period); - support_needed.passing(x, tally.support()) && approval_needed.passing(x, tally.approval()) + support_needed.passing(x, tally.support(id)) && approval_needed.passing(x, tally.approval(id)) } } diff --git a/frame/referenda/src/mock.rs b/frame/referenda/src/mock.rs index d4676b4e9c3cc..a3026ce78e986 100644 --- a/frame/referenda/src/mock.rs +++ b/frame/referenda/src/mock.rs @@ -245,22 +245,26 @@ pub fn new_test_ext_execute_with_cond(execute: impl FnOnce(bool) -> () + Clone) new_test_ext().execute_with(|| execute(true)); } -#[derive(Encode, Debug, Decode, TypeInfo, Eq, PartialEq, Clone, Default, MaxEncodedLen)] +#[derive(Encode, Debug, Decode, TypeInfo, Eq, PartialEq, Clone, MaxEncodedLen)] pub struct Tally { pub ayes: u32, pub nays: u32, } -impl VoteTally for Tally { - fn ayes(&self) -> u32 { +impl VoteTally for Tally { + fn new(_: Class) -> Self { + Self { ayes: 0, nays: 0 } + } + + fn ayes(&self, _: Class) -> u32 { self.ayes } - fn support(&self) -> Perbill { + fn support(&self, _: Class) -> Perbill { Perbill::from_percent(self.ayes) } - fn approval(&self) -> Perbill { + fn approval(&self, _: Class) -> Perbill { if self.ayes + self.nays > 0 { Perbill::from_rational(self.ayes, self.ayes + self.nays) } else { @@ -269,17 +273,17 @@ impl VoteTally for Tally { } #[cfg(feature = "runtime-benchmarks")] - fn unanimity() -> Self { + fn unanimity(_: Class) -> Self { Self { ayes: 100, nays: 0 } } #[cfg(feature = "runtime-benchmarks")] - fn rejection() -> Self { + fn rejection(_: Class) -> Self { Self { ayes: 0, nays: 100 } } #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(support: Perbill, approval: Perbill) -> Self { + fn from_requirements(support: Perbill, approval: Perbill, _: Class) -> Self { let ayes = support.mul_ceil(100u32); let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes; Self { ayes, nays } diff --git a/frame/support/src/traits/voting.rs b/frame/support/src/traits/voting.rs index 293170cc09cdd..6c802a6112246 100644 --- a/frame/support/src/traits/voting.rs +++ b/frame/support/src/traits/voting.rs @@ -95,18 +95,18 @@ impl + UniqueSaturatedFrom> CurrencyToVote } } -pub trait VoteTally { - fn ayes(&self) -> Votes; - fn support(&self) -> Perbill; - fn approval(&self) -> Perbill; +pub trait VoteTally { + fn new(_: Class) -> Self; + fn ayes(&self, class: Class) -> Votes; + fn support(&self, class: Class) -> Perbill; + fn approval(&self, class: Class) -> Perbill; #[cfg(feature = "runtime-benchmarks")] - fn unanimity() -> Self; + fn unanimity(class: Class) -> Self; #[cfg(feature = "runtime-benchmarks")] - fn rejection() -> Self; + fn rejection(class: Class) -> Self; #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(support: Perbill, approval: Perbill) -> Self; + fn from_requirements(support: Perbill, approval: Perbill, class: Class) -> Self; } - pub enum PollStatus { None, Ongoing(Tally, Class), From 91b10940efb20ac5de89888ae3afe5422298f9bb Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 14:10:39 +0100 Subject: [PATCH 32/48] Fixes --- frame/referenda/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/referenda/src/lib.rs b/frame/referenda/src/lib.rs index 7fa7035b55572..549b9d7e4384c 100644 --- a/frame/referenda/src/lib.rs +++ b/frame/referenda/src/lib.rs @@ -617,7 +617,7 @@ impl, I: 'static> Polling for Pallet { submission_deposit: Deposit { who: dummy_account_id, amount: Zero::zero() }, decision_deposit: None, deciding: None, - tally: Default::default(), + tally: TallyOf::::new(class), in_queue: false, alarm: None, }; From 471b43a2792666268b3a0fe102ea80eb633518aa Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 17:30:57 +0100 Subject: [PATCH 33/48] Fix overlay prefix removal result --- primitives/state-machine/src/ext.rs | 5 +++-- .../state-machine/src/overlayed_changes/changeset.rs | 7 +++++-- primitives/state-machine/src/overlayed_changes/mod.rs | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/primitives/state-machine/src/ext.rs b/primitives/state-machine/src/ext.rs index e33569e2a1f67..2d446b9c3b9a0 100644 --- a/primitives/state-machine/src/ext.rs +++ b/primitives/state-machine/src/ext.rs @@ -464,8 +464,9 @@ where } self.mark_dirty(); - self.overlay.clear_prefix(prefix); - self.limit_remove_from_backend(None, Some(prefix), limit) + let overlay_count = self.overlay.clear_prefix(prefix); + let (all, count) = self.limit_remove_from_backend(None, Some(prefix), limit); + (all, count + overlay_count) } fn clear_child_prefix( diff --git a/primitives/state-machine/src/overlayed_changes/changeset.rs b/primitives/state-machine/src/overlayed_changes/changeset.rs index 9e7f6ffeddfd7..1d9eb0dea2b1b 100644 --- a/primitives/state-machine/src/overlayed_changes/changeset.rs +++ b/primitives/state-machine/src/overlayed_changes/changeset.rs @@ -410,10 +410,13 @@ impl OverlayedChangeSet { &mut self, predicate: impl Fn(&[u8], &OverlayedValue) -> bool, at_extrinsic: Option, - ) { + ) -> u32 { + let mut count = 0; for (key, val) in self.changes.iter_mut().filter(|(k, v)| predicate(k, v)) { - val.set(None, insert_dirty(&mut self.dirty_keys, key.clone()), at_extrinsic); + val.set(None, insert_dirty(&mut self.dirty_keys, key.clone()), at_extrinsic); + count += 1; } + count } /// Get the iterator over all changes that follow the supplied `key`. diff --git a/primitives/state-machine/src/overlayed_changes/mod.rs b/primitives/state-machine/src/overlayed_changes/mod.rs index 2161b343711c9..2bb09e98bff62 100644 --- a/primitives/state-machine/src/overlayed_changes/mod.rs +++ b/primitives/state-machine/src/overlayed_changes/mod.rs @@ -315,8 +315,8 @@ impl OverlayedChanges { /// Removes all key-value pairs which keys share the given prefix. /// /// Can be rolled back or committed when called inside a transaction. - pub(crate) fn clear_prefix(&mut self, prefix: &[u8]) { - self.top.clear_where(|key, _| key.starts_with(prefix), self.extrinsic_index()); + pub(crate) fn clear_prefix(&mut self, prefix: &[u8]) -> u32 { + self.top.clear_where(|key, _| key.starts_with(prefix), self.extrinsic_index()) } /// Removes all key-value pairs which keys share the given prefix. From 92cd9a347ecabead6e75812ef119982bf12d2d61 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 17:48:21 +0100 Subject: [PATCH 34/48] Second part of the overlay prefix removal fix. --- primitives/state-machine/src/overlayed_changes/changeset.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/primitives/state-machine/src/overlayed_changes/changeset.rs b/primitives/state-machine/src/overlayed_changes/changeset.rs index 1d9eb0dea2b1b..ae5d47f6bb943 100644 --- a/primitives/state-machine/src/overlayed_changes/changeset.rs +++ b/primitives/state-machine/src/overlayed_changes/changeset.rs @@ -413,8 +413,10 @@ impl OverlayedChangeSet { ) -> u32 { let mut count = 0; for (key, val) in self.changes.iter_mut().filter(|(k, v)| predicate(k, v)) { - val.set(None, insert_dirty(&mut self.dirty_keys, key.clone()), at_extrinsic); + if val.value_ref().is_some() { count += 1; + } + val.set(None, insert_dirty(&mut self.dirty_keys, key.clone()), at_extrinsic); } count } From d504d70ae519812a60ddcb982cdfe205197b2c9f Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 14:49:59 +0100 Subject: [PATCH 35/48] Formatting --- frame/conviction-voting/src/types.rs | 25 ++++--------------------- frame/referenda/src/lib.rs | 10 ++++++++-- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/frame/conviction-voting/src/types.rs b/frame/conviction-voting/src/types.rs index 5ed059ccd65f2..2469009855c5f 100644 --- a/frame/conviction-voting/src/types.rs +++ b/frame/conviction-voting/src/types.rs @@ -17,17 +17,16 @@ //! Miscellaneous additional datatypes. -use sp_std::{marker::PhantomData, fmt::Debug}; use codec::{Codec, Decode, Encode, MaxEncodedLen}; use frame_support::{ - traits::VoteTally, CloneNoBound, EqNoBound, PartialEqNoBound, - RuntimeDebugNoBound, + traits::VoteTally, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use scale_info::TypeInfo; use sp_runtime::{ traits::{Saturating, Zero}, RuntimeDebug, }; +use sp_std::{fmt::Debug, marker::PhantomData}; use super::*; use crate::{AccountVote, Conviction, Vote}; @@ -56,15 +55,7 @@ pub struct Tally, Class, > VoteTally for Tally @@ -104,15 +95,7 @@ impl< } impl< - Votes: Clone - + Default - + PartialEq - + Eq - + Debug - + Copy - + AtLeast32BitUnsigned - + TypeInfo - + Codec, + Votes: Clone + Default + PartialEq + Eq + Debug + Copy + AtLeast32BitUnsigned + TypeInfo + Codec, Total: Get, > Tally { diff --git a/frame/referenda/src/lib.rs b/frame/referenda/src/lib.rs index 549b9d7e4384c..e3f31bc411328 100644 --- a/frame/referenda/src/lib.rs +++ b/frame/referenda/src/lib.rs @@ -152,7 +152,12 @@ pub mod pallet { /// The counting type for votes. Usually just balance. type Votes: AtLeast32BitUnsigned + Copy + Parameter + Member; /// The tallying type. - type Tally: VoteTally> + Clone + Codec + Eq + Debug + TypeInfo; + type Tally: VoteTally> + + Clone + + Codec + + Eq + + Debug + + TypeInfo; // Constants /// The minimum amount to be used as a deposit for a public referendum proposal. @@ -1080,6 +1085,7 @@ impl, I: 'static> Pallet { id: TrackIdOf, ) -> bool { let x = Perbill::from_rational(elapsed.min(period), period); - support_needed.passing(x, tally.support(id)) && approval_needed.passing(x, tally.approval(id)) + support_needed.passing(x, tally.support(id)) && + approval_needed.passing(x, tally.approval(id)) } } From cace11981631757d817ac14eaa2088799155a1ca Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 15:50:49 +0100 Subject: [PATCH 36/48] Fixes --- frame/support/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index a506706aa947c..443f00418e5f4 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -1119,7 +1119,7 @@ pub mod tests { DoubleMap::insert(&(key1 + 1), &(key2 + 1), &4u64); assert!(matches!( DoubleMap::remove_prefix(&key1, None), - sp_io::KillStorageResult::AllRemoved(0), // all in overlay + sp_io::KillStorageResult::AllRemoved(2), )); assert_eq!(DoubleMap::get(&key1, &key2), 0u64); assert_eq!(DoubleMap::get(&key1, &(key2 + 1)), 0u64); From e98865446f86849d8b9e39a64bcab6bb0cc6e22c Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 19:57:00 +0100 Subject: [PATCH 37/48] Add some tests and make clear rounding algo --- primitives/arithmetic/src/fixed_point.rs | 166 ++++++++++++++++---- primitives/arithmetic/src/helpers_128bit.rs | 30 +++- primitives/arithmetic/src/lib.rs | 3 +- primitives/arithmetic/src/per_things.rs | 102 ++++++++---- 4 files changed, 235 insertions(+), 66 deletions(-) diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index 04a48bc7b0601..e0572fe906c0f 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -23,7 +23,7 @@ use crate::{ Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedSub, One, SaturatedConversion, Saturating, UniqueSaturatedInto, Zero, }, - PerThing, Perbill, Rounding, + PerThing, Perbill, Rounding, SignedRounding, }; use codec::{CompactAs, Decode, Encode}; use sp_std::{ @@ -440,7 +440,7 @@ macro_rules! implement_fixed { self.0 as u128, 1_000_000_000, Self::DIV as u128, - Rounding::Nearest, + Rounding::NearestPrefDown, ) { Some(value) => { if value > (u32::max_value() as u128) { @@ -491,31 +491,6 @@ macro_rules! implement_fixed { Self(0 - self.0) } - /// A version of div with customisable rounding. - pub const fn checked_rounding_div( - self, - other: Self, - rounding: Rounding, - ) -> Option { - if other.0 == 0 { - return None - } - - let lhs = self.into_i129(); - let rhs = other.into_i129(); - let negative = lhs.negative != rhs.negative; - - match multiply_by_rational_with_rounding( - lhs.value, - Self::DIV as u128, - rhs.value, - rounding, - ) { - Some(value) => Self::from_i129(I129 { value, negative }), - None => None, - } - } - pub const fn sqrt(self) -> Self { match self.try_sqrt() { Some(v) => v, @@ -590,7 +565,7 @@ macro_rules! implement_fixed { <$inner_type>::min_value() } else { let unsigned_inner = n.value as $inner_type; - if unsigned_inner as u128 != n.value { + if unsigned_inner as u128 != n.value || (unsigned_inner > 0) != (n.value > 0) { return None }; if n.negative { @@ -609,10 +584,17 @@ macro_rules! implement_fixed { /// /// It is designed to be used in const expressions. This will panic if the input is bad. pub const fn from_rational(a: u128, b: u128) -> Self { + Self::from_rational_with_rounding(a, b, Rounding::NearestPrefDown) + } + + /// Const function for getting an (approximate) value from a rational. + /// + /// It is designed to be used in const expressions. This will panic if the input is bad. + pub const fn from_rational_with_rounding(a: u128, b: u128, rounding: Rounding) -> Self { if b == 0 { panic!("attempt to divide by zero in from_rational") } - match multiply_by_rational_with_rounding(Self::DIV as u128, a, b, Rounding::Nearest) + match multiply_by_rational_with_rounding(Self::DIV as u128, a, b, rounding) { Some(value) => match Self::from_i129(I129 { value, negative: false }) { Some(x) => x, @@ -623,6 +605,10 @@ macro_rules! implement_fixed { } pub const fn const_checked_mul(self, other: Self) -> Option { + self.const_checked_mul_with_rounding(other, SignedRounding::NearestPrefLow) + } + + pub const fn const_checked_mul_with_rounding(self, other: Self, rounding: SignedRounding) -> Option { let lhs = self.into_i129(); let rhs = other.into_i129(); let negative = lhs.negative != rhs.negative; @@ -631,7 +617,7 @@ macro_rules! implement_fixed { lhs.value, rhs.value, Self::DIV as u128, - Rounding::Nearest, + Rounding::from_signed(rounding, negative), ) { Some(value) => Self::from_i129(I129 { value, negative }), None => None, @@ -639,6 +625,15 @@ macro_rules! implement_fixed { } pub const fn const_checked_div(self, other: Self) -> Option { + self.checked_rounding_div(other, SignedRounding::NearestPrefLow) + } + + /// A version of div with customisable rounding. + pub const fn checked_rounding_div( + self, + other: Self, + rounding: SignedRounding, + ) -> Option { if other.0 == 0 { return None } @@ -651,7 +646,7 @@ macro_rules! implement_fixed { lhs.value, Self::DIV as u128, rhs.value, - Rounding::Nearest, + Rounding::from_signed(rounding, negative), ) { Some(value) => Self::from_i129(I129 { value, negative }), None => None, @@ -759,6 +754,10 @@ macro_rules! implement_fixed { let rhs: I129 = other.0.into(); let negative = lhs.negative != rhs.negative; + // Note that this uses the old (well-tested) code with sign-ignorant rounding. This + // is equivalent to the `SignedRounding::NearestPrefMinor`. This means it is + // expected to give exactly the same result as `const_checked_div` when the result + // is positive and a result up to one epsilon greater when it is negative. multiply_by_rational(lhs.value, Self::DIV as u128, rhs.value) .ok() .and_then(|value| from_i129(I129 { value, negative })) @@ -1380,6 +1379,41 @@ macro_rules! implement_fixed { assert_eq!(a.into_inner(), 0); } + #[test] + fn from_rational_works() { + let inner_max: u128 = <$name as FixedPointNumber>::Inner::max_value() as u128; + let inner_min: u128 = 0; + let accuracy: u128 = $name::accuracy() as u128; + + // Max - 1. + let a = $name::from_rational(inner_max - 1, accuracy); + assert_eq!(a.into_inner() as u128, inner_max - 1); + + // Min + 1. + let a = $name::from_rational(inner_min + 1, accuracy); + assert_eq!(a.into_inner() as u128, inner_min + 1); + + // Max. + let a = $name::from_rational(inner_max, accuracy); + assert_eq!(a.into_inner() as u128, inner_max); + + // Min. + let a = $name::from_rational(inner_min, accuracy); + assert_eq!(a.into_inner() as u128, inner_min); + + let a = $name::from_rational(inner_max, 3 * accuracy); + assert_eq!(a.into_inner() as u128, inner_max / 3); + + let a = $name::from_rational(1, accuracy); + assert_eq!(a.into_inner() as u128, 1); + + let a = $name::from_rational(1, accuracy + 1); + assert_eq!(a.into_inner() as u128, 1); + + let a = $name::from_rational_with_rounding(1, accuracy + 1, Rounding::Down); + assert_eq!(a.into_inner() as u128, 0); + } + #[test] fn checked_mul_int_works() { let a = $name::saturating_from_integer(2); @@ -1519,6 +1553,76 @@ macro_rules! implement_fixed { ); } + #[test] + fn const_checked_mul_works() { + let inner_max = <$name as FixedPointNumber>::Inner::max_value(); + let inner_min = <$name as FixedPointNumber>::Inner::min_value(); + + let a = $name::saturating_from_integer(2u32); + + // Max - 1. + let b = $name::from_inner(inner_max - 1); + assert_eq!(a.const_checked_mul((b / 2.into())), Some(b)); + + // Max. + let c = $name::from_inner(inner_max); + assert_eq!(a.const_checked_mul((c / 2.into())), Some(b)); + + // Max + 1 => None. + let e = $name::from_inner(1); + assert_eq!(a.const_checked_mul((c / 2.into() + e)), None); + + if $name::SIGNED { + // Min + 1. + let b = $name::from_inner(inner_min + 1) / 2.into(); + let c = $name::from_inner(inner_min + 2); + assert_eq!(a.const_checked_mul(b), Some(c)); + + // Min. + let b = $name::from_inner(inner_min) / 2.into(); + let c = $name::from_inner(inner_min); + assert_eq!(a.const_checked_mul(b), Some(c)); + + // Min - 1 => None. + let b = $name::from_inner(inner_min) / 2.into() - $name::from_inner(1); + assert_eq!(a.const_checked_mul(b), None); + + let b = $name::saturating_from_rational(1i32, -2i32); + let c = $name::saturating_from_integer(-21i32); + let d = $name::saturating_from_integer(42); + + assert_eq!(b.const_checked_mul(d), Some(c)); + + let minus_two = $name::saturating_from_integer(-2i32); + assert_eq!( + b.const_checked_mul($name::max_value()), + $name::max_value().const_checked_div(minus_two) + ); + assert_eq!( + b.const_checked_mul($name::min_value()), + $name::min_value().const_checked_div(minus_two) + ); + + let c = $name::saturating_from_integer(255u32); + assert_eq!(c.const_checked_mul($name::min_value()), None); + } + + let a = $name::saturating_from_rational(1i32, 2i32); + let c = $name::saturating_from_integer(255i32); + + assert_eq!(a.const_checked_mul(42.into()), Some(21.into())); + assert_eq!(c.const_checked_mul(2.into()), Some(510.into())); + assert_eq!(c.const_checked_mul($name::max_value()), None); + assert_eq!( + a.const_checked_mul($name::max_value()), + $name::max_value().checked_div(&2.into()) + ); + assert_eq!( + a.const_checked_mul($name::min_value()), + $name::min_value().const_checked_div($name::saturating_from_integer(2)) + ); + } + #[test] fn checked_div_int_works() { let inner_max = <$name as FixedPointNumber>::Inner::max_value(); diff --git a/primitives/arithmetic/src/helpers_128bit.rs b/primitives/arithmetic/src/helpers_128bit.rs index 4528fb7a63d3d..260f90ed60cd0 100644 --- a/primitives/arithmetic/src/helpers_128bit.rs +++ b/primitives/arithmetic/src/helpers_128bit.rs @@ -265,7 +265,9 @@ pub const fn multiply_by_rational_with_rounding( }; if match r { Rounding::Up => remainder > 0, - Rounding::Nearest => remainder >= c / 2 + c % 2, + // cannot be `(c + 1) / 2` since `c` might be `max_value` and overflow. + Rounding::NearestPrefUp => remainder >= c / 2 + c % 2, + Rounding::NearestPrefDown => remainder > c / 2, Rounding::Down => false, } { result = match result.checked_add(1) { @@ -316,10 +318,14 @@ mod tests { fn rational_multiply_basic_rounding_works() { assert_eq!(mulrat(1, 1, 1, Up), Some(1)); assert_eq!(mulrat(3, 1, 3, Up), Some(1)); - assert_eq!(mulrat(1, 2, 3, Down), Some(0)); assert_eq!(mulrat(1, 1, 3, Up), Some(1)); - assert_eq!(mulrat(1, 2, 3, Nearest), Some(1)); - assert_eq!(mulrat(1, 1, 3, Nearest), Some(0)); + assert_eq!(mulrat(1, 2, 3, Down), Some(0)); + assert_eq!(mulrat(1, 1, 3, NearestPrefDown), Some(0)); + assert_eq!(mulrat(1, 1, 2, NearestPrefDown), Some(0)); + assert_eq!(mulrat(1, 2, 3, NearestPrefDown), Some(1)); + assert_eq!(mulrat(1, 1, 3, NearestPrefUp), Some(0)); + assert_eq!(mulrat(1, 1, 2, NearestPrefUp), Some(1)); + assert_eq!(mulrat(1, 2, 3, NearestPrefUp), Some(1)); } #[test] @@ -330,8 +336,18 @@ mod tests { assert_eq!(mulrat(MAX, 1, MAX, Up), Some(1)); assert_eq!(mulrat(1, MAX - 1, MAX, Down), Some(0)); assert_eq!(mulrat(1, 1, MAX, Up), Some(1)); - assert_eq!(mulrat(1, MAX / 2, MAX, Nearest), Some(0)); - assert_eq!(mulrat(1, MAX / 2 + 1, MAX, Nearest), Some(1)); + assert_eq!(mulrat(1, MAX / 2, MAX, NearestPrefDown), Some(0)); + assert_eq!(mulrat(1, MAX / 2 + 1, MAX, NearestPrefDown), Some(1)); + assert_eq!(mulrat(1, MAX / 2, MAX, NearestPrefUp), Some(0)); + assert_eq!(mulrat(1, MAX / 2 + 1, MAX, NearestPrefUp), Some(1)); + } + + #[test] + fn sqrt_works() { + for i in 0..100_000u32 { + let a = sqrt(random_u128(i)); + assert_eq!(sqrt(a * a), a); + } } fn random_u128(seed: u32) -> u128 { @@ -344,7 +360,7 @@ mod tests { let a = random_u128(i); let b = random_u128(i + 1 << 30); let c = random_u128(i + 1 << 31); - let x = mulrat(a, b, c, Nearest); + let x = mulrat(a, b, c, NearestPrefDown); let y = multiply_by_rational(a, b, c).ok(); assert_eq!(x.is_some(), y.is_some()); let x = x.unwrap_or(0); diff --git a/primitives/arithmetic/src/lib.rs b/primitives/arithmetic/src/lib.rs index a4bdfe46d13a1..244242c0f7580 100644 --- a/primitives/arithmetic/src/lib.rs +++ b/primitives/arithmetic/src/lib.rs @@ -42,7 +42,8 @@ pub mod traits; pub use fixed_point::{FixedI128, FixedI64, FixedPointNumber, FixedPointOperand, FixedU128}; pub use per_things::{ - InnerOf, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, Rounding, UpperOf, + InnerOf, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, Rounding, SignedRounding, + UpperOf, }; pub use rational::{Rational128, RationalInfinite}; diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index 08be884e4b321..920448a819009 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -222,7 +222,7 @@ pub trait PerThing: + Unsigned, Self::Inner: Into, { - saturating_reciprocal_mul::(b, self.deconstruct(), Rounding::Nearest) + saturating_reciprocal_mul::(b, self.deconstruct(), Rounding::NearestPrefUp) } /// Saturating multiplication by the reciprocal of `self`. The result is rounded down to the @@ -351,11 +351,19 @@ pub trait PerThing: /// Percent::from_parts(98), /// ); /// assert_eq!( - /// Percent::from_rational_with_rounding(984u64, 1000, Nearest).unwrap(), + /// Percent::from_rational_with_rounding(984u64, 1000, NearestPrefUp).unwrap(), /// Percent::from_parts(98), /// ); /// assert_eq!( - /// Percent::from_rational_with_rounding(985u64, 1000, Nearest).unwrap(), + /// Percent::from_rational_with_rounding(985u64, 1000, NearestPrefDown).unwrap(), + /// Percent::from_parts(98), + /// ); + /// assert_eq!( + /// Percent::from_rational_with_rounding(985u64, 1000, NearestPrefUp).unwrap(), + /// Percent::from_parts(99), + /// ); + /// assert_eq!( + /// Percent::from_rational_with_rounding(986u64, 1000, NearestPrefDown).unwrap(), /// Percent::from_parts(99), /// ); /// assert_eq!( @@ -414,15 +422,52 @@ pub trait PerThing: } } -/// The rounding method to use. -/// -/// `PerThing`s are unsigned so `Up` means towards infinity and `Down` means towards zero. -/// `Nearest` will round an exact half down. +/// The rounding method to use for unsigned quantities. #[derive(sp_std::fmt::Debug)] pub enum Rounding { + // Towards infinity. Up, + // Towards zero. Down, - Nearest, + // Nearest integer, rounding as `Up` when equidistant. + NearestPrefUp, + // Nearest integer, rounding as `Down` when equidistant. + NearestPrefDown, +} + +/// The rounding method to use. +#[derive(sp_std::fmt::Debug)] +pub enum SignedRounding { + // Towards positive infinity. + High, + // Towards negative infinity. + Low, + // Nearest integer, rounding as `High` when exactly equidistant. + NearestPrefHigh, + // Nearest integer, rounding as `Low` when exactly equidistant. + NearestPrefLow, + // Away from zero (up when positive, down when negative). When positive, equivalent to `High`. + Major, + // Towards zero (down when positive, up when negative). When positive, equivalent to `Low`. + Minor, + // Nearest integer, rounding as `Major` when exactly equidistant. + NearestPrefMajor, + // Nearest integer, rounding as `Minor` when exactly equidistant. + NearestPrefMinor, +} + +impl Rounding { + /// Returns the value for `Rounding` which would give the same result ignorant of the sign. + pub const fn from_signed(rounding: SignedRounding, negative: bool) -> Self { + use SignedRounding::*; + use Rounding::*; + match (rounding, negative) { + (Low, true) | (Major, _) | (High, false) => Up, + (High, true) | (Minor, _) | (Low, false) => Down, + (NearestPrefMajor, _) | (NearestPrefHigh, false) | (NearestPrefLow, true) => NearestPrefUp, + (NearestPrefMinor, _) | (NearestPrefLow, false) | (NearestPrefHigh, true) => NearestPrefDown, + } + } } /// Saturating reciprocal multiplication. Compute `x / self`, saturating at the numeric @@ -500,14 +545,15 @@ where rem_mul_div_inner += 1.into(); } }, - // Round up if the fractional part of the result is greater than a half. An exact half is - // rounded down. - Rounding::Nearest => { - if rem_mul_upper % denom_upper > denom_upper / 2.into() { + Rounding::NearestPrefDown => if rem_mul_upper % denom_upper > denom_upper / 2.into() { + // `rem * numer / denom` is less than `numer`, so this will not overflow. + rem_mul_div_inner += 1.into(); + }, + Rounding::NearestPrefUp => + if rem_mul_upper % denom_upper >= denom_upper / 2.into() + denom_upper % 2.into() { // `rem * numer / denom` is less than `numer`, so this will not overflow. rem_mul_div_inner += 1.into(); - } - }, + }, } rem_mul_div_inner.into() } @@ -642,7 +688,8 @@ macro_rules! implement_per_thing { let mut o = n.clone() / d.clone(); if match r { Rounding::Up => { !((n % d).is_zero()) }, - Rounding::Nearest => { let rem = n % d.clone(); rem.clone() + rem >= d }, + Rounding::NearestPrefDown => { let rem = n % d.clone(); rem.clone() + rem > d }, + Rounding::NearestPrefUp => { let rem = n % d.clone(); rem.clone() + rem >= d }, Rounding::Down => false, } { o += N::one() @@ -680,7 +727,8 @@ macro_rules! implement_per_thing { let mut part = n / d; if match r { Rounding::Up => { !((n % d).is_zero()) }, - Rounding::Nearest => { let r = n % d; r + r >= d }, + Rounding::NearestPrefDown => { let r = n % d; r + r > d }, + Rounding::NearestPrefUp => { let r = n % d; r + r >= d }, Rounding::Down => false, } { part += 1 as $upper_type @@ -993,7 +1041,7 @@ macro_rules! implement_per_thing { { type Output = N; fn mul(self, b: N) -> Self::Output { - overflow_prune_mul::(b, self.deconstruct(), Rounding::Nearest) + overflow_prune_mul::(b, self.deconstruct(), Rounding::NearestPrefDown) } } @@ -1277,10 +1325,10 @@ macro_rules! implement_per_thing { #[test] fn per_thing_mul_rounds_to_nearest_number() { - assert_eq!($name::from_float(0.33) * 10u64, 3); - assert_eq!($name::from_float(0.34) * 10u64, 3); - assert_eq!($name::from_float(0.35) * 10u64, 3); - assert_eq!($name::from_float(0.36) * 10u64, 4); + assert_eq!($name::from_percent(33) * 10u64, 3); + assert_eq!($name::from_percent(34) * 10u64, 3); + assert_eq!($name::from_percent(35) * 10u64, 3); + assert_eq!($name::from_percent(36) * 10u64, 4); } #[test] @@ -1588,7 +1636,7 @@ macro_rules! implement_per_thing { <$type>::max_value(), <$type>::max_value(), <$type>::max_value(), - super::Rounding::Nearest, + super::Rounding::NearestPrefDown, ), 0, ); @@ -1597,7 +1645,7 @@ macro_rules! implement_per_thing { <$type>::max_value() - 1, <$type>::max_value(), <$type>::max_value(), - super::Rounding::Nearest, + super::Rounding::NearestPrefDown, ), <$type>::max_value() - 1, ); @@ -1606,7 +1654,7 @@ macro_rules! implement_per_thing { ((<$type>::max_value() - 1) as $upper_type).pow(2), <$type>::max_value(), <$type>::max_value(), - super::Rounding::Nearest, + super::Rounding::NearestPrefDown, ), 1, ); @@ -1616,7 +1664,7 @@ macro_rules! implement_per_thing { (<$type>::max_value() as $upper_type).pow(2) - 1, <$type>::max_value(), <$type>::max_value(), - super::Rounding::Nearest, + super::Rounding::NearestPrefDown, ), <$upper_type>::from((<$type>::max_value() - 1)), ); @@ -1626,7 +1674,7 @@ macro_rules! implement_per_thing { (<$type>::max_value() as $upper_type).pow(2), <$type>::max_value(), 2 as $type, - super::Rounding::Nearest, + super::Rounding::NearestPrefDown, ), <$type>::max_value() as $upper_type / 2, ); @@ -1636,7 +1684,7 @@ macro_rules! implement_per_thing { (<$type>::max_value() as $upper_type).pow(2) - 1, 2 as $type, <$type>::max_value(), - super::Rounding::Nearest, + super::Rounding::NearestPrefDown, ), 2, ); From 38f14fdc884df01639985aec36456c6fbc809eb0 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 20 May 2022 10:46:17 +0100 Subject: [PATCH 38/48] Fixes --- frame/referenda/src/types.rs | 8 ++++---- primitives/arithmetic/src/per_things.rs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs index 0f9ae16ca7767..cf6a147d46c7f 100644 --- a/frame/referenda/src/types.rs +++ b/frame/referenda/src/types.rs @@ -21,7 +21,7 @@ use super::*; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{traits::schedule::Anon, Parameter}; use scale_info::TypeInfo; -use sp_arithmetic::Rounding::*; +use sp_arithmetic::{Rounding::*, SignedRounding::*}; use sp_runtime::{FixedI64, PerThing, RuntimeDebug}; use sp_std::fmt::Debug; @@ -385,7 +385,7 @@ impl Curve { Self::SteppedDecreasing { begin, end, step, period } => (*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end), Self::Reciprocal { factor, x_offset, y_offset } => factor - .checked_rounding_div(FixedI64::from(x) + *x_offset, Down) + .checked_rounding_div(FixedI64::from(x) + *x_offset, Low) .map(|yp| (yp + *y_offset).into_clamped_perthing()) .unwrap_or_else(Perbill::one), } @@ -397,7 +397,7 @@ impl Curve { const fn const_threshold(&self, x: Perbill) -> Perbill { match self { Self::Reciprocal { factor, x_offset, y_offset } => { - match factor.checked_rounding_div(FixedI64::from_perbill(x).add(*x_offset), Down) { + match factor.checked_rounding_div(FixedI64::from_perbill(x).add(*x_offset), Low) { Some(yp) => (yp.add(*y_offset)).into_perbill(), None => Perbill::one(), } @@ -438,7 +438,7 @@ impl Curve { }, Self::Reciprocal { factor, x_offset, y_offset } => { let y = FixedI64::from(y); - let maybe_term = factor.checked_rounding_div(y - *y_offset, Up); + let maybe_term = factor.checked_rounding_div(y - *y_offset, High); maybe_term .and_then(|term| (term - *x_offset).try_into_perthing().ok()) .unwrap_or_else(Perbill::one) diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index 920448a819009..18243c861e459 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -688,6 +688,7 @@ macro_rules! implement_per_thing { let mut o = n.clone() / d.clone(); if match r { Rounding::Up => { !((n % d).is_zero()) }, + // TODO: check `rem` can never be big enough to make this overflow. e.g. (u16::max_value() - 1 / u16::max_value()) Rounding::NearestPrefDown => { let rem = n % d.clone(); rem.clone() + rem > d }, Rounding::NearestPrefUp => { let rem = n % d.clone(); rem.clone() + rem >= d }, Rounding::Down => false, From 064de3d611a3d08aab818dcbb3cd09d73b491466 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 20 May 2022 10:47:23 +0100 Subject: [PATCH 39/48] Formatting --- primitives/arithmetic/src/fixed_point.rs | 9 ++++++--- primitives/arithmetic/src/per_things.rs | 17 ++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index e0572fe906c0f..9b9ba6978dfae 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -594,8 +594,7 @@ macro_rules! implement_fixed { if b == 0 { panic!("attempt to divide by zero in from_rational") } - match multiply_by_rational_with_rounding(Self::DIV as u128, a, b, rounding) - { + match multiply_by_rational_with_rounding(Self::DIV as u128, a, b, rounding) { Some(value) => match Self::from_i129(I129 { value, negative: false }) { Some(x) => x, None => panic!("overflow in from_rational"), @@ -608,7 +607,11 @@ macro_rules! implement_fixed { self.const_checked_mul_with_rounding(other, SignedRounding::NearestPrefLow) } - pub const fn const_checked_mul_with_rounding(self, other: Self, rounding: SignedRounding) -> Option { + pub const fn const_checked_mul_with_rounding( + self, + other: Self, + rounding: SignedRounding, + ) -> Option { let lhs = self.into_i129(); let rhs = other.into_i129(); let negative = lhs.negative != rhs.negative; diff --git a/primitives/arithmetic/src/per_things.rs b/primitives/arithmetic/src/per_things.rs index 18243c861e459..6b1dc57e7ddae 100644 --- a/primitives/arithmetic/src/per_things.rs +++ b/primitives/arithmetic/src/per_things.rs @@ -459,13 +459,15 @@ pub enum SignedRounding { impl Rounding { /// Returns the value for `Rounding` which would give the same result ignorant of the sign. pub const fn from_signed(rounding: SignedRounding, negative: bool) -> Self { - use SignedRounding::*; use Rounding::*; + use SignedRounding::*; match (rounding, negative) { (Low, true) | (Major, _) | (High, false) => Up, (High, true) | (Minor, _) | (Low, false) => Down, - (NearestPrefMajor, _) | (NearestPrefHigh, false) | (NearestPrefLow, true) => NearestPrefUp, - (NearestPrefMinor, _) | (NearestPrefLow, false) | (NearestPrefHigh, true) => NearestPrefDown, + (NearestPrefMajor, _) | (NearestPrefHigh, false) | (NearestPrefLow, true) => + NearestPrefUp, + (NearestPrefMinor, _) | (NearestPrefLow, false) | (NearestPrefHigh, true) => + NearestPrefDown, } } } @@ -545,10 +547,11 @@ where rem_mul_div_inner += 1.into(); } }, - Rounding::NearestPrefDown => if rem_mul_upper % denom_upper > denom_upper / 2.into() { - // `rem * numer / denom` is less than `numer`, so this will not overflow. - rem_mul_div_inner += 1.into(); - }, + Rounding::NearestPrefDown => + if rem_mul_upper % denom_upper > denom_upper / 2.into() { + // `rem * numer / denom` is less than `numer`, so this will not overflow. + rem_mul_div_inner += 1.into(); + }, Rounding::NearestPrefUp => if rem_mul_upper % denom_upper >= denom_upper / 2.into() + denom_upper % 2.into() { // `rem * numer / denom` is less than `numer`, so this will not overflow. From 4f456f8d85f678774bde6599fac1f8281fcb42fb Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 24 May 2022 11:15:08 +0200 Subject: [PATCH 40/48] Revert questionable fix --- primitives/state-machine/src/ext.rs | 5 ++--- .../state-machine/src/overlayed_changes/changeset.rs | 7 +------ primitives/state-machine/src/overlayed_changes/mod.rs | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/primitives/state-machine/src/ext.rs b/primitives/state-machine/src/ext.rs index 2d446b9c3b9a0..e33569e2a1f67 100644 --- a/primitives/state-machine/src/ext.rs +++ b/primitives/state-machine/src/ext.rs @@ -464,9 +464,8 @@ where } self.mark_dirty(); - let overlay_count = self.overlay.clear_prefix(prefix); - let (all, count) = self.limit_remove_from_backend(None, Some(prefix), limit); - (all, count + overlay_count) + self.overlay.clear_prefix(prefix); + self.limit_remove_from_backend(None, Some(prefix), limit) } fn clear_child_prefix( diff --git a/primitives/state-machine/src/overlayed_changes/changeset.rs b/primitives/state-machine/src/overlayed_changes/changeset.rs index ae5d47f6bb943..9e7f6ffeddfd7 100644 --- a/primitives/state-machine/src/overlayed_changes/changeset.rs +++ b/primitives/state-machine/src/overlayed_changes/changeset.rs @@ -410,15 +410,10 @@ impl OverlayedChangeSet { &mut self, predicate: impl Fn(&[u8], &OverlayedValue) -> bool, at_extrinsic: Option, - ) -> u32 { - let mut count = 0; + ) { for (key, val) in self.changes.iter_mut().filter(|(k, v)| predicate(k, v)) { - if val.value_ref().is_some() { - count += 1; - } val.set(None, insert_dirty(&mut self.dirty_keys, key.clone()), at_extrinsic); } - count } /// Get the iterator over all changes that follow the supplied `key`. diff --git a/primitives/state-machine/src/overlayed_changes/mod.rs b/primitives/state-machine/src/overlayed_changes/mod.rs index 2bb09e98bff62..2161b343711c9 100644 --- a/primitives/state-machine/src/overlayed_changes/mod.rs +++ b/primitives/state-machine/src/overlayed_changes/mod.rs @@ -315,8 +315,8 @@ impl OverlayedChanges { /// Removes all key-value pairs which keys share the given prefix. /// /// Can be rolled back or committed when called inside a transaction. - pub(crate) fn clear_prefix(&mut self, prefix: &[u8]) -> u32 { - self.top.clear_where(|key, _| key.starts_with(prefix), self.extrinsic_index()) + pub(crate) fn clear_prefix(&mut self, prefix: &[u8]) { + self.top.clear_where(|key, _| key.starts_with(prefix), self.extrinsic_index()); } /// Removes all key-value pairs which keys share the given prefix. From 06d982319e5c1aaf4c197f14fe805e9821cc641c Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 15:56:26 +0200 Subject: [PATCH 41/48] Ranked Collective pallet --- Cargo.toml | 1 + frame/ranked-collective/Cargo.toml | 49 +++ frame/ranked-collective/README.md | 25 ++ frame/ranked-collective/src/benchmarking.rs | 85 ++++ frame/ranked-collective/src/lib.rs | 431 ++++++++++++++++++++ frame/ranked-collective/src/tests.rs | 287 +++++++++++++ frame/ranked-collective/src/weights.rs | 326 +++++++++++++++ 7 files changed, 1204 insertions(+) create mode 100644 frame/ranked-collective/Cargo.toml create mode 100644 frame/ranked-collective/README.md create mode 100644 frame/ranked-collective/src/benchmarking.rs create mode 100644 frame/ranked-collective/src/lib.rs create mode 100644 frame/ranked-collective/src/tests.rs create mode 100644 frame/ranked-collective/src/weights.rs diff --git a/Cargo.toml b/Cargo.toml index 41739fe6f1ebc..74e7aae7949c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,7 @@ members = [ "frame/nomination-pools", "frame/nomination-pools/benchmarking", "frame/randomness-collective-flip", + "frame/ranked-collective", "frame/recovery", "frame/referenda", "frame/remark", diff --git a/frame/ranked-collective/Cargo.toml b/frame/ranked-collective/Cargo.toml new file mode 100644 index 0000000000000..cb43b9ea4c831 --- /dev/null +++ b/frame/ranked-collective/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "pallet-ranked-collective" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Ranked collective system: Members of a set of account IDs can make their collective feelings known through dispatched calls from one of two specialized origins." +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, features = ["derive"] } +log = { version = "0.4.16", default-features = false } +scale-info = { version = "2.0.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" } +sp-arithmetic = { version = "5.0.0", default-features = false, path = "../../primitives/arithmetic" } +sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" } +sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/ranked-collective/README.md b/frame/ranked-collective/README.md new file mode 100644 index 0000000000000..444927e51da22 --- /dev/null +++ b/frame/ranked-collective/README.md @@ -0,0 +1,25 @@ +Collective system: Members of a set of account IDs can make their collective feelings known +through dispatched calls from one of two specialized origins. + +The membership can be provided in one of two ways: either directly, using the Root-dispatchable +function `set_members`, or indirectly, through implementing the `ChangeMembers`. +The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. + +A "prime" member may be set to help determine the default vote behavior based on chain +config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +abstentations will first follow the majority of the collective voting, and then the prime +member. + +Voting happens through motions comprising a proposal (i.e. a dispatchable) plus a +number of approvals required for it to pass and be called. Motions are open for members to +vote on for a minimum period given by `MotionDuration`. As soon as the required number of +approvals is given, the motion is closed and executed. If the number of approvals is not reached +during the voting period, then `close` may be called by any account in order to force the end +the motion explicitly. If a prime member is defined, then their vote is used instead of any +abstentions and the proposal is executed if there are enough approvals counting the new votes. + +If there are not, or if no prime member is set, then the motion is dropped without being executed. + +License: Apache-2.0 diff --git a/frame/ranked-collective/src/benchmarking.rs b/frame/ranked-collective/src/benchmarking.rs new file mode 100644 index 0000000000000..4d972e211fea9 --- /dev/null +++ b/frame/ranked-collective/src/benchmarking.rs @@ -0,0 +1,85 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-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. + +//! Staking pallet benchmarking. + +use super::*; +use crate::Pallet as Collective; + +use sp_runtime::traits::Bounded; +use sp_std::mem::size_of; + +use frame_benchmarking::{account, benchmarks_instance_pallet, whitelisted_caller}; +use frame_support::dispatch::UnfilteredDispatchable; +use frame_system::{Call as SystemCall, Pallet as System, RawOrigin as SystemOrigin}; + +const SEED: u32 = 0; + +const MAX_BYTES: u32 = 1_024; + +fn assert_last_event, I: 'static>(generic_event: >::Event) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn make_member, I: 'static>(rank: Rank) -> T::AccountId { + let who = account::("member", MemberCount::::get().0, SEED); + Pallet::::add_member(T::AdminOrigin::successful_origin(), who.clone(), rank); + who +} + +benchmarks_instance_pallet! { + add_member { + let old = MemberCount::::get().0; + let who = account::("member", 0, SEED); + let rank = 1; + let origin = T::AdminOrigin::successful_origin(); + let call = Call::::add_member { who: who.clone(), rank }; + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_eq!(MemberCount::::get().0, old + 1); + assert_last_event::(Event::MemberAdded { who, rank }.into()); + } + + remove_member { + let rank = 1; + let who = make_member::(rank); + let other = make_member::(rank); + let old = MemberCount::::get().0; + let other_index = Members::::get(&other).unwrap().index; + let origin = T::AdminOrigin::successful_origin(); + let call = Call::::remove_member { who: who.clone() }; + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_eq!(MemberCount::::get().0, old - 1); + assert_ne!(other_index, Members::::get(&other).unwrap().index); + assert_last_event::(Event::MemberRemoved { who }.into()); + } + + set_member_rank { + let old_rank = 1; + let rank = 2; + let who = make_member::(old_rank); + let origin = T::AdminOrigin::successful_origin(); + let call = Call::::set_member_rank { who: who.clone(), rank }; + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_eq!(Members::::get(&who).unwrap().rank, rank); + assert_last_event::(Event::RankChanged { who, rank }.into()); + } + + impl_benchmark_test_suite!(Collective, crate::tests::new_test_ext(), crate::tests::Test); +} diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs new file mode 100644 index 0000000000000..ef23cdf648c28 --- /dev/null +++ b/frame/ranked-collective/src/lib.rs @@ -0,0 +1,431 @@ +// 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. + +//! Ranked collective system: Members of a set of account IDs can make their collective feelings +//! known through dispatched calls from one of two specialized origins. +//! +//! The membership can be provided in one of two ways: either directly, using the Root-dispatchable +//! function `set_members`, or indirectly, through implementing the `ChangeMembers`. +//! The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +//! calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. +//! +//! A "prime" member may be set to help determine the default vote behavior based on chain +//! config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +//! abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +//! abstentions will first follow the majority of the collective voting, and then the prime +//! member. +//! +//! Voting happens through motions comprising a proposal (i.e. a curried dispatchable) plus a +//! number of approvals required for it to pass and be called. Motions are open for members to +//! vote on for a minimum period given by `MotionDuration`. As soon as the needed number of +//! approvals is given, the motion is closed and executed. If the number of approvals is not reached +//! during the voting period, then `close` may be called by any account in order to force the end +//! the motion explicitly. If a prime member is defined then their vote is used in place of any +//! abstentions and the proposal is executed if there are enough approvals counting the new votes. +//! +//! If there are not, or if no prime is set, then the motion is dropped without being executed. + +#![cfg_attr(not(feature = "std"), no_std)] +#![recursion_limit = "128"] + +use scale_info::TypeInfo; +use sp_arithmetic::traits::Saturating; +use sp_runtime::{ + ArithmeticError::{Overflow, Underflow}, + Perbill, RuntimeDebug, +}; +use sp_std::{marker::PhantomData, prelude::*}; + +use frame_support::{ + codec::{Decode, Encode, MaxEncodedLen}, + dispatch::{DispatchError, DispatchResultWithPostInfo}, + ensure, + traits::{EnsureOrigin, Get, PollStatus, Polling, VoteTally}, + CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, +}; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub mod weights; + +pub use pallet::*; +pub use weights::WeightInfo; + +/// A number of members. +pub type MemberIndex = u32; + +/// Member rank. +pub type Rank = u16; + +/// Votes. +pub type Votes = u32; + +/// Aggregated votes for an ongoing poll. +#[derive( + CloneNoBound, + DefaultNoBound, + PartialEqNoBound, + EqNoBound, + RuntimeDebugNoBound, + TypeInfo, + Encode, + Decode, + MaxEncodedLen, +)] +#[scale_info(skip_type_params(M))] +pub struct Tally> { + ayes: Votes, + nays: Votes, + dummy: PhantomData, +} + +impl> Tally { + fn from_parts(ayes: Votes, nays: Votes) -> Self { + Tally { ayes, nays, dummy: PhantomData } + } +} + +pub type TallyOf = Tally>; +pub type PollIndexOf = <>::Polls as Polling>>::Index; + +impl> VoteTally for Tally { + fn ayes(&self) -> Votes { + self.ayes + } + fn support(&self) -> Perbill { + Perbill::from_rational(self.ayes, M::get()) + } + fn approval(&self) -> Perbill { + Perbill::from_rational(self.ayes, 1.max(self.ayes + self.nays)) + } + #[cfg(feature = "runtime-benchmarks")] + fn unanimity() -> Self { + Self { ayes: M::get(), nays: 0, dummy: PhantomData } + } + #[cfg(feature = "runtime-benchmarks")] + fn rejection() -> Self { + Self { ayes: 0, nays: M::get(), dummy: PhantomData } + } + #[cfg(feature = "runtime-benchmarks")] + fn from_requirements(support: Perbill, approval: Perbill) -> Self { + let c = M::get(); + let ayes = support * c; + let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes; + Self { ayes, nays, dummy: PhantomData } + } +} + +/// Record needed for every member. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct MemberRecord { + /// The index of the member. + index: MemberIndex, + /// The rank of the member. + rank: Rank, +} + +/// Record needed for every vote. +#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum VoteRecord { + /// Vote was an aye with given vote weight. + Aye(Votes), + /// Vote was a nay with given vote weight. + Nay(Votes), +} + +impl From<(bool, Votes)> for VoteRecord { + fn from((aye, votes): (bool, Votes)) -> Self { + match aye { + true => VoteRecord::Aye(votes), + false => VoteRecord::Nay(votes), + } + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The outer event type. + type Event: From> + IsType<::Event>; + + /// The origin required to add, promote or remove a member. + type AdminOrigin: EnsureOrigin; + + /// The polling system used for our voting. + type Polls: Polling, Votes = Votes, Moment = Self::BlockNumber>; + } + + /// The number of members in the collective. + #[pallet::storage] + pub type MemberCount, I: 'static = ()> = + StorageValue<_, (MemberIndex, Votes), ValueQuery>; + + /// The current members of the collective. + #[pallet::storage] + pub type Members, I: 'static = ()> = + StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>; + + /// The members in the collective by index. All indices in the range `0..MemberCount` will + /// return `Some`, however a member's index is not guaranteed to remain unchanged over time. + #[pallet::storage] + pub type MemberByIndex, I: 'static = ()> = + StorageMap<_, Twox64Concat, MemberIndex, T::AccountId>; + + /// Votes on a given proposal, if it is ongoing. + #[pallet::storage] + pub type Voting, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + PollIndexOf, + Twox64Concat, + T::AccountId, + VoteRecord, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A member has been added. + MemberAdded { who: T::AccountId, rank: Rank }, + /// A member's rank has been changed. + RankChanged { who: T::AccountId, rank: Rank }, + /// A member has been removed. + MemberRemoved { who: T::AccountId }, + /// A motion (given hash) has been voted on by given account, leaving + /// a tally (yes votes and no votes given respectively as `MemberIndex`). + Voted { who: T::AccountId, poll: PollIndexOf, vote: VoteRecord, tally: TallyOf }, + } + + #[pallet::error] + pub enum Error { + /// Account is already a member. + AlreadyMember, + /// Account is not a member. + NotMember, + /// The given poll index is unknown or has closed. + NotPolling, + /// The given poll is still ongoing. + Ongoing, + /// There are no further records to be removed. + NoneRemaining, + /// Unexpected error in state. + Corruption, + } + + #[pallet::call] + impl, I: 'static> Pallet { + /// Introduce a new member. + /// + /// - `origin`: Must be the `AdminOrigin`. + /// - `who`: Account of non-member which will become a member. + /// - `rank`: The rank to give the new member. + /// + /// Weight: `O(1)` + #[pallet::weight(T::BlockWeights::get().max_block / 10)] + pub fn add_member(origin: OriginFor, who: T::AccountId, rank: Rank) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + ensure!(!Members::::contains_key(&who), Error::::AlreadyMember); + let (index, mut votes) = MemberCount::::get(); + let count = index.checked_add(1).ok_or(Overflow)?; + votes = votes.checked_add(Self::rank_to_votes(rank)).ok_or(Overflow)?; + + Members::::insert(&who, MemberRecord { rank, index }); + MemberByIndex::::insert(index, &who); + MemberCount::::put((count, votes)); + Self::deposit_event(Event::MemberAdded { who, rank }); + + Ok(()) + } + + /// Alter the rank of an existing member. + /// + /// - `origin`: Must be the `AdminOrigin`. + /// - `who`: Account of existing member. + /// - `rank`: The new rank to give the member. + /// + /// Weight: `O(1)` + #[pallet::weight(T::BlockWeights::get().max_block / 10)] + pub fn set_member_rank( + origin: OriginFor, + who: T::AccountId, + rank: Rank, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + let mut record = Self::ensure_member(&who)?; + let (count, mut votes) = MemberCount::::get(); + votes = votes + .checked_sub(Self::rank_to_votes(record.rank)) + .ok_or(Underflow)? + .checked_add(Self::rank_to_votes(rank)) + .ok_or(Overflow)?; + record.rank = rank; + + MemberCount::::put((count, votes)); + Members::::insert(&who, record); + Self::deposit_event(Event::RankChanged { who, rank }); + + Ok(()) + } + + /// Remove a member. + /// + /// - `origin`: Must be the `AdminOrigin`. + /// - `who`: Account of existing member to be removed. + /// + /// Weight: `O(1)`, less if the member's index is highest. + #[pallet::weight(T::BlockWeights::get().max_block / 10)] + pub fn remove_member(origin: OriginFor, who: T::AccountId) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + let record = Self::ensure_member(&who)?; + let (count, votes) = MemberCount::::get(); + let count = count.checked_sub(1).ok_or(Underflow)?; + let votes = votes.checked_sub(Self::rank_to_votes(record.rank)).ok_or(Underflow)?; + + let index = record.index; + if index != count { + let last = MemberByIndex::::get(count).ok_or(Error::::Corruption)?; + Members::::mutate(&last, |r| { + if let Some(ref mut r) = r { + r.index = index + } + }); + MemberByIndex::::insert(index, &last); + } + MemberByIndex::::remove(count); + Members::::remove(&who); + MemberCount::::put((count, votes)); + Self::deposit_event(Event::MemberRemoved { who }); + + Ok(()) + } + + /// Add an aye or nay vote for the sender to the given proposal. + /// + /// - `origin`: Must be `Signed` by a member account. + /// - `poll`: Index of a poll which is ongoing. + /// - `aye`: `true` if the vote is to approve the proposal, `false` otherwise. + /// + /// Transaction fees are be waived if the member is voting on any particular proposal + /// for the first time and the call is successful. Subsequent vote changes will charge a + /// fee. + /// + /// Weight: `O(1)`, less if there was no previous vote on the poll by the member. + #[pallet::weight(T::BlockWeights::get().max_block / 10)] + pub fn vote( + origin: OriginFor, + poll: PollIndexOf, + aye: bool, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let record = Self::ensure_member(&who)?; + use VoteRecord::*; + let votes = Self::rank_to_votes(record.rank); + let vote = VoteRecord::from((aye, votes)); + let mut pays = Pays::Yes; + + T::Polls::try_access_poll(poll, |mut status| -> DispatchResult { + match status { + PollStatus::None | PollStatus::Completed(..) => Err(Error::::NotPolling)?, + PollStatus::Ongoing(ref mut tally, _) => { + match Voting::::get(&poll, &who) { + Some(Aye(votes)) => tally.ayes.saturating_reduce(votes), + Some(Nay(votes)) => tally.nays.saturating_reduce(votes), + None => pays = Pays::No, + } + match aye { + true => tally.ayes.saturating_accrue(votes), + false => tally.nays.saturating_accrue(votes), + } + Voting::::insert(&poll, &who, &vote); + Self::deposit_event(Event::Voted { who, poll, vote, tally: tally.clone() }); + Ok(()) + }, + } + })?; + Ok(pays.into()) + } + + /// Remove votes from the given poll. It must have ended. + /// + /// - `origin`: Must be `Signed` by any account. + /// - `poll_index`: Index of a poll which is completed and for which votes continue to + /// exist. + /// - `max`: Maximum number of vote items from remove in this call. + /// + /// Transaction fees are waived if the operation is successful. + /// + /// Weight `O(max)` (less if there are fewer items to remove than `max`). + #[pallet::weight(T::BlockWeights::get().max_block / 10)] + pub fn cleanup_poll( + origin: OriginFor, + poll_index: PollIndexOf, + max: u32, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + ensure!(T::Polls::as_ongoing(poll_index).is_none(), Error::::Ongoing); + + use sp_io::KillStorageResult::*; + let _count = match Voting::::remove_prefix(poll_index, Some(max)) { + AllRemoved(0) => Err(Error::::NoneRemaining)?, + AllRemoved(n) | SomeRemaining(n) => n, + }; + // TODO: weight from count. + Ok(Pays::No.into()) + } + } + + impl, I: 'static> Pallet { + fn ensure_member(who: &T::AccountId) -> Result { + Members::::get(who).ok_or(Error::::NotMember.into()) + } + + fn rank_to_votes(r: Rank) -> Votes { + let r = r as Votes; + r * (r + 1) / 2 + } + + pub(crate) fn member_count() -> MemberIndex { + MemberCount::::get().0 + } + + pub(crate) fn max_turnout() -> Votes { + MemberCount::::get().1 + } + } + + impl, I: 'static> Get for Pallet { + fn get() -> Votes { + MemberCount::::get().1 + } + } +} diff --git a/frame/ranked-collective/src/tests.rs b/frame/ranked-collective/src/tests.rs new file mode 100644 index 0000000000000..6b559bc1064fd --- /dev/null +++ b/frame/ranked-collective/src/tests.rs @@ -0,0 +1,287 @@ +// 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. + +//! The crate's tests. + +use std::collections::BTreeMap; + +use frame_support::{ + assert_noop, assert_ok, parameter_types, + traits::{ConstU32, ConstU64, Everything, Polling}, +}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +use super::*; +use crate as pallet_ranked_collective; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Club: pallet_ranked_collective::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1_000_000); +} +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + 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 = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum TestPollState { + Ongoing(TallyOf, u8), + Completed(u64, bool), +} +use TestPollState::*; + +parameter_types! { + pub static Polls: BTreeMap = vec![ + (1, Completed(1, true)), + (2, Completed(2, false)), + (3, Ongoing(Tally::from_parts(0, 0), 0)), + ].into_iter().collect(); +} + +pub struct TestPolls; +impl Polling> for TestPolls { + type Index = u8; + type Votes = Votes; + type Moment = u64; + type Class = u8; + fn classes() -> Vec { + vec![0, 1, 2] + } + fn as_ongoing(index: u8) -> Option<(TallyOf, Self::Class)> { + Polls::get().remove(&index).and_then(|x| { + if let TestPollState::Ongoing(t, c) = x { + Some((t, c)) + } else { + None + } + }) + } + fn access_poll( + index: Self::Index, + f: impl FnOnce(PollStatus<&mut TallyOf, Self::Moment, Self::Class>) -> R, + ) -> R { + let mut polls = Polls::get(); + let entry = polls.get_mut(&index); + let r = match entry { + Some(Ongoing(ref mut tally_mut_ref, class)) => + f(PollStatus::Ongoing(tally_mut_ref, *class)), + Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), + None => f(PollStatus::None), + }; + Polls::set(polls); + r + } + fn try_access_poll( + index: Self::Index, + f: impl FnOnce( + PollStatus<&mut TallyOf, Self::Moment, Self::Class>, + ) -> Result, + ) -> Result { + let mut polls = Polls::get(); + let entry = polls.get_mut(&index); + let r = match entry { + Some(Ongoing(ref mut tally_mut_ref, class)) => + f(PollStatus::Ongoing(tally_mut_ref, *class)), + Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), + None => f(PollStatus::None), + }?; + Polls::set(polls); + Ok(r) + } + + #[cfg(feature = "runtime-benchmarks")] + fn create_ongoing(class: Self::Class) -> Result { + let mut polls = Polls::get(); + let i = polls.keys().rev().next().map_or(0, |x| x + 1); + polls.insert(i, Ongoing(Tally::default(), class)); + Polls::set(polls); + Ok(i) + } + + #[cfg(feature = "runtime-benchmarks")] + fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()> { + let mut polls = Polls::get(); + match polls.get(&index) { + Some(Ongoing(..)) => {}, + _ => return Err(()), + } + let now = frame_system::Pallet::::block_number(); + polls.insert(index, Completed(now, approved)); + Polls::set(polls); + Ok(()) + } +} + +impl Config for Test { + type WeightInfo = (); + type Event = Event; + type AdminOrigin = frame_system::EnsureRoot; + type Polls = TestPolls; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn next_block() { + System::set_block_number(System::block_number() + 1); +} + +#[allow(dead_code)] +fn run_to(n: u64) { + while System::block_number() < n { + next_block(); + } +} + +fn tally(index: u8) -> TallyOf { + >>::as_ongoing(index).expect("No poll").0 +} + +#[test] +#[ignore] +#[should_panic(expected = "No poll")] +fn unknown_poll_should_panic() { + let _ = tally(0); +} + +#[test] +#[ignore] +#[should_panic(expected = "No poll")] +fn completed_poll_should_panic() { + let _ = tally(1); +} + +#[test] +fn basic_stuff() { + new_test_ext().execute_with(|| { + assert_eq!(tally(3), Tally::from_parts(0, 0)); + }); +} + +#[test] +fn membership_works() { + new_test_ext().execute_with(|| { + assert_noop!(Club::add_member(Origin::signed(1), 1, 1), DispatchError::BadOrigin); + assert_ok!(Club::add_member(Origin::root(), 1, 1)); + assert_eq!(Club::member_count(), 1); + assert_eq!(Club::max_turnout(), 1); + + assert_ok!(Club::add_member(Origin::root(), 2, 4)); + assert_eq!(Club::member_count(), 2); + assert_eq!(Club::max_turnout(), 11); + + assert_ok!(Club::set_member_rank(Origin::root(), 1, 4)); + assert_eq!(Club::member_count(), 2); + assert_eq!(Club::max_turnout(), 20); + + assert_noop!(Club::remove_member(Origin::signed(1), 1), DispatchError::BadOrigin); + assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_eq!(Club::member_count(), 1); + assert_eq!(Club::max_turnout(), 10); + }); +} + +#[test] +fn voting_works() { + new_test_ext().execute_with(|| { + assert_ok!(Club::add_member(Origin::root(), 1, 1)); + assert_ok!(Club::add_member(Origin::root(), 2, 2)); + assert_ok!(Club::add_member(Origin::root(), 3, 3)); + + assert_ok!(Club::vote(Origin::signed(1), 3, true)); + assert_eq!(tally(3), Tally::from_parts(1, 0)); + assert_ok!(Club::vote(Origin::signed(1), 3, false)); + assert_eq!(tally(3), Tally::from_parts(0, 1)); + + assert_ok!(Club::vote(Origin::signed(2), 3, true)); + assert_eq!(tally(3), Tally::from_parts(3, 1)); + assert_ok!(Club::vote(Origin::signed(2), 3, false)); + assert_eq!(tally(3), Tally::from_parts(0, 4)); + + assert_ok!(Club::vote(Origin::signed(3), 3, true)); + assert_eq!(tally(3), Tally::from_parts(6, 4)); + assert_ok!(Club::vote(Origin::signed(3), 3, false)); + assert_eq!(tally(3), Tally::from_parts(0, 10)); + }); +} + +#[test] +fn cleanup_works() { + new_test_ext().execute_with(|| { + assert_ok!(Club::add_member(Origin::root(), 1, 1)); + assert_ok!(Club::add_member(Origin::root(), 2, 2)); + assert_ok!(Club::add_member(Origin::root(), 3, 3)); + + assert_ok!(Club::vote(Origin::signed(1), 3, true)); + assert_ok!(Club::vote(Origin::signed(2), 3, false)); + assert_ok!(Club::vote(Origin::signed(3), 3, true)); + + assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::::Ongoing); + Polls::set( + vec![(1, Completed(1, true)), (2, Completed(2, false)), (3, Completed(3, true))] + .into_iter() + .collect(), + ); + // NOTE: This will fail until #10016 is merged. + // assert_ok!(Club::cleanup_poll(Origin::signed(4), 3, 10)); + assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::::NoneRemaining); + }); +} diff --git a/frame/ranked-collective/src/weights.rs b/frame/ranked-collective/src/weights.rs new file mode 100644 index 0000000000000..1280ced89eeea --- /dev/null +++ b/frame/ranked-collective/src/weights.rs @@ -0,0 +1,326 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_collective +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-01-30, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_collective +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/collective/src/weights.rs +// --template=.maintain/frame-weight-template.hbs +// --header=HEADER-APACHE2 +// --raw + +#![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_collective. +pub trait WeightInfo { + fn set_members(m: u32, n: u32, p: u32, ) -> Weight; + fn execute(b: u32, m: u32, ) -> Weight; + fn propose_execute(b: u32, m: u32, ) -> Weight; + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight; + fn vote(m: u32, ) -> Weight; + fn close_early_disapproved(m: u32, p: u32, ) -> Weight; + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn close_disapproved(m: u32, p: u32, ) -> Weight; + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn disapprove_proposal(p: u32, ) -> Weight; +} + +/// Weights for pallet_collective using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Council Members (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Voting (r:100 w:100) + // Storage: Council Prime (r:0 w:1) + fn set_members(m: u32, n: u32, p: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 10_000 + .saturating_add((14_493_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 10_000 + .saturating_add((23_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 10_000 + .saturating_add((16_909_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(p as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(p as Weight))) + } + // Storage: Council Members (r:1 w:0) + fn execute(b: u32, m: u32, ) -> Weight { + (12_790_000 as Weight) + // Standard Error: 0 + .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 0 + .saturating_add((73_000 as Weight).saturating_mul(m as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:0) + fn propose_execute(b: u32, m: u32, ) -> Weight { + (15_087_000 as Weight) + // Standard Error: 0 + .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 0 + .saturating_add((135_000 as Weight).saturating_mul(m as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalCount (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + (18_269_000 as Weight) + // Standard Error: 0 + .saturating_add((8_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_000 + .saturating_add((77_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council Voting (r:1 w:1) + fn vote(m: u32, ) -> Weight { + (26_624_000 as Weight) + // Standard Error: 2_000 + .saturating_add((161_000 as Weight).saturating_mul(m as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + (26_527_000 as Weight) + // Standard Error: 1_000 + .saturating_add((127_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((155_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + (26_352_000 as Weight) + // Standard Error: 0 + .saturating_add((6_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_000 + .saturating_add((154_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_disapproved(m: u32, p: u32, ) -> Weight { + (28_638_000 as Weight) + // Standard Error: 1_000 + .saturating_add((133_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((162_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + (29_946_000 as Weight) + // Standard Error: 0 + .saturating_add((5_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 2_000 + .saturating_add((151_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 2_000 + .saturating_add((201_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Council Proposals (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn disapprove_proposal(p: u32, ) -> Weight { + (15_778_000 as Weight) + // Standard Error: 1_000 + .saturating_add((206_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Council Members (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Voting (r:100 w:100) + // Storage: Council Prime (r:0 w:1) + fn set_members(m: u32, n: u32, p: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 10_000 + .saturating_add((14_493_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 10_000 + .saturating_add((23_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 10_000 + .saturating_add((16_909_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(p as Weight))) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(p as Weight))) + } + // Storage: Council Members (r:1 w:0) + fn execute(b: u32, m: u32, ) -> Weight { + (12_790_000 as Weight) + // Standard Error: 0 + .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 0 + .saturating_add((73_000 as Weight).saturating_mul(m as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:0) + fn propose_execute(b: u32, m: u32, ) -> Weight { + (15_087_000 as Weight) + // Standard Error: 0 + .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 0 + .saturating_add((135_000 as Weight).saturating_mul(m as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalCount (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + (18_269_000 as Weight) + // Standard Error: 0 + .saturating_add((8_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_000 + .saturating_add((77_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council Voting (r:1 w:1) + fn vote(m: u32, ) -> Weight { + (26_624_000 as Weight) + // Standard Error: 2_000 + .saturating_add((161_000 as Weight).saturating_mul(m as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + (26_527_000 as Weight) + // Standard Error: 1_000 + .saturating_add((127_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((155_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + (26_352_000 as Weight) + // Standard Error: 0 + .saturating_add((6_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 1_000 + .saturating_add((154_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_disapproved(m: u32, p: u32, ) -> Weight { + (28_638_000 as Weight) + // Standard Error: 1_000 + .saturating_add((133_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 1_000 + .saturating_add((162_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + (29_946_000 as Weight) + // Standard Error: 0 + .saturating_add((5_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 2_000 + .saturating_add((151_000 as Weight).saturating_mul(m as Weight)) + // Standard Error: 2_000 + .saturating_add((201_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Council Proposals (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn disapprove_proposal(p: u32, ) -> Weight { + (15_778_000 as Weight) + // Standard Error: 1_000 + .saturating_add((206_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } +} From e8f076589053794cbfb8349db738b62655e85652 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 16:15:08 +0200 Subject: [PATCH 42/48] Fixes --- Cargo.lock | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 48b4de9f12d67..97647ba6efe22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5989,6 +5989,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-ranked-collective" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-recovery" version = "4.0.0-dev" From 60d357e9931bd74a5efbcdbef0c63bc27ab6cb70 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 17:14:09 +0200 Subject: [PATCH 43/48] benchmarks --- frame/ranked-collective/src/benchmarking.rs | 46 ++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/frame/ranked-collective/src/benchmarking.rs b/frame/ranked-collective/src/benchmarking.rs index 4d972e211fea9..26dc83ed0382c 100644 --- a/frame/ranked-collective/src/benchmarking.rs +++ b/frame/ranked-collective/src/benchmarking.rs @@ -24,7 +24,7 @@ use sp_runtime::traits::Bounded; use sp_std::mem::size_of; use frame_benchmarking::{account, benchmarks_instance_pallet, whitelisted_caller}; -use frame_support::dispatch::UnfilteredDispatchable; +use frame_support::{assert_ok, dispatch::UnfilteredDispatchable}; use frame_system::{Call as SystemCall, Pallet as System, RawOrigin as SystemOrigin}; const SEED: u32 = 0; @@ -37,7 +37,7 @@ fn assert_last_event, I: 'static>(generic_event: >:: fn make_member, I: 'static>(rank: Rank) -> T::AccountId { let who = account::("member", MemberCount::::get().0, SEED); - Pallet::::add_member(T::AdminOrigin::successful_origin(), who.clone(), rank); + assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), who.clone(), rank)); who } @@ -81,5 +81,47 @@ benchmarks_instance_pallet! { assert_last_event::(Event::RankChanged { who, rank }.into()); } + vote { + let rank = 1; + let caller: T::AccountId = whitelisted_caller(); + Pallet::::add_member(T::AdminOrigin::successful_origin(), caller.clone(), rank); + // Create a poll + let class = T::Polls::classes().into_iter().next().expect("Must always be at least one class"); + let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll"); + + // Vote once. + assert_ok!(Pallet::::vote(SystemOrigin::Signed(caller.clone()).into(), poll, true)); + }: _(SystemOrigin::Signed(caller.clone()), poll, false) + verify { + let tally = Tally::from_parts(0, 1); + let ev = Event::Voted { who: caller, poll, vote: VoteRecord::Nay(1), tally }; + assert_last_event::(ev.into()); + } + + cleanup_poll { + let rank = 1; + let n in 1 .. 100; + + dbg!(n); + // Create a poll + let class = T::Polls::classes().into_iter().next().expect("Must always be at least one class"); + let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll"); + + // Vote in the poll by each of `n` members + for i in 0..n { + let who = make_member::(rank); + assert_ok!(Pallet::::vote(SystemOrigin::Signed(who).into(), poll, true)); + } + + // End the poll. + T::Polls::end_ongoing(poll, false).expect("Must always be able to end a poll"); + + assert_eq!(Voting::::iter_prefix(poll).count(), n as usize); + dbg!(Voting::::iter_prefix(poll).count()); + }: _(SystemOrigin::Signed(whitelisted_caller()), poll, n) + verify { + assert_eq!(Voting::::iter().count(), 0); + } + impl_benchmark_test_suite!(Collective, crate::tests::new_test_ext(), crate::tests::Test); } From 369335063636d89611dee7a03e79ff7717458664 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 18 May 2022 17:19:12 +0100 Subject: [PATCH 44/48] Weights --- Cargo.lock | 1 + bin/node/runtime/Cargo.toml | 4 + bin/node/runtime/src/lib.rs | 30 +- frame/ranked-collective/src/benchmarking.rs | 16 +- frame/ranked-collective/src/lib.rs | 36 +-- frame/ranked-collective/src/tests.rs | 28 +- frame/ranked-collective/src/weights.rs | 333 +++++--------------- 7 files changed, 149 insertions(+), 299 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97647ba6efe22..b4e5ea7dfc181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4764,6 +4764,7 @@ dependencies = [ "pallet-preimage", "pallet-proxy", "pallet-randomness-collective-flip", + "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 1b3e9083d1a1d..5ecd6ccedaf01 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -83,6 +83,7 @@ pallet-offences-benchmarking = { version = "4.0.0-dev", path = "../../../frame/o pallet-preimage = { version = "4.0.0-dev", default-features = false, path = "../../../frame/preimage" } pallet-proxy = { version = "4.0.0-dev", default-features = false, path = "../../../frame/proxy" } pallet-randomness-collective-flip = { version = "4.0.0-dev", default-features = false, path = "../../../frame/randomness-collective-flip" } +pallet-ranked-collective = { version = "4.0.0-dev", default-features = false, path = "../../../frame/ranked-collective" } pallet-recovery = { version = "4.0.0-dev", default-features = false, path = "../../../frame/recovery" } pallet-referenda = { version = "4.0.0-dev", default-features = false, path = "../../../frame/referenda" } pallet-remark = { version = "4.0.0-dev", default-features = false, path = "../../../frame/remark" } @@ -176,6 +177,7 @@ std = [ "pallet-utility/std", "sp-version/std", "pallet-society/std", + "pallet-ranked-collective/std", "pallet-referenda/std", "pallet-remark/std", "pallet-recovery/std", @@ -218,6 +220,7 @@ runtime-benchmarks = [ "pallet-preimage/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", + "pallet-ranked-collective/runtime-benchmarks", "pallet-referenda/runtime-benchmarks", "pallet-recovery/runtime-benchmarks", "pallet-remark/runtime-benchmarks", @@ -265,6 +268,7 @@ try-runtime = [ "pallet-offences/try-runtime", "pallet-preimage/try-runtime", "pallet-proxy/try-runtime", + "pallet-ranked-collective/try-runtime", "pallet-randomness-collective-flip/try-runtime", "pallet-recovery/try-runtime", "pallet-referenda/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index fdb8fef06cbec..b26512f6107ab 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -20,7 +20,7 @@ #![cfg_attr(not(feature = "std"), no_std)] // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. -#![recursion_limit = "256"] +#![recursion_limit = "512"] use codec::{Decode, Encode, MaxEncodedLen}; use frame_election_provider_support::{ @@ -837,6 +837,31 @@ impl pallet_referenda::Config for Runtime { type Tracks = TracksInfo; } +impl pallet_referenda::Config for Runtime { + type WeightInfo = pallet_referenda::weights::SubstrateWeight; + type Call = Call; + type Event = Event; + type Scheduler = Scheduler; + type Currency = pallet_balances::Pallet; + type CancelOrigin = EnsureRoot; + type KillOrigin = EnsureRoot; + type Slash = (); + type Votes = pallet_ranked_collective::Votes; + type Tally = pallet_ranked_collective::TallyOf; + type SubmissionDeposit = SubmissionDeposit; + type MaxQueued = ConstU32<100>; + type UndecidingTimeout = UndecidingTimeout; + type AlarmInterval = AlarmInterval; + type Tracks = TracksInfo; +} + +impl pallet_ranked_collective::Config for Runtime { + type WeightInfo = pallet_ranked_collective::weights::SubstrateWeight; + type Event = Event; + type AdminOrigin = EnsureRoot; + type Polls = RankedPolls; +} + impl pallet_remark::Config for Runtime { type WeightInfo = pallet_remark::weights::SubstrateWeight; type Event = Event; @@ -1534,6 +1559,8 @@ construct_runtime!( ConvictionVoting: pallet_conviction_voting, Whitelist: pallet_whitelist, NominationPools: pallet_nomination_pools, + RankedPolls: pallet_referenda::, + RankedCollective: pallet_ranked_collective, } ); @@ -1622,6 +1649,7 @@ mod benches { [pallet_offences, OffencesBench::] [pallet_preimage, Preimage] [pallet_proxy, Proxy] + [pallet_ranked_collective, RankedCollective] [pallet_referenda, Referenda] [pallet_recovery, Recovery] [pallet_remark, Remark] diff --git a/frame/ranked-collective/src/benchmarking.rs b/frame/ranked-collective/src/benchmarking.rs index 26dc83ed0382c..2900773813a4a 100644 --- a/frame/ranked-collective/src/benchmarking.rs +++ b/frame/ranked-collective/src/benchmarking.rs @@ -18,19 +18,15 @@ //! Staking pallet benchmarking. use super::*; -use crate::Pallet as Collective; - -use sp_runtime::traits::Bounded; -use sp_std::mem::size_of; +#[allow(unused_imports)] +use crate::Pallet as RankedCollective; use frame_benchmarking::{account, benchmarks_instance_pallet, whitelisted_caller}; use frame_support::{assert_ok, dispatch::UnfilteredDispatchable}; -use frame_system::{Call as SystemCall, Pallet as System, RawOrigin as SystemOrigin}; +use frame_system::RawOrigin as SystemOrigin; const SEED: u32 = 0; -const MAX_BYTES: u32 = 1_024; - fn assert_last_event, I: 'static>(generic_event: >::Event) { frame_system::Pallet::::assert_last_event(generic_event.into()); } @@ -84,7 +80,7 @@ benchmarks_instance_pallet! { vote { let rank = 1; let caller: T::AccountId = whitelisted_caller(); - Pallet::::add_member(T::AdminOrigin::successful_origin(), caller.clone(), rank); + assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), caller.clone(), rank)); // Create a poll let class = T::Polls::classes().into_iter().next().expect("Must always be at least one class"); let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll"); @@ -102,7 +98,6 @@ benchmarks_instance_pallet! { let rank = 1; let n in 1 .. 100; - dbg!(n); // Create a poll let class = T::Polls::classes().into_iter().next().expect("Must always be at least one class"); let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll"); @@ -117,11 +112,10 @@ benchmarks_instance_pallet! { T::Polls::end_ongoing(poll, false).expect("Must always be able to end a poll"); assert_eq!(Voting::::iter_prefix(poll).count(), n as usize); - dbg!(Voting::::iter_prefix(poll).count()); }: _(SystemOrigin::Signed(whitelisted_caller()), poll, n) verify { assert_eq!(Voting::::iter().count(), 0); } - impl_benchmark_test_suite!(Collective, crate::tests::new_test_ext(), crate::tests::Test); + impl_benchmark_test_suite!(RankedCollective, crate::tests::new_test_ext(), crate::tests::Test); } diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs index ef23cdf648c28..9f3d9caba7c17 100644 --- a/frame/ranked-collective/src/lib.rs +++ b/frame/ranked-collective/src/lib.rs @@ -55,6 +55,7 @@ use frame_support::{ dispatch::{DispatchError, DispatchResultWithPostInfo}, ensure, traits::{EnsureOrigin, Get, PollStatus, Polling, VoteTally}, + weights::PostDispatchInfo, CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; @@ -251,7 +252,7 @@ pub mod pallet { /// - `rank`: The rank to give the new member. /// /// Weight: `O(1)` - #[pallet::weight(T::BlockWeights::get().max_block / 10)] + #[pallet::weight(T::WeightInfo::add_member())] pub fn add_member(origin: OriginFor, who: T::AccountId, rank: Rank) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; ensure!(!Members::::contains_key(&who), Error::::AlreadyMember); @@ -274,7 +275,7 @@ pub mod pallet { /// - `rank`: The new rank to give the member. /// /// Weight: `O(1)` - #[pallet::weight(T::BlockWeights::get().max_block / 10)] + #[pallet::weight(T::WeightInfo::set_member_rank())] pub fn set_member_rank( origin: OriginFor, who: T::AccountId, @@ -303,7 +304,7 @@ pub mod pallet { /// - `who`: Account of existing member to be removed. /// /// Weight: `O(1)`, less if the member's index is highest. - #[pallet::weight(T::BlockWeights::get().max_block / 10)] + #[pallet::weight(T::WeightInfo::remove_member())] pub fn remove_member(origin: OriginFor, who: T::AccountId) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; let record = Self::ensure_member(&who)?; @@ -340,7 +341,7 @@ pub mod pallet { /// fee. /// /// Weight: `O(1)`, less if there was no previous vote on the poll by the member. - #[pallet::weight(T::BlockWeights::get().max_block / 10)] + #[pallet::weight(T::WeightInfo::vote())] pub fn vote( origin: OriginFor, poll: PollIndexOf, @@ -353,7 +354,7 @@ pub mod pallet { let vote = VoteRecord::from((aye, votes)); let mut pays = Pays::Yes; - T::Polls::try_access_poll(poll, |mut status| -> DispatchResult { + let tally = T::Polls::try_access_poll(poll, |mut status| -> Result, DispatchError> { match status { PollStatus::None | PollStatus::Completed(..) => Err(Error::::NotPolling)?, PollStatus::Ongoing(ref mut tally, _) => { @@ -367,11 +368,11 @@ pub mod pallet { false => tally.nays.saturating_accrue(votes), } Voting::::insert(&poll, &who, &vote); - Self::deposit_event(Event::Voted { who, poll, vote, tally: tally.clone() }); - Ok(()) + Ok(tally.clone()) }, } })?; + Self::deposit_event(Event::Voted { who, poll, vote, tally }); Ok(pays.into()) } @@ -385,7 +386,7 @@ pub mod pallet { /// Transaction fees are waived if the operation is successful. /// /// Weight `O(max)` (less if there are fewer items to remove than `max`). - #[pallet::weight(T::BlockWeights::get().max_block / 10)] + #[pallet::weight(T::WeightInfo::cleanup_poll(*max))] pub fn cleanup_poll( origin: OriginFor, poll_index: PollIndexOf, @@ -395,12 +396,15 @@ pub mod pallet { ensure!(T::Polls::as_ongoing(poll_index).is_none(), Error::::Ongoing); use sp_io::KillStorageResult::*; - let _count = match Voting::::remove_prefix(poll_index, Some(max)) { - AllRemoved(0) => Err(Error::::NoneRemaining)?, + let count = match Voting::::remove_prefix(poll_index, Some(max)) { +// AllRemoved(0) => Err(Error::::NoneRemaining)?, + AllRemoved(0) => return Ok(Pays::Yes.into()), AllRemoved(n) | SomeRemaining(n) => n, }; - // TODO: weight from count. - Ok(Pays::No.into()) + Ok(PostDispatchInfo { + actual_weight: Some(T::WeightInfo::cleanup_poll(count)), + pays_fee: Pays::No, + }) } } @@ -413,14 +417,6 @@ pub mod pallet { let r = r as Votes; r * (r + 1) / 2 } - - pub(crate) fn member_count() -> MemberIndex { - MemberCount::::get().0 - } - - pub(crate) fn max_turnout() -> Votes { - MemberCount::::get().1 - } } impl, I: 'static> Get for Pallet { diff --git a/frame/ranked-collective/src/tests.rs b/frame/ranked-collective/src/tests.rs index 6b559bc1064fd..948387f35833d 100644 --- a/frame/ranked-collective/src/tests.rs +++ b/frame/ranked-collective/src/tests.rs @@ -184,6 +184,14 @@ fn next_block() { System::set_block_number(System::block_number() + 1); } +fn member_count() -> MemberIndex { + MemberCount::::get().0 +} + +fn max_turnout() -> Votes { + MemberCount::::get().1 +} + #[allow(dead_code)] fn run_to(n: u64) { while System::block_number() < n { @@ -221,21 +229,21 @@ fn membership_works() { new_test_ext().execute_with(|| { assert_noop!(Club::add_member(Origin::signed(1), 1, 1), DispatchError::BadOrigin); assert_ok!(Club::add_member(Origin::root(), 1, 1)); - assert_eq!(Club::member_count(), 1); - assert_eq!(Club::max_turnout(), 1); + assert_eq!(member_count(), 1); + assert_eq!(max_turnout(), 1); assert_ok!(Club::add_member(Origin::root(), 2, 4)); - assert_eq!(Club::member_count(), 2); - assert_eq!(Club::max_turnout(), 11); + assert_eq!(member_count(), 2); + assert_eq!(max_turnout(), 11); assert_ok!(Club::set_member_rank(Origin::root(), 1, 4)); - assert_eq!(Club::member_count(), 2); - assert_eq!(Club::max_turnout(), 20); + assert_eq!(member_count(), 2); + assert_eq!(max_turnout(), 20); assert_noop!(Club::remove_member(Origin::signed(1), 1), DispatchError::BadOrigin); assert_ok!(Club::remove_member(Origin::root(), 1)); - assert_eq!(Club::member_count(), 1); - assert_eq!(Club::max_turnout(), 10); + assert_eq!(member_count(), 1); + assert_eq!(max_turnout(), 10); }); } @@ -280,8 +288,8 @@ fn cleanup_works() { .into_iter() .collect(), ); + assert_ok!(Club::cleanup_poll(Origin::signed(4), 3, 10)); // NOTE: This will fail until #10016 is merged. - // assert_ok!(Club::cleanup_poll(Origin::signed(4), 3, 10)); - assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::::NoneRemaining); +// assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::::NoneRemaining); }); } diff --git a/frame/ranked-collective/src/weights.rs b/frame/ranked-collective/src/weights.rs index 1280ced89eeea..ed08564774c1d 100644 --- a/frame/ranked-collective/src/weights.rs +++ b/frame/ranked-collective/src/weights.rs @@ -15,27 +15,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Autogenerated weights for pallet_collective +//! Autogenerated weights for pallet_ranked_collective //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-01-30, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-05-18, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/substrate +// ../../../target/release/substrate // benchmark +// pallet // --chain=dev // --steps=50 // --repeat=20 -// --pallet=pallet_collective +// --pallet=pallet-ranked-collective // --extrinsic=* // --execution=wasm -// --wasm-execution=compiled // --heap-pages=4096 -// --output=./frame/collective/src/weights.rs -// --template=.maintain/frame-weight-template.hbs -// --header=HEADER-APACHE2 -// --raw +// --output=../../../frame/ranked-collective/src/weights.rs +// --template=../../../.maintain/frame-weight-template.hbs +// --header=../../../HEADER-APACHE2 +// --record-proof #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -44,283 +44,102 @@ use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; use sp_std::marker::PhantomData; -/// Weight functions needed for pallet_collective. +/// Weight functions needed for pallet_ranked_collective. pub trait WeightInfo { - fn set_members(m: u32, n: u32, p: u32, ) -> Weight; - fn execute(b: u32, m: u32, ) -> Weight; - fn propose_execute(b: u32, m: u32, ) -> Weight; - fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight; - fn vote(m: u32, ) -> Weight; - fn close_early_disapproved(m: u32, p: u32, ) -> Weight; - fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight; - fn close_disapproved(m: u32, p: u32, ) -> Weight; - fn close_approved(b: u32, m: u32, p: u32, ) -> Weight; - fn disapprove_proposal(p: u32, ) -> Weight; + fn add_member() -> Weight; + fn remove_member() -> Weight; + fn set_member_rank() -> Weight; + fn vote() -> Weight; + fn cleanup_poll(n: u32, ) -> Weight; } -/// Weights for pallet_collective using the Substrate node and recommended hardware. +/// Weights for pallet_ranked_collective using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - // Storage: Council Members (r:1 w:1) - // Storage: Council Proposals (r:1 w:0) - // Storage: Council Voting (r:100 w:100) - // Storage: Council Prime (r:0 w:1) - fn set_members(m: u32, n: u32, p: u32, ) -> Weight { - (0 as Weight) - // Standard Error: 10_000 - .saturating_add((14_493_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 10_000 - .saturating_add((23_000 as Weight).saturating_mul(n as Weight)) - // Standard Error: 10_000 - .saturating_add((16_909_000 as Weight).saturating_mul(p as Weight)) + // Storage: RankedCollective Members (r:1 w:1) + // Storage: RankedCollective MemberCount (r:1 w:1) + // Storage: RankedCollective MemberByIndex (r:0 w:1) + fn add_member() -> Weight { + (15_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(p as Weight))) - .saturating_add(T::DbWeight::get().writes(2 as Weight)) - .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(p as Weight))) - } - // Storage: Council Members (r:1 w:0) - fn execute(b: u32, m: u32, ) -> Weight { - (12_790_000 as Weight) - // Standard Error: 0 - .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 0 - .saturating_add((73_000 as Weight).saturating_mul(m as Weight)) - .saturating_add(T::DbWeight::get().reads(1 as Weight)) - } - // Storage: Council Members (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:0) - fn propose_execute(b: u32, m: u32, ) -> Weight { - (15_087_000 as Weight) - // Standard Error: 0 - .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 0 - .saturating_add((135_000 as Weight).saturating_mul(m as Weight)) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - } - // Storage: Council Members (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:1) - // Storage: Council Proposals (r:1 w:1) - // Storage: Council ProposalCount (r:1 w:1) - // Storage: Council Voting (r:0 w:1) - fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { - (18_269_000 as Weight) - // Standard Error: 0 - .saturating_add((8_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 1_000 - .saturating_add((77_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) - .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(4 as Weight)) - } - // Storage: Council Members (r:1 w:0) - // Storage: Council Voting (r:1 w:1) - fn vote(m: u32, ) -> Weight { - (26_624_000 as Weight) - // Standard Error: 2_000 - .saturating_add((161_000 as Weight).saturating_mul(m as Weight)) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council Proposals (r:1 w:1) - // Storage: Council ProposalOf (r:0 w:1) - fn close_early_disapproved(m: u32, p: u32, ) -> Weight { - (26_527_000 as Weight) - // Standard Error: 1_000 - .saturating_add((127_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((155_000 as Weight).saturating_mul(p as Weight)) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:1) - // Storage: Council Proposals (r:1 w:1) - fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { - (26_352_000 as Weight) - // Standard Error: 0 - .saturating_add((6_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 1_000 - .saturating_add((154_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) + // Storage: RankedCollective Members (r:2 w:2) + // Storage: RankedCollective MemberCount (r:1 w:1) + // Storage: RankedCollective MemberByIndex (r:1 w:2) + fn remove_member() -> Weight { + (23_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council Prime (r:1 w:0) - // Storage: Council Proposals (r:1 w:1) - // Storage: Council ProposalOf (r:0 w:1) - fn close_disapproved(m: u32, p: u32, ) -> Weight { - (28_638_000 as Weight) - // Standard Error: 1_000 - .saturating_add((133_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((162_000 as Weight).saturating_mul(p as Weight)) - .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(3 as Weight)) + // Storage: RankedCollective Members (r:1 w:1) + // Storage: RankedCollective MemberCount (r:1 w:1) + fn set_member_rank() -> Weight { + (15_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council Prime (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:1) - // Storage: Council Proposals (r:1 w:1) - fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { - (29_946_000 as Weight) - // Standard Error: 0 - .saturating_add((5_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 2_000 - .saturating_add((151_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 2_000 - .saturating_add((201_000 as Weight).saturating_mul(p as Weight)) + // Storage: RankedCollective Members (r:1 w:0) + // Storage: RankedPolls ReferendumInfoFor (r:1 w:1) + // Storage: RankedCollective Voting (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn vote() -> Weight { + (35_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) - .saturating_add(T::DbWeight::get().writes(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) } - // Storage: Council Proposals (r:1 w:1) - // Storage: Council Voting (r:0 w:1) - // Storage: Council ProposalOf (r:0 w:1) - fn disapprove_proposal(p: u32, ) -> Weight { - (15_778_000 as Weight) + // Storage: RankedPolls ReferendumInfoFor (r:1 w:0) + // Storage: RankedCollective Voting (r:0 w:1) + fn cleanup_poll(n: u32, ) -> Weight { + (7_800_000 as Weight) // Standard Error: 1_000 - .saturating_add((206_000 as Weight).saturating_mul(p as Weight)) + .saturating_add((860_000 as Weight).saturating_mul(n as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) - .saturating_add(T::DbWeight::get().writes(3 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(n as Weight))) } } // For backwards compatibility and tests impl WeightInfo for () { - // Storage: Council Members (r:1 w:1) - // Storage: Council Proposals (r:1 w:0) - // Storage: Council Voting (r:100 w:100) - // Storage: Council Prime (r:0 w:1) - fn set_members(m: u32, n: u32, p: u32, ) -> Weight { - (0 as Weight) - // Standard Error: 10_000 - .saturating_add((14_493_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 10_000 - .saturating_add((23_000 as Weight).saturating_mul(n as Weight)) - // Standard Error: 10_000 - .saturating_add((16_909_000 as Weight).saturating_mul(p as Weight)) + // Storage: RankedCollective Members (r:1 w:1) + // Storage: RankedCollective MemberCount (r:1 w:1) + // Storage: RankedCollective MemberByIndex (r:0 w:1) + fn add_member() -> Weight { + (15_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(p as Weight))) - .saturating_add(RocksDbWeight::get().writes(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(p as Weight))) - } - // Storage: Council Members (r:1 w:0) - fn execute(b: u32, m: u32, ) -> Weight { - (12_790_000 as Weight) - // Standard Error: 0 - .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 0 - .saturating_add((73_000 as Weight).saturating_mul(m as Weight)) - .saturating_add(RocksDbWeight::get().reads(1 as Weight)) - } - // Storage: Council Members (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:0) - fn propose_execute(b: u32, m: u32, ) -> Weight { - (15_087_000 as Weight) - // Standard Error: 0 - .saturating_add((2_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 0 - .saturating_add((135_000 as Weight).saturating_mul(m as Weight)) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - } - // Storage: Council Members (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:1) - // Storage: Council Proposals (r:1 w:1) - // Storage: Council ProposalCount (r:1 w:1) - // Storage: Council Voting (r:0 w:1) - fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { - (18_269_000 as Weight) - // Standard Error: 0 - .saturating_add((8_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 1_000 - .saturating_add((77_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) - .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(4 as Weight)) - } - // Storage: Council Members (r:1 w:0) - // Storage: Council Voting (r:1 w:1) - fn vote(m: u32, ) -> Weight { - (26_624_000 as Weight) - // Standard Error: 2_000 - .saturating_add((161_000 as Weight).saturating_mul(m as Weight)) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council Proposals (r:1 w:1) - // Storage: Council ProposalOf (r:0 w:1) - fn close_early_disapproved(m: u32, p: u32, ) -> Weight { - (26_527_000 as Weight) - // Standard Error: 1_000 - .saturating_add((127_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((155_000 as Weight).saturating_mul(p as Weight)) - .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:1) - // Storage: Council Proposals (r:1 w:1) - fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { - (26_352_000 as Weight) - // Standard Error: 0 - .saturating_add((6_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 1_000 - .saturating_add((154_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((203_000 as Weight).saturating_mul(p as Weight)) + // Storage: RankedCollective Members (r:2 w:2) + // Storage: RankedCollective MemberCount (r:1 w:1) + // Storage: RankedCollective MemberByIndex (r:1 w:2) + fn remove_member() -> Weight { + (23_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(5 as Weight)) } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council Prime (r:1 w:0) - // Storage: Council Proposals (r:1 w:1) - // Storage: Council ProposalOf (r:0 w:1) - fn close_disapproved(m: u32, p: u32, ) -> Weight { - (28_638_000 as Weight) - // Standard Error: 1_000 - .saturating_add((133_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 1_000 - .saturating_add((162_000 as Weight).saturating_mul(p as Weight)) - .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + // Storage: RankedCollective Members (r:1 w:1) + // Storage: RankedCollective MemberCount (r:1 w:1) + fn set_member_rank() -> Weight { + (15_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } - // Storage: Council Voting (r:1 w:1) - // Storage: Council Members (r:1 w:0) - // Storage: Council Prime (r:1 w:0) - // Storage: Council ProposalOf (r:1 w:1) - // Storage: Council Proposals (r:1 w:1) - fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { - (29_946_000 as Weight) - // Standard Error: 0 - .saturating_add((5_000 as Weight).saturating_mul(b as Weight)) - // Standard Error: 2_000 - .saturating_add((151_000 as Weight).saturating_mul(m as Weight)) - // Standard Error: 2_000 - .saturating_add((201_000 as Weight).saturating_mul(p as Weight)) + // Storage: RankedCollective Members (r:1 w:0) + // Storage: RankedPolls ReferendumInfoFor (r:1 w:1) + // Storage: RankedCollective Voting (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn vote() -> Weight { + (35_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } - // Storage: Council Proposals (r:1 w:1) - // Storage: Council Voting (r:0 w:1) - // Storage: Council ProposalOf (r:0 w:1) - fn disapprove_proposal(p: u32, ) -> Weight { - (15_778_000 as Weight) + // Storage: RankedPolls ReferendumInfoFor (r:1 w:0) + // Storage: RankedCollective Voting (r:0 w:1) + fn cleanup_poll(n: u32, ) -> Weight { + (7_800_000 as Weight) // Standard Error: 1_000 - .saturating_add((206_000 as Weight).saturating_mul(p as Weight)) + .saturating_add((860_000 as Weight).saturating_mul(n as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(n as Weight))) } } From 049b0f1099b1c21e2ceda7a0f8f95ab39566f600 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 12:17:49 +0100 Subject: [PATCH 45/48] Allow class voting in rank Use bare ayes for calculating support. Allow only promotion/demotion by one rank only. Allow removal of member with rank zero only. Use new Tally API --- frame/ranked-collective/src/benchmarking.rs | 68 ++++---- frame/ranked-collective/src/lib.rs | 172 ++++++++++++-------- frame/ranked-collective/src/tests.rs | 135 ++++++++++----- 3 files changed, 244 insertions(+), 131 deletions(-) diff --git a/frame/ranked-collective/src/benchmarking.rs b/frame/ranked-collective/src/benchmarking.rs index 2900773813a4a..f571d35debf7d 100644 --- a/frame/ranked-collective/src/benchmarking.rs +++ b/frame/ranked-collective/src/benchmarking.rs @@ -32,79 +32,89 @@ fn assert_last_event, I: 'static>(generic_event: >:: } fn make_member, I: 'static>(rank: Rank) -> T::AccountId { - let who = account::("member", MemberCount::::get().0, SEED); - assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), who.clone(), rank)); + let who = account::("member", MemberCount::::get(0), SEED); + assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), who.clone())); + for _ in 0..rank { + promote_member::(&who); + } who } +fn promote_member, I: 'static>(who: &T::AccountId) { + assert_ok!(Pallet::::promote(T::AdminOrigin::successful_origin(), who.clone())); +} + benchmarks_instance_pallet! { add_member { - let old = MemberCount::::get().0; let who = account::("member", 0, SEED); - let rank = 1; let origin = T::AdminOrigin::successful_origin(); - let call = Call::::add_member { who: who.clone(), rank }; + let call = Call::::add_member { who: who.clone() }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_eq!(MemberCount::::get().0, old + 1); - assert_last_event::(Event::MemberAdded { who, rank }.into()); + assert_eq!(MemberCount::::get(0), 1); + assert_last_event::(Event::MemberAdded { who }.into()); } remove_member { - let rank = 1; - let who = make_member::(rank); - let other = make_member::(rank); - let old = MemberCount::::get().0; - let other_index = Members::::get(&other).unwrap().index; + let first = make_member::(0); + let who = make_member::(0); + let last = make_member::(0); + let last_index = Members::::get(&last).unwrap().index; let origin = T::AdminOrigin::successful_origin(); let call = Call::::remove_member { who: who.clone() }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_eq!(MemberCount::::get().0, old - 1); - assert_ne!(other_index, Members::::get(&other).unwrap().index); + assert_eq!(MemberCount::::get(0), 2); + assert_ne!(last_index, Members::::get(&last).unwrap().index); assert_last_event::(Event::MemberRemoved { who }.into()); } - set_member_rank { - let old_rank = 1; - let rank = 2; - let who = make_member::(old_rank); + promote { + let who = make_member::(0); let origin = T::AdminOrigin::successful_origin(); - let call = Call::::set_member_rank { who: who.clone(), rank }; + let call = Call::::promote { who: who.clone() }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_eq!(Members::::get(&who).unwrap().rank, rank); - assert_last_event::(Event::RankChanged { who, rank }.into()); + assert_eq!(Members::::get(&who).unwrap().rank, 1); + assert_last_event::(Event::RankChanged { who, rank: 1 }.into()); + } + + demote { + let who = make_member::(1); + let origin = T::AdminOrigin::successful_origin(); + let call = Call::::demote { who: who.clone() }; + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_eq!(Members::::get(&who).unwrap().rank, 0); + assert_last_event::(Event::RankChanged { who, rank: 0 }.into()); } vote { - let rank = 1; let caller: T::AccountId = whitelisted_caller(); - assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), caller.clone(), rank)); + assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), caller.clone())); // Create a poll - let class = T::Polls::classes().into_iter().next().expect("Must always be at least one class"); - let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll"); + let class = 0; + let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll for rank 0"); // Vote once. assert_ok!(Pallet::::vote(SystemOrigin::Signed(caller.clone()).into(), poll, true)); }: _(SystemOrigin::Signed(caller.clone()), poll, false) verify { - let tally = Tally::from_parts(0, 1); + let tally = Tally::from_parts(0, 0, 1); let ev = Event::Voted { who: caller, poll, vote: VoteRecord::Nay(1), tally }; assert_last_event::(ev.into()); } cleanup_poll { - let rank = 1; let n in 1 .. 100; // Create a poll - let class = T::Polls::classes().into_iter().next().expect("Must always be at least one class"); + let class = 0; let poll = T::Polls::create_ongoing(class).expect("Must always be able to create a poll"); // Vote in the poll by each of `n` members for i in 0..n { - let who = make_member::(rank); + let who = make_member::(0); assert_ok!(Pallet::::vote(SystemOrigin::Signed(who).into(), poll, true)); } diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs index 9f3d9caba7c17..91b91c75cd346 100644 --- a/frame/ranked-collective/src/lib.rs +++ b/frame/ranked-collective/src/lib.rs @@ -54,9 +54,9 @@ use frame_support::{ codec::{Decode, Encode, MaxEncodedLen}, dispatch::{DispatchError, DispatchResultWithPostInfo}, ensure, - traits::{EnsureOrigin, Get, PollStatus, Polling, VoteTally}, + traits::{EnsureOrigin, PollStatus, Polling, VoteTally}, weights::PostDispatchInfo, - CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, + CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; #[cfg(test)] @@ -81,7 +81,6 @@ pub type Votes = u32; /// Aggregated votes for an ongoing poll. #[derive( CloneNoBound, - DefaultNoBound, PartialEqNoBound, EqNoBound, RuntimeDebugNoBound, @@ -91,45 +90,62 @@ pub type Votes = u32; MaxEncodedLen, )] #[scale_info(skip_type_params(M))] -pub struct Tally> { +pub struct Tally { + bare_ayes: MemberIndex, ayes: Votes, nays: Votes, dummy: PhantomData, } -impl> Tally { - fn from_parts(ayes: Votes, nays: Votes) -> Self { - Tally { ayes, nays, dummy: PhantomData } +impl Tally { + fn from_parts(bare_ayes: MemberIndex, ayes: Votes, nays: Votes) -> Self { + Tally { bare_ayes, ayes, nays, dummy: PhantomData } } } +// Use (non-rank-weighted) ayes for calculating support. +// Allow only promotion/demotion by one rank only. +// Allow removal of member with rank zero only. +// This keeps everything O(1) while still allowing arbitrary number of ranks. + +// All functions of VoteTally now include the class as a param. +// TODO: ** BEFORE COMMIT ** split and move into gg2t branch. + pub type TallyOf = Tally>; pub type PollIndexOf = <>::Polls as Polling>>::Index; -impl> VoteTally for Tally { - fn ayes(&self) -> Votes { - self.ayes +impl VoteTally for Tally { + fn new(_: Rank) -> Self { + Self { bare_ayes: 0, ayes: 0, nays: 0, dummy: PhantomData } + } + fn ayes(&self, _: Rank) -> Votes { + self.bare_ayes } - fn support(&self) -> Perbill { - Perbill::from_rational(self.ayes, M::get()) + fn support(&self, class: Rank) -> Perbill { + Perbill::from_rational(self.bare_ayes, M::get_max_voters(class)) } - fn approval(&self) -> Perbill { + fn approval(&self, _: Rank) -> Perbill { Perbill::from_rational(self.ayes, 1.max(self.ayes + self.nays)) } #[cfg(feature = "runtime-benchmarks")] - fn unanimity() -> Self { - Self { ayes: M::get(), nays: 0, dummy: PhantomData } + fn unanimity(class: Rank) -> Self { + Self { + bare_ayes: M::get_max_voters(class), + ayes: M::get_max_voters(class), + nays: 0, + dummy: PhantomData, + } } #[cfg(feature = "runtime-benchmarks")] - fn rejection() -> Self { - Self { ayes: 0, nays: M::get(), dummy: PhantomData } + fn rejection(class: Rank) -> Self { + Self { bare_ayes: 0, ayes: 0, nays: M::get_max_voters(class), dummy: PhantomData } } #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(support: Perbill, approval: Perbill) -> Self { - let c = M::get(); + fn from_requirements(support: Perbill, approval: Perbill, class: Rank) -> Self { + let c = M::get_max_voters(class); let ayes = support * c; let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes; - Self { ayes, nays, dummy: PhantomData } + Self { bare_ayes: ayes, ayes, nays, dummy: PhantomData } } } @@ -183,13 +199,14 @@ pub mod pallet { type AdminOrigin: EnsureOrigin; /// The polling system used for our voting. - type Polls: Polling, Votes = Votes, Moment = Self::BlockNumber>; + type Polls: Polling, Votes = Votes, Class = Rank, Moment = Self::BlockNumber>; } - /// The number of members in the collective. + /// The number of members in the collective who have at least the rank according to the index + /// of the vec. #[pallet::storage] pub type MemberCount, I: 'static = ()> = - StorageValue<_, (MemberIndex, Votes), ValueQuery>; + StorageMap<_, Twox64Concat, Rank, MemberIndex, ValueQuery>; /// The current members of the collective. #[pallet::storage] @@ -217,7 +234,7 @@ pub mod pallet { #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { /// A member has been added. - MemberAdded { who: T::AccountId, rank: Rank }, + MemberAdded { who: T::AccountId }, /// A member's rank has been changed. RankChanged { who: T::AccountId, rank: Rank }, /// A member has been removed. @@ -241,6 +258,11 @@ pub mod pallet { NoneRemaining, /// Unexpected error in state. Corruption, + /// The member's rank is too low to vote. + RankTooLow, + /// The member's rank is too high to be removed. + RankTooHigh, + } #[pallet::call] @@ -253,47 +275,58 @@ pub mod pallet { /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::add_member())] - pub fn add_member(origin: OriginFor, who: T::AccountId, rank: Rank) -> DispatchResult { + pub fn add_member(origin: OriginFor, who: T::AccountId) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; ensure!(!Members::::contains_key(&who), Error::::AlreadyMember); - let (index, mut votes) = MemberCount::::get(); + let index = MemberCount::::get(0); let count = index.checked_add(1).ok_or(Overflow)?; - votes = votes.checked_add(Self::rank_to_votes(rank)).ok_or(Overflow)?; - Members::::insert(&who, MemberRecord { rank, index }); + Members::::insert(&who, MemberRecord { rank: 0, index }); MemberByIndex::::insert(index, &who); - MemberCount::::put((count, votes)); - Self::deposit_event(Event::MemberAdded { who, rank }); + MemberCount::::insert(0, count); + Self::deposit_event(Event::MemberAdded { who }); Ok(()) } - /// Alter the rank of an existing member. + /// Increment the rank of an existing member by one. /// /// - `origin`: Must be the `AdminOrigin`. /// - `who`: Account of existing member. - /// - `rank`: The new rank to give the member. /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::set_member_rank())] - pub fn set_member_rank( + pub fn promote( + origin: OriginFor, + who: T::AccountId, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + let mut record = Self::ensure_member(&who)?; + record.rank = record.rank.checked_add(1).ok_or(Overflow)?; + MemberCount::::mutate(record.rank, |r| r.saturating_inc()); + Members::::insert(&who, &record); + Self::deposit_event(Event::RankChanged { who, rank: record.rank }); + + Ok(()) + } + + /// Decrement the rank of an existing member by one. + /// + /// - `origin`: Must be the `AdminOrigin`. + /// - `who`: Account of existing member of rank greater than zero. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::set_member_rank())] + pub fn demote( origin: OriginFor, who: T::AccountId, - rank: Rank, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; let mut record = Self::ensure_member(&who)?; - let (count, mut votes) = MemberCount::::get(); - votes = votes - .checked_sub(Self::rank_to_votes(record.rank)) - .ok_or(Underflow)? - .checked_add(Self::rank_to_votes(rank)) - .ok_or(Overflow)?; - record.rank = rank; - - MemberCount::::put((count, votes)); - Members::::insert(&who, record); - Self::deposit_event(Event::RankChanged { who, rank }); + record.rank = record.rank.checked_sub(1).ok_or(Underflow)?; + MemberCount::::mutate(record.rank + 1, |r| r.saturating_dec()); + Members::::insert(&who, &record); + Self::deposit_event(Event::RankChanged { who, rank: record.rank }); Ok(()) } @@ -308,13 +341,12 @@ pub mod pallet { pub fn remove_member(origin: OriginFor, who: T::AccountId) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; let record = Self::ensure_member(&who)?; - let (count, votes) = MemberCount::::get(); - let count = count.checked_sub(1).ok_or(Underflow)?; - let votes = votes.checked_sub(Self::rank_to_votes(record.rank)).ok_or(Underflow)?; + ensure!(record.rank == 0, Error::::RankTooHigh); + let last_index = MemberCount::::get(0).saturating_sub(1); let index = record.index; - if index != count { - let last = MemberByIndex::::get(count).ok_or(Error::::Corruption)?; + if index != last_index { + let last = MemberByIndex::::get(last_index).ok_or(Error::::Corruption)?; Members::::mutate(&last, |r| { if let Some(ref mut r) = r { r.index = index @@ -322,9 +354,9 @@ pub mod pallet { }); MemberByIndex::::insert(index, &last); } - MemberByIndex::::remove(count); + MemberCount::::insert(0, last_index); + MemberByIndex::::remove(last_index); Members::::remove(&who); - MemberCount::::put((count, votes)); Self::deposit_event(Event::MemberRemoved { who }); Ok(()) @@ -350,25 +382,31 @@ pub mod pallet { let who = ensure_signed(origin)?; let record = Self::ensure_member(&who)?; use VoteRecord::*; - let votes = Self::rank_to_votes(record.rank); - let vote = VoteRecord::from((aye, votes)); let mut pays = Pays::Yes; - let tally = T::Polls::try_access_poll(poll, |mut status| -> Result, DispatchError> { + let (tally, vote) = T::Polls::try_access_poll(poll, |mut status| -> Result<(TallyOf, VoteRecord), DispatchError> { match status { PollStatus::None | PollStatus::Completed(..) => Err(Error::::NotPolling)?, - PollStatus::Ongoing(ref mut tally, _) => { + PollStatus::Ongoing(ref mut tally, min_rank) => { match Voting::::get(&poll, &who) { - Some(Aye(votes)) => tally.ayes.saturating_reduce(votes), + Some(Aye(votes)) => { + tally.bare_ayes.saturating_dec(); + tally.ayes.saturating_reduce(votes); + }, Some(Nay(votes)) => tally.nays.saturating_reduce(votes), None => pays = Pays::No, } + let votes = Self::rank_to_votes(record.rank, min_rank)?; + let vote = VoteRecord::from((aye, votes)); match aye { - true => tally.ayes.saturating_accrue(votes), + true => { + tally.bare_ayes.saturating_inc(); + tally.ayes.saturating_accrue(votes); + }, false => tally.nays.saturating_accrue(votes), } Voting::::insert(&poll, &who, &vote); - Ok(tally.clone()) + Ok((tally.clone(), vote)) }, } })?; @@ -413,15 +451,19 @@ pub mod pallet { Members::::get(who).ok_or(Error::::NotMember.into()) } - fn rank_to_votes(r: Rank) -> Votes { - let r = r as Votes; - r * (r + 1) / 2 + fn rank_to_votes(rank: Rank, min: Rank) -> Result { + let excess = rank.checked_sub(min).ok_or(Error::::RankTooLow)?; + let v = (excess + 1) as Votes; + Ok(v * (v + 1) / 2) } } - impl, I: 'static> Get for Pallet { - fn get() -> Votes { - MemberCount::::get().1 + pub trait GetMaxVoters { + fn get_max_voters(r: Rank) -> MemberIndex; + } + impl, I: 'static> GetMaxVoters for Pallet { + fn get_max_voters(r: Rank) -> MemberIndex { + MemberCount::::get(r) } } } diff --git a/frame/ranked-collective/src/tests.rs b/frame/ranked-collective/src/tests.rs index 948387f35833d..44cc8c9b331ed 100644 --- a/frame/ranked-collective/src/tests.rs +++ b/frame/ranked-collective/src/tests.rs @@ -79,7 +79,7 @@ impl frame_system::Config for Test { #[derive(Clone, PartialEq, Eq, Debug)] pub enum TestPollState { - Ongoing(TallyOf, u8), + Ongoing(TallyOf, Rank), Completed(u64, bool), } use TestPollState::*; @@ -88,7 +88,7 @@ parameter_types! { pub static Polls: BTreeMap = vec![ (1, Completed(1, true)), (2, Completed(2, false)), - (3, Ongoing(Tally::from_parts(0, 0), 0)), + (3, Ongoing(Tally::from_parts(0, 0, 0), 1)), ].into_iter().collect(); } @@ -97,8 +97,8 @@ impl Polling> for TestPolls { type Index = u8; type Votes = Votes; type Moment = u64; - type Class = u8; - fn classes() -> Vec { + type Class = Rank; + fn classes() -> Vec { vec![0, 1, 2] } fn as_ongoing(index: u8) -> Option<(TallyOf, Self::Class)> { @@ -147,7 +147,7 @@ impl Polling> for TestPolls { fn create_ongoing(class: Self::Class) -> Result { let mut polls = Polls::get(); let i = polls.keys().rev().next().map_or(0, |x| x + 1); - polls.insert(i, Ongoing(Tally::default(), class)); + polls.insert(i, Ongoing(Tally::new(class), class)); Polls::set(polls); Ok(i) } @@ -184,12 +184,8 @@ fn next_block() { System::set_block_number(System::block_number() + 1); } -fn member_count() -> MemberIndex { - MemberCount::::get().0 -} - -fn max_turnout() -> Votes { - MemberCount::::get().1 +fn member_count(r: Rank) -> MemberIndex { + MemberCount::::get(r) } #[allow(dead_code)] @@ -220,63 +216,128 @@ fn completed_poll_should_panic() { #[test] fn basic_stuff() { new_test_ext().execute_with(|| { - assert_eq!(tally(3), Tally::from_parts(0, 0)); + assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); }); } + #[test] -fn membership_works() { +fn member_lifecycle_works() { new_test_ext().execute_with(|| { - assert_noop!(Club::add_member(Origin::signed(1), 1, 1), DispatchError::BadOrigin); - assert_ok!(Club::add_member(Origin::root(), 1, 1)); - assert_eq!(member_count(), 1); - assert_eq!(max_turnout(), 1); + assert_ok!(Club::add_member(Origin::root(), 1)); + assert_ok!(Club::promote(Origin::root(), 1)); + assert_ok!(Club::demote(Origin::root(), 1)); + assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_eq!(member_count(0), 0); + assert_eq!(member_count(1), 0); + }); +} + +#[test] +fn add_remove_works() { + new_test_ext().execute_with(|| { + assert_noop!(Club::add_member(Origin::signed(1), 1), DispatchError::BadOrigin); + assert_ok!(Club::add_member(Origin::root(), 1)); + assert_eq!(member_count(0), 1); + + assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_eq!(member_count(0), 0); + + assert_ok!(Club::add_member(Origin::root(), 1)); + assert_eq!(member_count(0), 1); + + assert_ok!(Club::add_member(Origin::root(), 2)); + assert_eq!(member_count(0), 2); + + assert_ok!(Club::add_member(Origin::root(), 3)); + assert_eq!(member_count(0), 3); + + assert_ok!(Club::remove_member(Origin::root(), 3)); + assert_eq!(member_count(0), 2); + + assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_eq!(member_count(0), 1); + + assert_ok!(Club::remove_member(Origin::root(), 2)); + assert_eq!(member_count(0), 0); + }); +} + +#[test] +fn promote_demote_works() { + new_test_ext().execute_with(|| { + assert_noop!(Club::add_member(Origin::signed(1), 1), DispatchError::BadOrigin); + assert_ok!(Club::add_member(Origin::root(), 1)); + assert_eq!(member_count(0), 1); + assert_eq!(member_count(1), 0); + + assert_ok!(Club::add_member(Origin::root(), 2)); + assert_eq!(member_count(0), 2); + assert_eq!(member_count(1), 0); + + assert_ok!(Club::promote(Origin::root(), 1)); + assert_eq!(member_count(0), 2); + assert_eq!(member_count(1), 1); - assert_ok!(Club::add_member(Origin::root(), 2, 4)); - assert_eq!(member_count(), 2); - assert_eq!(max_turnout(), 11); + assert_ok!(Club::promote(Origin::root(), 2)); + assert_eq!(member_count(0), 2); + assert_eq!(member_count(1), 2); - assert_ok!(Club::set_member_rank(Origin::root(), 1, 4)); - assert_eq!(member_count(), 2); - assert_eq!(max_turnout(), 20); + assert_ok!(Club::demote(Origin::root(), 1)); + assert_eq!(member_count(0), 2); + assert_eq!(member_count(1), 1); assert_noop!(Club::remove_member(Origin::signed(1), 1), DispatchError::BadOrigin); + assert_noop!(Club::remove_member(Origin::root(), 2), Error::::RankTooHigh); assert_ok!(Club::remove_member(Origin::root(), 1)); - assert_eq!(member_count(), 1); - assert_eq!(max_turnout(), 10); + assert_eq!(member_count(0), 1); + assert_eq!(member_count(1), 1); }); } #[test] fn voting_works() { new_test_ext().execute_with(|| { - assert_ok!(Club::add_member(Origin::root(), 1, 1)); - assert_ok!(Club::add_member(Origin::root(), 2, 2)); - assert_ok!(Club::add_member(Origin::root(), 3, 3)); + assert_ok!(Club::add_member(Origin::root(), 0)); + assert_ok!(Club::add_member(Origin::root(), 1)); + assert_ok!(Club::promote(Origin::root(), 1)); + assert_ok!(Club::add_member(Origin::root(), 2)); + assert_ok!(Club::promote(Origin::root(), 2)); + assert_ok!(Club::promote(Origin::root(), 2)); + assert_ok!(Club::add_member(Origin::root(), 3)); + assert_ok!(Club::promote(Origin::root(), 3)); + assert_ok!(Club::promote(Origin::root(), 3)); + assert_ok!(Club::promote(Origin::root(), 3)); + + assert_noop!(Club::vote(Origin::signed(0), 3, true), Error::::RankTooLow); + assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); assert_ok!(Club::vote(Origin::signed(1), 3, true)); - assert_eq!(tally(3), Tally::from_parts(1, 0)); + assert_eq!(tally(3), Tally::from_parts(1, 1, 0)); assert_ok!(Club::vote(Origin::signed(1), 3, false)); - assert_eq!(tally(3), Tally::from_parts(0, 1)); + assert_eq!(tally(3), Tally::from_parts(0, 0, 1)); assert_ok!(Club::vote(Origin::signed(2), 3, true)); - assert_eq!(tally(3), Tally::from_parts(3, 1)); + assert_eq!(tally(3), Tally::from_parts(1, 3, 1)); assert_ok!(Club::vote(Origin::signed(2), 3, false)); - assert_eq!(tally(3), Tally::from_parts(0, 4)); + assert_eq!(tally(3), Tally::from_parts(0, 0, 4)); assert_ok!(Club::vote(Origin::signed(3), 3, true)); - assert_eq!(tally(3), Tally::from_parts(6, 4)); + assert_eq!(tally(3), Tally::from_parts(1, 6, 4)); assert_ok!(Club::vote(Origin::signed(3), 3, false)); - assert_eq!(tally(3), Tally::from_parts(0, 10)); + assert_eq!(tally(3), Tally::from_parts(0, 0, 10)); }); } #[test] fn cleanup_works() { new_test_ext().execute_with(|| { - assert_ok!(Club::add_member(Origin::root(), 1, 1)); - assert_ok!(Club::add_member(Origin::root(), 2, 2)); - assert_ok!(Club::add_member(Origin::root(), 3, 3)); + assert_ok!(Club::add_member(Origin::root(), 1)); + assert_ok!(Club::promote(Origin::root(), 1)); + assert_ok!(Club::add_member(Origin::root(), 2)); + assert_ok!(Club::promote(Origin::root(), 2)); + assert_ok!(Club::add_member(Origin::root(), 3)); + assert_ok!(Club::promote(Origin::root(), 3)); assert_ok!(Club::vote(Origin::signed(1), 3, true)); assert_ok!(Club::vote(Origin::signed(2), 3, false)); From 374daab4ffb126de0d7379dde7569802ede19cd6 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 14:10:15 +0100 Subject: [PATCH 46/48] Index by rank, still O(1). --- bin/node/runtime/src/lib.rs | 6 +- frame/ranked-collective/src/benchmarking.rs | 58 +++++---- frame/ranked-collective/src/lib.rs | 134 ++++++++++++-------- frame/ranked-collective/src/tests.rs | 43 +++---- frame/ranked-collective/src/weights.rs | 116 +++++++++++------ 5 files changed, 218 insertions(+), 139 deletions(-) diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index b26512f6107ab..b9ada1eb748ca 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -780,11 +780,11 @@ parameter_types! { pub struct TracksInfo; impl pallet_referenda::TracksInfo for TracksInfo { - type Id = u8; + type Id = u16; type Origin = ::PalletsOrigin; fn tracks() -> &'static [(Self::Id, pallet_referenda::TrackInfo)] { - static DATA: [(u8, pallet_referenda::TrackInfo); 1] = [( - 0u8, + static DATA: [(u16, pallet_referenda::TrackInfo); 1] = [( + 0u16, pallet_referenda::TrackInfo { name: "root", max_deciding: 1, diff --git a/frame/ranked-collective/src/benchmarking.rs b/frame/ranked-collective/src/benchmarking.rs index f571d35debf7d..c8f3c0ba02652 100644 --- a/frame/ranked-collective/src/benchmarking.rs +++ b/frame/ranked-collective/src/benchmarking.rs @@ -35,15 +35,11 @@ fn make_member, I: 'static>(rank: Rank) -> T::AccountId { let who = account::("member", MemberCount::::get(0), SEED); assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), who.clone())); for _ in 0..rank { - promote_member::(&who); + assert_ok!(Pallet::::promote_member(T::AdminOrigin::successful_origin(), who.clone())); } who } -fn promote_member, I: 'static>(who: &T::AccountId) { - assert_ok!(Pallet::::promote(T::AdminOrigin::successful_origin(), who.clone())); -} - benchmarks_instance_pallet! { add_member { let who = account::("member", 0, SEED); @@ -56,37 +52,53 @@ benchmarks_instance_pallet! { } remove_member { - let first = make_member::(0); - let who = make_member::(0); - let last = make_member::(0); - let last_index = Members::::get(&last).unwrap().index; + let r in 0 .. 10; + let rank = r as u16; + let first = make_member::(rank); + let who = make_member::(rank); + let last = make_member::(rank); + let last_index = (0..=rank).map(|r| IdToIndex::::get(r, &last).unwrap()).collect::>(); let origin = T::AdminOrigin::successful_origin(); - let call = Call::::remove_member { who: who.clone() }; + let call = Call::::remove_member { who: who.clone(), min_rank: rank }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_eq!(MemberCount::::get(0), 2); - assert_ne!(last_index, Members::::get(&last).unwrap().index); - assert_last_event::(Event::MemberRemoved { who }.into()); + for r in 0..=rank { + assert_eq!(MemberCount::::get(r), 2); + assert_ne!(last_index[r as usize], IdToIndex::::get(r, &last).unwrap()); + } + assert_last_event::(Event::MemberRemoved { who, rank }.into()); } - promote { - let who = make_member::(0); + promote_member { + let r in 0 .. 10; + let rank = r as u16; + let who = make_member::(rank); let origin = T::AdminOrigin::successful_origin(); - let call = Call::::promote { who: who.clone() }; + let call = Call::::promote_member { who: who.clone() }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_eq!(Members::::get(&who).unwrap().rank, 1); - assert_last_event::(Event::RankChanged { who, rank: 1 }.into()); + assert_eq!(Members::::get(&who).unwrap().rank, rank + 1); + assert_last_event::(Event::RankChanged { who, rank: rank + 1 }.into()); } - demote { - let who = make_member::(1); + demote_member { + let r in 0 .. 10; + let rank = r as u16; + let first = make_member::(rank); + let who = make_member::(rank); + let last = make_member::(rank); + let last_index = IdToIndex::::get(rank, &last).unwrap(); let origin = T::AdminOrigin::successful_origin(); - let call = Call::::demote { who: who.clone() }; + let call = Call::::demote_member { who: who.clone() }; }: { call.dispatch_bypass_filter(origin)? } verify { - assert_eq!(Members::::get(&who).unwrap().rank, 0); - assert_last_event::(Event::RankChanged { who, rank: 0 }.into()); + assert_eq!(Members::::get(&who).map(|x| x.rank), rank.checked_sub(1)); + assert_eq!(MemberCount::::get(rank), 2); + assert_ne!(last_index, IdToIndex::::get(rank, &last).unwrap()); + assert_last_event::(match rank { + 0 => Event::MemberRemoved { who, rank: 0 }, + r => Event::RankChanged { who, rank: r - 1 }, + }.into()); } vote { diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs index 91b91c75cd346..f33e01d867461 100644 --- a/frame/ranked-collective/src/lib.rs +++ b/frame/ranked-collective/src/lib.rs @@ -45,7 +45,7 @@ use scale_info::TypeInfo; use sp_arithmetic::traits::Saturating; use sp_runtime::{ - ArithmeticError::{Overflow, Underflow}, + ArithmeticError::Overflow, Perbill, RuntimeDebug, }; use sp_std::{marker::PhantomData, prelude::*}; @@ -152,8 +152,6 @@ impl VoteTally for Tally { /// Record needed for every member. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct MemberRecord { - /// The index of the member. - index: MemberIndex, /// The rank of the member. rank: Rank, } @@ -213,11 +211,16 @@ pub mod pallet { pub type Members, I: 'static = ()> = StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>; + /// The index of each ranks's member into the group of members who have at least that rank. + #[pallet::storage] + pub type IdToIndex, I: 'static = ()> = + StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, T::AccountId, MemberIndex>; + /// The members in the collective by index. All indices in the range `0..MemberCount` will /// return `Some`, however a member's index is not guaranteed to remain unchanged over time. #[pallet::storage] - pub type MemberByIndex, I: 'static = ()> = - StorageMap<_, Twox64Concat, MemberIndex, T::AccountId>; + pub type IndexToId, I: 'static = ()> = + StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, MemberIndex, T::AccountId>; /// Votes on a given proposal, if it is ongoing. #[pallet::storage] @@ -233,14 +236,14 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { - /// A member has been added. + /// A member `who` has been added. MemberAdded { who: T::AccountId }, - /// A member's rank has been changed. + /// The member `who`'s rank has been changed to the given `rank`. RankChanged { who: T::AccountId, rank: Rank }, - /// A member has been removed. - MemberRemoved { who: T::AccountId }, - /// A motion (given hash) has been voted on by given account, leaving - /// a tally (yes votes and no votes given respectively as `MemberIndex`). + /// The member `who` of given `rank` has been removed from the collective. + MemberRemoved { who: T::AccountId, rank: Rank }, + /// The member `who` has voted for the `poll` with the given `vote` leading to an updated + /// `tally`. Voted { who: T::AccountId, poll: PollIndexOf, vote: VoteRecord, tally: TallyOf }, } @@ -260,9 +263,8 @@ pub mod pallet { Corruption, /// The member's rank is too low to vote. RankTooLow, - /// The member's rank is too high to be removed. - RankTooHigh, - + /// The information provided is incorrect. + InvalidWitness, } #[pallet::call] @@ -281,8 +283,9 @@ pub mod pallet { let index = MemberCount::::get(0); let count = index.checked_add(1).ok_or(Overflow)?; - Members::::insert(&who, MemberRecord { rank: 0, index }); - MemberByIndex::::insert(index, &who); + Members::::insert(&who, MemberRecord { rank: 0 }); + IdToIndex::::insert(0, &who, index); + IndexToId::::insert(0, index, &who); MemberCount::::insert(0, count); Self::deposit_event(Event::MemberAdded { who }); @@ -295,71 +298,82 @@ pub mod pallet { /// - `who`: Account of existing member. /// /// Weight: `O(1)` - #[pallet::weight(T::WeightInfo::set_member_rank())] - pub fn promote( + #[pallet::weight(T::WeightInfo::promote_member(0))] + pub fn promote_member( origin: OriginFor, who: T::AccountId, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; - let mut record = Self::ensure_member(&who)?; - record.rank = record.rank.checked_add(1).ok_or(Overflow)?; - MemberCount::::mutate(record.rank, |r| r.saturating_inc()); - Members::::insert(&who, &record); - Self::deposit_event(Event::RankChanged { who, rank: record.rank }); + let record = Self::ensure_member(&who)?; + let rank = record.rank.checked_add(1).ok_or(Overflow)?; + let index = MemberCount::::get(rank); + MemberCount::::insert(rank, index.checked_add(1).ok_or(Overflow)?); + IdToIndex::::insert(rank, &who, index); + IndexToId::::insert(rank, index, &who); + Members::::insert(&who, MemberRecord { rank, .. record }); + Self::deposit_event(Event::RankChanged { who, rank }); Ok(()) } - /// Decrement the rank of an existing member by one. + /// Decrement the rank of an existing member by one. If the member is already at rank zero, + /// then they are removed entirely. /// /// - `origin`: Must be the `AdminOrigin`. /// - `who`: Account of existing member of rank greater than zero. /// - /// Weight: `O(1)` - #[pallet::weight(T::WeightInfo::set_member_rank())] - pub fn demote( + /// Weight: `O(1)`, less if the member's index is highest in its rank. + #[pallet::weight(T::WeightInfo::demote_member(0))] + pub fn demote_member( origin: OriginFor, who: T::AccountId, ) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; let mut record = Self::ensure_member(&who)?; - record.rank = record.rank.checked_sub(1).ok_or(Underflow)?; - MemberCount::::mutate(record.rank + 1, |r| r.saturating_dec()); - Members::::insert(&who, &record); - Self::deposit_event(Event::RankChanged { who, rank: record.rank }); - + let rank = record.rank; + + Self::remove_from_rank(&who, rank)?; + let maybe_rank = rank.checked_sub(1); + match maybe_rank { + None => { + Members::::remove(&who); + Self::deposit_event(Event::MemberRemoved { who, rank: 0 }); + } + Some(rank) => { + record.rank = rank; + Members::::insert(&who, &record); + Self::deposit_event(Event::RankChanged { who, rank }); + } + } Ok(()) } - /// Remove a member. + /// Remove the member entirely. /// /// - `origin`: Must be the `AdminOrigin`. - /// - `who`: Account of existing member to be removed. + /// - `who`: Account of existing member of rank greater than zero. + /// - `rank`: The rank of the member. /// - /// Weight: `O(1)`, less if the member's index is highest. - #[pallet::weight(T::WeightInfo::remove_member())] - pub fn remove_member(origin: OriginFor, who: T::AccountId) -> DispatchResult { + /// Weight: `O(rank)`. + #[pallet::weight(T::WeightInfo::remove_member(*min_rank as u32))] + pub fn remove_member( + origin: OriginFor, + who: T::AccountId, + min_rank: Rank, + ) -> DispatchResultWithPostInfo { T::AdminOrigin::ensure_origin(origin)?; - let record = Self::ensure_member(&who)?; - ensure!(record.rank == 0, Error::::RankTooHigh); + let MemberRecord { rank, .. } = Self::ensure_member(&who)?; + ensure!(min_rank >= rank, Error::::InvalidWitness); - let last_index = MemberCount::::get(0).saturating_sub(1); - let index = record.index; - if index != last_index { - let last = MemberByIndex::::get(last_index).ok_or(Error::::Corruption)?; - Members::::mutate(&last, |r| { - if let Some(ref mut r) = r { - r.index = index - } - }); - MemberByIndex::::insert(index, &last); + for r in 0..=rank { + Self::remove_from_rank(&who, r)?; } - MemberCount::::insert(0, last_index); - MemberByIndex::::remove(last_index); Members::::remove(&who); - Self::deposit_event(Event::MemberRemoved { who }); - - Ok(()) + Self::deposit_event(Event::MemberRemoved { who, rank }); + Ok(PostDispatchInfo { + actual_weight: Some(T::WeightInfo::remove_member(rank as u32)), + pays_fee: Pays::Yes, + }) } /// Add an aye or nay vote for the sender to the given proposal. @@ -456,6 +470,18 @@ pub mod pallet { let v = (excess + 1) as Votes; Ok(v * (v + 1) / 2) } + + fn remove_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult { + let last_index = MemberCount::::get(rank).saturating_sub(1); + let index = IdToIndex::::get(rank, &who).ok_or(Error::::Corruption)?; + if index != last_index { + let last = IndexToId::::get(rank, last_index).ok_or(Error::::Corruption)?; + IdToIndex::::insert(rank, &last, index); + IndexToId::::insert(rank, index, &last); + } + MemberCount::::mutate(rank, |r| r.saturating_dec()); + Ok(()) + } } pub trait GetMaxVoters { diff --git a/frame/ranked-collective/src/tests.rs b/frame/ranked-collective/src/tests.rs index 44cc8c9b331ed..bd5fe1c6ba11a 100644 --- a/frame/ranked-collective/src/tests.rs +++ b/frame/ranked-collective/src/tests.rs @@ -225,9 +225,9 @@ fn basic_stuff() { fn member_lifecycle_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(Origin::root(), 1)); - assert_ok!(Club::promote(Origin::root(), 1)); - assert_ok!(Club::demote(Origin::root(), 1)); - assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_ok!(Club::promote_member(Origin::root(), 1)); + assert_ok!(Club::demote_member(Origin::root(), 1)); + assert_ok!(Club::demote_member(Origin::root(), 1)); assert_eq!(member_count(0), 0); assert_eq!(member_count(1), 0); }); @@ -240,7 +240,7 @@ fn add_remove_works() { assert_ok!(Club::add_member(Origin::root(), 1)); assert_eq!(member_count(0), 1); - assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_ok!(Club::demote_member(Origin::root(), 1)); assert_eq!(member_count(0), 0); assert_ok!(Club::add_member(Origin::root(), 1)); @@ -252,13 +252,13 @@ fn add_remove_works() { assert_ok!(Club::add_member(Origin::root(), 3)); assert_eq!(member_count(0), 3); - assert_ok!(Club::remove_member(Origin::root(), 3)); + assert_ok!(Club::demote_member(Origin::root(), 3)); assert_eq!(member_count(0), 2); - assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_ok!(Club::demote_member(Origin::root(), 1)); assert_eq!(member_count(0), 1); - assert_ok!(Club::remove_member(Origin::root(), 2)); + assert_ok!(Club::demote_member(Origin::root(), 2)); assert_eq!(member_count(0), 0); }); } @@ -275,21 +275,20 @@ fn promote_demote_works() { assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 0); - assert_ok!(Club::promote(Origin::root(), 1)); + assert_ok!(Club::promote_member(Origin::root(), 1)); assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 1); - assert_ok!(Club::promote(Origin::root(), 2)); + assert_ok!(Club::promote_member(Origin::root(), 2)); assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 2); - assert_ok!(Club::demote(Origin::root(), 1)); + assert_ok!(Club::demote_member(Origin::root(), 1)); assert_eq!(member_count(0), 2); assert_eq!(member_count(1), 1); - assert_noop!(Club::remove_member(Origin::signed(1), 1), DispatchError::BadOrigin); - assert_noop!(Club::remove_member(Origin::root(), 2), Error::::RankTooHigh); - assert_ok!(Club::remove_member(Origin::root(), 1)); + assert_noop!(Club::demote_member(Origin::signed(1), 1), DispatchError::BadOrigin); + assert_ok!(Club::demote_member(Origin::root(), 1)); assert_eq!(member_count(0), 1); assert_eq!(member_count(1), 1); }); @@ -300,14 +299,14 @@ fn voting_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(Origin::root(), 0)); assert_ok!(Club::add_member(Origin::root(), 1)); - assert_ok!(Club::promote(Origin::root(), 1)); + assert_ok!(Club::promote_member(Origin::root(), 1)); assert_ok!(Club::add_member(Origin::root(), 2)); - assert_ok!(Club::promote(Origin::root(), 2)); - assert_ok!(Club::promote(Origin::root(), 2)); + assert_ok!(Club::promote_member(Origin::root(), 2)); + assert_ok!(Club::promote_member(Origin::root(), 2)); assert_ok!(Club::add_member(Origin::root(), 3)); - assert_ok!(Club::promote(Origin::root(), 3)); - assert_ok!(Club::promote(Origin::root(), 3)); - assert_ok!(Club::promote(Origin::root(), 3)); + assert_ok!(Club::promote_member(Origin::root(), 3)); + assert_ok!(Club::promote_member(Origin::root(), 3)); + assert_ok!(Club::promote_member(Origin::root(), 3)); assert_noop!(Club::vote(Origin::signed(0), 3, true), Error::::RankTooLow); assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); @@ -333,11 +332,11 @@ fn voting_works() { fn cleanup_works() { new_test_ext().execute_with(|| { assert_ok!(Club::add_member(Origin::root(), 1)); - assert_ok!(Club::promote(Origin::root(), 1)); + assert_ok!(Club::promote_member(Origin::root(), 1)); assert_ok!(Club::add_member(Origin::root(), 2)); - assert_ok!(Club::promote(Origin::root(), 2)); + assert_ok!(Club::promote_member(Origin::root(), 2)); assert_ok!(Club::add_member(Origin::root(), 3)); - assert_ok!(Club::promote(Origin::root(), 3)); + assert_ok!(Club::promote_member(Origin::root(), 3)); assert_ok!(Club::vote(Origin::signed(1), 3, true)); assert_ok!(Club::vote(Origin::signed(2), 3, false)); diff --git a/frame/ranked-collective/src/weights.rs b/frame/ranked-collective/src/weights.rs index ed08564774c1d..f287137fedfdd 100644 --- a/frame/ranked-collective/src/weights.rs +++ b/frame/ranked-collective/src/weights.rs @@ -18,20 +18,19 @@ //! Autogenerated weights for pallet_ranked_collective //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-05-18, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 +//! DATE: 2022-05-19, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// ../../../target/release/substrate +// /Users/gav/Core/substrate/target/release/substrate // benchmark // pallet +// --pallet +// pallet-ranked-collective +// --extrinsic=* // --chain=dev // --steps=50 // --repeat=20 -// --pallet=pallet-ranked-collective -// --extrinsic=* -// --execution=wasm -// --heap-pages=4096 // --output=../../../frame/ranked-collective/src/weights.rs // --template=../../../.maintain/frame-weight-template.hbs // --header=../../../HEADER-APACHE2 @@ -47,8 +46,9 @@ use sp_std::marker::PhantomData; /// Weight functions needed for pallet_ranked_collective. pub trait WeightInfo { fn add_member() -> Weight; - fn remove_member() -> Weight; - fn set_member_rank() -> Weight; + fn remove_member(r: u32, ) -> Weight; + fn promote_member(r: u32, ) -> Weight; + fn demote_member(r: u32, ) -> Weight; fn vote() -> Weight; fn cleanup_poll(n: u32, ) -> Weight; } @@ -58,42 +58,63 @@ pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { // Storage: RankedCollective Members (r:1 w:1) // Storage: RankedCollective MemberCount (r:1 w:1) - // Storage: RankedCollective MemberByIndex (r:0 w:1) + // Storage: RankedCollective IndexToId (r:0 w:1) + // Storage: RankedCollective IdToIndex (r:0 w:1) fn add_member() -> Weight { - (15_000_000 as Weight) + (11_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) } - // Storage: RankedCollective Members (r:2 w:2) + // Storage: RankedCollective Members (r:1 w:1) // Storage: RankedCollective MemberCount (r:1 w:1) - // Storage: RankedCollective MemberByIndex (r:1 w:2) - fn remove_member() -> Weight { - (23_000_000 as Weight) + // Storage: RankedCollective IdToIndex (r:1 w:1) + // Storage: RankedCollective IndexToId (r:1 w:1) + fn remove_member(r: u32, ) -> Weight { + (16_814_000 as Weight) + // Standard Error: 29_000 + .saturating_add((8_128_000 as Weight).saturating_mul(r as Weight)) .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(5 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + .saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(r as Weight))) } // Storage: RankedCollective Members (r:1 w:1) // Storage: RankedCollective MemberCount (r:1 w:1) - fn set_member_rank() -> Weight { - (15_000_000 as Weight) + // Storage: RankedCollective IndexToId (r:0 w:1) + // Storage: RankedCollective IdToIndex (r:0 w:1) + fn promote_member(r: u32, ) -> Weight { + (12_036_000 as Weight) + // Standard Error: 7_000 + .saturating_add((7_000 as Weight).saturating_mul(r as Weight)) .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: RankedCollective Members (r:1 w:1) + // Storage: RankedCollective MemberCount (r:1 w:1) + // Storage: RankedCollective IdToIndex (r:1 w:1) + // Storage: RankedCollective IndexToId (r:1 w:1) + fn demote_member(r: u32, ) -> Weight { + (17_605_000 as Weight) + // Standard Error: 17_000 + .saturating_add((165_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) } // Storage: RankedCollective Members (r:1 w:0) // Storage: RankedPolls ReferendumInfoFor (r:1 w:1) // Storage: RankedCollective Voting (r:1 w:1) // Storage: Scheduler Agenda (r:2 w:2) fn vote() -> Weight { - (35_000_000 as Weight) + (22_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } // Storage: RankedPolls ReferendumInfoFor (r:1 w:0) // Storage: RankedCollective Voting (r:0 w:1) fn cleanup_poll(n: u32, ) -> Weight { - (7_800_000 as Weight) + (6_273_000 as Weight) // Standard Error: 1_000 - .saturating_add((860_000 as Weight).saturating_mul(n as Weight)) + .saturating_add((847_000 as Weight).saturating_mul(n as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(n as Weight))) } @@ -103,42 +124,63 @@ impl WeightInfo for SubstrateWeight { impl WeightInfo for () { // Storage: RankedCollective Members (r:1 w:1) // Storage: RankedCollective MemberCount (r:1 w:1) - // Storage: RankedCollective MemberByIndex (r:0 w:1) + // Storage: RankedCollective IndexToId (r:0 w:1) + // Storage: RankedCollective IdToIndex (r:0 w:1) fn add_member() -> Weight { - (15_000_000 as Weight) + (11_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } - // Storage: RankedCollective Members (r:2 w:2) + // Storage: RankedCollective Members (r:1 w:1) // Storage: RankedCollective MemberCount (r:1 w:1) - // Storage: RankedCollective MemberByIndex (r:1 w:2) - fn remove_member() -> Weight { - (23_000_000 as Weight) + // Storage: RankedCollective IdToIndex (r:1 w:1) + // Storage: RankedCollective IndexToId (r:1 w:1) + fn remove_member(r: u32, ) -> Weight { + (16_814_000 as Weight) + // Standard Error: 29_000 + .saturating_add((8_128_000 as Weight).saturating_mul(r as Weight)) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(r as Weight))) } // Storage: RankedCollective Members (r:1 w:1) // Storage: RankedCollective MemberCount (r:1 w:1) - fn set_member_rank() -> Weight { - (15_000_000 as Weight) + // Storage: RankedCollective IndexToId (r:0 w:1) + // Storage: RankedCollective IdToIndex (r:0 w:1) + fn promote_member(r: u32, ) -> Weight { + (12_036_000 as Weight) + // Standard Error: 7_000 + .saturating_add((7_000 as Weight).saturating_mul(r as Weight)) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: RankedCollective Members (r:1 w:1) + // Storage: RankedCollective MemberCount (r:1 w:1) + // Storage: RankedCollective IdToIndex (r:1 w:1) + // Storage: RankedCollective IndexToId (r:1 w:1) + fn demote_member(r: u32, ) -> Weight { + (17_605_000 as Weight) + // Standard Error: 17_000 + .saturating_add((165_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } // Storage: RankedCollective Members (r:1 w:0) // Storage: RankedPolls ReferendumInfoFor (r:1 w:1) // Storage: RankedCollective Voting (r:1 w:1) // Storage: Scheduler Agenda (r:2 w:2) fn vote() -> Weight { - (35_000_000 as Weight) + (22_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } // Storage: RankedPolls ReferendumInfoFor (r:1 w:0) // Storage: RankedCollective Voting (r:0 w:1) fn cleanup_poll(n: u32, ) -> Weight { - (7_800_000 as Weight) + (6_273_000 as Weight) // Standard Error: 1_000 - .saturating_add((860_000 as Weight).saturating_mul(n as Weight)) + .saturating_add((847_000 as Weight).saturating_mul(n as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(n as Weight))) } From aaca32000d631660be6d6e9232d70e4988a70d9a Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 19 May 2022 14:34:05 +0100 Subject: [PATCH 47/48] Custom vote weights --- bin/node/runtime/src/lib.rs | 1 + frame/ranked-collective/src/lib.rs | 49 ++++++++++++++++++++++++-- frame/ranked-collective/src/tests.rs | 1 + frame/ranked-collective/src/weights.rs | 44 +++++++++++------------ 4 files changed, 71 insertions(+), 24 deletions(-) diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index b9ada1eb748ca..182d73fb74438 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -860,6 +860,7 @@ impl pallet_ranked_collective::Config for Runtime { type Event = Event; type AdminOrigin = EnsureRoot; type Polls = RankedPolls; + type VoteWeight = pallet_ranked_collective::Geometric; } impl pallet_remark::Config for Runtime { diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs index f33e01d867461..49d0e0c4d1935 100644 --- a/frame/ranked-collective/src/lib.rs +++ b/frame/ranked-collective/src/lib.rs @@ -45,6 +45,7 @@ use scale_info::TypeInfo; use sp_arithmetic::traits::Saturating; use sp_runtime::{ + traits::Convert, ArithmeticError::Overflow, Perbill, RuntimeDebug, }; @@ -174,6 +175,45 @@ impl From<(bool, Votes)> for VoteRecord { } } +/// Vote-weight scheme where all voters get one vote regardless of rank. +pub struct Unit; +impl Convert for Unit { + fn convert(_: Rank) -> Votes { + 1 + } +} + +/// Vote-weight scheme where all voters get one vote plus an additional vote for every excess rank +/// they have. I.e.: +/// +/// - Each member with no excess rank gets 1 vote; +/// - ...with an excess rank of 1 gets 2 votes; +/// - ...with an excess rank of 2 gets 2 votes; +/// - ...with an excess rank of 3 gets 3 votes; +/// - ...with an excess rank of 4 gets 4 votes. +pub struct Linear; +impl Convert for Linear { + fn convert(r: Rank) -> Votes { + (r + 1) as Votes + } +} + +/// Vote-weight scheme where all voters get one vote plus additional votes for every excess rank +/// they have incrementing by one vote for each excess rank. I.e.: +/// +/// - Each member with no excess rank gets 1 vote; +/// - ...with an excess rank of 1 gets 2 votes; +/// - ...with an excess rank of 2 gets 3 votes; +/// - ...with an excess rank of 3 gets 6 votes; +/// - ...with an excess rank of 4 gets 10 votes. +pub struct Geometric; +impl Convert for Geometric { + fn convert(r: Rank) -> Votes { + let v = (r + 1) as Votes; + v * (v + 1) / 2 + } +} + #[frame_support::pallet] pub mod pallet { use super::*; @@ -198,6 +238,12 @@ pub mod pallet { /// The polling system used for our voting. type Polls: Polling, Votes = Votes, Class = Rank, Moment = Self::BlockNumber>; + + /// Convert a rank_delta into a number of votes the rank gets. + /// + /// Rank_delta is defined as the number of ranks above the minimum required to take part + /// in the poll. + type VoteWeight: Convert; } /// The number of members in the collective who have at least the rank according to the index @@ -467,8 +513,7 @@ pub mod pallet { fn rank_to_votes(rank: Rank, min: Rank) -> Result { let excess = rank.checked_sub(min).ok_or(Error::::RankTooLow)?; - let v = (excess + 1) as Votes; - Ok(v * (v + 1) / 2) + Ok(T::VoteWeight::convert(excess)) } fn remove_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult { diff --git a/frame/ranked-collective/src/tests.rs b/frame/ranked-collective/src/tests.rs index bd5fe1c6ba11a..f92027e6ace85 100644 --- a/frame/ranked-collective/src/tests.rs +++ b/frame/ranked-collective/src/tests.rs @@ -171,6 +171,7 @@ impl Config for Test { type Event = Event; type AdminOrigin = frame_system::EnsureRoot; type Polls = TestPolls; + type VoteWeight = Geometric; } pub fn new_test_ext() -> sp_io::TestExternalities { diff --git a/frame/ranked-collective/src/weights.rs b/frame/ranked-collective/src/weights.rs index f287137fedfdd..3048dd804a5e2 100644 --- a/frame/ranked-collective/src/weights.rs +++ b/frame/ranked-collective/src/weights.rs @@ -70,9 +70,9 @@ impl WeightInfo for SubstrateWeight { // Storage: RankedCollective IdToIndex (r:1 w:1) // Storage: RankedCollective IndexToId (r:1 w:1) fn remove_member(r: u32, ) -> Weight { - (16_814_000 as Weight) - // Standard Error: 29_000 - .saturating_add((8_128_000 as Weight).saturating_mul(r as Weight)) + (16_855_000 as Weight) + // Standard Error: 27_000 + .saturating_add((8_107_000 as Weight).saturating_mul(r as Weight)) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) .saturating_add(T::DbWeight::get().writes(4 as Weight)) @@ -83,9 +83,9 @@ impl WeightInfo for SubstrateWeight { // Storage: RankedCollective IndexToId (r:0 w:1) // Storage: RankedCollective IdToIndex (r:0 w:1) fn promote_member(r: u32, ) -> Weight { - (12_036_000 as Weight) - // Standard Error: 7_000 - .saturating_add((7_000 as Weight).saturating_mul(r as Weight)) + (11_936_000 as Weight) + // Standard Error: 3_000 + .saturating_add((9_000 as Weight).saturating_mul(r as Weight)) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -94,9 +94,9 @@ impl WeightInfo for SubstrateWeight { // Storage: RankedCollective IdToIndex (r:1 w:1) // Storage: RankedCollective IndexToId (r:1 w:1) fn demote_member(r: u32, ) -> Weight { - (17_605_000 as Weight) - // Standard Error: 17_000 - .saturating_add((165_000 as Weight).saturating_mul(r as Weight)) + (17_582_000 as Weight) + // Standard Error: 14_000 + .saturating_add((142_000 as Weight).saturating_mul(r as Weight)) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } @@ -112,9 +112,9 @@ impl WeightInfo for SubstrateWeight { // Storage: RankedPolls ReferendumInfoFor (r:1 w:0) // Storage: RankedCollective Voting (r:0 w:1) fn cleanup_poll(n: u32, ) -> Weight { - (6_273_000 as Weight) + (6_188_000 as Weight) // Standard Error: 1_000 - .saturating_add((847_000 as Weight).saturating_mul(n as Weight)) + .saturating_add((867_000 as Weight).saturating_mul(n as Weight)) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(n as Weight))) } @@ -136,9 +136,9 @@ impl WeightInfo for () { // Storage: RankedCollective IdToIndex (r:1 w:1) // Storage: RankedCollective IndexToId (r:1 w:1) fn remove_member(r: u32, ) -> Weight { - (16_814_000 as Weight) - // Standard Error: 29_000 - .saturating_add((8_128_000 as Weight).saturating_mul(r as Weight)) + (16_855_000 as Weight) + // Standard Error: 27_000 + .saturating_add((8_107_000 as Weight).saturating_mul(r as Weight)) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) @@ -149,9 +149,9 @@ impl WeightInfo for () { // Storage: RankedCollective IndexToId (r:0 w:1) // Storage: RankedCollective IdToIndex (r:0 w:1) fn promote_member(r: u32, ) -> Weight { - (12_036_000 as Weight) - // Standard Error: 7_000 - .saturating_add((7_000 as Weight).saturating_mul(r as Weight)) + (11_936_000 as Weight) + // Standard Error: 3_000 + .saturating_add((9_000 as Weight).saturating_mul(r as Weight)) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -160,9 +160,9 @@ impl WeightInfo for () { // Storage: RankedCollective IdToIndex (r:1 w:1) // Storage: RankedCollective IndexToId (r:1 w:1) fn demote_member(r: u32, ) -> Weight { - (17_605_000 as Weight) - // Standard Error: 17_000 - .saturating_add((165_000 as Weight).saturating_mul(r as Weight)) + (17_582_000 as Weight) + // Standard Error: 14_000 + .saturating_add((142_000 as Weight).saturating_mul(r as Weight)) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } @@ -178,9 +178,9 @@ impl WeightInfo for () { // Storage: RankedPolls ReferendumInfoFor (r:1 w:0) // Storage: RankedCollective Voting (r:0 w:1) fn cleanup_poll(n: u32, ) -> Weight { - (6_273_000 as Weight) + (6_188_000 as Weight) // Standard Error: 1_000 - .saturating_add((847_000 as Weight).saturating_mul(n as Weight)) + .saturating_add((867_000 as Weight).saturating_mul(n as Weight)) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(n as Weight))) } From abba95a3af9527c82ef9c6b3806610236f0dbb01 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 24 May 2022 11:29:31 +0200 Subject: [PATCH 48/48] Formatting --- frame/ranked-collective/src/benchmarking.rs | 5 +- frame/ranked-collective/src/lib.rs | 90 ++++++++++----------- frame/ranked-collective/src/tests.rs | 3 +- 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/frame/ranked-collective/src/benchmarking.rs b/frame/ranked-collective/src/benchmarking.rs index c8f3c0ba02652..042287900852c 100644 --- a/frame/ranked-collective/src/benchmarking.rs +++ b/frame/ranked-collective/src/benchmarking.rs @@ -35,7 +35,10 @@ fn make_member, I: 'static>(rank: Rank) -> T::AccountId { let who = account::("member", MemberCount::::get(0), SEED); assert_ok!(Pallet::::add_member(T::AdminOrigin::successful_origin(), who.clone())); for _ in 0..rank { - assert_ok!(Pallet::::promote_member(T::AdminOrigin::successful_origin(), who.clone())); + assert_ok!(Pallet::::promote_member( + T::AdminOrigin::successful_origin(), + who.clone() + )); } who } diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs index 49d0e0c4d1935..54114805f02fa 100644 --- a/frame/ranked-collective/src/lib.rs +++ b/frame/ranked-collective/src/lib.rs @@ -44,11 +44,7 @@ use scale_info::TypeInfo; use sp_arithmetic::traits::Saturating; -use sp_runtime::{ - traits::Convert, - ArithmeticError::Overflow, - Perbill, RuntimeDebug, -}; +use sp_runtime::{traits::Convert, ArithmeticError::Overflow, Perbill, RuntimeDebug}; use sp_std::{marker::PhantomData, prelude::*}; use frame_support::{ @@ -237,7 +233,12 @@ pub mod pallet { type AdminOrigin: EnsureOrigin; /// The polling system used for our voting. - type Polls: Polling, Votes = Votes, Class = Rank, Moment = Self::BlockNumber>; + type Polls: Polling< + TallyOf, + Votes = Votes, + Class = Rank, + Moment = Self::BlockNumber, + >; /// Convert a rank_delta into a number of votes the rank gets. /// @@ -345,10 +346,7 @@ pub mod pallet { /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::promote_member(0))] - pub fn promote_member( - origin: OriginFor, - who: T::AccountId, - ) -> DispatchResult { + pub fn promote_member(origin: OriginFor, who: T::AccountId) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; let record = Self::ensure_member(&who)?; let rank = record.rank.checked_add(1).ok_or(Overflow)?; @@ -356,7 +354,7 @@ pub mod pallet { MemberCount::::insert(rank, index.checked_add(1).ok_or(Overflow)?); IdToIndex::::insert(rank, &who, index); IndexToId::::insert(rank, index, &who); - Members::::insert(&who, MemberRecord { rank, .. record }); + Members::::insert(&who, MemberRecord { rank, ..record }); Self::deposit_event(Event::RankChanged { who, rank }); Ok(()) @@ -370,10 +368,7 @@ pub mod pallet { /// /// Weight: `O(1)`, less if the member's index is highest in its rank. #[pallet::weight(T::WeightInfo::demote_member(0))] - pub fn demote_member( - origin: OriginFor, - who: T::AccountId, - ) -> DispatchResult { + pub fn demote_member(origin: OriginFor, who: T::AccountId) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; let mut record = Self::ensure_member(&who)?; let rank = record.rank; @@ -384,12 +379,12 @@ pub mod pallet { None => { Members::::remove(&who); Self::deposit_event(Event::MemberRemoved { who, rank: 0 }); - } + }, Some(rank) => { record.rank = rank; Members::::insert(&who, &record); Self::deposit_event(Event::RankChanged { who, rank }); - } + }, } Ok(()) } @@ -444,32 +439,36 @@ pub mod pallet { use VoteRecord::*; let mut pays = Pays::Yes; - let (tally, vote) = T::Polls::try_access_poll(poll, |mut status| -> Result<(TallyOf, VoteRecord), DispatchError> { - match status { - PollStatus::None | PollStatus::Completed(..) => Err(Error::::NotPolling)?, - PollStatus::Ongoing(ref mut tally, min_rank) => { - match Voting::::get(&poll, &who) { - Some(Aye(votes)) => { - tally.bare_ayes.saturating_dec(); - tally.ayes.saturating_reduce(votes); - }, - Some(Nay(votes)) => tally.nays.saturating_reduce(votes), - None => pays = Pays::No, - } - let votes = Self::rank_to_votes(record.rank, min_rank)?; - let vote = VoteRecord::from((aye, votes)); - match aye { - true => { - tally.bare_ayes.saturating_inc(); - tally.ayes.saturating_accrue(votes); - }, - false => tally.nays.saturating_accrue(votes), - } - Voting::::insert(&poll, &who, &vote); - Ok((tally.clone(), vote)) - }, - } - })?; + let (tally, vote) = T::Polls::try_access_poll( + poll, + |mut status| -> Result<(TallyOf, VoteRecord), DispatchError> { + match status { + PollStatus::None | PollStatus::Completed(..) => + Err(Error::::NotPolling)?, + PollStatus::Ongoing(ref mut tally, min_rank) => { + match Voting::::get(&poll, &who) { + Some(Aye(votes)) => { + tally.bare_ayes.saturating_dec(); + tally.ayes.saturating_reduce(votes); + }, + Some(Nay(votes)) => tally.nays.saturating_reduce(votes), + None => pays = Pays::No, + } + let votes = Self::rank_to_votes(record.rank, min_rank)?; + let vote = VoteRecord::from((aye, votes)); + match aye { + true => { + tally.bare_ayes.saturating_inc(); + tally.ayes.saturating_accrue(votes); + }, + false => tally.nays.saturating_accrue(votes), + } + Voting::::insert(&poll, &who, &vote); + Ok((tally.clone(), vote)) + }, + } + }, + )?; Self::deposit_event(Event::Voted { who, poll, vote, tally }); Ok(pays.into()) } @@ -495,7 +494,7 @@ pub mod pallet { use sp_io::KillStorageResult::*; let count = match Voting::::remove_prefix(poll_index, Some(max)) { -// AllRemoved(0) => Err(Error::::NoneRemaining)?, + // AllRemoved(0) => Err(Error::::NoneRemaining)?, AllRemoved(0) => return Ok(Pays::Yes.into()), AllRemoved(n) | SomeRemaining(n) => n, }; @@ -520,7 +519,8 @@ pub mod pallet { let last_index = MemberCount::::get(rank).saturating_sub(1); let index = IdToIndex::::get(rank, &who).ok_or(Error::::Corruption)?; if index != last_index { - let last = IndexToId::::get(rank, last_index).ok_or(Error::::Corruption)?; + let last = + IndexToId::::get(rank, last_index).ok_or(Error::::Corruption)?; IdToIndex::::insert(rank, &last, index); IndexToId::::insert(rank, index, &last); } diff --git a/frame/ranked-collective/src/tests.rs b/frame/ranked-collective/src/tests.rs index f92027e6ace85..88b61228ffeff 100644 --- a/frame/ranked-collective/src/tests.rs +++ b/frame/ranked-collective/src/tests.rs @@ -221,7 +221,6 @@ fn basic_stuff() { }); } - #[test] fn member_lifecycle_works() { new_test_ext().execute_with(|| { @@ -351,6 +350,6 @@ fn cleanup_works() { ); assert_ok!(Club::cleanup_poll(Origin::signed(4), 3, 10)); // NOTE: This will fail until #10016 is merged. -// assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::::NoneRemaining); + // assert_noop!(Club::cleanup_poll(Origin::signed(4), 3, 10), Error::::NoneRemaining); }); }