--- a/Cargo.lock +++ b/Cargo.lock @@ -5389,7 +5389,7 @@ "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", "pallet-unique", - "pallet-unique-scheduler", + "pallet-unique-scheduler-v2", "pallet-xcm", "parachain-info", "parity-scale-codec 3.2.1", @@ -6725,9 +6725,10 @@ dependencies = [ "frame-support", "frame-system", - "pallet-unique-scheduler", + "pallet-unique-scheduler-v2", "parity-scale-codec 3.2.1", "scale-info", + "sp-std", ] [[package]] @@ -6851,22 +6852,21 @@ ] [[package]] -name = "pallet-unique-scheduler" -version = "0.1.1" +name = "pallet-unique-scheduler-v2" +version = "0.1.0" dependencies = [ "frame-benchmarking", "frame-support", "frame-system", "log", + "pallet-preimage", "parity-scale-codec 3.2.1", "scale-info", - "serde", "sp-core", "sp-io", "sp-runtime", "sp-std", "substrate-test-utils", - "up-sponsorship", ] [[package]] @@ -8861,7 +8861,6 @@ "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", "pallet-unique", - "pallet-unique-scheduler", "pallet-xcm", "parachain-info", "parity-scale-codec 3.2.1", @@ -12992,7 +12991,6 @@ "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", "pallet-unique", - "pallet-unique-scheduler", "pallet-xcm", "parachain-info", "parity-scale-codec 3.2.1", --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ .PHONY: bench-scheduler bench-scheduler: - make _bench PALLET=unique-scheduler PALLET_DIR=scheduler + make _bench PALLET=unique-scheduler-v2 PALLET_DIR=scheduler-v2 .PHONY: bench-rmrk-core bench-rmrk-core: --- /dev/null +++ b/pallets/scheduler-v2/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pallet-unique-scheduler-v2" +version = "0.1.0" +authors = ["Unique Network "] +edition = "2021" +license = "GPLv3" +homepage = "https://unique.network" +repository = "https://github.com/UniqueNetwork/unique-chain" +description = "Unique Scheduler pallet" +readme = "README.md" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-io = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-runtime = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-std = { version = "4.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-core = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } + +[dev-dependencies] +pallet-preimage = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +sp-core = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } +substrate-test-utils = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "sp-core/std", +] +try-runtime = ["frame-support/try-runtime"] --- /dev/null +++ b/pallets/scheduler-v2/src/benchmarking.rs @@ -0,0 +1,374 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +// Original license: +// 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. + +//! Scheduler pallet benchmarking. + +use super::*; +use frame_benchmarking::{account, benchmarks}; +use frame_support::{ + ensure, + traits::{schedule::Priority, PreimageRecipient}, +}; +use frame_system::RawOrigin; +use sp_std::{prelude::*, vec}; +use sp_io::hashing::blake2_256; + +use crate::{Pallet as Scheduler, ScheduledCall, EncodedCall}; +use frame_system::Call as SystemCall; + +const SEED: u32 = 0; + +const BLOCK_NUMBER: u32 = 2; + +/// Add `n` items to the schedule. +/// +/// For `resolved`: +/// - ` +/// - `None`: aborted (hash without preimage) +/// - `Some(true)`: hash resolves into call if possible, plain call otherwise +/// - `Some(false)`: plain call +fn fill_schedule(when: T::BlockNumber, n: u32) -> Result<(), &'static str> { + let t = DispatchTime::At(when); + let origin: ::PalletsOrigin = frame_system::RawOrigin::Root.into(); + for i in 0..n { + let call = make_call::(None); + let period = Some(((i + 100).into(), 100)); + let name = u32_to_name(i); + Scheduler::::do_schedule_named(name, t, period, 0, origin.clone(), call)?; + } + ensure!( + Agenda::::get(when).agenda.len() == n as usize, + "didn't fill schedule" + ); + Ok(()) +} + +/// Generate a name for a scheduled task from an unsigned integer. +fn u32_to_name(i: u32) -> TaskName { + i.using_encoded(blake2_256) +} + +/// A utility for creating simple scheduled tasks. +/// +/// # Arguments +/// * `periodic` - makes the task periodic. +/// Sets the task's period and repetition count to `100`. +/// * `named` - gives a name to the task: `u32_to_name(0)`. +/// * `signed` - determines the origin of the task. +/// If true, it will have the Signed origin. Otherwise it will have the Root origin. +/// See [`make_origin`] for details. +/// * maybe_lookup_len - sets optional lookup length. It is used to benchmark task fetching from the `Preimages` store. +/// * priority - the task's priority. +fn make_task( + periodic: bool, + named: bool, + signed: bool, + maybe_lookup_len: Option, + priority: Priority, +) -> ScheduledOf { + let call = make_call::(maybe_lookup_len); + let maybe_periodic = match periodic { + true => Some((100u32.into(), 100)), + false => None, + }; + let maybe_id = match named { + true => Some(u32_to_name(0)), + false => None, + }; + let origin = make_origin::(signed); + Scheduled { + maybe_id, + priority, + call, + maybe_periodic, + origin, + _phantom: PhantomData, + } +} + +/// Creates a `SystemCall::remark` scheduled call with a given `len` in bytes. +/// Returns `None` if the call is too large to encode. +fn bounded(len: u32) -> Option> { + let call = <::RuntimeCall>::from(SystemCall::remark { + remark: vec![0; len as usize], + }); + ScheduledCall::new(call).ok() +} + +/// Creates a scheduled call and maximizes its size. +/// +/// If the `maybe_lookup_len` is not supplied, the task will create the maximal `Inline` scheduled call. +/// +/// Otherwise, the function will take the length value from the `maybe_lookup_len` +/// and find a minimal length value that ensures that the scheduled call will require a Preimage lookup. +fn make_call(maybe_lookup_len: Option) -> ScheduledCall { + let bound = EncodedCall::bound() as u32; + let mut len = match maybe_lookup_len { + Some(len) => { + len.min(>::MaxSize::get() - 2) + .max(bound) - 3 + } + None => bound.saturating_sub(4), + }; + + loop { + let c = match bounded::(len) { + Some(x) => x, + None => { + len -= 1; + continue; + } + }; + if c.lookup_needed() == maybe_lookup_len.is_some() { + break c; + } + if maybe_lookup_len.is_some() { + len += 1; + } else { + if len > 0 { + len -= 1; + } else { + break c; + } + } + } +} + +/// Creates an origin for a scheduled call. +/// +/// If `signed` is true, it creates the Signed origin from a default account `account("origin", 0, SEED)`. +/// Otherwise, it creates the Root origin. +fn make_origin(signed: bool) -> ::PalletsOrigin { + match signed { + true => frame_system::RawOrigin::Signed(account("origin", 0, SEED)).into(), + false => frame_system::RawOrigin::Root.into(), + } +} + +/// Creates a dummy `WeightCounter` with the maximum possible weight limit. +fn dummy_counter() -> WeightCounter { + WeightCounter { + used: Weight::zero(), + limit: Weight::MAX, + } +} + +benchmarks! { + // `service_agendas` when no work is done. + // (multiple agendas - scheduled tasks in several blocks) + service_agendas_base { + let now = T::BlockNumber::from(BLOCK_NUMBER); + IncompleteSince::::put(now - One::one()); + }: { + Scheduler::::service_agendas(&mut dummy_counter(), now, 0); + } verify { + assert_eq!(IncompleteSince::::get(), Some(now - One::one())); + } + + // `service_agenda` when no work is done. + // (only one agenda - scheduled tasks in a single block) + service_agenda_base { + let now = BLOCK_NUMBER.into(); + let s in 0 .. T::MaxScheduledPerBlock::get(); + fill_schedule::(now, s)?; + let mut executed = 0; + }: { + Scheduler::::service_agenda(&mut dummy_counter(), &mut executed, now, now, 0); + } verify { + assert_eq!(executed, 0); + } + + // `service_task` when the task is a non-periodic, non-named, non-fetched call which is not + // dispatched (e.g. due to being overweight). + service_task_base { + let now = BLOCK_NUMBER.into(); + let task = make_task::(false, false, false, None, 0); + // prevent any tasks from actually being executed as we only want the surrounding weight. + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + }: { + let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + } verify { + //assert_eq!(result, Ok(())); + } + + // TODO uncomment if we will use the Preimages + // // `service_task` when the task is a non-periodic, non-named, fetched call (with a known + // // preimage length) and which is not dispatched (e.g. due to being overweight). + // service_task_fetched { + // let s in (EncodedCall::bound() as u32) .. (>::MaxSize::get()); + // let now = BLOCK_NUMBER.into(); + // let task = make_task::(false, false, false, Some(s), 0); + // // prevent any tasks from actually being executed as we only want the surrounding weight. + // let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + // }: { + // let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + // } verify { + // } + + // `service_task` when the task is a non-periodic, named, non-fetched call which is not + // dispatched (e.g. due to being overweight). + service_task_named { + let now = BLOCK_NUMBER.into(); + let task = make_task::(false, true, false, None, 0); + // prevent any tasks from actually being executed as we only want the surrounding weight. + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + }: { + let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + } verify { + } + + // `service_task` when the task is a periodic, non-named, non-fetched call which is not + // dispatched (e.g. due to being overweight). + service_task_periodic { + let now = BLOCK_NUMBER.into(); + let task = make_task::(true, false, false, None, 0); + // prevent any tasks from actually being executed as we only want the surrounding weight. + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::zero() }; + }: { + let result = Scheduler::::service_task(&mut counter, now, now, 0, true, task); + } verify { + } + + // `execute_dispatch` when the origin is `Signed`, not counting the dispatable's weight. + execute_dispatch_signed { + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::MAX }; + let origin = make_origin::(true); + let call = T::Preimages::realize(&make_call::(None)).unwrap().0; + }: { + assert!(Scheduler::::execute_dispatch(&mut counter, origin, call).is_ok()); + } + verify { + } + + // `execute_dispatch` when the origin is not `Signed`, not counting the dispatable's weight. + execute_dispatch_unsigned { + let mut counter = WeightCounter { used: Weight::zero(), limit: Weight::MAX }; + let origin = make_origin::(false); + let call = T::Preimages::realize(&make_call::(None)).unwrap().0; + }: { + assert!(Scheduler::::execute_dispatch(&mut counter, origin, call).is_ok()); + } + verify { + } + + schedule { + let s in 0 .. (T::MaxScheduledPerBlock::get() - 1); + let when = BLOCK_NUMBER.into(); + let periodic = Some((T::BlockNumber::one(), 100)); + let priority = Some(0); + // Essentially a no-op call. + let call = Box::new(SystemCall::set_storage { items: vec![] }.into()); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, when, periodic, priority, call) + verify { + ensure!( + Agenda::::get(when).agenda.len() == (s + 1) as usize, + "didn't add to schedule" + ); + } + + cancel { + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + + fill_schedule::(when, s)?; + assert_eq!(Agenda::::get(when).agenda.len(), s as usize); + }: _(RawOrigin::Root, when, 0) + verify { + ensure!( + Lookup::::get(u32_to_name(0)).is_none(), + "didn't remove from lookup" + ); + // Removed schedule is NONE + ensure!( + Agenda::::get(when).agenda[0].is_none(), + "didn't remove from schedule" + ); + } + + schedule_named { + let s in 0 .. (T::MaxScheduledPerBlock::get() - 1); + let id = u32_to_name(s); + let when = BLOCK_NUMBER.into(); + let periodic = Some((T::BlockNumber::one(), 100)); + let priority = Some(0); + // Essentially a no-op call. + let call = Box::new(SystemCall::set_storage { items: vec![] }.into()); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, id, when, periodic, priority, call) + verify { + ensure!( + Agenda::::get(when).agenda.len() == (s + 1) as usize, + "didn't add to schedule" + ); + } + + cancel_named { + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, u32_to_name(0)) + verify { + ensure!( + Lookup::::get(u32_to_name(0)).is_none(), + "didn't remove from lookup" + ); + // Removed schedule is NONE + ensure!( + Agenda::::get(when).agenda[0].is_none(), + "didn't remove from schedule" + ); + } + + change_named_priority { + let origin: RawOrigin = frame_system::RawOrigin::Root; + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + let idx = s - 1; + let id = u32_to_name(idx); + let priority = 42; + fill_schedule::(when, s)?; + }: _(origin, id, priority) + verify { + ensure!( + Agenda::::get(when).agenda[idx as usize].clone().unwrap().priority == priority, + "didn't change the priority" + ); + } + + impl_benchmark_test_suite!(Scheduler, crate::mock::new_test_ext(), crate::mock::Test); +} --- /dev/null +++ b/pallets/scheduler-v2/src/lib.rs @@ -0,0 +1,1330 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +// Original license: +// 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. + +//! # Scheduler +//! A Pallet for scheduling dispatches. +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Pallet`] +//! +//! ## Overview +//! +//! This Pallet exposes capabilities for scheduling dispatches to occur at a +//! specified block number or at a specified period. These scheduled dispatches +//! may be named or anonymous and may be canceled. +//! +//! **NOTE:** The scheduled calls will be dispatched with the default filter +//! for the origin: namely `frame_system::Config::BaseCallFilter` for all origin +//! except root which will get no filter. And not the filter contained in origin +//! use to call `fn schedule`. +//! +//! If a call is scheduled using proxy or whatever mecanism which adds filter, +//! then those filter will not be used when dispatching the schedule call. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! * `schedule` - schedule a dispatch, which may be periodic, to occur at a specified block and +//! with a specified priority. +//! * `cancel` - cancel a scheduled dispatch, specified by block number and index. +//! * `schedule_named` - augments the `schedule` interface with an additional `Vec` parameter +//! that can be used for identification. +//! * `cancel_named` - the named complement to the cancel function. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] +#![deny(missing_docs)] + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +pub mod weights; + +use codec::{Codec, Decode, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::{ + DispatchError, DispatchResult, Dispatchable, GetDispatchInfo, Parameter, PostDispatchInfo, + }, + traits::{ + schedule::{self, DispatchTime, LOWEST_PRIORITY}, + EnsureOrigin, Get, IsType, OriginTrait, PrivilegeCmp, StorageVersion, PreimageRecipient, + ConstU32, UnfilteredDispatchable, + }, + weights::Weight, + unsigned::TransactionValidityError, +}; + +use frame_system::{self as system}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{BadOrigin, One, Saturating, Zero, Hash}, + BoundedVec, RuntimeDebug, DispatchErrorWithPostInfo, +}; +use sp_core::H160; +use sp_std::{cmp::Ordering, marker::PhantomData, prelude::*}; +pub use weights::WeightInfo; + +pub use pallet::*; + +/// Just a simple index for naming period tasks. +pub type PeriodicIndex = u32; +/// The location of a scheduled task that can be used to remove it. +pub type TaskAddress = (BlockNumber, u32); + +/// A an encoded bounded `Call`. Its encoding must be at most 128 bytes. +pub type EncodedCall = BoundedVec>; + +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +/// A scheduled call is stored as is or as a preimage hash to lookup. +/// This enum represents both variants. +pub enum ScheduledCall { + /// A an encoded bounded `Call`. Its encoding must be at most 128 bytes. + Inline(EncodedCall), + + /// A Blake2-256 hash of the call together with an upper limit for its size. + PreimageLookup { + /// A call hash to lookup + hash: T::Hash, + + /// The length of the decoded call + unbounded_len: u32, + }, +} + +impl ScheduledCall { + /// Convert an otherwise unbounded or large value into a type ready for placing in storage. + /// + /// NOTE: Once this API is used, you should use either `drop` or `realize`. + pub fn new(call: ::RuntimeCall) -> Result { + let encoded = call.encode(); + let len = encoded.len(); + + match EncodedCall::try_from(encoded.clone()) { + Ok(bounded) => Ok(Self::Inline(bounded)), + Err(_) => { + let hash = ::Hashing::hash_of(&encoded); + ::Preimages::note_preimage( + encoded + .try_into() + .map_err(|_| >::TooBigScheduledCall)?, + ); + + Ok(Self::PreimageLookup { + hash, + unbounded_len: len as u32, + }) + } + } + } + + /// The maximum length of the lookup that is needed to peek `Self`. + pub fn lookup_len(&self) -> Option { + match self { + Self::Inline(..) => None, + Self::PreimageLookup { unbounded_len, .. } => Some(*unbounded_len), + } + } + + /// Returns whether the image will require a lookup to be peeked. + pub fn lookup_needed(&self) -> bool { + match self { + Self::Inline(_) => false, + Self::PreimageLookup { .. } => true, + } + } + + // Decodes a runtime call + fn decode(mut data: &[u8]) -> Result<::RuntimeCall, DispatchError> { + ::RuntimeCall::decode(&mut data) + .map_err(|_| >::ScheduledCallCorrupted.into()) + } +} + +/// Weight Info for the Preimages fetches. +pub trait SchedulerPreimagesWeightInfo { + /// Get the weight of a task fetches with a given decoded length. + fn service_task_fetched(call_length: u32) -> Weight; +} + +impl SchedulerPreimagesWeightInfo for () { + fn service_task_fetched(_call_length: u32) -> Weight { + W::service_task_base() + } +} + +/// A scheduler's interface for managing preimages to hashes +/// and looking up preimages from their hash on-chain. +pub trait SchedulerPreimages: + PreimageRecipient + SchedulerPreimagesWeightInfo +{ + /// No longer request that the data for decoding the given `call` is available. + fn drop(call: &ScheduledCall); + + /// Convert the given `call` instance back into its original instance, also returning the + /// exact size of its encoded form if it needed to be looked-up from a stored preimage. + /// + /// NOTE: This does not remove any data needed for realization. If you will no longer use the + /// `call`, use `realize` instead or use `drop` afterwards. + fn peek( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError>; + + /// Convert the given scheduled `call` value back into its original instance. If successful, + /// `drop` any data backing it. This will not break the realisability of independently + /// created instances of `ScheduledCall` which happen to have identical data. + fn realize( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError>; +} + +impl + SchedulerPreimagesWeightInfo> + SchedulerPreimages for PP +{ + fn drop(call: &ScheduledCall) { + match call { + ScheduledCall::Inline(_) => {} + ScheduledCall::PreimageLookup { hash, .. } => Self::unrequest_preimage(hash), + } + } + + fn peek( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError> { + match call { + ScheduledCall::Inline(data) => Ok((ScheduledCall::::decode(data)?, None)), + ScheduledCall::PreimageLookup { + hash, + unbounded_len, + } => { + let (preimage, len) = Self::get_preimage(hash) + .ok_or(>::PreimageNotFound) + .map(|preimage| (preimage, *unbounded_len))?; + + Ok((ScheduledCall::::decode(preimage.as_slice())?, Some(len))) + } + } + } + + fn realize( + call: &ScheduledCall, + ) -> Result<(::RuntimeCall, Option), DispatchError> { + let r = Self::peek(call)?; + Self::drop(call); + Ok(r) + } +} + +/// Scheduler's supported origins. +pub enum ScheduledEnsureOriginSuccess { + /// A scheduled transaction has the Root origin. + Root, + + /// A specific account has signed a scheduled transaction. + Signed(AccountId), +} + +/// An identifier of a scheduled task. +pub type TaskName = [u8; 32]; + +/// Information regarding an item to be executed in the future. +#[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] +#[derive(Clone, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct Scheduled { + /// The unique identity for this task, if there is one. + maybe_id: Option, + + /// This task's priority. + priority: schedule::Priority, + + /// The call to be dispatched. + call: Call, + + /// If the call is periodic, then this points to the information concerning that. + maybe_periodic: Option>, + + /// The origin with which to dispatch the call. + origin: PalletsOrigin, + _phantom: PhantomData, +} + +/// Information regarding an item to be executed in the future. +pub type ScheduledOf = Scheduled< + TaskName, + ScheduledCall, + ::BlockNumber, + ::PalletsOrigin, + ::AccountId, +>; + +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +/// A structure for storing scheduled tasks in a block. +/// The `BlockAgenda` tracks the available free space for a new task in a block.4 +/// +/// The agenda's maximum amount of tasks is `T::MaxScheduledPerBlock`. +pub struct BlockAgenda { + agenda: BoundedVec>, T::MaxScheduledPerBlock>, + free_places: u32, +} + +impl BlockAgenda { + /// Tries to push a new scheduled task into the block's agenda. + /// If there is a free place, the new task will take it, + /// and the `BlockAgenda` will record that the number of free places has decreased. + /// + /// An error containing the scheduled task will be returned if there are no free places. + /// + /// The complexity of the check for the *existence* of a free place is O(1). + /// The complexity of *finding* the free slot is O(n). + fn try_push(&mut self, scheduled: ScheduledOf) -> Result> { + if self.free_places == 0 { + return Err(scheduled); + } + + self.free_places = self.free_places.saturating_sub(1); + + if (self.agenda.len() as u32) < T::MaxScheduledPerBlock::get() { + // will always succeed due to the above check. + let _ = self.agenda.try_push(Some(scheduled)); + Ok((self.agenda.len() - 1) as u32) + } else { + match self.agenda.iter().position(|i| i.is_none()) { + Some(hole_index) => { + self.agenda[hole_index] = Some(scheduled); + Ok(hole_index as u32) + } + None => unreachable!("free_places was greater than 0; qed"), + } + } + } + + /// Sets a slot by the given index and the slot value. + /// + /// ### Panics + /// If the index is out of range, the function will panic. + fn set_slot(&mut self, index: u32, slot: Option>) { + self.agenda[index as usize] = slot; + } + + /// Returns an iterator containing references to the agenda's slots. + fn iter(&self) -> impl Iterator>> + '_ { + self.agenda.iter() + } + + /// Returns an immutable reference to a scheduled task if there is one under the given index. + /// + /// The function returns `None` if: + /// * The `index` is out of range + /// * No scheduled task occupies the agenda slot under the given index. + fn get(&self, index: u32) -> Option<&ScheduledOf> { + match self.agenda.get(index as usize) { + Some(Some(scheduled)) => Some(scheduled), + _ => None, + } + } + + /// Returns a mutable reference to a scheduled task if there is one under the given index. + /// + /// The function returns `None` if: + /// * The `index` is out of range + /// * No scheduled task occupies the agenda slot under the given index. + fn get_mut(&mut self, index: u32) -> Option<&mut ScheduledOf> { + match self.agenda.get_mut(index as usize) { + Some(Some(scheduled)) => Some(scheduled), + _ => None, + } + } + + /// Take a scheduled task by the given index. + /// + /// If there is a task under the index, the function will: + /// * Free the corresponding agenda slot. + /// * Decrease the number of free places. + /// * Return the scheduled task. + /// + /// The function returns `None` if there is no task under the index. + fn take(&mut self, index: u32) -> Option> { + let removed = self.agenda.get_mut(index as usize)?.take(); + + if removed.is_some() { + self.free_places = self.free_places.saturating_add(1); + } + + removed + } +} + +impl Default for BlockAgenda { + fn default() -> Self { + let agenda = Default::default(); + let free_places = T::MaxScheduledPerBlock::get(); + + Self { + agenda, + free_places, + } + } +} +/// A structure for tracking the used weight +/// and checking if it does not exceed the weight limit. +struct WeightCounter { + used: Weight, + limit: Weight, +} + +impl WeightCounter { + /// Checks if the weight `w` can be accommodated by the counter. + /// + /// If there is room for the additional weight `w`, + /// the function will update the used weight and return true. + fn check_accrue(&mut self, w: Weight) -> bool { + let test = self.used.saturating_add(w); + if test.any_gt(self.limit) { + false + } else { + self.used = test; + true + } + } + + /// Checks if the weight `w` can be accommodated by the counter. + fn can_accrue(&mut self, w: Weight) -> bool { + self.used.saturating_add(w).all_lte(self.limit) + } +} + +pub(crate) struct MarginalWeightInfo(sp_std::marker::PhantomData); + +impl MarginalWeightInfo { + /// Return the weight of servicing a single task. + fn service_task(maybe_lookup_len: Option, named: bool, periodic: bool) -> Weight { + let base = T::WeightInfo::service_task_base(); + let mut total = match maybe_lookup_len { + None => base, + Some(l) => T::Preimages::service_task_fetched(l as u32), + }; + if named { + total.saturating_accrue(T::WeightInfo::service_task_named().saturating_sub(base)); + } + if periodic { + total.saturating_accrue(T::WeightInfo::service_task_periodic().saturating_sub(base)); + } + total + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{dispatch::PostDispatchInfo, pallet_prelude::*}; + use system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The aggregated origin which the dispatch will take. + type RuntimeOrigin: OriginTrait + + From + + IsType<::RuntimeOrigin> + + Clone; + + /// The caller origin, overarching type of all pallets origins. + type PalletsOrigin: From> + + Codec + + Clone + + Eq + + TypeInfo + + MaxEncodedLen; + + /// The aggregated call type. + type RuntimeCall: Parameter + + Dispatchable< + RuntimeOrigin = ::RuntimeOrigin, + PostInfo = PostDispatchInfo, + > + UnfilteredDispatchable::RuntimeOrigin> + + GetDispatchInfo + + From>; + + /// The maximum weight that may be scheduled per block for any dispatchables. + #[pallet::constant] + type MaximumWeight: Get; + + /// Required origin to schedule or cancel calls. + type ScheduleOrigin: EnsureOrigin< + ::RuntimeOrigin, + Success = ScheduledEnsureOriginSuccess, + >; + + /// Compare the privileges of origins. + /// + /// This will be used when canceling a task, to ensure that the origin that tries + /// to cancel has greater or equal privileges as the origin that created the scheduled task. + /// + /// For simplicity the [`EqualPrivilegeOnly`](frame_support::traits::EqualPrivilegeOnly) can + /// be used. This will only check if two given origins are equal. + type OriginPrivilegeCmp: PrivilegeCmp; + + /// The maximum number of scheduled calls in the queue for a single block. + #[pallet::constant] + type MaxScheduledPerBlock: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The preimage provider with which we look up call hashes to get the call. + type Preimages: SchedulerPreimages; + + /// The helper type used for custom transaction fee logic. + type CallExecutor: DispatchCall; + + /// Required origin to set/change calls' priority. + type PrioritySetOrigin: EnsureOrigin<::RuntimeOrigin>; + } + + /// It contains the block number from which we should service tasks. + /// It's used for delaying the servicing of future blocks' agendas if we had overweight tasks. + #[pallet::storage] + pub type IncompleteSince = StorageValue<_, T::BlockNumber>; + + /// Items to be executed, indexed by the block number that they should be executed on. + #[pallet::storage] + pub type Agenda = + StorageMap<_, Twox64Concat, T::BlockNumber, BlockAgenda, ValueQuery>; + + /// Lookup from a name to the block number and index of the task. + #[pallet::storage] + pub(crate) type Lookup = + StorageMap<_, Twox64Concat, TaskName, TaskAddress>; + + /// Events type. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Scheduled some task. + Scheduled { + /// The block number in which the scheduled task should be executed. + when: T::BlockNumber, + + /// The index of the block's agenda slot. + index: u32, + }, + /// Canceled some task. + Canceled { + /// The block number in which the canceled task has been. + when: T::BlockNumber, + + /// The index of the block's agenda slot that had become available. + index: u32, + }, + /// Dispatched some task. + Dispatched { + /// The task's address - the block number and the block's agenda index. + task: TaskAddress, + + /// The task's name if it is not anonymous. + id: Option<[u8; 32]>, + + /// The task's execution result. + result: DispatchResult, + }, + /// Scheduled task's priority has changed + PriorityChanged { + /// The task's address - the block number and the block's agenda index. + task: TaskAddress, + + /// The new priority of the task. + priority: schedule::Priority, + }, + /// The call for the provided hash was not found so the task has been aborted. + CallUnavailable { + /// The task's address - the block number and the block's agenda index. + task: TaskAddress, + + /// The task's name if it is not anonymous. + id: Option<[u8; 32]>, + }, + /// The given task can never be executed since it is overweight. + PermanentlyOverweight { + /// The task's address - the block number and the block's agenda index. + task: TaskAddress, + + /// The task's name if it is not anonymous. + id: Option<[u8; 32]>, + }, + } + + #[pallet::error] + pub enum Error { + /// Failed to schedule a call + FailedToSchedule, + /// There is no place for a new task in the agenda + AgendaIsExhausted, + /// Scheduled call is corrupted + ScheduledCallCorrupted, + /// Scheduled call preimage is not found + PreimageNotFound, + /// Scheduled call is too big + TooBigScheduledCall, + /// Cannot find the scheduled call. + NotFound, + /// Given target block number is in the past. + TargetBlockNumberInPast, + /// Attempt to use a non-named function on a named task. + Named, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Execute the scheduled calls + fn on_initialize(now: T::BlockNumber) -> Weight { + let mut weight_counter = WeightCounter { + used: Weight::zero(), + limit: T::MaximumWeight::get(), + }; + Self::service_agendas(&mut weight_counter, now, u32::max_value()); + weight_counter.used + } + } + + #[pallet::call] + impl Pallet { + /// Anonymously schedule a task. + /// + /// Only `T::ScheduleOrigin` is allowed to schedule a task. + /// Only `T::PrioritySetOrigin` is allowed to set the task's priority. + #[pallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] + pub fn schedule( + origin: OriginFor, + when: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule( + DispatchTime::At(when), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + /// Cancel an anonymously scheduled task. + /// + /// The `T::OriginPrivilegeCmp` decides whether the given origin is allowed to cancel the task or not. + #[pallet::weight(::WeightInfo::cancel(T::MaxScheduledPerBlock::get()))] + pub fn cancel(origin: OriginFor, when: T::BlockNumber, index: u32) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::RuntimeOrigin::from(origin); + Self::do_cancel(Some(origin.caller().clone()), (when, index))?; + Ok(()) + } + + /// Schedule a named task. + /// + /// Only `T::ScheduleOrigin` is allowed to schedule a task. + /// Only `T::PrioritySetOrigin` is allowed to set the task's priority. + #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] + pub fn schedule_named( + origin: OriginFor, + id: TaskName, + when: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule_named( + id, + DispatchTime::At(when), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + /// Cancel a named scheduled task. + /// + /// The `T::OriginPrivilegeCmp` decides whether the given origin is allowed to cancel the task or not. + #[pallet::weight(::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get()))] + pub fn cancel_named(origin: OriginFor, id: TaskName) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::RuntimeOrigin::from(origin); + Self::do_cancel_named(Some(origin.caller().clone()), id)?; + Ok(()) + } + + /// Anonymously schedule a task after a delay. + /// + /// # + /// Same as [`schedule`]. + /// # + #[pallet::weight(::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))] + pub fn schedule_after( + origin: OriginFor, + after: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule( + DispatchTime::After(after), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + /// Schedule a named task after a delay. + /// + /// Only `T::ScheduleOrigin` is allowed to schedule a task. + /// Only `T::PrioritySetOrigin` is allowed to set the task's priority. + /// + /// # + /// Same as [`schedule_named`](Self::schedule_named). + /// # + #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] + pub fn schedule_named_after( + origin: OriginFor, + id: TaskName, + after: T::BlockNumber, + maybe_periodic: Option>, + priority: Option, + call: Box<::RuntimeCall>, + ) -> DispatchResult { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + + if priority.is_some() { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + } + + let origin = ::RuntimeOrigin::from(origin); + Self::do_schedule_named( + id, + DispatchTime::After(after), + maybe_periodic, + priority.unwrap_or(LOWEST_PRIORITY), + origin.caller().clone(), + >::new(*call)?, + )?; + Ok(()) + } + + /// Change a named task's priority. + /// + /// Only the `T::PrioritySetOrigin` is allowed to change the task's priority. + #[pallet::weight(::WeightInfo::change_named_priority(T::MaxScheduledPerBlock::get()))] + pub fn change_named_priority( + origin: OriginFor, + id: TaskName, + priority: schedule::Priority, + ) -> DispatchResult { + T::PrioritySetOrigin::ensure_origin(origin.clone())?; + let origin = ::RuntimeOrigin::from(origin); + Self::do_change_named_priority(origin.caller().clone(), id, priority) + } + } +} + +impl Pallet { + /// Converts the `DispatchTime` to the `BlockNumber`. + /// + /// Returns an error if the block number is in the past. + fn resolve_time(when: DispatchTime) -> Result { + let now = frame_system::Pallet::::block_number(); + + let when = match when { + DispatchTime::At(x) => x, + // The current block has already completed it's scheduled tasks, so + // Schedule the task at lest one block after this current block. + DispatchTime::After(x) => now.saturating_add(x).saturating_add(One::one()), + }; + + if when <= now { + return Err(Error::::TargetBlockNumberInPast.into()); + } + + Ok(when) + } + + /// Places the mandatory task. + /// + /// It will try to place the task into the block pointed by the `when` parameter. + /// + /// If the block has no room for a task, + /// the function will search for a future block that can accommodate the task. + fn mandatory_place_task(when: T::BlockNumber, what: ScheduledOf) { + Self::place_task(when, what, true).expect("mandatory place task always succeeds; qed"); + } + + /// Tries to place a task `what` into the given block `when`. + /// + /// Returns an error if the block has no room for the task. + fn try_place_task( + when: T::BlockNumber, + what: ScheduledOf, + ) -> Result, DispatchError> { + Self::place_task(when, what, false) + } + + /// If `is_mandatory` is true, the function behaves like [`mandatory_place_task`](Self::mandatory_place_task); + /// otherwise it acts like [`try_place_task`](Self::try_place_task). + /// + /// The function also updates the `Lookup` storage. + fn place_task( + mut when: T::BlockNumber, + what: ScheduledOf, + is_mandatory: bool, + ) -> Result, DispatchError> { + let maybe_name = what.maybe_id; + let index = Self::push_to_agenda(&mut when, what, is_mandatory)?; + let address = (when, index); + if let Some(name) = maybe_name { + Lookup::::insert(name, address) + } + Self::deposit_event(Event::Scheduled { + when: address.0, + index: address.1, + }); + Ok(address) + } + + /// Pushes the scheduled task into the block's agenda. + /// + /// If `is_mandatory` is true, it searches for a block with a free slot for the given task. + /// + /// If `is_mandatory` is false and there is no free slot, the function returns an error. + fn push_to_agenda( + when: &mut T::BlockNumber, + mut what: ScheduledOf, + is_mandatory: bool, + ) -> Result { + let mut agenda; + + let index = loop { + agenda = Agenda::::get(*when); + + match agenda.try_push(what) { + Ok(index) => break index, + Err(returned_what) if is_mandatory => { + what = returned_what; + when.saturating_inc(); + } + Err(_) => return Err(>::AgendaIsExhausted.into()), + } + }; + + Agenda::::insert(when, agenda); + Ok(index) + } + + fn do_schedule( + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ScheduledCall, + ) -> Result, DispatchError> { + let when = Self::resolve_time(when)?; + + // sanitize maybe_periodic + let maybe_periodic = maybe_periodic + .filter(|p| p.1 > 1 && !p.0.is_zero()) + // Remove one from the number of repetitions since we will schedule one now. + .map(|(p, c)| (p, c - 1)); + let task = Scheduled { + maybe_id: None, + priority, + call, + maybe_periodic, + origin, + _phantom: PhantomData, + }; + Self::try_place_task(when, task) + } + + fn do_cancel( + origin: Option, + (when, index): TaskAddress, + ) -> Result<(), DispatchError> { + let scheduled = Agenda::::try_mutate( + when, + |agenda| -> Result>, DispatchError> { + let scheduled = match agenda.get(index) { + Some(scheduled) => scheduled, + None => return Ok(None), + }; + + if let Some(ref o) = origin { + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(o, &scheduled.origin), + Some(Ordering::Less) | None + ) { + return Err(BadOrigin.into()); + } + } + + Ok(agenda.take(index)) + }, + )?; + if let Some(s) = scheduled { + T::Preimages::drop(&s.call); + + if let Some(id) = s.maybe_id { + Lookup::::remove(id); + } + Self::deposit_event(Event::Canceled { when, index }); + Ok(()) + } else { + Err(Error::::NotFound.into()) + } + } + + fn do_schedule_named( + id: TaskName, + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ScheduledCall, + ) -> Result, DispatchError> { + // ensure id it is unique + if Lookup::::contains_key(&id) { + return Err(Error::::FailedToSchedule.into()); + } + + let when = Self::resolve_time(when)?; + + // sanitize maybe_periodic + let maybe_periodic = maybe_periodic + .filter(|p| p.1 > 1 && !p.0.is_zero()) + // Remove one from the number of repetitions since we will schedule one now. + .map(|(p, c)| (p, c - 1)); + + let task = Scheduled { + maybe_id: Some(id), + priority, + call, + maybe_periodic, + origin, + _phantom: Default::default(), + }; + Self::try_place_task(when, task) + } + + fn do_cancel_named(origin: Option, id: TaskName) -> DispatchResult { + Lookup::::try_mutate_exists(id, |lookup| -> DispatchResult { + if let Some((when, index)) = lookup.take() { + Agenda::::try_mutate(when, |agenda| -> DispatchResult { + let scheduled = match agenda.get(index) { + Some(scheduled) => scheduled, + None => return Ok(()), + }; + + if let Some(ref o) = origin { + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(o, &scheduled.origin), + Some(Ordering::Less) | None + ) { + return Err(BadOrigin.into()); + } + T::Preimages::drop(&scheduled.call); + } + + agenda.take(index); + + Ok(()) + })?; + Self::deposit_event(Event::Canceled { when, index }); + Ok(()) + } else { + Err(Error::::NotFound.into()) + } + }) + } + + fn do_change_named_priority( + origin: T::PalletsOrigin, + id: TaskName, + priority: schedule::Priority, + ) -> DispatchResult { + match Lookup::::get(id) { + Some((when, index)) => Agenda::::try_mutate(when, |agenda| { + let scheduled = match agenda.get_mut(index) { + Some(scheduled) => scheduled, + None => return Ok(()), + }; + + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(&origin, &scheduled.origin), + Some(Ordering::Less) | None + ) { + return Err(BadOrigin.into()); + } + + scheduled.priority = priority; + Self::deposit_event(Event::PriorityChanged { + task: (when, index), + priority, + }); + + Ok(()) + }), + None => Err(Error::::NotFound.into()), + } + } +} + +enum ServiceTaskError { + /// Could not be executed due to missing preimage. + Unavailable, + /// Could not be executed due to weight limitations. + Overweight, +} +use ServiceTaskError::*; + +/// A Scheduler-Runtime interface for finer payment handling. +pub trait DispatchCall { + /// Resolve the call dispatch, including any post-dispatch operations. + fn dispatch_call( + signer: Option, + function: ::RuntimeCall, + ) -> Result< + Result>, + TransactionValidityError, + >; +} + +impl Pallet { + /// Service up to `max` agendas queue starting from earliest incompletely executed agenda. + fn service_agendas(weight: &mut WeightCounter, now: T::BlockNumber, max: u32) { + if !weight.check_accrue(T::WeightInfo::service_agendas_base()) { + return; + } + + let mut incomplete_since = now + One::one(); + let mut when = IncompleteSince::::take().unwrap_or(now); + let mut executed = 0; + + let max_items = T::MaxScheduledPerBlock::get(); + let mut count_down = max; + let service_agenda_base_weight = T::WeightInfo::service_agenda_base(max_items); + while count_down > 0 && when <= now && weight.can_accrue(service_agenda_base_weight) { + if !Self::service_agenda(weight, &mut executed, now, when, u32::max_value()) { + incomplete_since = incomplete_since.min(when); + } + when.saturating_inc(); + count_down.saturating_dec(); + } + incomplete_since = incomplete_since.min(when); + if incomplete_since <= now { + IncompleteSince::::put(incomplete_since); + } + } + + /// Returns `true` if the agenda was fully completed, `false` if it should be revisited at a + /// later block. + fn service_agenda( + weight: &mut WeightCounter, + executed: &mut u32, + now: T::BlockNumber, + when: T::BlockNumber, + max: u32, + ) -> bool { + let mut agenda = Agenda::::get(when); + let mut ordered = agenda + .iter() + .enumerate() + .filter_map(|(index, maybe_item)| { + maybe_item + .as_ref() + .map(|item| (index as u32, item.priority)) + }) + .collect::>(); + ordered.sort_by_key(|k| k.1); + let within_limit = + weight.check_accrue(T::WeightInfo::service_agenda_base(ordered.len() as u32)); + debug_assert!( + within_limit, + "weight limit should have been checked in advance" + ); + + // Items which we know can be executed and have postponed for execution in a later block. + let mut postponed = (ordered.len() as u32).saturating_sub(max); + // Items which we don't know can ever be executed. + let mut dropped = 0; + + for (agenda_index, _) in ordered.into_iter().take(max as usize) { + let task = match agenda.take(agenda_index).take() { + None => continue, + Some(t) => t, + }; + let base_weight = MarginalWeightInfo::::service_task( + task.call.lookup_len().map(|x| x as usize), + task.maybe_id.is_some(), + task.maybe_periodic.is_some(), + ); + if !weight.can_accrue(base_weight) { + postponed += 1; + break; + } + let result = Self::service_task(weight, now, when, agenda_index, *executed == 0, task); + match result { + Err((Unavailable, slot)) => { + dropped += 1; + agenda.set_slot(agenda_index, slot); + } + Err((Overweight, slot)) => { + postponed += 1; + agenda.set_slot(agenda_index, slot); + } + Ok(()) => { + *executed += 1; + } + }; + } + if postponed > 0 || dropped > 0 { + Agenda::::insert(when, agenda); + } else { + Agenda::::remove(when); + } + postponed == 0 + } + + /// Service (i.e. execute) the given task, being careful not to overflow the `weight` counter. + /// + /// This involves: + /// - removing and potentially replacing the `Lookup` entry for the task. + /// - realizing the task's call which can include a preimage lookup. + /// - Rescheduling the task for execution in a later agenda if periodic. + fn service_task( + weight: &mut WeightCounter, + now: T::BlockNumber, + when: T::BlockNumber, + agenda_index: u32, + is_first: bool, + mut task: ScheduledOf, + ) -> Result<(), (ServiceTaskError, Option>)> { + let (call, lookup_len) = match T::Preimages::peek(&task.call) { + Ok(c) => c, + Err(_) => { + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + return Err((Unavailable, Some(task))); + } + }; + + weight.check_accrue(MarginalWeightInfo::::service_task( + lookup_len.map(|x| x as usize), + task.maybe_id.is_some(), + task.maybe_periodic.is_some(), + )); + + match Self::execute_dispatch(weight, task.origin.clone(), call) { + Err(Unavailable) => { + debug_assert!(false, "Checked to exist with `peek`"); + + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + Self::deposit_event(Event::CallUnavailable { + task: (when, agenda_index), + id: task.maybe_id, + }); + Err((Unavailable, Some(task))) + } + Err(Overweight) if is_first && !Self::is_runtime_upgraded() => { + T::Preimages::drop(&task.call); + + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + Self::deposit_event(Event::PermanentlyOverweight { + task: (when, agenda_index), + id: task.maybe_id, + }); + Err((Unavailable, Some(task))) + } + Err(Overweight) => { + // Preserve Lookup -- the task will be postponed. + Err((Overweight, Some(task))) + } + Ok(result) => { + Self::deposit_event(Event::Dispatched { + task: (when, agenda_index), + id: task.maybe_id, + result, + }); + + let is_canceled = task + .maybe_id + .as_ref() + .map(|id| !Lookup::::contains_key(id)) + .unwrap_or(false); + + match &task.maybe_periodic { + &Some((period, count)) if !is_canceled => { + if count > 1 { + task.maybe_periodic = Some((period, count - 1)); + } else { + task.maybe_periodic = None; + } + let wake = now.saturating_add(period); + Self::mandatory_place_task(wake, task); + } + _ => { + if let Some(ref id) = task.maybe_id { + Lookup::::remove(id); + } + + T::Preimages::drop(&task.call) + } + } + Ok(()) + } + } + } + + fn is_runtime_upgraded() -> bool { + let last = system::LastRuntimeUpgrade::::get(); + let current = T::Version::get(); + + last.map(|v| v.was_upgraded(¤t)).unwrap_or(true) + } + + /// Make a dispatch to the given `call` from the given `origin`, ensuring that the `weight` + /// counter does not exceed its limit and that it is counted accurately (e.g. accounted using + /// post info if available). + /// + /// NOTE: Only the weight for this function will be counted (origin lookup, dispatch and the + /// call itself). + fn execute_dispatch( + weight: &mut WeightCounter, + origin: T::PalletsOrigin, + call: ::RuntimeCall, + ) -> Result { + let dispatch_origin: ::RuntimeOrigin = origin.into(); + let base_weight = match dispatch_origin.clone().as_signed() { + Some(_) => T::WeightInfo::execute_dispatch_signed(), + _ => T::WeightInfo::execute_dispatch_unsigned(), + }; + let call_weight = call.get_dispatch_info().weight; + // We only allow a scheduled call if it cannot push the weight past the limit. + let max_weight = base_weight.saturating_add(call_weight); + + if !weight.can_accrue(max_weight) { + return Err(Overweight); + } + + let ensured_origin = T::ScheduleOrigin::ensure_origin(dispatch_origin.into()); + + let r = match ensured_origin { + Ok(ScheduledEnsureOriginSuccess::Root) => { + Ok(call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into())) + } + Ok(ScheduledEnsureOriginSuccess::Signed(sender)) => { + // Execute transaction via chain default pipeline + // That means dispatch will be processed like any user's extrinsic e.g. transaction fees will be taken + T::CallExecutor::dispatch_call(Some(sender), call) + } + Err(e) => Ok(Err(e.into())), + }; + + let (maybe_actual_call_weight, result) = match r { + Ok(result) => match result { + Ok(post_info) => (post_info.actual_weight, Ok(())), + Err(error_and_info) => ( + error_and_info.post_info.actual_weight, + Err(error_and_info.error), + ), + }, + Err(_) => { + log::error!( + target: "runtime::scheduler", + "Warning: Scheduler has failed to execute a post-dispatch transaction. \ + This block might have become invalid."); + (None, Err(DispatchError::CannotLookup)) + } + }; + let call_weight = maybe_actual_call_weight.unwrap_or(call_weight); + weight.check_accrue(base_weight); + weight.check_accrue(call_weight); + Ok(result) + } +} --- /dev/null +++ b/pallets/scheduler-v2/src/mock.rs @@ -0,0 +1,287 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +// Original license: +// 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. + +//! # Scheduler test environment. + +use super::*; + +use crate as scheduler; +use frame_support::{ + ord_parameter_types, parameter_types, + traits::{ConstU32, ConstU64, Contains, EqualPrivilegeOnly, OnFinalize, OnInitialize}, + weights::constants::RocksDbWeight, +}; +use frame_system::{EnsureRoot, RawOrigin}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +// Logger module to track execution. +#[frame_support::pallet] +pub mod logger { + use super::{OriginCaller, OriginTrait}; + use frame_support::{pallet_prelude::*, parameter_types}; + use frame_system::pallet_prelude::*; + + parameter_types! { + static Log: Vec<(OriginCaller, u32)> = Vec::new(); + } + pub fn log() -> Vec<(OriginCaller, u32)> { + Log::get().clone() + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData); + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Logged(u32, Weight), + } + + #[pallet::call] + impl Pallet + where + ::RuntimeOrigin: OriginTrait, + { + #[pallet::weight(*weight)] + pub fn log(origin: OriginFor, i: u32, weight: Weight) -> DispatchResult { + Self::deposit_event(Event::Logged(i, weight)); + Log::mutate(|log| { + log.push((origin.caller().clone(), i)); + }); + Ok(()) + } + + #[pallet::weight(*weight)] + pub fn log_without_filter(origin: OriginFor, i: u32, weight: Weight) -> DispatchResult { + Self::deposit_event(Event::Logged(i, weight)); + Log::mutate(|log| { + log.push((origin.caller().clone(), i)); + }); + Ok(()) + } + } +} + +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}, + Logger: logger::{Pallet, Call, Event}, + Scheduler: scheduler::{Pallet, Call, Storage, Event}, + } +); + +// Scheduler must dispatch with root and no filter, this tests base filter is indeed not used. +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(call: &RuntimeCall) -> bool { + !matches!(call, RuntimeCall::Logger(LoggerCall::log { .. })) + } +} + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max( + Weight::from_ref_time(2_000_000_000_000) + // .set_proof_size(u64::MAX), + ); +} +impl system::Config for Test { + type BaseCallFilter = BaseFilter; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = RocksDbWeight; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + 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>; +} +impl logger::Config for Test { + type RuntimeEvent = RuntimeEvent; +} +ord_parameter_types! { + pub const One: u64 = 1; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn service_agendas_base() -> Weight { + Weight::from_ref_time(0b0000_0001) + } + fn service_agenda_base(i: u32) -> Weight { + Weight::from_ref_time((i << 8) as u64 + 0b0000_0010) + } + fn service_task_base() -> Weight { + Weight::from_ref_time(0b0000_0100) + } + fn service_task_periodic() -> Weight { + Weight::from_ref_time(0b0000_1100) + } + fn service_task_named() -> Weight { + Weight::from_ref_time(0b0001_0100) + } + // fn service_task_fetched(s: u32) -> Weight { + // Weight::from_ref_time((s << 8) as u64 + 0b0010_0100) + // } + fn execute_dispatch_signed() -> Weight { + Weight::from_ref_time(0b0100_0000) + } + fn execute_dispatch_unsigned() -> Weight { + Weight::from_ref_time(0b1000_0000) + } + fn schedule(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn cancel(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn schedule_named(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn cancel_named(_s: u32) -> Weight { + Weight::from_ref_time(50) + } + fn change_named_priority(_s: u32) -> Weight { + Weight::from_ref_time(50) + } +} +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * + BlockWeights::get().max_block; +} + +pub struct EnsureSignedOneOrRoot; +impl, O>> + From>> EnsureOrigin + for EnsureSignedOneOrRoot +{ + type Success = ScheduledEnsureOriginSuccess; + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Root => Ok(ScheduledEnsureOriginSuccess::Root), + RawOrigin::Signed(1) => Ok(ScheduledEnsureOriginSuccess::Signed(1)), + r => Err(O::from(r)), + }) + } +} + +pub struct Executor; +impl DispatchCall for Executor { + fn dispatch_call( + signer: Option, + function: RuntimeCall, + ) -> Result< + Result>, + TransactionValidityError, + > { + let origin = match signer { + Some(who) => RuntimeOrigin::signed(who), + None => RuntimeOrigin::none(), + }; + Ok(function.dispatch(origin)) + } +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureSignedOneOrRoot; + type MaxScheduledPerBlock = ConstU32<10>; + type WeightInfo = TestWeightInfo; + type OriginPrivilegeCmp = EqualPrivilegeOnly; + type Preimages = (); + type PrioritySetOrigin = EnsureRoot; + type CallExecutor = Executor; +} + +pub type LoggerCall = logger::Call; + +pub type SystemCall = frame_system::Call; + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + t.into() +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + Scheduler::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + Scheduler::on_initialize(System::block_number()); + } +} + +pub fn root() -> OriginCaller { + system::RawOrigin::Root.into() +} --- /dev/null +++ b/pallets/scheduler-v2/src/tests.rs @@ -0,0 +1,900 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +// Original license: +// 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. + +//! # Scheduler tests. + +use super::*; +use crate::mock::{ + logger, new_test_ext, root, run_to_block, LoggerCall, RuntimeCall, Scheduler, Test, *, +}; +use frame_support::{ + assert_noop, assert_ok, + traits::{Contains, OnInitialize}, + assert_err, +}; + +#[test] +fn basic_scheduling_works() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + assert!(!::BaseCallFilter::contains( + &call + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); +} + +#[test] +fn schedule_after_works() { + new_test_ext().execute_with(|| { + run_to_block(2); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + assert!(!::BaseCallFilter::contains( + &call + )); + // This will schedule the call 3 blocks after the next block... so block 3 + 3 = 6 + assert_ok!(Scheduler::do_schedule( + DispatchTime::After(3), + None, + 127, + root(), + >::new(call).unwrap(), + )); + run_to_block(5); + assert!(logger::log().is_empty()); + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); +} + +#[test] +fn schedule_after_zero_works() { + new_test_ext().execute_with(|| { + run_to_block(2); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + assert!(!::BaseCallFilter::contains( + &call + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::After(0), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // Will trigger on the next block. + run_to_block(3); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); +} + +#[test] +fn periodic_scheduling_works() { + new_test_ext().execute_with(|| { + // at #4, every 3 blocks, 3 times. + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + Some((3, 3)), + 127, + root(), + >::new(RuntimeCall::Logger(logger::Call::log { + i: 42, + weight: Weight::from_ref_time(10) + })) + .unwrap() + )); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(7); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); + run_to_block(9); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); + run_to_block(10); + assert_eq!( + logger::log(), + vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)] + ); + run_to_block(100); + assert_eq!( + logger::log(), + vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)] + ); + }); +} + +#[test] +fn cancel_named_scheduling_works_with_normal_cancel() { + new_test_ext().execute_with(|| { + // at #4. + Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(4), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + let i = Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + run_to_block(3); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::do_cancel_named(None, [1u8; 32])); + assert_ok!(Scheduler::do_cancel(None, i)); + run_to_block(100); + assert!(logger::log().is_empty()); + }); +} + +#[test] +fn cancel_named_periodic_scheduling_works() { + new_test_ext().execute_with(|| { + // at #4, every 3 blocks, 3 times. + Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(4), + Some((3, 3)), + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + // same id results in error. + assert!(Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(4), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10) + })) + .unwrap(), + ) + .is_err()); + // different id is ok. + Scheduler::do_schedule_named( + [2u8; 32], + DispatchTime::At(8), + None, + 127, + root(), + >::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })) + .unwrap(), + ) + .unwrap(); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(6); + assert_ok!(Scheduler::do_cancel_named(None, [1u8; 32])); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); + }); +} + +#[test] +fn scheduler_respects_weight_limits() { + let max_weight: Weight = ::MaximumWeight::get(); + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight / 3 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: max_weight / 3 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // 69 and 42 do not fit together + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(5); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); + }); +} + +/// Permanently overweight calls are not deleted but also not executed. +#[test] +fn scheduler_does_not_delete_permanently_overweight_call() { + let max_weight: Weight = ::MaximumWeight::get(); + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // Never executes. + run_to_block(100); + assert_eq!(logger::log(), vec![]); + + // Assert the `PermanentlyOverweight` event. + assert_eq!( + System::events().last().unwrap().event, + crate::Event::PermanentlyOverweight { + task: (4, 0), + id: None + } + .into(), + ); + // The call is still in the agenda. + assert!(Agenda::::get(4).agenda[0].is_some()); + }); +} + +#[test] +fn scheduler_periodic_tasks_always_find_place() { + let max_weight: Weight = ::MaximumWeight::get(); + let max_per_block = ::MaxScheduledPerBlock::get(); + + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: (max_weight / 3) * 2, + }); + let call = >::new(call).unwrap(); + + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + Some((4, u32::MAX)), + 127, + root(), + call.clone(), + )); + // Executes 5 times till block 20. + run_to_block(20); + assert_eq!(logger::log().len(), 5); + + // Block 28 will already be full. + for _ in 0..max_per_block { + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(28), + None, + 120, + root(), + call.clone(), + )); + } + + run_to_block(24); + assert_eq!(logger::log().len(), 6); + + // The periodic task should be postponed + assert_eq!(>::get(29).agenda.len(), 1); + + run_to_block(27); // will call on_initialize(28) + assert_eq!(logger::log().len(), 6); + + run_to_block(28); // will call on_initialize(29) + assert_eq!(logger::log().len(), 7); + }); +} + +#[test] +fn scheduler_respects_priority_ordering() { + let max_weight: Weight = ::MaximumWeight::get(); + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight / 3, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 1, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: max_weight / 3, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 0, + root(), + >::new(call).unwrap(), + )); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 69u32), (root(), 42u32)]); + }); +} + +#[test] +fn scheduler_respects_priority_ordering_with_soft_deadlines() { + new_test_ext().execute_with(|| { + let max_weight: Weight = ::MaximumWeight::get(); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: max_weight / 5 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 255, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: max_weight / 5 * 2, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 2600, + weight: max_weight / 5 * 4, + }); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 126, + root(), + >::new(call).unwrap(), + )); + + // 2600 does not fit with 69 or 42, but has higher priority, so will go through + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 2600u32)]); + // 69 and 42 fit together + run_to_block(5); + assert_eq!( + logger::log(), + vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)] + ); + }); +} + +#[test] +fn on_initialize_weight_is_correct() { + new_test_ext().execute_with(|| { + let call_weight = Weight::from_ref_time(25); + + // Named + let call = RuntimeCall::Logger(LoggerCall::log { + i: 3, + weight: call_weight + Weight::from_ref_time(1), + }); + assert_ok!(Scheduler::do_schedule_named( + [1u8; 32], + DispatchTime::At(3), + None, + 255, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: call_weight + Weight::from_ref_time(2), + }); + // Anon Periodic + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(2), + Some((1000, 3)), + 128, + root(), + >::new(call).unwrap(), + )); + let call = RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: call_weight + Weight::from_ref_time(3), + }); + // Anon + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(2), + None, + 127, + root(), + >::new(call).unwrap(), + )); + // Named Periodic + let call = RuntimeCall::Logger(LoggerCall::log { + i: 2600, + weight: call_weight + Weight::from_ref_time(4), + }); + assert_ok!(Scheduler::do_schedule_named( + [2u8; 32], + DispatchTime::At(1), + Some((1000, 3)), + 126, + root(), + >::new(call).unwrap(), + )); + + // Will include the named periodic only + assert_eq!( + Scheduler::on_initialize(1), + TestWeightInfo::service_agendas_base() + + TestWeightInfo::service_agenda_base(1) + + >::service_task(None, true, true) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(4) + ); + assert_eq!(IncompleteSince::::get(), None); + assert_eq!(logger::log(), vec![(root(), 2600u32)]); + + // Will include anon and anon periodic + assert_eq!( + Scheduler::on_initialize(2), + TestWeightInfo::service_agendas_base() + + TestWeightInfo::service_agenda_base(2) + + >::service_task(None, false, true) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(3) + + >::service_task(None, false, false) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(2) + ); + assert_eq!(IncompleteSince::::get(), None); + assert_eq!( + logger::log(), + vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)] + ); + + // Will include named only + assert_eq!( + Scheduler::on_initialize(3), + TestWeightInfo::service_agendas_base() + + TestWeightInfo::service_agenda_base(1) + + >::service_task(None, true, false) + + TestWeightInfo::execute_dispatch_unsigned() + + call_weight + Weight::from_ref_time(1) + ); + assert_eq!(IncompleteSince::::get(), None); + assert_eq!( + logger::log(), + vec![ + (root(), 2600u32), + (root(), 69u32), + (root(), 42u32), + (root(), 3u32) + ] + ); + + // Will contain none + let actual_weight = Scheduler::on_initialize(4); + assert_eq!( + actual_weight, + TestWeightInfo::service_agendas_base() + TestWeightInfo::service_agenda_base(0) + ); + }); +} + +#[test] +fn root_calls_works() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_ok!(Scheduler::schedule_named( + RuntimeOrigin::root(), + [1u8; 32], + 4, + None, + Some(127), + call, + )); + assert_ok!(Scheduler::schedule( + RuntimeOrigin::root(), + 4, + None, + Some(127), + call2 + )); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).agenda.len(), 2); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::cancel_named(RuntimeOrigin::root(), [1u8; 32])); + assert_ok!(Scheduler::cancel(RuntimeOrigin::root(), 4, 1)); + // Scheduled calls are made NONE, so should not effect state + run_to_block(100); + assert!(logger::log().is_empty()); + }); +} + +#[test] +fn fails_to_schedule_task_in_the_past() { + new_test_ext().execute_with(|| { + run_to_block(3); + + let call1 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + let call3 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + + assert_noop!( + Scheduler::schedule_named(RuntimeOrigin::root(), [1u8; 32], 2, None, Some(127), call1), + Error::::TargetBlockNumberInPast, + ); + + assert_noop!( + Scheduler::schedule(RuntimeOrigin::root(), 2, None, Some(127), call2), + Error::::TargetBlockNumberInPast, + ); + + assert_noop!( + Scheduler::schedule(RuntimeOrigin::root(), 3, None, Some(127), call3), + Error::::TargetBlockNumberInPast, + ); + }); +} + +#[test] +fn should_use_origin() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_ok!(Scheduler::schedule_named( + system::RawOrigin::Signed(1).into(), + [1u8; 32], + 4, + None, + None, + call, + )); + assert_ok!(Scheduler::schedule( + system::RawOrigin::Signed(1).into(), + 4, + None, + None, + call2, + )); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).agenda.len(), 2); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::cancel_named( + system::RawOrigin::Signed(1).into(), + [1u8; 32] + )); + assert_ok!(Scheduler::cancel(system::RawOrigin::Signed(1).into(), 4, 1)); + // Scheduled calls are made NONE, so should not effect state + run_to_block(100); + assert!(logger::log().is_empty()); + }); +} + +#[test] +fn should_check_origin() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_noop!( + Scheduler::schedule_named( + system::RawOrigin::Signed(2).into(), + [1u8; 32], + 4, + None, + None, + call + ), + BadOrigin + ); + assert_noop!( + Scheduler::schedule(system::RawOrigin::Signed(2).into(), 4, None, None, call2), + BadOrigin + ); + }); +} + +#[test] +fn should_check_origin_for_cancel() { + new_test_ext().execute_with(|| { + let call = Box::new(RuntimeCall::Logger(LoggerCall::log_without_filter { + i: 69, + weight: Weight::from_ref_time(10), + })); + let call2 = Box::new(RuntimeCall::Logger(LoggerCall::log_without_filter { + i: 42, + weight: Weight::from_ref_time(10), + })); + assert_ok!(Scheduler::schedule_named( + system::RawOrigin::Signed(1).into(), + [1u8; 32], + 4, + None, + None, + call, + )); + assert_ok!(Scheduler::schedule( + system::RawOrigin::Signed(1).into(), + 4, + None, + None, + call2, + )); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).agenda.len(), 2); + assert!(logger::log().is_empty()); + assert_noop!( + Scheduler::cancel_named(system::RawOrigin::Signed(2).into(), [1u8; 32]), + BadOrigin + ); + assert_noop!( + Scheduler::cancel(system::RawOrigin::Signed(2).into(), 4, 1), + BadOrigin + ); + assert_noop!( + Scheduler::cancel_named(system::RawOrigin::Root.into(), [1u8; 32]), + BadOrigin + ); + assert_noop!( + Scheduler::cancel(system::RawOrigin::Root.into(), 4, 1), + BadOrigin + ); + run_to_block(5); + assert_eq!( + logger::log(), + vec![ + (system::RawOrigin::Signed(1).into(), 69u32), + (system::RawOrigin::Signed(1).into(), 42u32) + ] + ); + }); +} + +/// Cancelling a call and then scheduling a second call for the same +/// block results in different addresses. +#[test] +fn schedule_does_not_resuse_addr() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + + // Schedule both calls. + let addr_1 = Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call.clone()).unwrap(), + ) + .unwrap(); + // Cancel the call. + assert_ok!(Scheduler::do_cancel(None, addr_1)); + let addr_2 = Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + >::new(call).unwrap(), + ) + .unwrap(); + + // Should not re-use the address. + assert!(addr_1 != addr_2); + }); +} + +#[test] +fn schedule_agenda_overflows() { + let max: u32 = ::MaxScheduledPerBlock::get(); + + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + let call = >::new(call).unwrap(); + + // Schedule the maximal number allowed per block. + for _ in 0..max { + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call.clone()).unwrap(); + } + + // One more time and it errors. + assert_noop!( + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call,), + >::AgendaIsExhausted, + ); + + run_to_block(4); + // All scheduled calls are executed. + assert_eq!(logger::log().len() as u32, max); + }); +} + +/// Cancelling and scheduling does not overflow the agenda but fills holes. +#[test] +fn cancel_and_schedule_fills_holes() { + let max: u32 = ::MaxScheduledPerBlock::get(); + assert!( + max > 3, + "This test only makes sense for MaxScheduledPerBlock > 3" + ); + + new_test_ext().execute_with(|| { + let call = RuntimeCall::Logger(LoggerCall::log { + i: 42, + weight: Weight::from_ref_time(10), + }); + let call = >::new(call).unwrap(); + let mut addrs = Vec::<_>::default(); + + // Schedule the maximal number allowed per block. + for _ in 0..max { + addrs.push( + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call.clone()) + .unwrap(), + ); + } + // Cancel three of them. + for addr in addrs.into_iter().take(3) { + Scheduler::do_cancel(None, addr).unwrap(); + } + // Schedule three new ones. + for i in 0..3 { + let (_block, index) = + Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call.clone()) + .unwrap(); + assert_eq!(i, index); + } + + run_to_block(4); + // Maximum number of calls are executed. + assert_eq!(logger::log().len() as u32, max); + }); +} + +#[test] +fn cannot_schedule_too_big_tasks() { + new_test_ext().execute_with(|| { + let call = Box::new(<::RuntimeCall>::from(SystemCall::remark { + remark: vec![0; EncodedCall::bound() - 4], + })); + + assert_ok!(Scheduler::schedule( + RuntimeOrigin::root(), + 4, + None, + Some(127), + call + )); + + let call = Box::new(<::RuntimeCall>::from(SystemCall::remark { + remark: vec![0; EncodedCall::bound() - 3], + })); + + assert_err!( + Scheduler::schedule(RuntimeOrigin::root(), 4, None, Some(127), call), + >::TooBigScheduledCall + ); + }); +} --- /dev/null +++ b/pallets/scheduler-v2/src/weights.rs @@ -0,0 +1,234 @@ +// Template adopted from https://github.com/paritytech/substrate/blob/master/.maintain/frame-weight-template.hbs + +//! Autogenerated weights for pallet_unique_scheduler_v2 +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-10-28, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 + +// Executed Command: +// target/release/unique-collator +// benchmark +// pallet +// --pallet +// pallet-unique-scheduler-v2 +// --wasm-execution +// compiled +// --extrinsic +// * +// --template +// .maintain/frame-weight-template.hbs +// --steps=50 +// --repeat=80 +// --heap-pages=4096 +// --output=./pallets/scheduler-v2/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_unique_scheduler_v2. +pub trait WeightInfo { + fn service_agendas_base() -> Weight; + fn service_agenda_base(s: u32, ) -> Weight; + fn service_task_base() -> Weight; + fn service_task_named() -> Weight; + fn service_task_periodic() -> Weight; + fn execute_dispatch_signed() -> Weight; + fn execute_dispatch_unsigned() -> Weight; + fn schedule(s: u32, ) -> Weight; + fn cancel(s: u32, ) -> Weight; + fn schedule_named(s: u32, ) -> Weight; + fn cancel_named(s: u32, ) -> Weight; + fn change_named_priority(s: u32, ) -> Weight; +} + +/// Weights for pallet_unique_scheduler_v2 using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Scheduler IncompleteSince (r:1 w:1) + fn service_agendas_base() -> Weight { + Weight::from_ref_time(5_253_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn service_agenda_base(s: u32, ) -> Weight { + Weight::from_ref_time(3_858_000 as u64) + // Standard Error: 2_617 + .saturating_add(Weight::from_ref_time(579_704 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: System LastRuntimeUpgrade (r:1 w:0) + fn service_task_base() -> Weight { + Weight::from_ref_time(10_536_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: System LastRuntimeUpgrade (r:1 w:0) + // Storage: Scheduler Lookup (r:0 w:1) + fn service_task_named() -> Weight { + Weight::from_ref_time(12_018_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: System LastRuntimeUpgrade (r:1 w:0) + fn service_task_periodic() -> Weight { + Weight::from_ref_time(10_669_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: System Account (r:1 w:1) + // Storage: System AllExtrinsicsLen (r:1 w:1) + // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + fn execute_dispatch_signed() -> Weight { + Weight::from_ref_time(36_083_000 as u64) + .saturating_add(T::DbWeight::get().reads(5 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + fn execute_dispatch_unsigned() -> Weight { + Weight::from_ref_time(4_386_000 as u64) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule(s: u32, ) -> Weight { + Weight::from_ref_time(17_257_000 as u64) + // Standard Error: 2_791 + .saturating_add(Weight::from_ref_time(574_832 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn cancel(s: u32, ) -> Weight { + Weight::from_ref_time(19_803_000 as u64) + // Standard Error: 1_177 + .saturating_add(Weight::from_ref_time(475_027 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule_named(s: u32, ) -> Weight { + Weight::from_ref_time(18_746_000 as u64) + // Standard Error: 2_997 + .saturating_add(Weight::from_ref_time(635_697 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn cancel_named(s: u32, ) -> Weight { + Weight::from_ref_time(20_983_000 as u64) + // Standard Error: 1_850 + .saturating_add(Weight::from_ref_time(518_812 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:0) + // Storage: Scheduler Agenda (r:1 w:1) + fn change_named_priority(s: u32, ) -> Weight { + Weight::from_ref_time(21_591_000 as u64) + // Standard Error: 4_187 + .saturating_add(Weight::from_ref_time(531_231 as u64).saturating_mul(s as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Scheduler IncompleteSince (r:1 w:1) + fn service_agendas_base() -> Weight { + Weight::from_ref_time(5_253_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn service_agenda_base(s: u32, ) -> Weight { + Weight::from_ref_time(3_858_000 as u64) + // Standard Error: 2_617 + .saturating_add(Weight::from_ref_time(579_704 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: System LastRuntimeUpgrade (r:1 w:0) + fn service_task_base() -> Weight { + Weight::from_ref_time(10_536_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: System LastRuntimeUpgrade (r:1 w:0) + // Storage: Scheduler Lookup (r:0 w:1) + fn service_task_named() -> Weight { + Weight::from_ref_time(12_018_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: System LastRuntimeUpgrade (r:1 w:0) + fn service_task_periodic() -> Weight { + Weight::from_ref_time(10_669_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: System Account (r:1 w:1) + // Storage: System AllExtrinsicsLen (r:1 w:1) + // Storage: System BlockWeight (r:1 w:1) + // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) + // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) + fn execute_dispatch_signed() -> Weight { + Weight::from_ref_time(36_083_000 as u64) + .saturating_add(RocksDbWeight::get().reads(5 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + fn execute_dispatch_unsigned() -> Weight { + Weight::from_ref_time(4_386_000 as u64) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule(s: u32, ) -> Weight { + Weight::from_ref_time(17_257_000 as u64) + // Standard Error: 2_791 + .saturating_add(Weight::from_ref_time(574_832 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn cancel(s: u32, ) -> Weight { + Weight::from_ref_time(19_803_000 as u64) + // Standard Error: 1_177 + .saturating_add(Weight::from_ref_time(475_027 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule_named(s: u32, ) -> Weight { + Weight::from_ref_time(18_746_000 as u64) + // Standard Error: 2_997 + .saturating_add(Weight::from_ref_time(635_697 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn cancel_named(s: u32, ) -> Weight { + Weight::from_ref_time(20_983_000 as u64) + // Standard Error: 1_850 + .saturating_add(Weight::from_ref_time(518_812 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Scheduler Lookup (r:1 w:0) + // Storage: Scheduler Agenda (r:1 w:1) + fn change_named_priority(s: u32, ) -> Weight { + Weight::from_ref_time(21_591_000 as u64) + // Standard Error: 4_187 + .saturating_add(Weight::from_ref_time(531_231 as u64).saturating_mul(s as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } +} --- a/pallets/scheduler/CHANGELOG.md +++ /dev/null @@ -1,10 +0,0 @@ - -## [v0.1.1] 2022-08-16 - -### Other changes - -- build: Upgrade polkadot to v0.9.27 2c498572636f2b34d53b1c51b7283a761a7dc90a - -- build: Upgrade polkadot to v0.9.26 85515e54c4ca1b82a2630034e55dcc804c643bf8 - -- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b \ No newline at end of file --- a/pallets/scheduler/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "pallet-unique-scheduler" -version = "0.1.1" -authors = ["Unique Network "] -edition = "2021" -license = "GPLv3" -homepage = "https://unique.network" -repository = "https://github.com/UniqueNetwork/unique-chain" -description = "Unique Scheduler pallet" -readme = "README.md" - -[dependencies] -serde = { version = "1.0.130", default-features = false } -codec = { package = "parity-scale-codec", version = "3.1.2", default-features = false } -scale-info = { version = "2.0.1", default-features = false, features = [ - "derive", -] } - -frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } -frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } -sp-io = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } -sp-core = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.30' } -frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } - -up-sponsorship = { version = "0.1.0", default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.30" } -log = { version = "0.4.16", default-features = false } - -[dev-dependencies] -sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } -substrate-test-utils = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } - -[features] -default = ["std"] -std = [ - "codec/std", - "sp-runtime/std", - "frame-benchmarking/std", - "frame-support/std", - "frame-system/std", - "up-sponsorship/std", - "sp-io/std", - "sp-std/std", - "sp-core/std", - "log/std", -] -runtime-benchmarks = [ - "frame-benchmarking", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", -] -try-runtime = ["frame-support/try-runtime"] --- a/pallets/scheduler/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Scheduler -A module for scheduling dispatches. - -- [`scheduler::Trait`](https://docs.rs/pallet-scheduler/latest/pallet_scheduler/trait.Trait.html) -- [`Call`](https://docs.rs/pallet-scheduler/latest/pallet_scheduler/enum.Call.html) -- [`Module`](https://docs.rs/pallet-scheduler/latest/pallet_scheduler/struct.Module.html) - -## Overview - -This module exposes capabilities for scheduling dispatches to occur at a -specified block number or at a specified period. These scheduled dispatches -may be named or anonymous and may be canceled. - -**NOTE:** The scheduled calls will be dispatched with the default filter -for the origin: namely `frame_system::Config::BaseCallFilter` for all origin -except root which will get no filter. And not the filter contained in origin -use to call `fn schedule`. - -If a call is scheduled using proxy or whatever mecanism which adds filter, -then those filter will not be used when dispatching the schedule call. - -## Interface - -### Dispatchable Functions - -* `schedule` - schedule a dispatch, which may be periodic, to occur at a - specified block and with a specified priority. -* `cancel` - cancel a scheduled dispatch, specified by block number and - index. -* `schedule_named` - augments the `schedule` interface with an additional - `Vec` parameter that can be used for identification. -* `cancel_named` - the named complement to the cancel function. - -License: Unlicense --- a/pallets/scheduler/src/benchmarking.rs +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -// Original license -// This file is part of Substrate. - -// Copyright (C) 2020-2021 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. - -//! Scheduler pallet benchmarking. - -use super::*; -use frame_benchmarking::{benchmarks, account}; -use frame_support::{ - ensure, - traits::{OnInitialize}, -}; -use frame_system::RawOrigin; -use sp_runtime::traits::Hash; -use sp_std::{prelude::*, vec}; - -use crate::Pallet as Scheduler; -use frame_system::Pallet as System; -use frame_support::traits::Currency; - -const BLOCK_NUMBER: u32 = 2; - -fn make_scheduled_id(src: u32) -> ScheduledId { - let slice_id: [u8; 4] = src.encode().try_into().unwrap(); - let mut id: [u8; 16] = [0; 16]; - id[..4].clone_from_slice(&slice_id); - id -} - -/// Add `n` named items to the schedule. -/// -/// For `resolved`: -/// - `None`: aborted (hash without preimage) -/// - `Some(true)`: hash resolves into call if possible, plain call otherwise -/// - `Some(false)`: plain call -fn fill_schedule( - when: T::BlockNumber, - n: u32, - periodic: bool, - resolved: Option, -) -> Result<(), &'static str> { - let t = DispatchTime::At(when); - let caller = account("user", 0, 1); - - // Give the sender account max funds for transfer (their account will never reasonably be killed). - T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); - - for i in 0..n { - let (call, hash) = call_and_hash::(i); - let call_or_hash = match resolved { - Some(_) => call.into(), - None => CallOrHashOf::::Hash(hash), - }; - let period = match periodic { - true => Some(((i + 100).into(), 100)), - false => None, - }; - - let id = make_scheduled_id(i); - let origin = frame_system::RawOrigin::Signed(caller.clone()).into(); - Scheduler::::do_schedule_named(id, t, period, 0, origin, call_or_hash)?; - } - ensure!( - Agenda::::get(when).len() == n as usize, - "didn't fill schedule" - ); - Ok(()) -} - -fn call_and_hash(i: u32) -> (::RuntimeCall, T::Hash) { - // Essentially a no-op call. - let call: ::RuntimeCall = frame_system::Call::remark { remark: i.encode() }.into(); - let hash = T::Hashing::hash_of(&call); - (call, hash) -} - -benchmarks! { - on_initialize_periodic_named_resolved { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, true, Some(true))?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - assert_eq!(System::::event_count(), s); - for i in 0..s { - assert_eq!(Agenda::::get(when + (i + 100).into()).len(), 1 as usize); - } - } - - on_initialize_named_resolved { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, false, Some(true))?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - assert_eq!(System::::event_count(), s); - assert!(Agenda::::iter().count() == 0); - } - - on_initialize_periodic { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, true, Some(false))?; - }: { Scheduler::::on_initialize(when); } - verify { - assert_eq!(System::::event_count(), s); - for i in 0..s { - assert_eq!(Agenda::::get(when + (i + 100).into()).len(), 1 as usize); - } - } - - on_initialize_periodic_resolved { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, true, Some(true))?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - assert_eq!(System::::event_count(), s ); - for i in 0..s { - assert_eq!(Agenda::::get(when + (i + 100).into()).len(), 1 as usize); - } - } - - on_initialize_aborted { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, false, None)?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - assert_eq!(System::::event_count(), 0); - } - - on_initialize_named_aborted { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, false, Some(false))?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - } - - on_initialize_named { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, false, None)?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - assert_eq!(System::::event_count(), 0); - } - - on_initialize { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, false, Some(false))?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - assert_eq!(System::::event_count(), s); - assert!(Agenda::::iter().count() == 0); - } - - on_initialize_resolved { - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - fill_schedule::(when, s, false, Some(true))?; - }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } - verify { - assert_eq!(System::::event_count(), s); - assert!(Agenda::::iter().count() == 0); - } - - schedule_named { - let caller: T::AccountId = account("user", 0, 1); - let origin: RawOrigin = frame_system::RawOrigin::Signed(caller.clone()); - let s in 0 .. T::MaxScheduledPerBlock::get(); - let slice_id: [u8; 4] = s.encode().try_into().unwrap(); - let mut id: [u8; 16] = [0; 16]; - id[..4].clone_from_slice(&slice_id); - let when = BLOCK_NUMBER.into(); - let periodic = Some((T::BlockNumber::one(), 100)); - let priority = None; - // Essentially a no-op call. - let inner_call = frame_system::Call::set_storage { items: vec![] }.into(); - let call = Box::new(CallOrHashOf::::Value(inner_call)); - fill_schedule::(when, s, true, Some(false))?; - }: _(origin, id, when, periodic, priority, call) - verify { - ensure!( - Agenda::::get(when).len() == (s + 1) as usize, - "didn't add to schedule" - ); - } - - cancel_named { - let caller: T::AccountId = account("user", 0, 1); - let origin: RawOrigin = frame_system::RawOrigin::Signed(caller.clone()); - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - let idx = s - 1; - let id = make_scheduled_id(idx); - fill_schedule::(when, s, true, Some(false))?; - }: _(origin, id) - verify { - ensure!( - Lookup::::get(id).is_none(), - "didn't remove from lookup" - ); - // Removed schedule is NONE - ensure!( - Agenda::::get(when)[idx as usize].is_none(), - "didn't remove from schedule" - ); - } - - change_named_priority { - let origin: RawOrigin = frame_system::RawOrigin::Root; - let s in 1 .. T::MaxScheduledPerBlock::get(); - let when = BLOCK_NUMBER.into(); - let idx = s - 1; - let id = make_scheduled_id(idx); - let priority = 42; - fill_schedule::(when, s, true, Some(false))?; - }: _(origin, id, priority) - verify { - ensure!( - Agenda::::get(when)[idx as usize].clone().unwrap().priority == priority, - "didn't change the priority" - ); - } - - impl_benchmark_test_suite!(Scheduler, crate::mock::new_test_ext(), crate::mock::Test); -} --- a/pallets/scheduler/src/lib.rs +++ /dev/null @@ -1,802 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -// Original license: -// 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 -// -// -// -// 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. - -//! # Unique scheduler -//! A Pallet for scheduling dispatches. -//! -//! - [`Config`] -//! - [`Call`] -//! - [`Pallet`] -//! -//! ## Overview -//! -//! This Pallet exposes capabilities for scheduling dispatches to occur at a -//! specified block number or at a specified period. These scheduled dispatches -//! should be named and may be canceled. -//! -//! **NOTE:** The unique scheduler is designed for deferred transaction calls by block number. -//! Any user can book a call of a certain transaction to a specific block number. -//! Also possible to book a call with a certain frequency. -//! -//! Key differences from the original pallet: -//! -//! Schedule Id restricted by 16 bytes. Identificator for booked call. -//! Priority limited by HARD DEADLINE (<= 63). Calls over maximum weight don't include to block. -//! The maximum weight that may be scheduled per block for any dispatchables of less priority than `schedule::HARD_DEADLINE`. -//! Maybe_periodic limit is 100 calls. Reserved for future sponsored transaction support. -//! At 100 calls reserved amount is not so much and this is avoid potential problems with balance locks. -//! Any account allowed to schedule any calls. Account withdraw implemented through default transaction logic. -//! -//! ## Interface -//! -//! ### Dispatchable Functions -//! -//! * `schedule_named` - augments the `schedule` interface with an additional `Vec` parameter -//! that can be used for identification. -//! * `cancel_named` - the named complement to the cancel function. - -// Ensure we're `no_std` when compiling for Wasm. -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - -pub mod weights; - -use sp_core::H160; -use codec::{Codec, Decode, Encode}; -use frame_system::{self as system, ensure_signed}; -pub use pallet::*; -use scale_info::TypeInfo; -use sp_runtime::{ - traits::{BadOrigin, One, Saturating, Zero}, - RuntimeDebug, DispatchErrorWithPostInfo, -}; -use sp_std::{borrow::Borrow, cmp::Ordering, marker::PhantomData, prelude::*}; - -use frame_support::{ - dispatch::{ - DispatchError, DispatchResult, Dispatchable, UnfilteredDispatchable, Parameter, - GetDispatchInfo, - }, - traits::{ - schedule::{self, DispatchTime, MaybeHashed}, - NamedReservableCurrency, EnsureOrigin, Get, IsType, OriginTrait, PrivilegeCmp, - StorageVersion, - }, - weights::{Weight}, -}; - -pub use weights::WeightInfo; - -/// The location of a scheduled task that can be used to remove it. -pub type TaskAddress = (BlockNumber, u32); -pub const MAX_TASK_ID_LENGTH_IN_BYTES: u8 = 16; - -pub type ScheduledId = [u8; MAX_TASK_ID_LENGTH_IN_BYTES as usize]; -pub type CallOrHashOf = - MaybeHashed<::RuntimeCall, ::Hash>; - -/// Information regarding an item to be executed in the future. -#[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] -#[derive(Clone, RuntimeDebug, Encode, Decode, TypeInfo)] -pub struct ScheduledV3 { - /// The unique identity for this task, if there is one. - maybe_id: Option, - /// This task's priority. - priority: schedule::Priority, - /// The call to be dispatched. - call: Call, - /// If the call is periodic, then this points to the information concerning that. - maybe_periodic: Option>, - /// The origin to dispatch the call. - origin: PalletsOrigin, - _phantom: PhantomData, -} - -pub type ScheduledV3Of = ScheduledV3< - CallOrHashOf, - ::BlockNumber, - ::PalletsOrigin, - ::AccountId, ->; - -pub type ScheduledOf = ScheduledV3Of; - -/// The current version of Scheduled struct. -pub type Scheduled = - ScheduledV3; - -pub enum ScheduledEnsureOriginSuccess { - Root, - Signed(AccountId), -} - -#[cfg(feature = "runtime-benchmarks")] -mod preimage_provider { - use frame_support::traits::PreimageRecipient; - pub trait PreimageProviderAndMaybeRecipient: PreimageRecipient {} - impl> PreimageProviderAndMaybeRecipient for T {} -} - -#[cfg(not(feature = "runtime-benchmarks"))] -mod preimage_provider { - use frame_support::traits::PreimageProvider; - pub trait PreimageProviderAndMaybeRecipient: PreimageProvider {} - impl> PreimageProviderAndMaybeRecipient for T {} -} - -pub use preimage_provider::PreimageProviderAndMaybeRecipient; - -pub(crate) trait MarginalWeightInfo: WeightInfo { - fn item(periodic: bool, named: bool, resolved: Option) -> Weight { - match (periodic, named, resolved) { - (_, false, None) => Self::on_initialize_aborted(2) - Self::on_initialize_aborted(1), - (_, true, None) => { - Self::on_initialize_named_aborted(2) - Self::on_initialize_named_aborted(1) - } - (false, false, Some(false)) => Self::on_initialize(2) - Self::on_initialize(1), - (false, true, Some(false)) => { - Self::on_initialize_named(2) - Self::on_initialize_named(1) - } - (true, false, Some(false)) => { - Self::on_initialize_periodic(2) - Self::on_initialize_periodic(1) - } - (true, true, Some(false)) => { - Self::on_initialize_periodic_named_resolved(2) - - Self::on_initialize_periodic_named_resolved(1) - } - (false, false, Some(true)) => Self::on_initialize(2) - Self::on_initialize(1), - (false, true, Some(true)) => { - Self::on_initialize_named_resolved(2) - Self::on_initialize_named_resolved(1) - } - (true, false, Some(true)) => { - Self::on_initialize_periodic_resolved(2) - Self::on_initialize_periodic_resolved(1) - } - (true, true, Some(true)) => { - Self::on_initialize_periodic_named_resolved(2) - - Self::on_initialize_periodic_named_resolved(1) - } - } - } -} -impl MarginalWeightInfo for T {} - -#[frame_support::pallet] -pub mod pallet { - use super::*; - use frame_support::{ - dispatch::PostDispatchInfo, - pallet_prelude::*, - traits::{ - schedule::{LookupError, LOWEST_PRIORITY}, - PreimageProvider, - }, - }; - use frame_system::pallet_prelude::*; - - /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(3); - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - #[pallet::storage_version(STORAGE_VERSION)] - #[pallet::without_storage_info] - pub struct Pallet(_); - - /// `system::Config` should always be included in our implied traits. - #[pallet::config] - pub trait Config: frame_system::Config { - /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - /// The aggregated origin which the dispatch will take. - type RuntimeOrigin: OriginTrait - + From - + IsType<::RuntimeOrigin>; - - /// The caller origin, overarching type of all pallets origins. - type PalletsOrigin: From> + Codec + Clone + Eq + TypeInfo; - - type Currency: NamedReservableCurrency; - - /// The aggregated call type. - type RuntimeCall: Parameter - + Dispatchable< - RuntimeOrigin = ::RuntimeOrigin, - PostInfo = PostDispatchInfo, - > + UnfilteredDispatchable::RuntimeOrigin> - + GetDispatchInfo - + From>; - - /// The maximum weight that may be scheduled per block for any dispatchables of less - /// priority than `schedule::HARD_DEADLINE`. - #[pallet::constant] - type MaximumWeight: Get; - - /// Required origin to schedule or cancel calls. - type ScheduleOrigin: EnsureOrigin< - ::RuntimeOrigin, - Success = ScheduledEnsureOriginSuccess, - >; - - /// Required origin to set/change calls' priority. - type PrioritySetOrigin: EnsureOrigin<::RuntimeOrigin>; - - /// Compare the privileges of origins. - /// - /// This will be used when canceling a task, to ensure that the origin that tries - /// to cancel has greater or equal privileges as the origin that created the scheduled task. - /// - /// For simplicity the [`EqualPrivilegeOnly`](frame_support::traits::EqualPrivilegeOnly) can - /// be used. This will only check if two given origins are equal. - type OriginPrivilegeCmp: PrivilegeCmp; - - /// The maximum number of scheduled calls in the queue for a single block. - /// Not strictly enforced, but used for weight estimation. - #[pallet::constant] - type MaxScheduledPerBlock: Get; - - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; - - /// The preimage provider with which we look up call hashes to get the call. - type PreimageProvider: PreimageProviderAndMaybeRecipient; - - /// If `Some` then the number of blocks to postpone execution for when the item is delayed. - type NoPreimagePostponement: Get>; - - /// Sponsoring function. - // type SponsorshipHandler: SponsorshipHandler::Call>; - - /// The helper type used for custom transaction fee logic. - type CallExecutor: DispatchCall; - } - - /// A Scheduler-Runtime interface for finer payment handling. - pub trait DispatchCall { - /// Reserve (lock) the maximum spendings on a call, calculated from its weight and the repetition count. - fn reserve_balance( - id: ScheduledId, - sponsor: ::AccountId, - call: ::RuntimeCall, - count: u32, - ) -> Result<(), DispatchError>; - - /// Unreserve (unlock) a certain amount from the payer's reserved funds, returning the change. - fn pay_for_call( - id: ScheduledId, - sponsor: ::AccountId, - call: ::RuntimeCall, - ) -> Result; - - /// Resolve the call dispatch, including any post-dispatch operations. - fn dispatch_call( - signer: Option, - function: ::RuntimeCall, - ) -> Result< - Result>, - TransactionValidityError, - >; - - /// Release unspent reserved funds in case of a schedule cancel. - fn cancel_reserve( - id: ScheduledId, - sponsor: ::AccountId, - ) -> Result; - } - - /// Items to be executed, indexed by the block number that they should be executed on. - #[pallet::storage] - pub type Agenda = - StorageMap<_, Twox64Concat, T::BlockNumber, Vec>>, ValueQuery>; - - /// Lookup from identity to the block number and index of the task. - #[pallet::storage] - pub(crate) type Lookup = - StorageMap<_, Twox64Concat, ScheduledId, TaskAddress>; - - /// Events type. - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// Scheduled some task. - Scheduled { when: T::BlockNumber, index: u32 }, - /// Canceled some task. - Canceled { when: T::BlockNumber, index: u32 }, - /// Scheduled task's priority has changed - PriorityChanged { - when: T::BlockNumber, - index: u32, - priority: schedule::Priority, - }, - /// Dispatched some task. - Dispatched { - task: TaskAddress, - id: Option, - result: DispatchResult, - }, - /// The call for the provided hash was not found so the task has been aborted. - CallLookupFailed { - task: TaskAddress, - id: Option, - error: LookupError, - }, - } - - #[pallet::error] - pub enum Error { - /// Failed to schedule a call - FailedToSchedule, - /// Cannot find the scheduled call. - NotFound, - /// Given target block number is in the past. - TargetBlockNumberInPast, - /// Reschedule failed because it does not change scheduled time. - RescheduleNoChange, - } - - #[pallet::hooks] - impl Hooks> for Pallet { - /// Execute the scheduled calls - fn on_initialize(now: T::BlockNumber) -> Weight { - let limit = T::MaximumWeight::get(); - - let mut queued = Agenda::::take(now) - .into_iter() - .enumerate() - .filter_map(|(index, s)| Some((index as u32, s?))) - .collect::>(); - - if queued.len() as u32 > T::MaxScheduledPerBlock::get() { - log::warn!( - target: "runtime::scheduler", - "Warning: This block has more items queued in Scheduler than \ - expected from the runtime configuration. An update might be needed." - ); - } - - queued.sort_by_key(|(_, s)| s.priority); - - let next = now + One::one(); - - let mut total_weight: Weight = T::WeightInfo::on_initialize(0); - for (order, (index, mut s)) in queued.into_iter().enumerate() { - let named = s.maybe_id.is_some(); - - let (call, maybe_completed) = s.call.resolved::(); - s.call = call; - - let resolved = if let Some(completed) = maybe_completed { - T::PreimageProvider::unrequest_preimage(&completed); - true - } else { - false - }; - let call = match s.call.as_value().cloned() { - Some(c) => c, - None => { - // Preimage not available - postpone until some block. - total_weight.saturating_accrue(T::WeightInfo::item(false, named, None)); - if let Some(delay) = T::NoPreimagePostponement::get() { - let until = now.saturating_add(delay); - if let Some(ref id) = s.maybe_id { - let index = Agenda::::decode_len(until).unwrap_or(0); - Lookup::::insert(id, (until, index as u32)); - } - Agenda::::append(until, Some(s)); - } else if let Some(ref id) = s.maybe_id { - Lookup::::remove(id); - } - continue; - } - }; - - let periodic = s.maybe_periodic.is_some(); - let call_weight = call.get_dispatch_info().weight; - let mut item_weight = T::WeightInfo::item(periodic, named, Some(resolved)); - let origin = <::RuntimeOrigin as From>::from( - s.origin.clone(), - ) - .into(); - if ensure_signed(origin).is_ok() { - // Weights of Signed dispatches expect their signing account to be whitelisted. - item_weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); - } - - // We allow a scheduled call if any is true: - // - It's priority is `HARD_DEADLINE` - // - It does not push the weight past the limit. - // - It is the first item in the schedule - let hard_deadline = s.priority <= schedule::HARD_DEADLINE; - let test_weight = total_weight - .saturating_add(call_weight) - .saturating_add(item_weight); - if !hard_deadline && order > 0 && test_weight.all_gt(limit) { - // Cannot be scheduled this block - postpone until next. - total_weight.saturating_accrue(T::WeightInfo::item(false, named, None)); - if let Some(ref id) = s.maybe_id { - // NOTE: We could reasonably not do this (in which case there would be one - // block where the named and delayed item could not be referenced by name), - // but we will do it anyway since it should be mostly free in terms of - // weight and it is slightly cleaner. - let index = Agenda::::decode_len(next).unwrap_or(0); - Lookup::::insert(id, (next, index as u32)); - } - Agenda::::append(next, Some(s)); - continue; - } - - let scheduled_origin = - <::RuntimeOrigin as From>::from( - s.origin.clone(), - ); - let ensured_origin = T::ScheduleOrigin::ensure_origin(scheduled_origin.into()); - - let r = match ensured_origin { - Ok(ScheduledEnsureOriginSuccess::Root) => { - Ok(call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into())) - } - Ok(ScheduledEnsureOriginSuccess::Signed(sender)) => { - // Execute transaction via chain default pipeline - // That means dispatch will be processed like any user's extrinsic e.g. transaction fees will be taken - T::CallExecutor::dispatch_call(Some(sender), call.clone()) - } - Err(e) => Ok(Err(e.into())), - }; - - let mut actual_call_weight: Weight = item_weight; - let result: Result<_, DispatchError> = match r { - Ok(o) => match o { - Ok(di) => { - actual_call_weight = di.actual_weight.unwrap_or(item_weight); - Ok(()) - } - Err(err) => Err(err.error), - }, - Err(_) => { - log::error!( - target: "runtime::scheduler", - "Warning: Scheduler has failed to execute a post-dispatch transaction. \ - This block might have become invalid."); - Err(DispatchError::CannotLookup) - } // todo possibly force a skip/return here, do something with the error - }; - - total_weight.saturating_accrue(item_weight); - total_weight.saturating_accrue(actual_call_weight); - - Self::deposit_event(Event::Dispatched { - task: (now, index), - id: s.maybe_id.clone(), - result, - }); - - if let &Some((period, count)) = &s.maybe_periodic { - if count > 1 { - s.maybe_periodic = Some((period, count - 1)); - } else { - s.maybe_periodic = None; - } - let wake = now + period; - let is_canceled; - - // If scheduled is named, place its information in `Lookup` - if let Some(ref id) = s.maybe_id { - is_canceled = Lookup::::get(id).is_none(); - let wake_index = Agenda::::decode_len(wake).unwrap_or(0); - - if !is_canceled { - Lookup::::insert(id, (wake, wake_index as u32)); - } - } else { - is_canceled = false; - } - - if !is_canceled { - Agenda::::append(wake, Some(s)); - } - } else if let Some(ref id) = s.maybe_id { - Lookup::::remove(id); - } - } - total_weight - } - } - - #[pallet::call] - impl Pallet { - /// Schedule a named task. - #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] - pub fn schedule_named( - origin: OriginFor, - id: ScheduledId, - when: T::BlockNumber, - maybe_periodic: Option>, - priority: Option, - call: Box>, - ) -> DispatchResult { - T::ScheduleOrigin::ensure_origin(origin.clone())?; - - if priority.is_some() { - T::PrioritySetOrigin::ensure_origin(origin.clone())?; - } - - let origin = ::RuntimeOrigin::from(origin); - Self::do_schedule_named( - id, - DispatchTime::At(when), - maybe_periodic, - priority.unwrap_or(LOWEST_PRIORITY), - origin.caller().clone(), - *call, - )?; - Ok(()) - } - - /// Cancel a named scheduled task. - #[pallet::weight(::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get()))] - pub fn cancel_named(origin: OriginFor, id: ScheduledId) -> DispatchResult { - T::ScheduleOrigin::ensure_origin(origin.clone())?; - let origin = ::RuntimeOrigin::from(origin); - Self::do_cancel_named(Some(origin.caller().clone()), id)?; - Ok(()) - } - - /// Schedule a named task after a delay. - /// - /// # - /// Same as [`schedule_named`](Self::schedule_named). - /// # - #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] - pub fn schedule_named_after( - origin: OriginFor, - id: ScheduledId, - after: T::BlockNumber, - maybe_periodic: Option>, - priority: Option, - call: Box>, - ) -> DispatchResult { - T::ScheduleOrigin::ensure_origin(origin.clone())?; - - if priority.is_some() { - T::PrioritySetOrigin::ensure_origin(origin.clone())?; - } - - let origin = ::RuntimeOrigin::from(origin); - Self::do_schedule_named( - id, - DispatchTime::After(after), - maybe_periodic, - priority.unwrap_or(LOWEST_PRIORITY), - origin.caller().clone(), - *call, - )?; - Ok(()) - } - - #[pallet::weight(::WeightInfo::change_named_priority(T::MaxScheduledPerBlock::get()))] - pub fn change_named_priority( - origin: OriginFor, - id: ScheduledId, - priority: schedule::Priority, - ) -> DispatchResult { - T::PrioritySetOrigin::ensure_origin(origin.clone())?; - let origin = ::RuntimeOrigin::from(origin); - Self::do_change_named_priority(origin.caller().clone(), id, priority) - } - } -} - -impl Pallet { - #[cfg(feature = "try-runtime")] - pub fn pre_migrate_to_v3() -> Result<(), &'static str> { - Ok(()) - } - - #[cfg(feature = "try-runtime")] - pub fn post_migrate_to_v3() -> Result<(), &'static str> { - use frame_support::dispatch::GetStorageVersion; - - assert!(Self::current_storage_version() == 3); - for k in Agenda::::iter_keys() { - let _ = Agenda::::try_get(k).map_err(|()| "Invalid item in Agenda")?; - } - Ok(()) - } - - /// Helper to migrate scheduler when the pallet origin type has changed. - pub fn migrate_origin + codec::Decode>() { - Agenda::::translate::< - Vec, T::BlockNumber, OldOrigin, T::AccountId>>>, - _, - >(|_, agenda| { - Some( - agenda - .into_iter() - .map(|schedule| { - schedule.map(|schedule| Scheduled { - maybe_id: schedule.maybe_id, - priority: schedule.priority, - call: schedule.call, - maybe_periodic: schedule.maybe_periodic, - origin: schedule.origin.into(), - _phantom: Default::default(), - }) - }) - .collect::>(), - ) - }); - } - - fn resolve_time(when: DispatchTime) -> Result { - let now = frame_system::Pallet::::block_number(); - - let when = match when { - DispatchTime::At(x) => x, - // The current block has already completed it's scheduled tasks, so - // Schedule the task at lest one block after this current block. - DispatchTime::After(x) => now.saturating_add(x).saturating_add(One::one()), - }; - - if when <= now { - return Err(Error::::TargetBlockNumberInPast.into()); - } - - Ok(when) - } - - fn do_schedule_named( - id: ScheduledId, - when: DispatchTime, - maybe_periodic: Option>, - priority: schedule::Priority, - origin: T::PalletsOrigin, - call: CallOrHashOf, - ) -> Result, DispatchError> { - // ensure id it is unique - if Lookup::::contains_key(&id) { - return Err(Error::::FailedToSchedule)?; - } - - let when = Self::resolve_time(when)?; - - call.ensure_requested::(); - - // sanitize maybe_periodic - let maybe_periodic = maybe_periodic - .filter(|p| p.1 > 1 && !p.0.is_zero()) - // Remove one from the number of repetitions since we will schedule one now. - .map(|(p, c)| (p, c - 1)); - - let s = Scheduled { - maybe_id: Some(id.clone()), - priority, - call: call.clone(), - maybe_periodic, - origin: origin.clone(), - _phantom: Default::default(), - }; - - // reserve balance for periodic execution - // let sender = - // ensure_signed(<::Origin as From>::from(origin).into())?; - // let repeats = match maybe_periodic { - // Some(p) => p.1, - // None => 1, - // }; - // let _ = T::CallExecutor::reserve_balance( - // id.clone(), - // sender, - // call.as_value().unwrap().clone(), - // repeats, - // ); - - Agenda::::append(when, Some(s)); - let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; - let address = (when, index); - Lookup::::insert(&id, &address); - Self::deposit_event(Event::Scheduled { when, index }); - - Ok(address) - } - - fn do_cancel_named(origin: Option, id: ScheduledId) -> DispatchResult { - Lookup::::try_mutate_exists(id, |lookup| -> DispatchResult { - if let Some((when, index)) = lookup.take() { - let i = index as usize; - Agenda::::try_mutate(when, |agenda| -> DispatchResult { - if let Some(s) = agenda.get_mut(i) { - if let (Some(ref o), Some(ref s)) = (origin.clone(), s.borrow()) { - if matches!( - T::OriginPrivilegeCmp::cmp_privilege(o, &s.origin), - Some(Ordering::Less) | None - ) { - return Err(BadOrigin.into()); - } - // release balance reserve - // let sender = ensure_signed( - // <::Origin as From>::from( - // origin.unwrap(), - // ) - // .into(), - // )?; - // let _ = T::CallExecutor::cancel_reserve(id, sender); - - s.call.ensure_unrequested::(); - } - *s = None; - } - Ok(()) - })?; - - Self::deposit_event(Event::Canceled { when, index }); - Ok(()) - } else { - Err(Error::::NotFound)? - } - }) - } - - fn do_change_named_priority( - origin: T::PalletsOrigin, - id: ScheduledId, - priority: schedule::Priority, - ) -> DispatchResult { - match Lookup::::get(id) { - Some((when, index)) => { - let i = index as usize; - Agenda::::try_mutate(when, |agenda| { - if let Some(Some(s)) = agenda.get_mut(i) { - if matches!( - T::OriginPrivilegeCmp::cmp_privilege(&origin, &s.origin), - Some(Ordering::Less) | None - ) { - return Err(BadOrigin.into()); - } - - s.priority = priority; - Self::deposit_event(Event::PriorityChanged { - when, - index, - priority, - }); - } - Ok(()) - }) - } - None => Err(Error::::NotFound.into()), - } - } -} --- a/pallets/scheduler/src/weights.rs +++ /dev/null @@ -1,369 +0,0 @@ -// Template adopted from https://github.com/paritytech/substrate/blob/master/.maintain/frame-weight-template.hbs - -//! Autogenerated weights for pallet_unique_scheduler -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-09-14, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 - -// Executed Command: -// target/release/unique-collator -// benchmark -// pallet -// --pallet -// pallet-unique-scheduler -// --wasm-execution -// compiled -// --extrinsic -// * -// --template -// .maintain/frame-weight-template.hbs -// --steps=50 -// --repeat=80 -// --heap-pages=4096 -// --output=./pallets/scheduler/src/weights.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] -#![allow(clippy::unnecessary_cast)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use sp_std::marker::PhantomData; - -/// Weight functions needed for pallet_unique_scheduler. -pub trait WeightInfo { - fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight; - fn on_initialize_named_resolved(s: u32, ) -> Weight; - fn on_initialize_periodic(s: u32, ) -> Weight; - fn on_initialize_periodic_resolved(s: u32, ) -> Weight; - fn on_initialize_aborted(s: u32, ) -> Weight; - fn on_initialize_named_aborted(s: u32, ) -> Weight; - fn on_initialize_named(s: u32, ) -> Weight; - fn on_initialize(s: u32, ) -> Weight; - fn on_initialize_resolved(s: u32, ) -> Weight; - fn schedule_named(s: u32, ) -> Weight; - fn cancel_named(s: u32, ) -> Weight; - fn change_named_priority(s: u32, ) -> Weight; -} - -/// Weights for pallet_unique_scheduler using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:1 w:1) - fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(26_641_000) - // Standard Error: 9_000 - .saturating_add(Weight::from_ref_time(8_547_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(s as u64))) - .saturating_add(T::DbWeight::get().writes(4 as u64)) - .saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_named_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(23_941_000) - // Standard Error: 17_000 - .saturating_add(Weight::from_ref_time(5_282_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().writes(4 as u64)) - .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:1 w:1) - fn on_initialize_periodic(s: u32, ) -> Weight { - Weight::from_ref_time(24_858_000) - // Standard Error: 7_000 - .saturating_add(Weight::from_ref_time(8_657_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(s as u64))) - .saturating_add(T::DbWeight::get().writes(4 as u64)) - .saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:1 w:1) - fn on_initialize_periodic_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(25_515_000) - // Standard Error: 14_000 - .saturating_add(Weight::from_ref_time(8_656_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(s as u64))) - .saturating_add(T::DbWeight::get().writes(4 as u64)) - .saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_aborted(s: u32, ) -> Weight { - Weight::from_ref_time(7_584_000) - // Standard Error: 1_000 - .saturating_add(Weight::from_ref_time(2_065_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(2 as u64)) - .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_named_aborted(s: u32, ) -> Weight { - Weight::from_ref_time(25_552_000) - // Standard Error: 4_000 - .saturating_add(Weight::from_ref_time(5_187_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().writes(4 as u64)) - .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_named(s: u32, ) -> Weight { - Weight::from_ref_time(8_980_000) - // Standard Error: 12_000 - .saturating_add(Weight::from_ref_time(2_050_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(2 as u64)) - .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize(s: u32, ) -> Weight { - Weight::from_ref_time(24_482_000) - // Standard Error: 4_000 - .saturating_add(Weight::from_ref_time(5_249_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().writes(4 as u64)) - .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(25_187_000) - // Standard Error: 4_000 - .saturating_add(Weight::from_ref_time(5_216_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().writes(4 as u64)) - .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Lookup (r:1 w:1) - // Storage: Scheduler Agenda (r:1 w:1) - fn schedule_named(s: u32, ) -> Weight { - Weight::from_ref_time(17_316_000) - // Standard Error: 3_000 - .saturating_add(Weight::from_ref_time(82_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(2 as u64)) - } - // Storage: Scheduler Lookup (r:1 w:1) - // Storage: Scheduler Agenda (r:1 w:1) - fn cancel_named(s: u32, ) -> Weight { - Weight::from_ref_time(15_652_000) - // Standard Error: 1_000 - .saturating_add(Weight::from_ref_time(436_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(2 as u64)) - } - // Storage: Scheduler Lookup (r:1 w:1) - // Storage: Scheduler Agenda (r:1 w:1) - fn change_named_priority(s: u32, ) -> Weight { - Weight::from_ref_time(8_642_000) - // Standard Error: 0 - .saturating_add(Weight::from_ref_time(431_000).saturating_mul(s as u64)) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(2 as u64)) - } -} - -// For backwards compatibility and tests -impl WeightInfo for () { - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:1 w:1) - fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(26_641_000) - // Standard Error: 9_000 - .saturating_add(Weight::from_ref_time(8_547_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(s as u64))) - .saturating_add(RocksDbWeight::get().writes(4 as u64)) - .saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_named_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(23_941_000) - // Standard Error: 17_000 - .saturating_add(Weight::from_ref_time(5_282_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().writes(4 as u64)) - .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:1 w:1) - fn on_initialize_periodic(s: u32, ) -> Weight { - Weight::from_ref_time(24_858_000) - // Standard Error: 7_000 - .saturating_add(Weight::from_ref_time(8_657_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(s as u64))) - .saturating_add(RocksDbWeight::get().writes(4 as u64)) - .saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:1 w:1) - fn on_initialize_periodic_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(25_515_000) - // Standard Error: 14_000 - .saturating_add(Weight::from_ref_time(8_656_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(s as u64))) - .saturating_add(RocksDbWeight::get().writes(4 as u64)) - .saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_aborted(s: u32, ) -> Weight { - Weight::from_ref_time(7_584_000) - // Standard Error: 1_000 - .saturating_add(Weight::from_ref_time(2_065_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(2 as u64)) - .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_named_aborted(s: u32, ) -> Weight { - Weight::from_ref_time(25_552_000) - // Standard Error: 4_000 - .saturating_add(Weight::from_ref_time(5_187_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().writes(4 as u64)) - .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:2 w:2) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_named(s: u32, ) -> Weight { - Weight::from_ref_time(8_980_000) - // Standard Error: 12_000 - .saturating_add(Weight::from_ref_time(2_050_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(2 as u64)) - .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize(s: u32, ) -> Weight { - Weight::from_ref_time(24_482_000) - // Standard Error: 4_000 - .saturating_add(Weight::from_ref_time(5_249_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().writes(4 as u64)) - .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Agenda (r:1 w:1) - // Storage: System Account (r:1 w:1) - // Storage: System AllExtrinsicsLen (r:1 w:1) - // Storage: System BlockWeight (r:1 w:1) - // Storage: Configuration WeightToFeeCoefficientOverride (r:1 w:0) - // Storage: TransactionPayment NextFeeMultiplier (r:1 w:0) - // Storage: Scheduler Lookup (r:0 w:1) - fn on_initialize_resolved(s: u32, ) -> Weight { - Weight::from_ref_time(25_187_000) - // Standard Error: 4_000 - .saturating_add(Weight::from_ref_time(5_216_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().writes(4 as u64)) - .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(s as u64))) - } - // Storage: Scheduler Lookup (r:1 w:1) - // Storage: Scheduler Agenda (r:1 w:1) - fn schedule_named(s: u32, ) -> Weight { - Weight::from_ref_time(17_316_000) - // Standard Error: 3_000 - .saturating_add(Weight::from_ref_time(82_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(2 as u64)) - } - // Storage: Scheduler Lookup (r:1 w:1) - // Storage: Scheduler Agenda (r:1 w:1) - fn cancel_named(s: u32, ) -> Weight { - Weight::from_ref_time(15_652_000) - // Standard Error: 1_000 - .saturating_add(Weight::from_ref_time(436_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(2 as u64)) - } - - // Storage: Scheduler Lookup (r:1 w:1) - // Storage: Scheduler Agenda (r:1 w:1) - fn change_named_priority(s: u32, ) -> Weight { - Weight::from_ref_time(8_642_000) - // Standard Error: 0 - .saturating_add(Weight::from_ref_time(431_000).saturating_mul(s as u64)) - .saturating_add(RocksDbWeight::get().reads(2 as u64)) - .saturating_add(RocksDbWeight::get().writes(2 as u64)) - } -} --- a/runtime/common/config/pallets/scheduler.rs +++ b/runtime/common/config/pallets/scheduler.rs @@ -25,9 +25,9 @@ use codec::Decode; use crate::{ runtime_common::{scheduler::SchedulerPaymentExecutor, config::substrate::RuntimeBlockWeights}, - Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, OriginCaller, Balances, + Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, OriginCaller, }; -use pallet_unique_scheduler::ScheduledEnsureOriginSuccess; +use pallet_unique_scheduler_v2::ScheduledEnsureOriginSuccess; use up_common::types::AccountId; parameter_types! { @@ -70,19 +70,17 @@ } } -impl pallet_unique_scheduler::Config for Runtime { +impl pallet_unique_scheduler_v2::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeOrigin = RuntimeOrigin; - type Currency = Balances; type PalletsOrigin = OriginCaller; type RuntimeCall = RuntimeCall; type MaximumWeight = MaximumSchedulerWeight; type ScheduleOrigin = EnsureSignedOrRoot; - type PrioritySetOrigin = EnsureRoot; + type OriginPrivilegeCmp = EqualOrRootOnly; type MaxScheduledPerBlock = MaxScheduledPerBlock; type WeightInfo = (); + type Preimages = (); type CallExecutor = SchedulerPaymentExecutor; - type OriginPrivilegeCmp = EqualOrRootOnly; - type PreimageProvider = (); - type NoPreimagePostponement = NoPreimagePostponement; + type PrioritySetOrigin = EnsureRoot; } --- a/runtime/common/config/test_pallets.rs +++ b/runtime/common/config/test_pallets.rs @@ -14,8 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -use crate::{Runtime, RuntimeEvent}; +use crate::{Runtime, RuntimeEvent, RuntimeCall}; impl pallet_test_utils::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; } --- a/runtime/common/construct_runtime/mod.rs +++ b/runtime/common/construct_runtime/mod.rs @@ -58,7 +58,7 @@ Unique: pallet_unique::{Pallet, Call, Storage, Event} = 61, #[runtimes(opal)] - Scheduler: pallet_unique_scheduler::{Pallet, Call, Storage, Event} = 62, + Scheduler: pallet_unique_scheduler_v2::{Pallet, Call, Storage, Event} = 62, Configuration: pallet_configuration::{Pallet, Call, Storage} = 63, --- a/runtime/common/runtime_apis.rs +++ b/runtime/common/runtime_apis.rs @@ -683,8 +683,8 @@ #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] list_benchmark!(list, extra, pallet_refungible, Refungible); - // #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] - // list_benchmark!(list, extra, pallet_unique_scheduler, Scheduler); + #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] + list_benchmark!(list, extra, pallet_unique_scheduler_v2, Scheduler); #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] list_benchmark!(list, extra, pallet_proxy_rmrk_core, RmrkCore); @@ -743,8 +743,8 @@ #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] add_benchmark!(params, batches, pallet_refungible, Refungible); - // #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] - // add_benchmark!(params, batches, pallet_unique_scheduler, Scheduler); + #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] + add_benchmark!(params, batches, pallet_unique_scheduler_v2, Scheduler); #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))] add_benchmark!(params, batches, pallet_proxy_rmrk_core, RmrkCore); --- a/runtime/common/scheduler.rs +++ b/runtime/common/scheduler.rs @@ -15,44 +15,29 @@ // along with Unique Network. If not, see . use frame_support::{ - traits::NamedReservableCurrency, dispatch::{GetDispatchInfo, PostDispatchInfo, DispatchInfo}, }; use sp_runtime::{ traits::{Dispatchable, Applyable, Member}, - generic::Era, transaction_validity::TransactionValidityError, - DispatchErrorWithPostInfo, DispatchError, + DispatchErrorWithPostInfo, }; use codec::Encode; -use crate::{Runtime, RuntimeCall, RuntimeOrigin, Balances, maintenance}; -use up_common::types::{AccountId, Balance}; +use crate::{Runtime, RuntimeCall, RuntimeOrigin, maintenance}; +use up_common::types::AccountId; use fp_self_contained::SelfContainedCall; -use pallet_unique_scheduler::DispatchCall; +use pallet_unique_scheduler_v2::DispatchCall; use pallet_transaction_payment::ChargeTransactionPayment; -type SponsorshipChargeTransactionPayment = - pallet_charge_transaction::ChargeTransactionPayment; - /// The SignedExtension to the basic transaction logic. pub type SignedExtraScheduler = ( - frame_system::CheckSpecVersion, - frame_system::CheckGenesis, - frame_system::CheckEra, - frame_system::CheckNonce, frame_system::CheckWeight, maintenance::CheckMaintenance, ChargeTransactionPayment, ); -fn get_signed_extras(from: ::AccountId) -> SignedExtraScheduler { +fn get_signed_extras() -> SignedExtraScheduler { ( - frame_system::CheckSpecVersion::::new(), - frame_system::CheckGenesis::::new(), - frame_system::CheckEra::::from(Era::Immortal), - frame_system::CheckNonce::::from(frame_system::Pallet::::account_nonce( - from, - )), frame_system::CheckWeight::::new(), maintenance::CheckMaintenance, ChargeTransactionPayment::::from(0), @@ -61,7 +46,7 @@ pub struct SchedulerPaymentExecutor; -impl +impl DispatchCall for SchedulerPaymentExecutor where ::RuntimeCall: Member @@ -71,13 +56,13 @@ + From>, SelfContainedSignedInfo: Send + Sync + 'static, RuntimeCall: From<::RuntimeCall> - + From<::RuntimeCall> + + From<::RuntimeCall> + SelfContainedCall, sp_runtime::AccountId32: From<::AccountId>, { fn dispatch_call( signer: Option<::AccountId>, - call: ::RuntimeCall, + call: ::RuntimeCall, ) -> Result< Result>, TransactionValidityError, @@ -88,7 +73,7 @@ let signed = match signer { Some(signer) => fp_self_contained::CheckedSignature::Signed( signer.clone().into(), - get_signed_extras(signer.into()), + get_signed_extras(), ), None => fp_self_contained::CheckedSignature::Unsigned, }; @@ -104,53 +89,5 @@ }; extrinsic.apply::(&dispatch_info, len) - } - - fn reserve_balance( - id: [u8; 16], - sponsor: ::AccountId, - call: ::RuntimeCall, - count: u32, - ) -> Result<(), DispatchError> { - let dispatch_info = call.get_dispatch_info(); - let weight: Balance = - SponsorshipChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0) - .saturating_mul(count.into()); - - >::reserve_named( - &id, - &(sponsor.into()), - weight, - ) - } - - fn pay_for_call( - id: [u8; 16], - sponsor: ::AccountId, - call: ::RuntimeCall, - ) -> Result { - let dispatch_info = call.get_dispatch_info(); - let weight: Balance = - SponsorshipChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0); - Ok( - >::unreserve_named( - &id, - &(sponsor.into()), - weight, - ), - ) - } - - fn cancel_reserve( - id: [u8; 16], - sponsor: ::AccountId, - ) -> Result { - Ok( - >::unreserve_named( - &id, - &(sponsor.into()), - u128::MAX, - ), - ) } } --- a/runtime/opal/Cargo.toml +++ b/runtime/opal/Cargo.toml @@ -41,7 +41,7 @@ 'pallet-unique/runtime-benchmarks', 'pallet-inflation/runtime-benchmarks', 'pallet-app-promotion/runtime-benchmarks', - 'pallet-unique-scheduler/runtime-benchmarks', + 'pallet-unique-scheduler-v2/runtime-benchmarks', 'pallet-xcm/runtime-benchmarks', 'sp-runtime/runtime-benchmarks', 'xcm-builder/runtime-benchmarks', @@ -71,7 +71,6 @@ 'cumulus-pallet-dmp-queue/try-runtime', 'pallet-inflation/try-runtime', 'pallet-unique/try-runtime', - 'pallet-unique-scheduler/try-runtime', 'pallet-configuration/try-runtime', 'pallet-charge-transaction/try-runtime', 'pallet-common/try-runtime', @@ -142,7 +141,7 @@ 'pallet-proxy-rmrk-core/std', 'pallet-proxy-rmrk-equip/std', 'pallet-unique/std', - 'pallet-unique-scheduler/std', + 'pallet-unique-scheduler-v2/std', 'pallet-charge-transaction/std', 'up-data-structs/std', 'sp-api/std', @@ -474,8 +473,8 @@ pallet-nonfungible = { default-features = false, path = "../../pallets/nonfungible" } pallet-proxy-rmrk-core = { default-features = false, path = "../../pallets/proxy-rmrk-core", package = "pallet-rmrk-core" } pallet-proxy-rmrk-equip = { default-features = false, path = "../../pallets/proxy-rmrk-equip", package = "pallet-rmrk-equip" } -pallet-unique-scheduler = { path = '../../pallets/scheduler', default-features = false } pallet-charge-transaction = { git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.30", package = "pallet-template-transaction-payment", default-features = false, version = "3.0.0" } +pallet-unique-scheduler-v2 = { path = '../../pallets/scheduler-v2', default-features = false } pallet-evm-migration = { path = '../../pallets/evm-migration', default-features = false } pallet-evm-contract-helpers = { path = '../../pallets/evm-contract-helpers', default-features = false } pallet-evm-transaction-payment = { path = '../../pallets/evm-transaction-payment', default-features = false } --- a/runtime/quartz/Cargo.toml +++ b/runtime/quartz/Cargo.toml @@ -40,7 +40,6 @@ 'pallet-unique/runtime-benchmarks', 'pallet-foreign-assets/runtime-benchmarks', 'pallet-inflation/runtime-benchmarks', - 'pallet-unique-scheduler/runtime-benchmarks', 'pallet-xcm/runtime-benchmarks', 'sp-runtime/runtime-benchmarks', 'xcm-builder/runtime-benchmarks', @@ -70,7 +69,6 @@ 'cumulus-pallet-dmp-queue/try-runtime', 'pallet-inflation/try-runtime', 'pallet-unique/try-runtime', - 'pallet-unique-scheduler/try-runtime', 'pallet-configuration/try-runtime', 'pallet-charge-transaction/try-runtime', 'pallet-common/try-runtime', @@ -139,7 +137,6 @@ 'pallet-proxy-rmrk-core/std', 'pallet-proxy-rmrk-equip/std', 'pallet-unique/std', - 'pallet-unique-scheduler/std', 'pallet-charge-transaction/std', 'up-data-structs/std', 'sp-api/std', @@ -475,7 +472,6 @@ pallet-nonfungible = { default-features = false, path = "../../pallets/nonfungible" } pallet-proxy-rmrk-core = { default-features = false, path = "../../pallets/proxy-rmrk-core", package = "pallet-rmrk-core" } pallet-proxy-rmrk-equip = { default-features = false, path = "../../pallets/proxy-rmrk-equip", package = "pallet-rmrk-equip" } -pallet-unique-scheduler = { path = '../../pallets/scheduler', default-features = false } # pallet-contract-helpers = { path = '../pallets/contract-helpers', default-features = false, version = '0.1.0' } pallet-charge-transaction = { git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.30", package = "pallet-template-transaction-payment", default-features = false, version = "3.0.0" } pallet-evm-migration = { path = '../../pallets/evm-migration', default-features = false } --- a/runtime/unique/Cargo.toml +++ b/runtime/unique/Cargo.toml @@ -40,7 +40,6 @@ 'pallet-unique/runtime-benchmarks', 'pallet-foreign-assets/runtime-benchmarks', 'pallet-inflation/runtime-benchmarks', - 'pallet-unique-scheduler/runtime-benchmarks', 'pallet-xcm/runtime-benchmarks', 'sp-runtime/runtime-benchmarks', 'xcm-builder/runtime-benchmarks', @@ -71,7 +70,6 @@ 'cumulus-pallet-dmp-queue/try-runtime', 'pallet-inflation/try-runtime', 'pallet-unique/try-runtime', - 'pallet-unique-scheduler/try-runtime', 'pallet-configuration/try-runtime', 'pallet-charge-transaction/try-runtime', 'pallet-common/try-runtime', @@ -140,7 +138,6 @@ 'pallet-proxy-rmrk-core/std', 'pallet-proxy-rmrk-equip/std', 'pallet-unique/std', - 'pallet-unique-scheduler/std', 'pallet-charge-transaction/std', 'up-data-structs/std', 'sp-api/std', @@ -469,7 +466,6 @@ pallet-nonfungible = { default-features = false, path = "../../pallets/nonfungible" } pallet-proxy-rmrk-core = { default-features = false, path = "../../pallets/proxy-rmrk-core", package = "pallet-rmrk-core" } pallet-proxy-rmrk-equip = { default-features = false, path = "../../pallets/proxy-rmrk-equip", package = "pallet-rmrk-equip" } -pallet-unique-scheduler = { path = '../../pallets/scheduler', default-features = false } # pallet-contract-helpers = { path = '../pallets/contract-helpers', default-features = false, version = '0.1.0' } pallet-charge-transaction = { git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.30", package = "pallet-template-transaction-payment", default-features = false, version = "3.0.0" } pallet-evm-migration = { path = '../../pallets/evm-migration', default-features = false } --- a/test-pallets/utils/Cargo.toml +++ b/test-pallets/utils/Cargo.toml @@ -10,7 +10,9 @@ scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } -pallet-unique-scheduler = { path = '../../pallets/scheduler', default-features = false } +# pallet-unique-scheduler = { path = '../../pallets/scheduler', default-features = false } +pallet-unique-scheduler-v2 = { path = '../../pallets/scheduler-v2', default-features = false } +sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.30" } [features] default = ["std"] @@ -19,6 +21,7 @@ "scale-info/std", "frame-support/std", "frame-system/std", - "pallet-unique-scheduler/std", + "pallet-unique-scheduler-v2/std", + "sp-std/std", ] -try-runtime = ["frame-support/try-runtime", "pallet-unique-scheduler/try-runtime"] +try-runtime = ["frame-support/try-runtime", "pallet-unique-scheduler-v2/try-runtime"] --- a/test-pallets/utils/src/lib.rs +++ b/test-pallets/utils/src/lib.rs @@ -22,13 +22,29 @@ #[frame_support::pallet] pub mod pallet { - use frame_support::pallet_prelude::*; + use frame_support::{ + pallet_prelude::*, + dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, + traits::{UnfilteredDispatchable, IsSubType, OriginTrait}, + }; use frame_system::pallet_prelude::*; - use pallet_unique_scheduler::{ScheduledId, Pallet as SchedulerPallet}; + use sp_std::vec::Vec; + use pallet_unique_scheduler_v2::{TaskName, Pallet as SchedulerPallet}; #[pallet::config] - pub trait Config: frame_system::Config + pallet_unique_scheduler::Config { + pub trait Config: frame_system::Config + pallet_unique_scheduler_v2::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The overarching call type. + type RuntimeCall: Parameter + + Dispatchable< + RuntimeOrigin = ::RuntimeOrigin, + PostInfo = PostDispatchInfo, + > + GetDispatchInfo + + From> + + UnfilteredDispatchable::RuntimeOrigin> + + IsSubType> + + IsType<::RuntimeCall>; } #[pallet::event] @@ -36,6 +52,7 @@ pub enum Event { ValueIsSet, ShouldRollback, + BatchCompleted, } #[pallet::pallet] @@ -94,14 +111,13 @@ #[pallet::weight(10_000)] pub fn self_canceling_inc( origin: OriginFor, - id: ScheduledId, + id: TaskName, max_test_value: u32, ) -> DispatchResult { Self::ensure_origin_and_enabled(origin.clone())?; + Self::inc_test_value(origin.clone())?; - if >::get() < max_test_value { - Self::inc_test_value(origin)?; - } else { + if >::get() == max_test_value { SchedulerPallet::::cancel_named(origin, id)?; } @@ -113,6 +129,35 @@ Self::ensure_origin_and_enabled(origin)?; Ok(()) } + + #[pallet::weight(10_000)] + pub fn batch_all( + origin: OriginFor, + calls: Vec<::RuntimeCall>, + ) -> DispatchResultWithPostInfo { + Self::ensure_origin_and_enabled(origin.clone())?; + + let is_root = ensure_root(origin.clone()).is_ok(); + + for call in calls { + if is_root { + call.dispatch_bypass_filter(origin.clone())?; + } else { + let mut filtered_origin = origin.clone(); + // Don't allow users to nest `batch_all` calls. + filtered_origin.add_filter( + move |c: &::RuntimeCall| { + let c = ::RuntimeCall::from_ref(c); + !matches!(c.is_sub_type(), Some(Call::batch_all { .. })) + }, + ); + call.dispatch(filtered_origin)?; + } + } + + Self::deposit_event(Event::BatchCompleted); + Ok(None::.into()) + } } } --- a/tests/src/eth/scheduling.test.ts +++ b/tests/src/eth/scheduling.test.ts @@ -15,7 +15,7 @@ // along with Unique Network. If not, see . import {expect} from 'chai'; -import {EthUniqueHelper, itEth} from './util'; +import {EthUniqueHelper, itSchedEth} from './util'; import {Pallets, usingPlaygrounds} from '../util'; describe('Scheduing EVM smart contracts', () => { @@ -26,11 +26,11 @@ }); }); - itEth.ifWithPallets('Successfully schedules and periodically executes an EVM contract', [Pallets.Scheduler], async ({helper, privateKey}) => { + itSchedEth.ifWithPallets('Successfully schedules and periodically executes an EVM contract', [Pallets.Scheduler], async (scheduleKind, {helper, privateKey}) => { const donor = await privateKey({filename: __filename}); const [alice] = await helper.arrange.createAccounts([1000n], donor); - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; const deployer = await helper.eth.createAccountWithBalance(alice); const flipper = await helper.eth.deployFlipper(deployer); @@ -44,7 +44,7 @@ repetitions: 2, }; - await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic}) .eth.sendEVM( alice, flipper.options.address, --- a/tests/src/eth/util/index.ts +++ b/tests/src/eth/util/index.ts @@ -8,6 +8,7 @@ import {EthUniqueHelper} from './playgrounds/unique.dev'; import {SilentLogger, SilentConsole} from '../../util/playgrounds/unique.dev'; +import {SchedKind} from '../../util'; export {EthUniqueHelper} from './playgrounds/unique.dev'; @@ -81,3 +82,19 @@ itEthIfWithPallet.only = (name: string, required: string[], cb: (apis: { helper: EthUniqueHelper, privateKey: (seed: string | {filename: string}) => Promise }) => any) => itEthIfWithPallet(name, required, cb, {only: true}); itEthIfWithPallet.skip = (name: string, required: string[], cb: (apis: { helper: EthUniqueHelper, privateKey: (seed: string | {filename: string}) => Promise }) => any) => itEthIfWithPallet(name, required, cb, {skip: true}); itEth.ifWithPallets = itEthIfWithPallet; + +export function itSchedEth( + name: string, + cb: (schedKind: SchedKind, apis: { helper: EthUniqueHelper, privateKey: (seed: string | {filename: string}) => Promise }) => any, + opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {}, +) { + itEth(name + ' (anonymous scheduling)', (apis) => cb('anon', apis), opts); + itEth(name + ' (named scheduling)', (apis) => cb('named', apis), opts); +} +itSchedEth.only = (name: string, cb: (schedKind: SchedKind, apis: { helper: EthUniqueHelper, privateKey: (seed: string | {filename: string}) => Promise }) => any) => itSchedEth(name, cb, {only: true}); +itSchedEth.skip = (name: string, cb: (schedKind: SchedKind, apis: { helper: EthUniqueHelper, privateKey: (seed: string | {filename: string}) => Promise }) => any) => itSchedEth(name, cb, {skip: true}); +itSchedEth.ifWithPallets = itSchedIfWithPallets; + +function itSchedIfWithPallets(name: string, required: string[], cb: (schedKind: SchedKind, apis: { helper: EthUniqueHelper, privateKey: (seed: string | {filename: string}) => Promise }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {}) { + return itSchedEth(name, cb, {requiredPallets: required, ...opts}); +} --- a/tests/src/maintenanceMode.seqtest.ts +++ b/tests/src/maintenanceMode.seqtest.ts @@ -16,7 +16,7 @@ import {IKeyringPair} from '@polkadot/types/types'; import {ApiPromise} from '@polkadot/api'; -import {expect, itSub, Pallets, usingPlaygrounds} from './util'; +import {expect, itSched, itSub, Pallets, usingPlaygrounds} from './util'; import {itEth} from './eth/util'; async function maintenanceEnabled(api: ApiPromise): Promise { @@ -162,34 +162,38 @@ await expect(helper.balance.transferToSubstrate(bob, superuser.address, 1n)).to.be.fulfilled; }); - itSub.ifWithPallets('MM blocks scheduled calls and the scheduler itself', [Pallets.Scheduler], async ({helper}) => { + itSched.ifWithPallets('MM blocks scheduled calls and the scheduler itself', [Pallets.Scheduler], async (scheduleKind, {helper}) => { const collection = await helper.nft.mintCollection(bob); const nftBeforeMM = await collection.mintToken(bob); const nftDuringMM = await collection.mintToken(bob); const nftAfterMM = await collection.mintToken(bob); - const scheduledIdBeforeMM = '0x' + '0'.repeat(31) + '0'; - const scheduledIdDuringMM = '0x' + '0'.repeat(31) + '1'; - const scheduledIdBunkerThroughMM = '0x' + '0'.repeat(31) + '2'; - const scheduledIdAttemptDuringMM = '0x' + '0'.repeat(31) + '3'; - const scheduledIdAfterMM = '0x' + '0'.repeat(31) + '4'; + const [ + scheduledIdBeforeMM, + scheduledIdDuringMM, + scheduledIdBunkerThroughMM, + scheduledIdAttemptDuringMM, + scheduledIdAfterMM, + ] = scheduleKind == 'named' + ? helper.arrange.makeScheduledIds(5) + : new Array(5); const blocksToWait = 6; // Scheduling works before the maintenance - await nftBeforeMM.scheduleAfter(scheduledIdBeforeMM, blocksToWait) + await nftBeforeMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdBeforeMM}) .transfer(bob, {Substrate: superuser.address}); await helper.wait.newBlocks(blocksToWait + 1); expect(await nftBeforeMM.getOwner()).to.be.deep.equal({Substrate: superuser.address}); // Schedule a transaction that should occur *during* the maintenance - await nftDuringMM.scheduleAfter(scheduledIdDuringMM, blocksToWait) + await nftDuringMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdDuringMM}) .transfer(bob, {Substrate: superuser.address}); // Schedule a transaction that should occur *after* the maintenance - await nftDuringMM.scheduleAfter(scheduledIdBunkerThroughMM, blocksToWait * 2) + await nftDuringMM.scheduleAfter(blocksToWait * 2, {scheduledId: scheduledIdBunkerThroughMM}) .transfer(bob, {Substrate: superuser.address}); await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []); @@ -200,7 +204,7 @@ expect(await nftDuringMM.getOwner()).to.be.deep.equal({Substrate: bob.address}); // Any attempts to schedule a tx during the MM should be rejected - await expect(nftDuringMM.scheduleAfter(scheduledIdAttemptDuringMM, blocksToWait) + await expect(nftDuringMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAttemptDuringMM}) .transfer(bob, {Substrate: superuser.address})) .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/); @@ -208,7 +212,7 @@ expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false; // Scheduling works after the maintenance - await nftAfterMM.scheduleAfter(scheduledIdAfterMM, blocksToWait) + await nftAfterMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAfterMM}) .transfer(bob, {Substrate: superuser.address}); await helper.wait.newBlocks(blocksToWait + 1); --- a/tests/src/scheduler.seqtest.ts +++ b/tests/src/scheduler.seqtest.ts @@ -14,43 +14,53 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {expect, itSub, Pallets, usingPlaygrounds} from './util'; +import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util'; import {IKeyringPair} from '@polkadot/types/types'; import {DevUniqueHelper} from './util/playgrounds/unique.dev'; describe('Scheduling token and balance transfers', () => { + let superuser: IKeyringPair; let alice: IKeyringPair; let bob: IKeyringPair; let charlie: IKeyringPair; - before(async () => { - await usingPlaygrounds(async (helper, privateKeyWrapper) => { - alice = await privateKeyWrapper('//Alice'); - bob = await privateKeyWrapper('//Bob'); - charlie = await privateKeyWrapper('//Charlie'); + before(async function() { + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.Scheduler]); + superuser = await privateKey('//Alice'); + const donor = await privateKey({filename: __filename}); + [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor); + await helper.testUtils.enable(); }); }); - itSub.ifWithPallets('Can delay a transfer of an owned token', [Pallets.Scheduler], async ({helper}) => { + beforeEach(async () => { + await usingPlaygrounds(async (helper) => { + await helper.wait.noScheduledTasks(); + }); + }); + + itSched('Can delay a transfer of an owned token', async (scheduleKind, {helper}) => { const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); const token = await collection.mintToken(alice); - const schedulerId = await helper.arrange.makeScheduledId(); + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; const blocksBeforeExecution = 4; - - await token.scheduleAfter(schedulerId, blocksBeforeExecution) + + await token.scheduleAfter(blocksBeforeExecution, {scheduledId}) .transfer(alice, {Substrate: bob.address}); + const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1; expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); - await helper.wait.newBlocks(blocksBeforeExecution + 1); + await helper.wait.forParachainBlockNumber(executionBlock); expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); }); - itSub.ifWithPallets('Can transfer funds periodically', [Pallets.Scheduler], async ({helper}) => { - const scheduledId = await helper.arrange.makeScheduledId(); + itSched('Can transfer funds periodically', async (scheduleKind, {helper}) => { + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; const waitForBlocks = 1; const amount = 1n * helper.balance.getOneTokenNominal(); @@ -61,10 +71,11 @@ const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address); - await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic}) .balance.transferToSubstrate(alice, bob.address, amount); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address); expect(bobsBalanceAfterFirst) @@ -73,7 +84,7 @@ '#1 Balance of the recipient should be increased by 1 * amount', ); - await helper.wait.newBlocks(periodic.period); + await helper.wait.forParachainBlockNumber(executionBlock + periodic.period); const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address); expect(bobsBalanceAfterSecond) @@ -83,42 +94,44 @@ ); }); - itSub.ifWithPallets('Can cancel a scheduled operation which has not yet taken effect', [Pallets.Scheduler], async ({helper}) => { + itSub('Can cancel a scheduled operation which has not yet taken effect', async ({helper}) => { const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); const token = await collection.mintToken(alice); - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = helper.arrange.makeScheduledId(); const waitForBlocks = 4; expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); - await token.scheduleAfter(scheduledId, waitForBlocks) + await token.scheduleAfter(waitForBlocks, {scheduledId}) .transfer(alice, {Substrate: bob.address}); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; await helper.scheduler.cancelScheduled(alice, scheduledId); - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address}); }); - itSub.ifWithPallets('Can cancel a periodic operation (transfer of funds)', [Pallets.Scheduler], async ({helper}) => { + itSub('Can cancel a periodic operation (transfer of funds)', async ({helper}) => { const waitForBlocks = 1; const periodic = { period: 3, repetitions: 2, }; - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = helper.arrange.makeScheduledId(); const amount = 1n * helper.balance.getOneTokenNominal(); const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address); - await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic}) .balance.transferToSubstrate(alice, bob.address, amount); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address); @@ -129,7 +142,7 @@ ); await helper.scheduler.cancelScheduled(alice, scheduledId); - await helper.wait.newBlocks(periodic.period); + await helper.wait.forParachainBlockNumber(executionBlock + periodic.period); const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address); expect(bobsBalanceAfterSecond) @@ -139,8 +152,54 @@ ); }); - itSub.ifWithPallets('Scheduled tasks are transactional', [Pallets.Scheduler, Pallets.TestUtils], async ({helper}) => { - const scheduledId = await helper.arrange.makeScheduledId(); + itSched('scheduler will not insert more tasks than allowed', async (scheduleKind, {helper}) => { + const maxScheduledPerBlock = 50; + let fillScheduledIds = new Array(maxScheduledPerBlock); + let extraScheduledId = undefined; + + if (scheduleKind == 'named') { + const scheduledIds = helper.arrange.makeScheduledIds(maxScheduledPerBlock + 1); + fillScheduledIds = scheduledIds.slice(0, maxScheduledPerBlock); + extraScheduledId = scheduledIds[maxScheduledPerBlock]; + } + + // Since the dev node has Instant Seal, + // we need a larger gap between the current block and the target one. + // + // We will schedule `maxScheduledPerBlock` transaction into the target block, + // so we need at least `maxScheduledPerBlock`-wide gap. + // We add some additional blocks to this gap to mitigate possible PolkadotJS delays. + const waitForBlocks = await helper.arrange.isDevNode() ? maxScheduledPerBlock + 5 : 5; + + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks; + + const amount = 1n * helper.balance.getOneTokenNominal(); + + const balanceBefore = await helper.balance.getSubstrate(bob.address); + + // Fill the target block + for (let i = 0; i < maxScheduledPerBlock; i++) { + await helper.scheduler.scheduleAt(executionBlock, {scheduledId: fillScheduledIds[i]}) + .balance.transferToSubstrate(superuser, bob.address, amount); + } + + // Try to schedule a task into a full block + await expect(helper.scheduler.scheduleAt(executionBlock, {scheduledId: extraScheduledId}) + .balance.transferToSubstrate(superuser, bob.address, amount)) + .to.be.rejectedWith(/scheduler\.AgendaIsExhausted/); + + await helper.wait.forParachainBlockNumber(executionBlock); + + const balanceAfter = await helper.balance.getSubstrate(bob.address); + + expect(balanceAfter > balanceBefore).to.be.true; + + const diff = balanceAfter - balanceBefore; + expect(diff).to.be.equal(amount * BigInt(maxScheduledPerBlock)); + }); + + itSched.ifWithPallets('Scheduled tasks are transactional', [Pallets.TestUtils], async (scheduleKind, {helper}) => { + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; const waitForBlocks = 4; const initTestVal = 42; @@ -148,18 +207,19 @@ await helper.testUtils.setTestValue(alice, initTestVal); - await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks) + await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId}) .testUtils.setTestValueAndRollback(alice, changedTestVal); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); const testVal = await helper.testUtils.testValue(); expect(testVal, 'The test value should NOT be commited') .to.be.equal(initTestVal); }); - itSub.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.Scheduler, Pallets.TestUtils], async function({helper}) { - const scheduledId = await helper.arrange.makeScheduledId(); + itSched.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.TestUtils], async function(scheduleKind, {helper}) { + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; const waitForBlocks = 4; const periodic = { period: 2, @@ -172,15 +232,14 @@ const expectedScheduledFee = (await helper.getPaymentInfo(alice, dummyTx, scheduledLen)) .partialFee.toBigInt(); - await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic}) .testUtils.justTakeFee(alice); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - await helper.wait.newBlocks(1); - const aliceInitBalance = await helper.balance.getSubstrate(alice.address); let diff; - await helper.wait.newBlocks(waitForBlocks); + await helper.wait.forParachainBlockNumber(executionBlock); const aliceBalanceAfterFirst = await helper.balance.getSubstrate(alice.address); expect( @@ -194,7 +253,7 @@ 'Scheduled task should take the right amount of fees', ); - await helper.wait.newBlocks(periodic.period); + await helper.wait.forParachainBlockNumber(executionBlock + periodic.period); const aliceBalanceAfterSecond = await helper.balance.getSubstrate(alice.address); expect( @@ -211,7 +270,7 @@ // Check if we can cancel a scheduled periodic operation // in the same block in which it is running - itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.Scheduler, Pallets.TestUtils], async ({helper}) => { + itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.TestUtils], async ({helper}) => { const currentBlockNumber = await helper.chain.getLatestBlockNumber(); const blocksBeforeExecution = 10; const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution; @@ -219,7 +278,7 @@ const [ scheduledId, scheduledCancelId, - ] = await helper.arrange.makeScheduledIds(2); + ] = helper.arrange.makeScheduledIds(2); const periodic = { period: 5, @@ -232,14 +291,14 @@ await helper.testUtils.setTestValue(alice, initTestVal); - await helper.scheduler.scheduleAt(scheduledId, firstExecutionBlockNumber, {periodic}) + await helper.scheduler.scheduleAt(firstExecutionBlockNumber, {scheduledId, periodic}) .testUtils.incTestValue(alice); // Cancel the inc tx after 2 executions // *in the same block* in which the second execution is scheduled await helper.scheduler.scheduleAt( - scheduledCancelId, firstExecutionBlockNumber + periodic.period, + {scheduledId: scheduledCancelId}, ).scheduler.cancelScheduled(alice, scheduledId); await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber); @@ -261,8 +320,8 @@ } }); - itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.Scheduler, Pallets.TestUtils], async ({helper}) => { - const scheduledId = await helper.arrange.makeScheduledId(); + itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.TestUtils], async ({helper}) => { + const scheduledId = helper.arrange.makeScheduledId(); const waitForBlocks = 4; const periodic = { period: 2, @@ -274,47 +333,49 @@ await helper.testUtils.setTestValue(alice, initTestVal); - await helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {periodic}) + await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic}) .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); // execution #0 expect(await helper.testUtils.testValue()) .to.be.equal(initTestVal + 1); - await helper.wait.newBlocks(periodic.period); + await helper.wait.forParachainBlockNumber(executionBlock + periodic.period); // execution #1 expect(await helper.testUtils.testValue()) .to.be.equal(initTestVal + 2); - await helper.wait.newBlocks(periodic.period); + await helper.wait.forParachainBlockNumber(executionBlock + 2 * periodic.period); // expect(await helper.testUtils.testValue()) .to.be.equal(initTestVal + 2); }); - itSub.ifWithPallets('Root can cancel any scheduled operation', [Pallets.Scheduler], async ({helper}) => { + itSub('Root can cancel any scheduled operation', async ({helper}) => { const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'}); const token = await collection.mintToken(bob); - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = helper.arrange.makeScheduledId(); const waitForBlocks = 4; - await token.scheduleAfter(scheduledId, waitForBlocks) + await token.scheduleAfter(waitForBlocks, {scheduledId}) .transfer(bob, {Substrate: alice.address}); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - await helper.getSudo().scheduler.cancelScheduled(alice, scheduledId); + await helper.getSudo().scheduler.cancelScheduled(superuser, scheduledId); - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); }); - itSub.ifWithPallets('Root can set prioritized scheduled operation', [Pallets.Scheduler], async ({helper}) => { - const scheduledId = await helper.arrange.makeScheduledId(); + itSched('Root can set prioritized scheduled operation', async (scheduleKind, {helper}) => { + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; const waitForBlocks = 4; const amount = 42n * helper.balance.getOneTokenNominal(); @@ -322,10 +383,11 @@ const balanceBefore = await helper.balance.getSubstrate(charlie.address); await helper.getSudo() - .scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42}) - .balance.forceTransferToSubstrate(alice, bob.address, charlie.address, amount); + .scheduler.scheduleAfter(waitForBlocks, {scheduledId, priority: 42}) + .balance.forceTransferToSubstrate(superuser, bob.address, charlie.address, amount); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); const balanceAfter = await helper.balance.getSubstrate(charlie.address); @@ -335,18 +397,19 @@ expect(diff).to.be.equal(amount); }); - itSub.ifWithPallets("Root can change scheduled operation's priority", [Pallets.Scheduler], async ({helper}) => { + itSub("Root can change scheduled operation's priority", async ({helper}) => { const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'}); const token = await collection.mintToken(bob); - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = helper.arrange.makeScheduledId(); const waitForBlocks = 6; - await token.scheduleAfter(scheduledId, waitForBlocks) + await token.scheduleAfter(waitForBlocks, {scheduledId}) .transfer(bob, {Substrate: alice.address}); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; const priority = 112; - await helper.getSudo().scheduler.changePriority(alice, scheduledId, priority); + await helper.getSudo().scheduler.changePriority(superuser, scheduledId, priority); const priorityChanged = await helper.wait.event( waitForBlocks, @@ -355,14 +418,19 @@ ); expect(priorityChanged !== null).to.be.true; - expect(priorityChanged!.event.data[2].toString()).to.be.equal(priority.toString()); + + const [blockNumber, index] = priorityChanged!.event.data[0].toJSON() as any[]; + expect(blockNumber).to.be.equal(executionBlock); + expect(index).to.be.equal(0); + + expect(priorityChanged!.event.data[1].toString()).to.be.equal(priority.toString()); }); - itSub.ifWithPallets('Prioritized operations executes in valid order', [Pallets.Scheduler], async ({helper}) => { + itSub('Prioritized operations execute in valid order', async ({helper}) => { const [ scheduledFirstId, scheduledSecondId, - ] = await helper.arrange.makeScheduledIds(2); + ] = helper.arrange.makeScheduledIds(2); const currentBlockNumber = await helper.chain.getLatestBlockNumber(); const blocksBeforeExecution = 6; @@ -379,19 +447,25 @@ const amount = 1n * helper.balance.getOneTokenNominal(); // Scheduler a task with a lower priority first, then with a higher priority - await helper.getSudo().scheduler.scheduleAt(scheduledFirstId, firstExecutionBlockNumber, {priority: prioLow, periodic}) - .balance.forceTransferToSubstrate(alice, alice.address, bob.address, amount); + await helper.getSudo().scheduler.scheduleAt(firstExecutionBlockNumber, { + scheduledId: scheduledFirstId, + priority: prioLow, + periodic, + }).balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount); - await helper.getSudo().scheduler.scheduleAt(scheduledSecondId, firstExecutionBlockNumber, {priority: prioHigh, periodic}) - .balance.forceTransferToSubstrate(alice, alice.address, bob.address, amount); + await helper.getSudo().scheduler.scheduleAt(firstExecutionBlockNumber, { + scheduledId: scheduledSecondId, + priority: prioHigh, + periodic, + }).balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount); const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched'); await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber); // Flip priorities - await helper.getSudo().scheduler.changePriority(alice, scheduledFirstId, prioHigh); - await helper.getSudo().scheduler.changePriority(alice, scheduledSecondId, prioLow); + await helper.getSudo().scheduler.changePriority(superuser, scheduledFirstId, prioHigh); + await helper.getSudo().scheduler.changePriority(superuser, scheduledSecondId, prioLow); await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period); @@ -409,38 +483,123 @@ expect(secondExecuctionIds[0]).to.be.equal(scheduledFirstId); expect(secondExecuctionIds[1]).to.be.equal(scheduledSecondId); }); + + itSched('Periodic operations always can be rescheduled', async (scheduleKind, {helper}) => { + const maxScheduledPerBlock = 50; + const numFilledBlocks = 3; + const ids = helper.arrange.makeScheduledIds(numFilledBlocks * maxScheduledPerBlock + 1); + const periodicId = scheduleKind == 'named' ? ids[0] : undefined; + const fillIds = ids.slice(1); + + const fillScheduleFn = scheduleKind == 'named' ? 'scheduleNamed' : 'schedule'; + + const initTestVal = 0; + const firstExecTestVal = 1; + const secondExecTestVal = 2; + await helper.testUtils.setTestValue(alice, initTestVal); + + const currentBlockNumber = await helper.chain.getLatestBlockNumber(); + const blocksBeforeExecution = 8; + const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution; + + const period = 5; + + const periodic = { + period, + repetitions: 2, + }; + + // Fill `numFilledBlocks` blocks beginning from the block in which the second execution should occur + const txs = []; + for (let offset = 0; offset < numFilledBlocks; offset ++) { + for (let i = 0; i < maxScheduledPerBlock; i++) { + + const scheduledTx = helper.constructApiCall('api.tx.balances.transfer', [bob.address, 1n]); + + const when = firstExecutionBlockNumber + period + offset; + const mandatoryArgs = [when, null, null, scheduledTx]; + const scheduleArgs = scheduleKind == 'named' + ? [fillIds[i + offset * maxScheduledPerBlock], ...mandatoryArgs] + : mandatoryArgs; + + const tx = helper.constructApiCall(`api.tx.scheduler.${fillScheduleFn}`, scheduleArgs); + + txs.push(tx); + } + } + await helper.executeExtrinsic(alice, 'api.tx.testUtils.batchAll', [txs], true); + + await helper.scheduler.scheduleAt(firstExecutionBlockNumber, { + scheduledId: periodicId, + periodic, + }).testUtils.incTestValue(alice); + + await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber); + expect(await helper.testUtils.testValue()).to.be.equal(firstExecTestVal); + + await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + period + numFilledBlocks); + + // The periodic operation should be postponed by `numFilledBlocks` + for (let i = 0; i < numFilledBlocks; i++) { + expect(await helper.testUtils.testValue(firstExecutionBlockNumber + period + i)).to.be.equal(firstExecTestVal); + } + + // After the `numFilledBlocks` the periodic operation will eventually be executed + expect(await helper.testUtils.testValue()).to.be.equal(secondExecTestVal); + }); + + itSched('scheduled operations does not change nonce', async (scheduleKind, {helper}) => { + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; + const blocksBeforeExecution = 4; + + await helper.scheduler + .scheduleAfter(blocksBeforeExecution, {scheduledId}) + .balance.transferToSubstrate(alice, bob.address, 1n); + const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1; + + const initNonce = await helper.chain.getNonce(alice.address); + + await helper.wait.forParachainBlockNumber(executionBlock); + + const finalNonce = await helper.chain.getNonce(alice.address); + + expect(initNonce).to.be.equal(finalNonce); + }); }); describe('Negative Test: Scheduling', () => { let alice: IKeyringPair; let bob: IKeyringPair; - before(async () => { - await usingPlaygrounds(async (helper, privateKeyWrapper) => { - alice = await privateKeyWrapper('//Alice'); - bob = await privateKeyWrapper('//Bob'); + before(async function() { + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.Scheduler]); + + const donor = await privateKey({filename: __filename}); + [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor); await helper.testUtils.enable(); }); }); - itSub.ifWithPallets("Can't overwrite a scheduled ID", [Pallets.Scheduler], async ({helper}) => { + itSub("Can't overwrite a scheduled ID", async ({helper}) => { const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); const token = await collection.mintToken(alice); - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = helper.arrange.makeScheduledId(); const waitForBlocks = 4; - await token.scheduleAfter(scheduledId, waitForBlocks) + await token.scheduleAfter(waitForBlocks, {scheduledId}) .transfer(alice, {Substrate: bob.address}); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; - const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks); + const scheduled = helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId}); await expect(scheduled.balance.transferToSubstrate(alice, bob.address, 1n * helper.balance.getOneTokenNominal())) .to.be.rejectedWith(/scheduler\.FailedToSchedule/); const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address); - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); const bobsBalanceAfter = await helper.balance.getSubstrate(bob.address); @@ -448,58 +607,61 @@ expect(bobsBalanceBefore).to.be.equal(bobsBalanceAfter); }); - itSub.ifWithPallets("Can't cancel an operation which is not scheduled", [Pallets.Scheduler], async ({helper}) => { - const scheduledId = await helper.arrange.makeScheduledId(); + itSub("Can't cancel an operation which is not scheduled", async ({helper}) => { + const scheduledId = helper.arrange.makeScheduledId(); await expect(helper.scheduler.cancelScheduled(alice, scheduledId)) .to.be.rejectedWith(/scheduler\.NotFound/); }); - itSub.ifWithPallets("Can't cancel a non-owned scheduled operation", [Pallets.Scheduler], async ({helper}) => { + itSub("Can't cancel a non-owned scheduled operation", async ({helper}) => { const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'}); const token = await collection.mintToken(alice); - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = helper.arrange.makeScheduledId(); const waitForBlocks = 4; - await token.scheduleAfter(scheduledId, waitForBlocks) + await token.scheduleAfter(waitForBlocks, {scheduledId}) .transfer(alice, {Substrate: bob.address}); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; await expect(helper.scheduler.cancelScheduled(bob, scheduledId)) .to.be.rejectedWith(/BadOrigin/); - await helper.wait.newBlocks(waitForBlocks + 1); + await helper.wait.forParachainBlockNumber(executionBlock); expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address}); }); - itSub.ifWithPallets("Regular user can't set prioritized scheduled operation", [Pallets.Scheduler], async ({helper}) => { - const scheduledId = await helper.arrange.makeScheduledId(); + itSched("Regular user can't set prioritized scheduled operation", async (scheduleKind, {helper}) => { + const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined; const waitForBlocks = 4; const amount = 42n * helper.balance.getOneTokenNominal(); const balanceBefore = await helper.balance.getSubstrate(bob.address); - const scheduled = helper.scheduler.scheduleAfter(scheduledId, waitForBlocks, {priority: 42}); + const scheduled = helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, priority: 42}); await expect(scheduled.balance.transferToSubstrate(alice, bob.address, amount)) .to.be.rejectedWith(/BadOrigin/); - await helper.wait.newBlocks(waitForBlocks + 1); + const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1; + + await helper.wait.forParachainBlockNumber(executionBlock); const balanceAfter = await helper.balance.getSubstrate(bob.address); expect(balanceAfter).to.be.equal(balanceBefore); }); - itSub.ifWithPallets("Regular user can't change scheduled operation's priority", [Pallets.Scheduler], async ({helper}) => { + itSub("Regular user can't change scheduled operation's priority", async ({helper}) => { const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'}); const token = await collection.mintToken(bob); - const scheduledId = await helper.arrange.makeScheduledId(); + const scheduledId = helper.arrange.makeScheduledId(); const waitForBlocks = 4; - await token.scheduleAfter(scheduledId, waitForBlocks) + await token.scheduleAfter(waitForBlocks, {scheduledId}) .transfer(bob, {Substrate: alice.address}); const priority = 112; @@ -522,9 +684,9 @@ // let bob: IKeyringPair; // before(async() => { - // await usingApi(async (_, privateKeyWrapper) => { - // alice = privateKeyWrapper('//Alice'); - // bob = privateKeyWrapper('//Bob'); + // await usingApi(async (_, privateKey) => { + // alice = privateKey('//Alice'); + // bob = privateKey('//Bob'); // }); // }); @@ -550,9 +712,9 @@ }); it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => { - // await usingApi(async (api, privateKeyWrapper) => { + // await usingApi(async (api, privateKey) => { // // Find an empty, unused account - // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + // const zeroBalance = await findUnusedAddress(api, privateKey); // const collectionId = await createCollectionExpectSuccess(); @@ -591,8 +753,8 @@ it('Sponsor going bankrupt does not impact a scheduled transaction', async () => { // const collectionId = await createCollectionExpectSuccess(); - // await usingApi(async (api, privateKeyWrapper) => { - // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + // await usingApi(async (api, privateKey) => { + // const zeroBalance = await findUnusedAddress(api, privateKey); // const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); // await submitTransactionAsync(alice, balanceTx); @@ -622,8 +784,8 @@ // await setCollectionSponsorExpectSuccess(collectionId, bob.address); // await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); - // await usingApi(async (api, privateKeyWrapper) => { - // const zeroBalance = await findUnusedAddress(api, privateKeyWrapper); + // await usingApi(async (api, privateKey) => { + // const zeroBalance = await findUnusedAddress(api, privateKey); // await enablePublicMintingExpectSuccess(alice, collectionId); // await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); --- a/tests/src/util/index.ts +++ b/tests/src/util/index.ts @@ -130,6 +130,24 @@ itSubIfWithPallet.skip = (name: string, required: string[], cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise }) => any) => itSubIfWithPallet(name, required, cb, {skip: true}); itSub.ifWithPallets = itSubIfWithPallet; +export type SchedKind = 'anon' | 'named'; + +export function itSched( + name: string, + cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise }) => any, + opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {}, +) { + itSub(name + ' (anonymous scheduling)', (apis) => cb('anon', apis), opts); + itSub(name + ' (named scheduling)', (apis) => cb('named', apis), opts); +} +itSched.only = (name: string, cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise }) => any) => itSched(name, cb, {only: true}); +itSched.skip = (name: string, cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise }) => any) => itSched(name, cb, {skip: true}); +itSched.ifWithPallets = itSchedIfWithPallets; + +function itSchedIfWithPallets(name: string, required: string[], cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {}) { + return itSched(name, cb, {requiredPallets: required, ...opts}); +} + export function describeXCM(title: string, fn: (this: Mocha.Suite) => void, opts: {skip?: boolean} = {}) { (process.env.RUN_XCM_TESTS && !opts.skip ? describe --- a/tests/src/util/playgrounds/types.ts +++ b/tests/src/util/playgrounds/types.ts @@ -172,6 +172,7 @@ } export interface ISchedulerOptions { + scheduledId?: string, priority?: number, periodic?: { period: number, --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -308,11 +308,9 @@ return encodeAddress(address); } - async makeScheduledIds(num: number): Promise { - await this.helper.wait.noScheduledTasks(); - + makeScheduledIds(num: number): string[] { function makeId(slider: number) { - const scheduledIdSize = 32; + const scheduledIdSize = 64; const hexId = slider.toString(16); const prefixSize = scheduledIdSize - hexId.length; @@ -330,8 +328,8 @@ return ids; } - async makeScheduledId(): Promise { - return (await this.makeScheduledIds(1))[0]; + makeScheduledId(): string { + return (this.makeScheduledIds(1))[0]; } async captureEvents(eventSection: string, eventMethod: string): Promise { @@ -537,8 +535,12 @@ await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true); } - async testValue() { - return (await this.helper.callRpc('api.query.testUtils.testValue', [])).toNumber(); + async testValue(blockIdx?: number) { + const api = blockIdx + ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx])) + : this.helper.getApi(); + + return (await api.query.testUtils.testValue()).toJSON(); } async justTakeFee(signer: TSigner) { --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -2560,24 +2560,21 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - return this.schedule('scheduleNamed', scheduledId, executionBlockNumber, options); + return this.schedule('schedule', executionBlockNumber, options); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - return this.schedule('scheduleNamedAfter', scheduledId, blocksBeforeExecution, options); + return this.schedule('scheduleAfter', blocksBeforeExecution, options); } schedule( - scheduleFn: 'scheduleNamed' | 'scheduleNamedAfter', - scheduledId: string, + scheduleFn: 'schedule' | 'scheduleAfter', blocksNum: number, options: ISchedulerOptions = {}, ) { @@ -2585,7 +2582,6 @@ const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase); return this.helper.clone(ScheduledHelperType, { scheduleFn, - scheduledId, blocksNum, options, }) as T; @@ -2874,16 +2870,14 @@ // eslint-disable-next-line @typescript-eslint/naming-convention function ScheduledUniqueHelper(Base: T) { return class extends Base { - scheduleFn: 'scheduleNamed' | 'scheduleNamedAfter'; - scheduledId: string; + scheduleFn: 'schedule' | 'scheduleAfter'; blocksNum: number; options: ISchedulerOptions; constructor(...args: any[]) { const logger = args[0] as ILogger; const options = args[1] as { - scheduleFn: 'scheduleNamed' | 'scheduleNamedAfter', - scheduledId: string, + scheduleFn: 'schedule' | 'scheduleAfter', blocksNum: number, options: ISchedulerOptions }; @@ -2891,25 +2885,42 @@ super(logger); this.scheduleFn = options.scheduleFn; - this.scheduledId = options.scheduledId; this.blocksNum = options.blocksNum; this.options = options.options; } executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise { const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams); - const extrinsic = 'api.tx.scheduler.' + this.scheduleFn; + + const mandatorySchedArgs = [ + this.blocksNum, + this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null, + this.options.priority ?? null, + scheduledTx, + ]; + + let schedArgs; + let scheduleFn; + + if (this.options.scheduledId) { + schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs]; + if (this.scheduleFn == 'schedule') { + scheduleFn = 'scheduleNamed'; + } else if (this.scheduleFn == 'scheduleAfter') { + scheduleFn = 'scheduleNamedAfter'; + } + } else { + schedArgs = mandatorySchedArgs; + scheduleFn = this.scheduleFn; + } + + const extrinsic = 'api.tx.scheduler.' + scheduleFn; + return super.executeExtrinsic( sender, extrinsic, - [ - this.scheduledId, - this.blocksNum, - this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null, - this.options.priority ?? null, - {Value: scheduledTx}, - ], + schedArgs, expectSuccess, ); } @@ -3046,20 +3057,18 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAt(scheduledId, executionBlockNumber, options); + const scheduledHelper = this.helper.scheduler.scheduleAt(executionBlockNumber, options); return new UniqueBaseCollection(this.collectionId, scheduledHelper); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAfter(scheduledId, blocksBeforeExecution, options); + const scheduledHelper = this.helper.scheduler.scheduleAfter(blocksBeforeExecution, options); return new UniqueBaseCollection(this.collectionId, scheduledHelper); } @@ -3155,20 +3164,18 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAt(scheduledId, executionBlockNumber, options); + const scheduledHelper = this.helper.scheduler.scheduleAt(executionBlockNumber, options); return new UniqueNFTCollection(this.collectionId, scheduledHelper); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAfter(scheduledId, blocksBeforeExecution, options); + const scheduledHelper = this.helper.scheduler.scheduleAfter(blocksBeforeExecution, options); return new UniqueNFTCollection(this.collectionId, scheduledHelper); } @@ -3260,20 +3267,18 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAt(scheduledId, executionBlockNumber, options); + const scheduledHelper = this.helper.scheduler.scheduleAt(executionBlockNumber, options); return new UniqueRFTCollection(this.collectionId, scheduledHelper); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAfter(scheduledId, blocksBeforeExecution, options); + const scheduledHelper = this.helper.scheduler.scheduleAfter(blocksBeforeExecution, options); return new UniqueRFTCollection(this.collectionId, scheduledHelper); } @@ -3329,20 +3334,18 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAt(scheduledId, executionBlockNumber, options); + const scheduledHelper = this.helper.scheduler.scheduleAt(executionBlockNumber, options); return new UniqueFTCollection(this.collectionId, scheduledHelper); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - const scheduledHelper = this.helper.scheduler.scheduleAfter(scheduledId, blocksBeforeExecution, options); + const scheduledHelper = this.helper.scheduler.scheduleAfter(blocksBeforeExecution, options); return new UniqueFTCollection(this.collectionId, scheduledHelper); } @@ -3388,20 +3391,18 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - const scheduledCollection = this.collection.scheduleAt(scheduledId, executionBlockNumber, options); + const scheduledCollection = this.collection.scheduleAt(executionBlockNumber, options); return new UniqueBaseToken(this.tokenId, scheduledCollection); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - const scheduledCollection = this.collection.scheduleAfter(scheduledId, blocksBeforeExecution, options); + const scheduledCollection = this.collection.scheduleAfter(blocksBeforeExecution, options); return new UniqueBaseToken(this.tokenId, scheduledCollection); } @@ -3468,20 +3469,18 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - const scheduledCollection = this.collection.scheduleAt(scheduledId, executionBlockNumber, options); + const scheduledCollection = this.collection.scheduleAt(executionBlockNumber, options); return new UniqueNFToken(this.tokenId, scheduledCollection); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - const scheduledCollection = this.collection.scheduleAfter(scheduledId, blocksBeforeExecution, options); + const scheduledCollection = this.collection.scheduleAfter(blocksBeforeExecution, options); return new UniqueNFToken(this.tokenId, scheduledCollection); } @@ -3543,20 +3542,18 @@ } scheduleAt( - scheduledId: string, executionBlockNumber: number, options: ISchedulerOptions = {}, ) { - const scheduledCollection = this.collection.scheduleAt(scheduledId, executionBlockNumber, options); + const scheduledCollection = this.collection.scheduleAt(executionBlockNumber, options); return new UniqueRFToken(this.tokenId, scheduledCollection); } scheduleAfter( - scheduledId: string, blocksBeforeExecution: number, options: ISchedulerOptions = {}, ) { - const scheduledCollection = this.collection.scheduleAfter(scheduledId, blocksBeforeExecution, options); + const scheduledCollection = this.collection.scheduleAfter(blocksBeforeExecution, options); return new UniqueRFToken(this.tokenId, scheduledCollection); }