diff --git a/substrate/frame/referenda/src/lib.rs b/substrate/frame/referenda/src/lib.rs index e616056c3022a..ae3d2c16cd380 100644 --- a/substrate/frame/referenda/src/lib.rs +++ b/substrate/frame/referenda/src/lib.rs @@ -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, @@ -74,7 +74,7 @@ use frame_support::{ DispatchTime, }, Currency, LockIdentifier, OnUnbalanced, OriginTrait, PollStatus, Polling, QueryPreimage, - ReservableCurrency, StorePreimage, VoteTally, + Randomness, ReservableCurrency, StorePreimage, VoteTally, }, BoundedVec, }; @@ -174,6 +174,8 @@ pub mod pallet { PalletsOriginOf, Hasher = Self::Hashing, >; + /// Something that provides randomness in the runtime. + type Randomness: Randomness>; /// Currency type for this pallet. type Currency: ReservableCurrency; // Origins and unbalances. @@ -247,6 +249,18 @@ pub mod pallet { pub type ReferendumInfoFor, I: 'static = ()> = StorageMap<_, Blake2_128Concat, ReferendumIndex, ReferendumInfoOf>; + /// State change within confirm period. + #[pallet::storage] + pub type PassingStatusInConfirmPeriod, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + ReferendumIndex, + Blake2_128Concat, + BlockNumberFor, + bool, + ValueQuery, + >; + /// The sorted list of referenda ready to be decided but not yet being decided, ordered by /// conviction-weighted approvals. /// @@ -613,10 +627,23 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { ensure_root(origin)?; let now = frame_system::Pallet::::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::::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::::NotOngoing)?, + }; + if dirty { ReferendumInfoFor::::insert(index, info); } @@ -1055,8 +1082,10 @@ impl, I: 'static> Pallet { /// 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 @@ -1159,26 +1188,32 @@ impl, I: 'static> Pallet { 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::::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::::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::::insert(index, now, is_passing); + ServiceBranch::ContinueConfirming + }, None => { // Start confirming dirty = true; @@ -1188,29 +1223,64 @@ impl, I: 'static> Pallet { }, } } 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::::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::::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::::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::::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::::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::::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); @@ -1225,6 +1295,92 @@ impl, I: 'static> Pallet { (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, + index: ReferendumIndex, + mut status: ReferendumStatusOf, + ) -> (ReferendumInfoOf, 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 = >::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::::iter_prefix(index).collect::>(); + 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::::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::::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(