Several tweaks needed for Governance 2.0 (#11124)

* Add stepped curve for referenda

* Treasury SpendOrigin

* Add tests

* Better Origin Or-gating

* Reciprocal curve

* Tests for reciprical and rounding in PerThings

* Tweaks and new quad curve

* Const derivation of reciprocal curve parameters

* Remove some unneeded code

* Actually useful linear curve

* Fixes

* Provisional curves

* Rejig 'turnout' as 'support'

* Use TypedGet

* Fixes

* Enable curve's ceil to be configured

* Formatting

* Fixes

* Fixes

* Fixes

* Remove EnsureOneOf

* Fixes

* Fixes

* Fixes

* Formatting

* Fixes

* Update frame/support/src/traits/dispatch.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Grumbles

* Formatting

* Fixes

* APIs of VoteTally should include class

* Fixes

* Fix overlay prefix removal result

* Second part of the overlay prefix removal fix.

* Formatting

* Fixes

* Add some tests and make clear rounding algo

* Fixes

* Formatting

* Revert questionable fix

* Introduce test for kill_prefix

* Fixes

* Formatting

* Fixes

* Fix possible overflow

* Docs

* Add benchmark test

* Formatting

* Update frame/referenda/src/types.rs

Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>

* Docs

* Fixes

* Use latest API in tests

* Formatting

* Whitespace

* Use latest API in tests

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
This commit is contained in:
Gavin Wood
2022-05-31 11:12:34 +01:00
committed by GitHub
parent c808340d9a
commit 7808b0c349
34 changed files with 2050 additions and 339 deletions
@@ -101,27 +101,27 @@ fn info<T: Config>(index: ReferendumIndex) -> &'static TrackInfoOf<T> {
}
fn make_passing_after<T: Config>(index: ReferendumIndex, period_portion: Perbill) {
let turnout = info::<T>(index).min_turnout.threshold(period_portion);
let support = info::<T>(index).min_support.threshold(period_portion);
let approval = info::<T>(index).min_approval.threshold(period_portion);
Referenda::<T>::access_poll(index, |status| {
if let PollStatus::Ongoing(tally, ..) = status {
*tally = T::Tally::from_requirements(turnout, approval);
if let PollStatus::Ongoing(tally, class) = status {
*tally = T::Tally::from_requirements(support, approval, class);
}
});
}
fn make_passing<T: Config>(index: ReferendumIndex) {
Referenda::<T>::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<T: Config>(index: ReferendumIndex) {
Referenda::<T>::access_poll(index, |status| {
if let PollStatus::Ongoing(tally, ..) = status {
*tally = T::Tally::default();
if let PollStatus::Ongoing(tally, class) = status {
*tally = T::Tally::rejection(class);
}
});
}
@@ -501,6 +501,7 @@ benchmarks! {
let (_caller, index) = create_referendum::<T>();
place_deposit::<T>(index);
skip_prepare_period::<T>(index);
make_failing::<T>(index);
nudge::<T>(index);
skip_decision_period::<T>(index);
}: nudge_referendum(RawOrigin::Root, index)
+31 -17
View File
@@ -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`]
@@ -148,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<Self::Votes> + Default + Clone + Codec + Eq + Debug + TypeInfo;
type Tally: VoteTally<Self::Votes, TrackIdOf<Self, I>>
+ Clone
+ Codec
+ Eq
+ Debug
+ TypeInfo;
// Constants
/// The minimum amount to be used as a deposit for a public referendum proposal.
@@ -369,7 +378,7 @@ pub mod pallet {
submission_deposit,
decision_deposit: None,
deciding: None,
tally: Default::default(),
tally: TallyOf::<T, I>::new(track),
in_queue: false,
alarm: Self::set_alarm(nudge_call, now.saturating_add(T::UndecidingTimeout::get())),
};
@@ -613,7 +622,7 @@ impl<T: Config<I>, I: 'static> Polling<T::Tally> for Pallet<T, I> {
submission_deposit: Deposit { who: dummy_account_id, amount: Zero::zero() },
decision_deposit: None,
deciding: None,
tally: Default::default(),
tally: TallyOf::<T, I>::new(class),
in_queue: false,
alarm: None,
};
@@ -723,8 +732,9 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
&status.tally,
Zero::zero(),
track.decision_period,
&track.min_turnout,
&track.min_support,
&track.min_approval,
status.track,
);
status.in_queue = false;
Self::deposit_event(Event::<T, I>::DecisionStarted {
@@ -740,7 +750,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
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 };
@@ -765,7 +775,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
(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::<T, I>::mutate(status.track, |q| q.insert_sorted_by_key(item, |x| x.1));
(None, ServiceBranch::Queued)
@@ -872,7 +882,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
// 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::<T, I>::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);
@@ -930,8 +940,9 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
&status.tally,
now.saturating_sub(deciding.since),
track.decision_period,
&track.min_turnout,
&track.min_support,
&track.min_approval,
status.track,
);
branch = if is_passing {
match deciding.confirming {
@@ -996,7 +1007,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
ServiceBranch::ContinueNotConfirming
}
};
alarm = Self::decision_time(deciding, &status.tally, track);
alarm = Self::decision_time(deciding, &status.tally, status.track, track);
},
}
@@ -1009,15 +1020,16 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
fn decision_time(
deciding: &DecidingStatusOf<T>,
tally: &T::Tally,
track_id: TrackIdOf<T, I>,
track: &TrackInfoOf<T, I>,
) -> 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 turnout = tally.turnout();
let approval = tally.approval(track_id);
let support = tally.support(track_id);
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 +1074,18 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
}
/// 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,
id: TrackIdOf<T, I>,
) -> 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(id)) &&
approval_needed.passing(x, tally.approval(id))
}
}
+39 -22
View File
@@ -160,12 +160,14 @@ impl TracksInfo<u64, u64> 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),
ceil: Perbill::from_percent(100),
},
min_turnout: Curve::LinearDecreasing {
begin: Perbill::from_percent(100),
delta: Perbill::from_percent(100),
min_support: Curve::LinearDecreasing {
length: Perbill::from_percent(100),
floor: Perbill::from_percent(0),
ceil: Perbill::from_percent(100),
},
},
),
@@ -180,12 +182,14 @@ impl TracksInfo<u64, u64> 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(100),
floor: Perbill::from_percent(95),
ceil: Perbill::from_percent(100),
},
min_turnout: Curve::LinearDecreasing {
begin: Perbill::from_percent(10),
delta: Perbill::from_percent(10),
min_support: Curve::LinearDecreasing {
length: Perbill::from_percent(100),
floor: Perbill::from_percent(90),
ceil: Perbill::from_percent(100),
},
},
),
@@ -241,35 +245,48 @@ 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<u32> for Tally {
fn ayes(&self) -> u32 {
impl<Class> VoteTally<u32, Class> for Tally {
fn new(_: Class) -> Self {
Self { ayes: 0, nays: 0 }
}
fn ayes(&self, _: Class) -> u32 {
self.ayes
}
fn turnout(&self) -> Perbill {
Perbill::from_percent(self.ayes + self.nays)
fn support(&self, _: Class) -> Perbill {
Perbill::from_percent(self.ayes)
}
fn approval(&self) -> Perbill {
Perbill::from_rational(self.ayes, self.ayes + self.nays)
fn approval(&self, _: Class) -> Perbill {
if self.ayes + self.nays > 0 {
Perbill::from_rational(self.ayes, self.ayes + self.nays)
} else {
Perbill::zero()
}
}
#[cfg(feature = "runtime-benchmarks")]
fn unanimity() -> Self {
fn unanimity(_: Class) -> Self {
Self { ayes: 100, nays: 0 }
}
#[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 rejection(_: Class) -> Self {
Self { ayes: 0, nays: 100 }
}
#[cfg(feature = "runtime-benchmarks")]
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 }
}
}
+6 -4
View File
@@ -504,12 +504,14 @@ 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::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 { begin: Perbill::zero(), delta: Perbill::one() };
assert_eq!(delay, Perbill::one());
let threshold = test_curve.threshold(Perbill::one());
assert_eq!(threshold, Perbill::zero());
+359 -50
View File
@@ -21,7 +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;
use sp_arithmetic::{Rounding::*, SignedRounding::*};
use sp_runtime::{FixedI64, PerThing, RuntimeDebug};
use sp_std::fmt::Debug;
pub type BalanceOf<T, I = ()> =
@@ -91,40 +92,6 @@ impl<T: Ord, S: Get<u32>> InsertSorted<T> for BoundedVec<T, S> {
}
}
#[cfg(test)]
mod tests {
use super::*;
use frame_support::traits::ConstU32;
#[test]
fn insert_sorted_works() {
let mut b: BoundedVec<u32, ConstU32<6>> = 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<BlockNumber> {
/// When this referendum began being "decided". If confirming, then the
@@ -143,7 +110,7 @@ pub struct Deposit<AccountId, Balance> {
#[derive(Clone, Encode, TypeInfo)]
pub struct TrackInfo<Balance, Moment> {
/// Name of this track. TODO was &'static str
/// Name of this track.
pub name: &'static str,
/// A limit for the number of referenda on this track that can be being decided at once.
/// For Root origin this should generally be just one.
@@ -161,9 +128,9 @@ pub struct TrackInfo<Balance, Moment> {
/// 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.
@@ -282,21 +249,186 @@ 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, ceil)`, proceeding linearly to `(length, floor)`, then
/// remaining at `floor` until the end of the period.
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
/// but the `y` component has a lower limit of `end`.
SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill },
/// A recipocal (`K/(x+S)-T`) curve: `factor` is `K` and `x_offset` is `S`, `y_offset` is `T`.
Reciprocal { factor: FixedI64, x_offset: FixedI64, y_offset: FixedI64 },
}
/// Calculate the quadratic solution for the given curve.
///
/// WARNING: This is a `const` function designed for convenient use at build time and
/// will panic on overflow. Ensure that any inputs are sensible.
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 {
/// Create a `Curve::Linear` instance from a high-level description.
///
/// WARNING: This is a `const` function designed for convenient use at build time and
/// will panic on overflow. Ensure that any inputs are sensible.
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();
Curve::LinearDecreasing { length, floor, ceil }
}
/// Create a `Curve::Reciprocal` instance from a high-level description.
///
/// WARNING: This is a `const` function designed for convenient use at build time and
/// will panic on overflow. Ensure that any inputs are sensible.
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, 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);
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)));
} else {
bounds = ((factor, curve, level.sub(curve_level)), bounds.1);
}
}
if (bounds.0).2.into_inner() < (bounds.1).2.into_inner() {
(bounds.0).1
} else {
(bounds.1).1
}
}
/// Create a `Curve::Reciprocal` instance from basic parameters.
///
/// WARNING: This is a `const` function designed for convenient use at build time and
/// will panic on overflow. Ensure that any inputs are sensible.
const fn reciprocal_from_parts(factor: FixedI64, floor: FixedI64, ceil: FixedI64) -> Self {
let delta = ceil.sub(floor);
let x_offset = pos_quad_solution(delta, delta, 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);
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 + {}d: {:?}", t, 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::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 } => factor
.checked_rounding_div(FixedI64::from(x) + *x_offset, Low)
.map(|yp| (yp + *y_offset).into_clamped_perthing())
.unwrap_or_else(Perbill::one),
}
}
/// Determine the `y` value for the given `x` value.
///
/// This is a partial implementation designed only for use in const functions.
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), Low) {
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.
///
/// 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.
@@ -307,12 +439,27 @@ impl Curve {
/// ```
pub fn delay(&self, y: Perbill) -> Perbill {
match self {
Self::LinearDecreasing { begin, delta } =>
if delta.is_zero() {
*delta
Self::LinearDecreasing { length, floor, ceil } =>
if y < *floor {
Perbill::one()
} else if y > *ceil {
Perbill::zero()
} else {
(*begin - y.min(*begin)).min(*delta) / *delta
(*ceil - y).saturating_div(*ceil - *floor, Up).saturating_mul(*length)
},
Self::SteppedDecreasing { begin, end, step, period } =>
if y < *end {
Perbill::one()
} else {
period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step))
},
Self::Reciprocal { factor, x_offset, y_offset } => {
let y = FixedI64::from(y);
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)
},
}
}
@@ -326,14 +473,176 @@ 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, ceil } => {
write!(
f,
"Linear[(0%, {}%) -> (100%, {}%)]",
*begin * 100u32,
(*begin - *delta) * 100u32,
"Linear[(0%, {:?}) -> ({:?}, {:?}) -> (100%, {:?})]",
ceil, length, floor, floor,
)
},
Self::SteppedDecreasing { begin, end, step, period } => {
write!(
f,
"Stepped[(0%, {:?}) -> (100%, {:?}) by ({:?}, {:?})]",
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,
)
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use frame_support::traits::ConstU32;
use sp_runtime::PerThing;
const fn percent(x: u128) -> FixedI64 {
FixedI64::from_rational(x, 100)
}
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]
fn check_curves() {
TIP_APP.info(28u32, "Tip Approval");
TIP_SUP.info(28u32, "Tip Support");
ROOT_APP.info(28u32, "Root Approval");
ROOT_SUP.info(28u32, "Root Support");
WHITE_APP.info(28u32, "Whitelist Approval");
WHITE_SUP.info(28u32, "Whitelist Support");
SMALL_APP.info(28u32, "Small Spend Approval");
SMALL_SUP.info(28u32, "Small Spend Support");
MID_APP.info(28u32, "Mid Spend Approval");
MID_SUP.info(28u32, "Mid Spend Support");
BIG_APP.info(28u32, "Big Spend Approval");
BIG_SUP.info(28u32, "Big Spend Support");
HUGE_APP.info(28u32, "Huge Spend Approval");
HUGE_SUP.info(28u32, "Huge Spend Support");
PARAM_APP.info(28u32, "Mid-tier Parameter Change Approval");
PARAM_SUP.info(28u32, "Mid-tier Parameter Change Support");
ADMIN_APP.info(28u32, "Admin (e.g. Cancel Slash) Approval");
ADMIN_SUP.info(28u32, "Admin (e.g. Cancel Slash) Support");
assert!(false);
}
#[test]
fn insert_sorted_works() {
let mut b: BoundedVec<u32, ConstU32<6>> = 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 translated_reciprocal_works() {
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),
};
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 stepped_decreasing_works() {
fn pc(x: u32) -> Perbill {
Perbill::from_percent(x)
}
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);
// 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(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));
}
}