Skip to content

Commit

Permalink
feat(pallet-referenda): implement Finalizing state via service_auction
Browse files Browse the repository at this point in the history
  • Loading branch information
pandres95 committed Mar 26, 2024
1 parent 471ef8b commit e9f7d28
Showing 1 changed file with 202 additions and 46 deletions.
248 changes: 202 additions & 46 deletions substrate/frame/referenda/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
#![recursion_limit = "256"]
#![cfg_attr(not(feature = "std"), no_std)]

use codec::{Codec, Encode};
use codec::{Codec, Decode, Encode};
use frame_support::{
dispatch::DispatchResult,
ensure,
Expand All @@ -74,7 +74,7 @@ use frame_support::{
DispatchTime,
},
Currency, LockIdentifier, OnUnbalanced, OriginTrait, PollStatus, Polling, QueryPreimage,
ReservableCurrency, StorePreimage, VoteTally,
Randomness, ReservableCurrency, StorePreimage, VoteTally,
},
BoundedVec,
};
Expand Down Expand Up @@ -174,6 +174,8 @@ pub mod pallet {
PalletsOriginOf<Self>,
Hasher = Self::Hashing,
>;
/// Something that provides randomness in the runtime.
type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
/// Currency type for this pallet.
type Currency: ReservableCurrency<Self::AccountId>;
// Origins and unbalances.
Expand Down Expand Up @@ -247,6 +249,18 @@ pub mod pallet {
pub type ReferendumInfoFor<T: Config<I>, I: 'static = ()> =
StorageMap<_, Blake2_128Concat, ReferendumIndex, ReferendumInfoOf<T, I>>;

/// State change within confirm period.
#[pallet::storage]
pub type PassingStatusInConfirmPeriod<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
_,
Blake2_128Concat,
ReferendumIndex,
Blake2_128Concat,
BlockNumberFor<T>,
bool,
ValueQuery,
>;

/// The sorted list of referenda ready to be decided but not yet being decided, ordered by
/// conviction-weighted approvals.
///
Expand Down Expand Up @@ -613,10 +627,23 @@ pub mod pallet {
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
let now = frame_system::Pallet::<T>::block_number();
let mut status = Self::ensure_ongoing(index)?;
// This is our wake-up, so we can disregard the alarm.
status.alarm = None;
let (info, dirty, branch) = Self::service_referendum(now, index, status);

let info = ReferendumInfoFor::<T, I>::get(index);

let (info, dirty, branch) = match info {
Some(ReferendumInfo::Ongoing(mut status)) => {
// This is our wake-up, so we can disregard the alarm.
status.alarm = None;
Self::service_referendum(now, index, status)
},
Some(ReferendumInfo::Finalizing(mut status)) => {
status.alarm = None;
Self::service_auction(now, index, status)
},
Some(info) => (info, false, ServiceBranch::Fail),
_ => Err(Error::<T, I>::NotOngoing)?,
};

if dirty {
ReferendumInfoFor::<T, I>::insert(index, info);
}
Expand Down Expand Up @@ -1055,8 +1082,10 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Advance the state of a referendum, which comes down to:
/// - If it's ready to be decided, start deciding;
/// - If it's not ready to be decided and non-deciding timeout has passed, fail;
/// - If it's ongoing and passing, ensure confirming; if at end of confirmation period, pass.
/// - If it's ongoing and not passing, stop confirming; if it has reached end time, fail.
/// - If it's ongoing and passing, ensure confirming; if at end of confirmation period, decide
/// by "candle".
/// - If it's ongoing and not passing, stop confirming; if it has reached end time, decide by
/// "candle".
///
/// Weight will be a bit different depending on what it does, but it's designed so as not to
/// differ dramatically, especially if `MaxQueue` is kept small. In particular _there are no
Expand Down Expand Up @@ -1159,26 +1188,32 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
branch = if is_passing {
match deciding.confirming {
Some(t) if now >= t => {
// Passed!
Self::ensure_no_alarm(&mut status);
Self::note_one_fewer_deciding(status.track);
let (desired, call) = (status.enactment, status.proposal);
Self::schedule_enactment(index, track, desired, status.origin, call);
Self::deposit_event(Event::<T, I>::Confirmed {
// Ongoing is now Finished. From now on, it'll be impossible to modify the
// poll. The passing status will be now decided by the auction, and the
// information stored under `PassingStatusInConfirmPeriod`.

// Sent to Auction only after confirm period finishes. Whether it will pass
// or not depends on the status history when confirming, and a random point
// in time within confirm period.
PassingStatusInConfirmPeriod::<T, I>::insert(
index,
tally: status.tally,
});
now.saturating_less_one(),
is_passing,
);
Self::ensure_alarm_at(&mut status, index, now.saturating_plus_one());

return (
ReferendumInfo::Approved(
now,
Some(status.submission_deposit),
status.decision_deposit,
),
ReferendumInfo::Finalizing(status),
true,
ServiceBranch::Approved,
ServiceBranch::ContinueConfirming,
)
},
Some(_) => ServiceBranch::ContinueConfirming,
Some(_) => {
// We don't care if failing within confirm period.
// Report the change of state, and continue.
PassingStatusInConfirmPeriod::<T, I>::insert(index, now, is_passing);
ServiceBranch::ContinueConfirming
},
None => {
// Start confirming
dirty = true;
Expand All @@ -1188,29 +1223,64 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
},
}
} else {
if now >= deciding.since.saturating_add(track.decision_period) {
// Failed!
Self::ensure_no_alarm(&mut status);
Self::note_one_fewer_deciding(status.track);
Self::deposit_event(Event::<T, I>::Rejected { index, tally: status.tally });
return (
ReferendumInfo::Rejected(
now,
Some(status.submission_deposit),
status.decision_deposit,
),
true,
ServiceBranch::Rejected,
)
}
if deciding.confirming.is_some() {
// Stop confirming
dirty = true;
deciding.confirming = None;
Self::deposit_event(Event::<T, I>::ConfirmAborted { index });
ServiceBranch::EndConfirming
} else {
ServiceBranch::ContinueNotConfirming
match deciding.confirming {
Some(_) if now <= deciding.since.saturating_add(track.decision_period) => {
// Stop confirming
dirty = true;
deciding.confirming = None;
Self::deposit_event(Event::<T, I>::ConfirmAborted { index });
ServiceBranch::EndConfirming
},
Some(t) if now >= t => {
// Ongoing is now Finished. From now on, it'll be impossible to modify the
// poll. The passing status will be now decided by the auction, and the
// information stored under `PassingStatusInConfirmPeriod`.

// Sent to Auction only after confirm period finishes. Whether it will pass
// or not depends on the status history when confirming, and a random point
// in time within confirm period.
PassingStatusInConfirmPeriod::<T, I>::insert(
index,
now.saturating_less_one(),
is_passing,
);
Self::ensure_alarm_at(&mut status, index, now.saturating_plus_one());

return (
ReferendumInfo::Finalizing(status),
true,
ServiceBranch::ContinueConfirming,
)
},
Some(_) => {
// We don't care if failing within confirm period.
// Report the change of state, and continue.
PassingStatusInConfirmPeriod::<T, I>::insert(index, now, is_passing);
ServiceBranch::ContinueConfirming
},
None => {
if now >= deciding.since.saturating_add(track.decision_period) {
// Exceeded decision period without having passed in first place.
// Failed!
Self::ensure_no_alarm(&mut status);
Self::note_one_fewer_deciding(status.track);
Self::deposit_event(Event::<T, I>::Rejected {
index,
tally: status.tally,
});
return (
ReferendumInfo::Rejected(
now,
Some(status.submission_deposit),
status.decision_deposit,
),
true,
ServiceBranch::Rejected,
);
} else {
ServiceBranch::ContinueNotConfirming
}
},
}
};
alarm = Self::decision_time(deciding, &status.tally, status.track, track);
Expand All @@ -1225,6 +1295,92 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
(ReferendumInfo::Ongoing(status), dirty_alarm || dirty, branch)
}

/// "Candle" auction to decide the winning status of confirm period.
///
/// The "candle": passing or failing of a referendum is ultimately decided as a candle auction
/// where, given a random point in time (defined as `t`), the definitive status of the the
/// referendum is decided by the last status registered before `t`.
fn service_auction(
now: BlockNumberFor<T>,
index: ReferendumIndex,
mut status: ReferendumStatusOf<T, I>,
) -> (ReferendumInfoOf<T, I>, bool, ServiceBranch) {
// Note: it'd be weird to come up to this point and yet not have a track
let track = match Self::track(status.clone().track) {
Some(x) => x,
None => return (ReferendumInfo::Finalizing(status), false, ServiceBranch::Fail),
};

let confirming_until = status
.clone()
.deciding
.expect("having passed ongoing, we should have times for decision; qed")
.confirming
.expect("having finished confirming, we should have a confirming_until time; qed");

let confirming_since = confirming_until.saturating_sub(track.confirm_period);

let (raw_offset, known_since) = T::Randomness::random(&b"confirm_auction"[..]);

// Do not use random until made sure random seed is not known before confirm ends
if known_since <= confirming_until {
Self::ensure_alarm_at(&mut status, index, now.saturating_plus_one());
return (ReferendumInfo::Finalizing(status), false, ServiceBranch::ContinueConfirming);
}

let raw_offset_block_number = <BlockNumberFor<T>>::decode(&mut raw_offset.as_ref())
.expect("secure hashes should always be bigger than the block number; qed");

let candle_block_number = if confirming_since <= raw_offset_block_number
&& raw_offset_block_number < confirming_until
{
raw_offset_block_number
} else {
confirming_since.saturating_add(raw_offset_block_number % track.confirm_period)
};

let mut statuses =
PassingStatusInConfirmPeriod::<T, I>::iter_prefix(index).collect::<Vec<_>>();
statuses.sort_by(|(a, _), (b, _)| b.cmp(a));

let (_, winning_status) = statuses
.into_iter()
.find(|(when, _)| when <= &candle_block_number)
.unwrap_or((now, true));

if winning_status {
// Passed!
Self::ensure_no_alarm(&mut status);
Self::note_one_fewer_deciding(status.track);
let (desired, call) = (status.enactment, status.proposal);
Self::schedule_enactment(index, track, desired, status.origin, call);
Self::deposit_event(Event::<T, I>::Confirmed { index, tally: status.tally });
(
ReferendumInfo::Approved(
now,
Some(status.submission_deposit),
status.decision_deposit,
),
true,
ServiceBranch::Approved,
)
} else {
// Failed!
Self::ensure_no_alarm(&mut status);
Self::note_one_fewer_deciding(status.track);
Self::deposit_event(Event::<T, I>::Rejected { index, tally: status.tally });
return (
ReferendumInfo::Rejected(
now,
Some(status.submission_deposit),
status.decision_deposit,
),
true,
ServiceBranch::Rejected,
);
}
}

/// Determine the point at which a referendum will be accepted, move into confirmation with the
/// given `tally` or end with rejection (whichever happens sooner).
fn decision_time(
Expand Down

0 comments on commit e9f7d28

Please sign in to comment.