--- a/pallets/scheduler/Cargo.toml +++ b/pallets/scheduler/Cargo.toml @@ -15,11 +15,13 @@ 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.22" } frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.22" } sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.22" } sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.22" } sp-io = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.22" } +sp-core = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.22' } frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.22" } up-sponsorship = { version = "0.1.0", default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.22" } @@ -40,6 +42,7 @@ "up-sponsorship/std", "sp-io/std", "sp-std/std", + "sp-core/std", "log/std", ] runtime-benchmarks = [ --- a/pallets/scheduler/src/lib.rs +++ b/pallets/scheduler/src/lib.rs @@ -1,23 +1,6 @@ -// 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-2021 Parity Technologies (UK) Ltd. +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,16 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! # Scheduler -//! A module for scheduling dispatches. -//! -//! - [`Config`] -//! - [`Call`] -//! - [`Module`] -//! -//! ## Overview +//! # Schedulerdo_reschedule //! -//! This module exposes capabilities for scheduling dispatches to occur at a +//! 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. //! @@ -57,106 +33,59 @@ //! //! ### 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. +//! * `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)] -#![allow(clippy::type_complexity, clippy::boxed_local, clippy::unused_unit)] +#[cfg(feature = "runtime-benchmarks")] mod benchmarking; + pub mod weights; -use sp_std::{prelude::*, marker::PhantomData, borrow::Borrow}; -use codec::{Encode, Decode, Codec}; +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::{ - RuntimeDebug, - traits::{Zero, One, BadOrigin, Saturating}, + traits::{BadOrigin, One, Saturating, Zero}, + RuntimeDebug, DispatchErrorWithPostInfo, }; +use sp_std::{borrow::Borrow, cmp::Ordering, marker::PhantomData, prelude::*}; + use frame_support::{ - decl_module, decl_storage, decl_event, decl_error, - dispatch::{Dispatchable, DispatchError, DispatchResult, Parameter}, + dispatch::{DispatchError, DispatchResult, Dispatchable, Parameter}, traits::{ - Get, - schedule::{self, DispatchTime}, - OriginTrait, EnsureOrigin, IsType, + schedule::{self, DispatchTime, MaybeHashed}, + NamedReservableCurrency, EnsureOrigin, Get, IsType, OriginTrait, PrivilegeCmp, + StorageVersion, }, weights::{GetDispatchInfo, Weight}, }; -use frame_system::{self as system, ensure_signed}; -pub use weights::WeightInfo; -use up_sponsorship::SponsorshipHandler; -use scale_info::TypeInfo; -/// Our pallet's configuration trait. All our types and constants go in here. If the -/// pallet is dependent on specific other pallets, then their configuration traits -/// should be added to our implied traits list. -/// -/// `system::Config` should always be included in our implied traits. -/// // -pub trait Config: system::Config { - /// The overarching event type. - type Event: From> + Into<::Event>; - - /// The aggregated origin which the dispatch will take. - type Origin: OriginTrait - + From - + IsType<::Origin>; - - /// The caller origin, overarching type of all pallets origins. - type PalletsOrigin: From> + Codec + TypeInfo + Clone + Eq; - - /// The aggregated call type. - type Call: Parameter - + Dispatchable::Origin> - + GetDispatchInfo - + From>; - - /// The maximum weight that may be scheduled per block for any dispatchables of less priority - /// than `schedule::HARD_DEADLINE`. - type MaximumWeight: Get; - - /// Required origin to schedule or cancel calls. - type ScheduleOrigin: EnsureOrigin<::Origin>; - - /// The maximum number of scheduled calls in the queue for a single block. - /// Not strictly enforced, but used for weight estimation. - type MaxScheduledPerBlock: Get; - - /// Sponsoring function - type SponsorshipHandler: SponsorshipHandler::Call>; - - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; -} +pub use weights::WeightInfo; -// pub type SelfWeightInfo = ::WeightInfo; - /// 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); +pub const MAX_TASK_ID_LENGTH_IN_BYTES: u8 = 16; -#[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] -#[derive(Clone, RuntimeDebug, Encode, Decode)] -struct ScheduledV1 { - maybe_id: Option>, - priority: schedule::Priority, - call: Call, - maybe_periodic: Option>, -} +type ScheduledId = [u8; MAX_TASK_ID_LENGTH_IN_BYTES as usize]; +pub type CallOrHashOf = MaybeHashed<::Call, ::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 ScheduledV2 { +pub struct ScheduledV3 { /// The unique identity for this task, if there is one. - maybe_id: Option>, + maybe_id: Option, /// This task's priority. priority: schedule::Priority, /// The call to be dispatched. @@ -168,63 +97,213 @@ _phantom: PhantomData, } +pub type ScheduledV3Of = ScheduledV3< + CallOrHashOf, + ::BlockNumber, + ::PalletsOrigin, + ::AccountId, +>; + +pub type ScheduledOf = ScheduledV3Of; + /// The current version of Scheduled struct. pub type Scheduled = - ScheduledV2; + ScheduledV3; -// A value placed in storage that represents the current version of the Scheduler storage. -// This value is used by the `on_runtime_upgrade` logic to determine whether we run -// storage migration logic. -#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)] -enum Releases { - V1, - V2, +#[cfg(feature = "runtime-benchmarks")] +mod preimage_provider { + use frame_support::traits::PreimageRecipient; + pub trait PreimageProviderAndMaybeRecipient: PreimageRecipient {} + impl> PreimageProviderAndMaybeRecipient for T {} } -impl Default for Releases { - fn default() -> Self { - Releases::V1 - } +#[cfg(not(feature = "runtime-benchmarks"))] +mod preimage_provider { + use frame_support::traits::PreimageProvider; + pub trait PreimageProviderAndMaybeRecipient: PreimageProvider {} + impl> PreimageProviderAndMaybeRecipient for T {} } -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] -pub struct CallSpec { - module: u32, - method: u32, +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(2) - Self::on_initialize_periodic_named(1) + } + (false, false, Some(true)) => { + Self::on_initialize_resolved(2) - Self::on_initialize_resolved(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 {} -decl_storage! { - trait Store for Module as Scheduler { - /// Items to be executed, indexed by the block number that they should be executed on. - pub Agenda: map hasher(twox_64_concat) T::BlockNumber - => Vec::Call, T::BlockNumber, T::PalletsOrigin, T::AccountId>>>; +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + dispatch::PostDispatchInfo, + pallet_prelude::*, + traits::{schedule::LookupError, 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 Event: From> + IsType<::Event>; + + /// The aggregated origin which the dispatch will take. + type Origin: OriginTrait + + From + + IsType<::Origin>; + + /// The caller origin, overarching type of all pallets origins. + type PalletsOrigin: From> + Codec + Clone + Eq + TypeInfo; + + type Currency: NamedReservableCurrency; + + /// The aggregated call type. + type Call: Parameter + + Dispatchable::Origin, PostInfo = PostDispatchInfo> + + GetDispatchInfo + + From>; - pub SpecAgenda: map hasher(twox_64_concat) T::BlockNumber - => Vec>; + /// The maximum weight that may be scheduled per block for any dispatchables of less + /// priority than `schedule::HARD_DEADLINE`. + #[pallet::constant] + type MaximumWeight: Get; - /// Lookup from identity to the block number and index of the task. - Lookup: map hasher(twox_64_concat) Vec => Option>; + /// Required origin to schedule or cancel calls. + type ScheduleOrigin: EnsureOrigin<::Origin>; - /// Storage version of the pallet. + /// 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. /// - /// New networks start with last version. - StorageVersion build(|_| Releases::V2): Releases; + /// 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; } -} -decl_event!( - pub enum Event where ::BlockNumber { - /// Scheduled some task. \[when, index\] - Scheduled(BlockNumber, u32), - /// Canceled some task. \[when, index\] - Canceled(BlockNumber, u32), - /// Dispatched some task. \[task, id, result\] - Dispatched(TaskAddress, Option>, DispatchResult), + /// A Scheduler-Runtime interface for finer payment handling. + pub trait DispatchCall { + fn reserve_balance( + id: ScheduledId, + sponsor: ::AccountId, + call: ::Call, + count: u32, + ) -> Result<(), DispatchError>; + + fn pay_for_call( + id: ScheduledId, + sponsor: ::AccountId, + call: ::Call, + ) -> Result; + + /// Resolve the call dispatch, including any post-dispatch operations. + fn dispatch_call( + signer: T::AccountId, + function: ::Call, + ) -> Result< + Result>, + TransactionValidityError, + >; + + fn cancel_reserve( + id: ScheduledId, + sponsor: ::AccountId, + ) -> Result; } -); -decl_error! { - pub enum Error for Module { + /// 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 }, + /// 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. @@ -234,249 +313,268 @@ /// Reschedule failed because it does not change scheduled time. RescheduleNoChange, } -} -decl_module! { - /// Scheduler module declaration. - pub struct Module for enum Call - where - origin: ::Origin - { - type Error = Error; - fn deposit_event() = default; + #[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::>(); - /// Anonymously schedule a task. - /// - /// # - /// - S = Number of already scheduled calls - /// - Base Weight: 22.29 + .126 * S µs - /// - DB Weight: - /// - Read: Agenda - /// - Write: Agenda - /// - Will use base weight of 25 which should be good for up to 30 scheduled calls - /// # - #[weight = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get())] - fn schedule(origin, - when: T::BlockNumber, - maybe_periodic: Option>, - priority: schedule::Priority, - call: Box<::Call>, - ) - { - let origin = ::Origin::from(origin); - Self::do_schedule(DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), *call)?; - } + 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." + ); + } - /// Cancel an anonymously scheduled task. - /// - /// # - /// - S = Number of already scheduled calls - /// - Base Weight: 22.15 + 2.869 * S µs - /// - DB Weight: - /// - Read: Agenda - /// - Write: Agenda, Lookup - /// - Will use base weight of 100 which should be good for up to 30 scheduled calls - /// # - #[weight = ::WeightInfo::cancel(T::MaxScheduledPerBlock::get())] - fn cancel(origin, when: T::BlockNumber, index: u32) { - T::ScheduleOrigin::ensure_origin(origin.clone())?; - let origin = ::Origin::from(origin); - Self::do_cancel(Some(origin.caller().clone()), (when, index))?; + 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 = if let Some(ref id) = s.maybe_id { + Lookup::::remove(id); + true + } else { + false + }; + + 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)); + } + 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 = + <::Origin 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 > 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 sender = ensure_signed( + <::Origin as From>::from(s.origin.clone()) + .into(), + ) + .unwrap(); + + // // if call have id it was be reserved + // if s.maybe_id.is_some() { + // let _ = T::CallExecutor::pay_for_call( + // s.maybe_id.unwrap(), + // sender.clone(), + // call.clone(), + // ); + // } + + let r = T::CallExecutor::dispatch_call(sender, call.clone()); + + 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; + // If scheduled is named, place its information in `Lookup` + if let Some(ref id) = s.maybe_id { + let wake_index = Agenda::::decode_len(wake).unwrap_or(0); + Lookup::::insert(id, (wake, wake_index as u32)); + } + Agenda::::append(wake, Some(s)); + } + } + 0 + //total_weight } + } + #[pallet::call] + impl Pallet { /// Schedule a named task. - /// - /// # - /// - S = Number of already scheduled calls - /// - Base Weight: 29.6 + .159 * S µs - /// - DB Weight: - /// - Read: Agenda, Lookup - /// - Write: Agenda, Lookup - /// - Will use base weight of 35 which should be good for more than 30 scheduled calls - /// # - #[weight = ::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get())] - fn schedule_named(origin, - id: Vec, + #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] + pub fn schedule_named( + origin: OriginFor, + id: ScheduledId, when: T::BlockNumber, maybe_periodic: Option>, priority: schedule::Priority, - call: Box<::Call>, - ) { + call: Box>, + ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_schedule_named( - id, DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), *call + id, + DispatchTime::At(when), + maybe_periodic, + priority, + origin.caller().clone(), + *call, )?; + Ok(()) } /// Cancel a named scheduled task. - /// - /// # - /// - S = Number of already scheduled calls - /// - Base Weight: 24.91 + 2.907 * S µs - /// - DB Weight: - /// - Read: Agenda, Lookup - /// - Write: Agenda, Lookup - /// - Will use base weight of 100 which should be good for up to 30 scheduled calls - /// # - #[weight = ::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get())] - fn cancel_named(origin, id: Vec) { + #[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 = ::Origin::from(origin); Self::do_cancel_named(Some(origin.caller().clone()), id)?; - } - - /// Anonymously schedule a task after a delay. - /// - /// # - /// Same as [`schedule`]. - /// # - #[weight = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get())] - fn schedule_after(origin, - after: T::BlockNumber, - maybe_periodic: Option>, - priority: schedule::Priority, - call: Box<::Call>, - ) { - T::ScheduleOrigin::ensure_origin(origin.clone())?; - let origin = ::Origin::from(origin); - Self::do_schedule( - DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), *call - )?; + Ok(()) } /// Schedule a named task after a delay. /// /// # - /// Same as [`schedule_named`]. + /// Same as [`schedule_named`](Self::schedule_named). /// # - #[weight = ::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get())] - fn schedule_named_after(origin, - id: Vec, + #[pallet::weight(::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))] + pub fn schedule_named_after( + origin: OriginFor, + id: ScheduledId, after: T::BlockNumber, maybe_periodic: Option>, priority: schedule::Priority, - call: Box<::Call>, - ) { + call: Box>, + ) -> DispatchResult { T::ScheduleOrigin::ensure_origin(origin.clone())?; let origin = ::Origin::from(origin); Self::do_schedule_named( - id, DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), *call + id, + DispatchTime::After(after), + maybe_periodic, + priority, + origin.caller().clone(), + *call, )?; + Ok(()) } - - /// Execute the scheduled calls - /// - /// # - /// - S = Number of already scheduled calls - /// - N = Named scheduled calls - /// - P = Periodic Calls - /// - Base Weight: 9.243 + 23.45 * S µs - /// - DB Weight: - /// - Read: Agenda + Lookup * N + Agenda(Future) * P - /// - Write: Agenda + Lookup * N + Agenda(future) * P - /// # - 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)| s.map(|inner| (index as u32, inner))) - .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 base_weight: Weight = T::DbWeight::get().reads_writes(1, 2); // Agenda + Agenda(next) - let mut total_weight: Weight = 0; - queued.into_iter() - .enumerate() - .scan(base_weight, |cumulative_weight, (order, (index, s))| { - *cumulative_weight = cumulative_weight - .saturating_add(s.call.get_dispatch_info().weight); - - let origin = <::Origin as From>::from( - s.origin.clone() - ).into(); - - if ensure_signed(origin).is_ok() { - // AccountData for inner call origin accountdata. - *cumulative_weight = cumulative_weight - .saturating_add(T::DbWeight::get().reads_writes(1, 1)); - } - - if s.maybe_id.is_some() { - // Remove/Modify Lookup - *cumulative_weight = cumulative_weight.saturating_add(T::DbWeight::get().writes(1)); - } - if s.maybe_periodic.is_some() { - // Read/Write Agenda for future block - *cumulative_weight = cumulative_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); - } + } +} - Some((order, index, *cumulative_weight, s)) - }) - .filter_map(|(order, index, cumulative_weight, mut s)| { - // 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 - if s.priority <= schedule::HARD_DEADLINE || cumulative_weight <= limit || order == 0 { +impl Pallet { + #[cfg(feature = "try-runtime")] + pub fn pre_migrate_to_v3() -> Result<(), &'static str> { + Ok(()) + } - let origin = <::Origin as From>::from( - s.origin.clone() - ).into(); - let sender = match ensure_signed(origin) { - Ok(v) => v, - // TODO: Support for unsigned extrinsics? - Err(_) => return Some(Some(s)) - }; - let who_will_pay = T::SponsorshipHandler::get_sponsor(&sender, &s.call).unwrap_or(sender); - let sponsor = T::PalletsOrigin::from(system::RawOrigin::Signed(who_will_pay)); - let r = s.call.clone().dispatch(sponsor.into()); - let maybe_id = s.maybe_id.clone(); - if let Some((period, count)) = s.maybe_periodic { - if count > 1 { - s.maybe_periodic = Some((period, count - 1)); - } else { - s.maybe_periodic = None; - } - let next = now + period; - // If scheduled is named, place it's information in `Lookup` - if let Some(ref id) = s.maybe_id { - let next_index = Agenda::::decode_len(now + period).unwrap_or(0); - Lookup::::insert(id, (next, next_index as u32)); - } - Agenda::::append(next, Some(s)); - } else if let Some(ref id) = s.maybe_id { - Lookup::::remove(id); - } - Self::deposit_event(RawEvent::Dispatched( - (now, index), - maybe_id, - r.map(|_| ()).map_err(|e| e.error) - )); - total_weight = cumulative_weight; - None - } else { - Some(Some(s)) - } - }) - .for_each(|unused| { - let next = now + One::one(); - Agenda::::append(next, unused); - }); + #[cfg(feature = "try-runtime")] + pub fn post_migrate_to_v3() -> Result<(), &'static str> { + use frame_support::dispatch::GetStorageVersion; - total_weight + 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::>(), + ) + }); } -} -impl Module { fn resolve_time(when: DispatchTime) -> Result { let now = frame_system::Pallet::::block_number(); @@ -499,9 +597,10 @@ maybe_periodic: Option>, priority: schedule::Priority, origin: T::PalletsOrigin, - call: ::Call, + call: CallOrHashOf, ) -> Result, DispatchError> { let when = Self::resolve_time(when)?; + call.ensure_requested::(); // sanitize maybe_periodic let maybe_periodic = maybe_periodic @@ -518,14 +617,7 @@ }); Agenda::::append(when, s); let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; - if index > T::MaxScheduledPerBlock::get() { - log::warn!( - target: "runtime::scheduler", - "Warning: There are more items queued in the Scheduler than \ - expected from the runtime configuration. An update might be needed.", - ); - } - Self::deposit_event(RawEvent::Scheduled(when, index)); + Self::deposit_event(Event::Scheduled { when, index }); Ok((when, index)) } @@ -539,7 +631,10 @@ Ok(None), |s| -> Result>, DispatchError> { if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) { - if *o != s.origin { + if matches!( + T::OriginPrivilegeCmp::cmp_privilege(o, &s.origin), + Some(Ordering::Less) | None + ) { return Err(BadOrigin.into()); } }; @@ -548,13 +643,14 @@ ) })?; if let Some(s) = scheduled { + s.call.ensure_unrequested::(); if let Some(id) = s.maybe_id { Lookup::::remove(id); } - Self::deposit_event(RawEvent::Canceled(when, index)); + Self::deposit_event(Event::Canceled { when, index }); Ok(()) } else { - Err(Error::::NotFound.into()) + Err(Error::::NotFound)? } } @@ -576,27 +672,32 @@ })?; let new_index = Agenda::::decode_len(new_time).unwrap_or(1) as u32 - 1; - Self::deposit_event(RawEvent::Canceled(when, index)); - Self::deposit_event(RawEvent::Scheduled(new_time, new_index)); + Self::deposit_event(Event::Canceled { when, index }); + Self::deposit_event(Event::Scheduled { + when: new_time, + index: new_index, + }); Ok((new_time, new_index)) } fn do_schedule_named( - id: Vec, + id: ScheduledId, when: DispatchTime, maybe_periodic: Option>, priority: schedule::Priority, origin: T::PalletsOrigin, - call: ::Call, + call: CallOrHashOf, ) -> Result, DispatchError> { // ensure id it is unique if Lookup::::contains_key(&id) { - return Err(Error::::FailedToSchedule.into()); + 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()) @@ -606,52 +707,74 @@ let s = Scheduled { maybe_id: Some(id.clone()), priority, - call, + call: call.clone(), maybe_periodic, - origin, + 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; - if index > T::MaxScheduledPerBlock::get() { - log::warn!( - target: "runtime::scheduler", - "Warning: There are more items queued in the Scheduler than \ - expected from the runtime configuration. An update might be needed.", - ); - } let address = (when, index); Lookup::::insert(&id, &address); - Self::deposit_event(RawEvent::Scheduled(when, index)); + Self::deposit_event(Event::Scheduled { when, index }); Ok(address) } - fn do_cancel_named(origin: Option, id: Vec) -> DispatchResult { + 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, s.borrow()) { - if *o != s.origin { + 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(RawEvent::Canceled(when, index)); + + Self::deposit_event(Event::Canceled { when, index }); Ok(()) } else { - Err(Error::::NotFound.into()) + Err(Error::::NotFound)? } }) } fn do_reschedule_named( - id: Vec, + id: ScheduledId, new_time: DispatchTime, ) -> Result, DispatchError> { let new_time = Self::resolve_time(new_time)?; @@ -674,8 +797,11 @@ })?; let new_index = Agenda::::decode_len(new_time).unwrap_or(1) as u32 - 1; - Self::deposit_event(RawEvent::Canceled(when, index)); - Self::deposit_event(RawEvent::Scheduled(new_time, new_index)); + Self::deposit_event(Event::Canceled { when, index }); + Self::deposit_event(Event::Scheduled { + when: new_time, + index: new_index, + }); *lookup = Some((new_time, new_index)); @@ -684,161 +810,86 @@ ) } } - -#[cfg(test)] -#[allow(clippy::from_over_into)] -mod tests { - use super::*; - use frame_support::{ - ord_parameter_types, parameter_types, - traits::{Contains, ConstU32, EnsureOneOf}, - weights::constants::RocksDbWeight, - }; - use sp_core::H256; - use sp_runtime::{ - Perbill, - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, - }; - use frame_system::{EnsureRoot, EnsureSignedBy}; - use crate as scheduler; +impl schedule::v2::Anon::Call, T::PalletsOrigin> + for Pallet +{ + type Address = TaskAddress; + type Hash = T::Hash; - #[frame_support::pallet] - pub mod logger { - use super::{OriginCaller, OriginTrait}; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - use std::cell::RefCell; - - thread_local! { - static LOG: RefCell> = RefCell::new(Vec::new()); - } - pub fn log() -> Vec<(OriginCaller, u32)> { - LOG.with(|log| log.borrow().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 Event: From> + IsType<::Event>; - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - Logged(u32, Weight), - } + fn schedule( + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: CallOrHashOf, + ) -> Result { + Self::do_schedule(when, maybe_periodic, priority, origin, call) + } - #[pallet::call] - impl Pallet - where - ::Origin: OriginTrait, - { - #[pallet::weight(*weight)] - pub fn log(origin: OriginFor, i: u32, weight: Weight) -> DispatchResult { - Self::deposit_event(Event::Logged(i, weight)); - LOG.with(|log| { - log.borrow_mut().push((origin.caller().clone(), i)); - }); - Ok(()) - } + fn cancel((when, index): Self::Address) -> Result<(), ()> { + Self::do_cancel(None, (when, index)).map_err(|_| ()) + } - #[pallet::weight(*weight)] - pub fn log_without_filter( - origin: OriginFor, - i: u32, - weight: Weight, - ) -> DispatchResult { - Self::deposit_event(Event::Logged(i, weight)); - LOG.with(|log| { - log.borrow_mut().push((origin.caller().clone(), i)); - }); - Ok(()) - } - } + fn reschedule( + address: Self::Address, + when: DispatchTime, + ) -> Result { + Self::do_reschedule(address, when) } - type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; - type Block = frame_system::mocking::MockBlock; + fn next_dispatch_time((when, index): Self::Address) -> Result { + Agenda::::get(when) + .get(index as usize) + .ok_or(()) + .map(|_| when) + } +} - 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}, - } - ); +impl schedule::v2::Named::Call, T::PalletsOrigin> + for Pallet +{ + type Address = TaskAddress; + type Hash = T::Hash; - // 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: &Call) -> bool { - !matches!(call, Call::Logger(logger::Call::log { .. })) - } + fn schedule_named( + id: Vec, + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: CallOrHashOf, + ) -> Result { + let inner_id: ScheduledId = id + .try_into() + .unwrap_or([0; MAX_TASK_ID_LENGTH_IN_BYTES as usize]); + Self::do_schedule_named(inner_id, when, maybe_periodic, priority, origin, call) + .map_err(|_| ()) } - parameter_types! { - pub const BlockHashCount: u64 = 250; - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max(2_000_000_000_000); + fn cancel_named(id: Vec) -> Result<(), ()> { + let inner_id: ScheduledId = id + .try_into() + .unwrap_or([0; MAX_TASK_ID_LENGTH_IN_BYTES as usize]); + Self::do_cancel_named(None, inner_id).map_err(|_| ()) } - impl system::Config for Test { - type BaseCallFilter = BaseFilter; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = RocksDbWeight; - type Origin = Origin; - type Call = Call; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type Event = Event; - type BlockHashCount = BlockHashCount; - 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 Event = Event; - } - parameter_types! { - pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; - pub const MaxScheduledPerBlock: u32 = 10; - } - ord_parameter_types! { - pub const One: u64 = 1; + + fn reschedule_named( + id: Vec, + when: DispatchTime, + ) -> Result { + let inner_id: ScheduledId = id + .try_into() + .unwrap_or([0; MAX_TASK_ID_LENGTH_IN_BYTES as usize]); + Self::do_reschedule_named(inner_id, when) } - impl Config for Test { - type Event = Event; - type Origin = Origin; - type PalletsOrigin = OriginCaller; - type Call = Call; - type MaximumWeight = MaximumSchedulerWeight; - type ScheduleOrigin = EnsureOneOf, EnsureSignedBy>; - type MaxScheduledPerBlock = MaxScheduledPerBlock; - type WeightInfo = (); - type SponsorshipHandler = (); + fn next_dispatch_time(id: Vec) -> Result { + let inner_id: ScheduledId = id + .try_into() + .unwrap_or([0; MAX_TASK_ID_LENGTH_IN_BYTES as usize]); + Lookup::::get(inner_id) + .and_then(|(when, index)| Agenda::::get(when).get(index as usize).map(|_| when)) + .ok_or(()) } } --- a/pallets/scheduler/src/weights.rs +++ b/pallets/scheduler/src/weights.rs @@ -1,23 +1,6 @@ -// 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. +// Copyright (C) 2022 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,13 +15,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Weights for pallet_scheduler -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0 -//! DATE: 2020-10-27, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 +//! Autogenerated weights for pallet_scheduler +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-01-31, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// target/release/substrate +// ./target/production/substrate // benchmark // --chain=dev // --steps=50 @@ -49,78 +33,332 @@ // --wasm-execution=compiled // --heap-pages=4096 // --output=./frame/scheduler/src/weights.rs -// --template=./.maintain/frame-weight-template.hbs +// --template=.maintain/frame-weight-template.hbs +// --header=HEADER-APACHE2 +// --raw +#![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] -use frame_support::{ - traits::Get, - weights::{Weight, constants::RocksDbWeight}, -}; +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; use sp_std::marker::PhantomData; /// Weight functions needed for pallet_scheduler. pub trait WeightInfo { - fn schedule(s: u32) -> Weight; - fn cancel(s: u32) -> Weight; - fn schedule_named(s: u32) -> Weight; - fn cancel_named(s: u32) -> Weight; + fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight; + fn on_initialize_named_resolved(s: u32, ) -> Weight; + fn on_initialize_periodic_resolved(s: u32, ) -> Weight; + fn on_initialize_resolved(s: u32, ) -> Weight; + fn on_initialize_named_aborted(s: u32, ) -> Weight; + fn on_initialize_aborted(s: u32, ) -> Weight; + fn on_initialize_periodic_named(s: u32, ) -> Weight; + fn on_initialize_periodic(s: u32, ) -> Weight; + fn on_initialize_named(s: u32, ) -> Weight; + fn on_initialize(s: u32, ) -> Weight; + fn schedule(s: u32, ) -> Weight; + fn cancel(s: u32, ) -> Weight; + fn schedule_named(s: u32, ) -> Weight; + fn cancel_named(s: u32, ) -> Weight; } /// Weights for pallet_scheduler using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - fn schedule(s: u32) -> Weight { - 35_029_000_u64 - .saturating_add(77_000_u64.saturating_mul(s as Weight)) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight { + (11_587_000 as Weight) + // Standard Error: 17_000 + .saturating_add((17_428_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((4 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_named_resolved(s: u32, ) -> Weight { + (8_965_000 as Weight) + // Standard Error: 11_000 + .saturating_add((13_410_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(s as Weight))) } - fn cancel(s: u32) -> Weight { - 31_419_000_u64 - .saturating_add(4_015_000_u64.saturating_mul(s as Weight)) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + fn on_initialize_periodic_resolved(s: u32, ) -> Weight { + (8_654_000 as Weight) + // Standard Error: 17_000 + .saturating_add((14_990_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + fn on_initialize_resolved(s: u32, ) -> Weight { + (9_303_000 as Weight) + // Standard Error: 10_000 + .saturating_add((12_244_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:0) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_named_aborted(s: u32, ) -> Weight { + (7_506_000 as Weight) + // Standard Error: 3_000 + .saturating_add((5_208_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:0) + fn on_initialize_aborted(s: u32, ) -> Weight { + (8_046_000 as Weight) + // Standard Error: 3_000 + .saturating_add((2_914_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_periodic_named(s: u32, ) -> Weight { + (13_704_000 as Weight) + // Standard Error: 4_000 + .saturating_add((8_186_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:2 w:2) + fn on_initialize_periodic(s: u32, ) -> Weight { + (12_668_000 as Weight) + // Standard Error: 5_000 + .saturating_add((5_868_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_named(s: u32, ) -> Weight { + (13_946_000 as Weight) + // Standard Error: 4_000 + .saturating_add((4_367_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn on_initialize(s: u32, ) -> Weight { + (13_151_000 as Weight) + // Standard Error: 4_000 + .saturating_add((3_455_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule(s: u32, ) -> Weight { + (14_040_000 as Weight) + // Standard Error: 1_000 + .saturating_add((89_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn cancel(s: u32, ) -> Weight { + (14_376_000 as Weight) + // Standard Error: 1_000 + .saturating_add((576_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) } - fn schedule_named(s: u32) -> Weight { - 44_752_000_u64 - .saturating_add(123_000_u64.saturating_mul(s as Weight)) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule_named(s: u32, ) -> Weight { + (16_806_000 as Weight) + // Standard Error: 1_000 + .saturating_add((102_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) } - fn cancel_named(s: u32) -> Weight { - 35_712_000_u64 - .saturating_add(4_008_000_u64.saturating_mul(s as Weight)) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn cancel_named(s: u32, ) -> Weight { + (15_852_000 as Weight) + // Standard Error: 2_000 + .saturating_add((590_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) } } // For backwards compatibility and tests impl WeightInfo for () { - fn schedule(s: u32) -> Weight { - 35_029_000_u64 - .saturating_add(77_000_u64.saturating_mul(s as Weight)) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_periodic_named_resolved(s: u32, ) -> Weight { + (11_587_000 as Weight) + // Standard Error: 17_000 + .saturating_add((17_428_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((4 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_named_resolved(s: u32, ) -> Weight { + (8_965_000 as Weight) + // Standard Error: 11_000 + .saturating_add((13_410_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((2 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(s as Weight))) } - fn cancel(s: u32) -> Weight { - 31_419_000_u64 - .saturating_add(4_015_000_u64.saturating_mul(s as Weight)) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + fn on_initialize_periodic_resolved(s: u32, ) -> Weight { + (8_654_000 as Weight) + // Standard Error: 17_000 + .saturating_add((14_990_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(s as Weight))) } - fn schedule_named(s: u32) -> Weight { - 44_752_000_u64 - .saturating_add(123_000_u64.saturating_mul(s as Weight)) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Preimage PreimageFor (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + fn on_initialize_resolved(s: u32, ) -> Weight { + (9_303_000 as Weight) + // Standard Error: 10_000 + .saturating_add((12_244_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((2 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(s as Weight))) } - fn cancel_named(s: u32) -> Weight { - 35_712_000_u64 - .saturating_add(4_008_000_u64.saturating_mul(s as Weight)) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:0) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_named_aborted(s: u32, ) -> Weight { + (7_506_000 as Weight) + // Standard Error: 3_000 + .saturating_add((5_208_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Preimage PreimageFor (r:1 w:0) + fn on_initialize_aborted(s: u32, ) -> Weight { + (8_046_000 as Weight) + // Standard Error: 3_000 + .saturating_add((2_914_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_periodic_named(s: u32, ) -> Weight { + (13_704_000 as Weight) + // Standard Error: 4_000 + .saturating_add((8_186_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:2 w:2) + fn on_initialize_periodic(s: u32, ) -> Weight { + (12_668_000 as Weight) + // Standard Error: 5_000 + .saturating_add((5_868_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn on_initialize_named(s: u32, ) -> Weight { + (13_946_000 as Weight) + // Standard Error: 4_000 + .saturating_add((4_367_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(s as Weight))) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn on_initialize(s: u32, ) -> Weight { + (13_151_000 as Weight) + // Standard Error: 4_000 + .saturating_add((3_455_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule(s: u32, ) -> Weight { + (14_040_000 as Weight) + // Standard Error: 1_000 + .saturating_add((89_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Scheduler Lookup (r:0 w:1) + fn cancel(s: u32, ) -> Weight { + (14_376_000 as Weight) + // Standard Error: 1_000 + .saturating_add((576_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn schedule_named(s: u32, ) -> Weight { + (16_806_000 as Weight) + // Standard Error: 1_000 + .saturating_add((102_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn cancel_named(s: u32, ) -> Weight { + (15_852_000 as Weight) + // Standard Error: 2_000 + .saturating_add((590_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } } --- a/runtime/opal/src/lib.rs +++ b/runtime/opal/src/lib.rs @@ -28,6 +28,8 @@ use sp_api::impl_runtime_apis; use sp_core::{crypto::KeyTypeId, OpaqueMetadata, H256, U256, H160}; use sp_runtime::DispatchError; +use fp_self_contained::*; +use sp_runtime::traits::{Member}; // #[cfg(any(feature = "std", test))] // pub use sp_runtime::BuildStorage; @@ -59,7 +61,7 @@ traits::{ tokens::currency::Currency as CurrencyT, OnUnbalanced as OnUnbalancedT, Everything, Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem, LockIdentifier, - OnUnbalanced, Randomness, FindAuthor, ConstU32, Imbalance, + OnUnbalanced, Randomness, FindAuthor, ConstU32, Imbalance, PrivilegeCmp, }, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, @@ -67,8 +69,13 @@ WeightToFeePolynomial, WeightToFeeCoefficient, WeightToFeeCoefficients, ConstantMultiplier, }, }; -use up_data_structs::mapping::{EvmTokenAddressMapping, CrossTokenAddressMapping}; -use up_data_structs::*; +use pallet_unq_scheduler::DispatchCall; +use up_data_structs::{ + CollectionId, TokenId, TokenData, Property, PropertyKeyPermission, CollectionLimits, + CollectionStats, RpcCollection, + mapping::{EvmTokenAddressMapping, CrossTokenAddressMapping}, +}; + // use pallet_contracts::weights::WeightInfo; // #[cfg(any(feature = "std", test))] use frame_system::{ @@ -79,12 +86,17 @@ traits::{BaseArithmetic, Unsigned}, }; use smallvec::smallvec; +// use scale_info::TypeInfo; use codec::{Encode, Decode}; use fp_rpc::TransactionStatus; use sp_runtime::{ - traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf, DispatchInfoOf, Saturating}, + traits::{ + Applyable, BlockNumberProvider, Dispatchable, PostDispatchInfoOf, DispatchInfoOf, + Saturating, CheckedConversion, + }, + generic::Era, transaction_validity::TransactionValidityError, - SaturatedConversion, + DispatchErrorWithPostInfo, SaturatedConversion, }; // pub use pallet_timestamp::Call as TimestampCall; @@ -102,7 +114,7 @@ ParentIsPreset, }; use xcm_executor::{Config, XcmExecutor, Assets}; -use sp_std::{marker::PhantomData}; +use sp_std::{cmp::Ordering, marker::PhantomData}; use xcm::latest::{ // Xcm, @@ -113,7 +125,6 @@ }; use xcm_executor::traits::{MatchesFungible, WeightTrader}; //use xcm_executor::traits::MatchesFungible; -use sp_runtime::traits::CheckedConversion; use unique_runtime_common::{ impl_common_runtime_apis, @@ -406,12 +417,13 @@ // pub const ExistentialDeposit: u128 = 500; pub const ExistentialDeposit: u128 = 0; pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; } impl pallet_balances::Config for Runtime { type MaxLocks = MaxLocks; - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 16]; /// The type for recording an account's balance. type Balance = Balance; /// The ubiquitous event type. @@ -930,33 +942,156 @@ type BlockNumberProvider = RelayChainBlockNumberProvider; } -// parameter_types! { -// pub MaximumSchedulerWeight: Weight = Perbill::from_percent(50) * -// RuntimeBlockWeights::get().max_block; -// pub const MaxScheduledPerBlock: u32 = 50; -// } +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(50) * + RuntimeBlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 50; +} + +type ChargeTransactionPayment = pallet_charge_transaction::ChargeTransactionPayment; +use frame_support::traits::NamedReservableCurrency; + +fn get_signed_extras(from: ::AccountId) -> 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(), + // sponsoring transaction logic + // pallet_charge_transaction::ChargeTransactionPayment::::new(0), + ) +} + +pub struct SchedulerPaymentExecutor; +impl + DispatchCall for SchedulerPaymentExecutor +where + ::Call: Member + + Dispatchable + + SelfContainedCall + + GetDispatchInfo + + From>, + SelfContainedSignedInfo: Send + Sync + 'static, + Call: From<::Call> + + From<::Call> + + SelfContainedCall, + sp_runtime::AccountId32: From<::AccountId>, +{ + fn dispatch_call( + signer: ::AccountId, + call: ::Call, + ) -> Result< + Result>, + TransactionValidityError, + > { + let dispatch_info = call.get_dispatch_info(); + let extrinsic = fp_self_contained::CheckedExtrinsic::< + AccountId, + Call, + SignedExtraScheduler, + SelfContainedSignedInfo, + > { + signed: + CheckedSignature::::Signed( + signer.clone().into(), + get_signed_extras(signer.into()), + ), + function: call.into(), + }; + + extrinsic.apply::(&dispatch_info, 0) + } + + fn reserve_balance( + id: [u8; 16], + sponsor: ::AccountId, + call: ::Call, + count: u32, + ) -> Result<(), DispatchError> { + let dispatch_info = call.get_dispatch_info(); + let weight: Balance = ChargeTransactionPayment::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: ::Call, + ) -> Result { + let dispatch_info = call.get_dispatch_info(); + let weight: Balance = ChargeTransactionPayment::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, + ), + ) + } +} + +parameter_types! { + pub const NoPreimagePostponement: Option = Some(10); + pub const Preimage: Option = Some(10); +} + +/// Used the compare the privilege of an origin inside the scheduler. +pub struct OriginPrivilegeCmp; + +impl PrivilegeCmp for OriginPrivilegeCmp { + fn cmp_privilege(_left: &OriginCaller, _right: &OriginCaller) -> Option { + Some(Ordering::Equal) + } +} + +impl pallet_unq_scheduler::Config for Runtime { + type Event = Event; + type Origin = Origin; + type Currency = Balances; + type PalletsOrigin = OriginCaller; + type Call = Call; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureSigned; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type WeightInfo = (); + type CallExecutor = SchedulerPaymentExecutor; + type OriginPrivilegeCmp = OriginPrivilegeCmp; + type PreimageProvider = (); + type NoPreimagePostponement = NoPreimagePostponement; +} type EvmSponsorshipHandler = ( UniqueEthSponsorshipHandler, pallet_evm_contract_helpers::HelpersContractSponsoring, ); + type SponsorshipHandler = ( UniqueSponsorshipHandler, //pallet_contract_helpers::ContractSponsorshipHandler, pallet_evm_transaction_payment::BridgeSponsorshipHandler, ); - -// impl pallet_unq_scheduler::Config for Runtime { -// type Event = Event; -// type Origin = Origin; -// type PalletsOrigin = OriginCaller; -// type Call = Call; -// type MaximumWeight = MaximumSchedulerWeight; -// type ScheduleOrigin = EnsureSigned; -// type MaxScheduledPerBlock = MaxScheduledPerBlock; -// type SponsorshipHandler = SponsorshipHandler; -// type WeightInfo = (); -// } impl pallet_evm_transaction_payment::Config for Runtime { type EvmSponsorshipHandler = EvmSponsorshipHandler; @@ -1020,7 +1155,7 @@ // Unique Pallets Inflation: pallet_inflation::{Pallet, Call, Storage} = 60, Unique: pallet_unique::{Pallet, Call, Storage, Event} = 61, - // Scheduler: pallet_unq_scheduler::{Pallet, Call, Storage, Event} = 62, + Scheduler: pallet_unq_scheduler::{Pallet, Call, Storage, Event} = 62, // free = 63 Charging: pallet_charge_transaction::{Pallet, Call, Storage } = 64, // ContractHelpers: pallet_contract_helpers::{Pallet, Call, Storage} = 65, @@ -1087,10 +1222,17 @@ frame_system::CheckEra, frame_system::CheckNonce, frame_system::CheckWeight, - pallet_charge_transaction::ChargeTransactionPayment, + ChargeTransactionPayment, //pallet_contract_helpers::ContractHelpersExtension, pallet_ethereum::FakeTransactionFinalizer, ); +pub type SignedExtraScheduler = ( + frame_system::CheckSpecVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, +); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = fp_self_contained::UncheckedExtrinsic; --- a/runtime/quartz/src/lib.rs +++ b/runtime/quartz/src/lib.rs @@ -33,7 +33,7 @@ use sp_runtime::{ Permill, Perbill, Percent, create_runtime_str, generic, impl_opaque_keys, - traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, AccountIdConversion, Zero}, + traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, AccountIdConversion, Zero, Member}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, RuntimeAppPublic, }; @@ -58,7 +58,7 @@ traits::{ tokens::currency::Currency as CurrencyT, OnUnbalanced as OnUnbalancedT, Everything, Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem, LockIdentifier, - OnUnbalanced, Randomness, FindAuthor, ConstU32, Imbalance, + OnUnbalanced, Randomness, FindAuthor, ConstU32, Imbalance, PrivilegeCmp, }, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, @@ -81,18 +81,28 @@ use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping}; use fp_rpc::TransactionStatus; use sp_runtime::{ - traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf, Saturating}, + traits::{ + Applyable, BlockNumberProvider, Dispatchable, PostDispatchInfoOf, Saturating, + CheckedConversion, + }, + generic::Era, transaction_validity::TransactionValidityError, - SaturatedConversion, + SaturatedConversion, DispatchErrorWithPostInfo, }; +use fp_self_contained::{SelfContainedCall, CheckedSignature}; + // pub use pallet_timestamp::Call as TimestampCall; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; // Polkadot imports use pallet_xcm::XcmPassthrough; use polkadot_parachain::primitives::Sibling; -use up_data_structs::mapping::{CrossTokenAddressMapping, EvmTokenAddressMapping}; +use up_data_structs::{ + CollectionId, TokenId, TokenData, Property, PropertyKeyPermission, CollectionLimits, + CollectionStats, RpcCollection, + mapping::{EvmTokenAddressMapping, CrossTokenAddressMapping} +}; use xcm::v1::{BodyId, Junction::*, MultiLocation, NetworkId, Junctions::*}; use xcm_builder::{ AccountId32Aliases, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, CurrencyAdapter, @@ -102,7 +112,8 @@ ParentIsPreset, }; use xcm_executor::{Config, XcmExecutor, Assets}; -use sp_std::{marker::PhantomData}; +use sp_std::{cmp::Ordering, marker::PhantomData}; +use pallet_unq_scheduler::DispatchCall; use xcm::latest::{ // Xcm, @@ -112,8 +123,6 @@ Error as XcmError, }; use xcm_executor::traits::{MatchesFungible, WeightTrader}; -//use xcm_executor::traits::MatchesFungible; -use sp_runtime::traits::CheckedConversion; use unique_runtime_common::{ impl_common_runtime_apis, @@ -390,7 +399,7 @@ impl pallet_balances::Config for Runtime { type MaxLocks = MaxLocks; type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; + type ReserveIdentifier = [u8; 16]; /// The type for recording an account's balance. type Balance = Balance; /// The ubiquitous event type. @@ -913,11 +922,11 @@ type BlockNumberProvider = RelayChainBlockNumberProvider; } -// parameter_types! { -// pub MaximumSchedulerWeight: Weight = Perbill::from_percent(50) * -// RuntimeBlockWeights::get().max_block; -// pub const MaxScheduledPerBlock: u32 = 50; -// } +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(50) * + RuntimeBlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 50; +} type EvmSponsorshipHandler = ( UniqueEthSponsorshipHandler, @@ -929,17 +938,34 @@ pallet_evm_transaction_payment::BridgeSponsorshipHandler, ); -// impl pallet_unq_scheduler::Config for Runtime { -// type Event = Event; -// type Origin = Origin; -// type PalletsOrigin = OriginCaller; -// type Call = Call; -// type MaximumWeight = MaximumSchedulerWeight; -// type ScheduleOrigin = EnsureSigned; -// type MaxScheduledPerBlock = MaxScheduledPerBlock; -// type SponsorshipHandler = SponsorshipHandler; -// type WeightInfo = (); -// } +parameter_types! { + pub const NoPreimagePostponement: Option = Some(10); + pub const Preimage: Option = Some(10); +} + +/// Used the compare the privilege of an origin inside the scheduler. +pub struct OriginPrivilegeCmp; +impl PrivilegeCmp for OriginPrivilegeCmp { + fn cmp_privilege(_left: &OriginCaller, _right: &OriginCaller) -> Option { + Some(Ordering::Equal) + } +} + +impl pallet_unq_scheduler::Config for Runtime { + type Event = Event; + type Origin = Origin; + type Currency = Balances; + type PalletsOrigin = OriginCaller; + type Call = Call; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureSigned; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type WeightInfo = (); + type CallExecutor = SchedulerPaymentExecutor; + type OriginPrivilegeCmp = OriginPrivilegeCmp; + type PreimageProvider = (); + type NoPreimagePostponement = NoPreimagePostponement; +} impl pallet_evm_transaction_payment::Config for Runtime { type EvmSponsorshipHandler = EvmSponsorshipHandler; @@ -954,6 +980,110 @@ // type DefaultSponsoringRateLimit = DefaultSponsoringRateLimit; // } +type ChargeTransactionPayment = pallet_charge_transaction::ChargeTransactionPayment; +use frame_support::traits::NamedReservableCurrency; + +fn get_signed_extras(from: ::AccountId) -> 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(), + // sponsoring transaction logic + // pallet_charge_transaction::ChargeTransactionPayment::::new(0), + ) +} + +pub struct SchedulerPaymentExecutor; +impl + DispatchCall for SchedulerPaymentExecutor +where + ::Call: Member + + Dispatchable + + SelfContainedCall + + GetDispatchInfo + + From>, + SelfContainedSignedInfo: Send + Sync + 'static, + Call: From<::Call> + + From<::Call> + + SelfContainedCall, + sp_runtime::AccountId32: From<::AccountId>, +{ + fn dispatch_call( + signer: ::AccountId, + call: ::Call, + ) -> Result< + Result>, + TransactionValidityError, + > { + let dispatch_info = call.get_dispatch_info(); + let extrinsic = fp_self_contained::CheckedExtrinsic::< + AccountId, + Call, + SignedExtraScheduler, + SelfContainedSignedInfo, + > { + signed: + CheckedSignature::::Signed( + signer.clone().into(), + get_signed_extras(signer.into()), + ), + function: call.into(), + }; + + extrinsic.apply::(&dispatch_info, 0) + } + + fn reserve_balance( + id: [u8; 16], + sponsor: ::AccountId, + call: ::Call, + count: u32, + ) -> Result<(), DispatchError> { + let dispatch_info = call.get_dispatch_info(); + let weight: Balance = ChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0) + .saturating_mul(count.into()); + + >::reserve_named( + &id, + &(sponsor.into()), + weight.into(), + ) + } + + fn pay_for_call( + id: [u8; 16], + sponsor: ::AccountId, + call: ::Call, + ) -> Result { + let dispatch_info = call.get_dispatch_info(); + let weight: Balance = ChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0); + Ok( + >::unreserve_named( + &id, + &(sponsor.into()), + weight.into(), + ), + ) + } + + fn cancel_reserve( + id: [u8; 16], + sponsor: ::AccountId, + ) -> Result { + Ok( + >::unreserve_named( + &id, + &(sponsor.into()), + u128::MAX, + ), + ) + } +} + parameter_types! { // 0x842899ECF380553E8a4de75bF534cdf6fBF64049 pub const HelpersContractAddress: H160 = H160([ @@ -1003,7 +1133,7 @@ // Unique Pallets Inflation: pallet_inflation::{Pallet, Call, Storage} = 60, Unique: pallet_unique::{Pallet, Call, Storage, Event} = 61, - // Scheduler: pallet_unq_scheduler::{Pallet, Call, Storage, Event} = 62, + Scheduler: pallet_unq_scheduler::{Pallet, Call, Storage, Event} = 62, // free = 63 Charging: pallet_charge_transaction::{Pallet, Call, Storage } = 64, // ContractHelpers: pallet_contract_helpers::{Pallet, Call, Storage} = 65, @@ -1073,6 +1203,15 @@ //pallet_contract_helpers::ContractHelpersExtension, pallet_ethereum::FakeTransactionFinalizer, ); + +pub type SignedExtraScheduler = ( + frame_system::CheckSpecVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, + // pallet_charge_transaction::ChargeTransactionPayment, +); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = fp_self_contained::UncheckedExtrinsic; --- a/runtime/unique/src/lib.rs +++ b/runtime/unique/src/lib.rs @@ -33,11 +33,20 @@ use sp_runtime::{ Permill, Perbill, Percent, create_runtime_str, generic, impl_opaque_keys, - traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, AccountIdConversion, Zero}, - transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, RuntimeAppPublic, + generic::Era, + traits::{ + Applyable, BlockNumberProvider, Dispatchable, PostDispatchInfoOf, Saturating, + CheckedConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, AccountIdConversion, + Zero, Member, + }, + transaction_validity::{TransactionSource, TransactionValidity, TransactionValidityError}, + ApplyExtrinsicResult, RuntimeAppPublic, SaturatedConversion, DispatchErrorWithPostInfo, }; +use fp_self_contained::{SelfContainedCall, CheckedSignature}; +use sp_std::{cmp::Ordering, marker::PhantomData}; +use pallet_unq_scheduler::DispatchCall; + use sp_std::prelude::*; #[cfg(feature = "std")] @@ -59,7 +68,7 @@ traits::{ tokens::currency::Currency as CurrencyT, OnUnbalanced as OnUnbalancedT, Everything, Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem, LockIdentifier, - OnUnbalanced, Randomness, FindAuthor, ConstU32, Imbalance, + OnUnbalanced, Randomness, FindAuthor, ConstU32, Imbalance, PrivilegeCmp, }, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, @@ -86,18 +95,17 @@ use smallvec::smallvec; use codec::{Encode, Decode}; use fp_rpc::TransactionStatus; -use sp_runtime::{ - traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf, Saturating}, - transaction_validity::TransactionValidityError, - SaturatedConversion, -}; // pub use pallet_timestamp::Call as TimestampCall; // Polkadot imports use pallet_xcm::XcmPassthrough; use polkadot_parachain::primitives::Sibling; -use up_data_structs::mapping::{CrossTokenAddressMapping, EvmTokenAddressMapping}; +use up_data_structs::{ + CollectionId, TokenId, TokenData, Property, PropertyKeyPermission, CollectionLimits, + CollectionStats, RpcCollection, + mapping::{EvmTokenAddressMapping, CrossTokenAddressMapping} +}; use xcm::v1::{BodyId, Junction::*, MultiLocation, NetworkId, Junctions::*}; use xcm_builder::{ AccountId32Aliases, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, CurrencyAdapter, @@ -107,7 +115,6 @@ ParentIsPreset, }; use xcm_executor::{Config, XcmExecutor, Assets}; -use sp_std::{marker::PhantomData}; use xcm::latest::{ // Xcm, @@ -117,7 +124,6 @@ Error as XcmError, }; use xcm_executor::traits::{MatchesFungible, WeightTrader}; -//use xcm_executor::traits::MatchesFungible; use sp_runtime::traits::CheckedConversion; use unique_runtime_common::{ @@ -395,7 +401,7 @@ impl pallet_balances::Config for Runtime { type MaxLocks = MaxLocks; type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; + type ReserveIdentifier = [u8; 16]; /// The type for recording an account's balance. type Balance = Balance; /// The ubiquitous event type. @@ -918,12 +924,145 @@ type BlockNumberProvider = RelayChainBlockNumberProvider; } -// parameter_types! { -// pub MaximumSchedulerWeight: Weight = Perbill::from_percent(50) * -// RuntimeBlockWeights::get().max_block; -// pub const MaxScheduledPerBlock: u32 = 50; -// } +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(50) * + RuntimeBlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 50; +} + +parameter_types! { + pub const NoPreimagePostponement: Option = Some(10); + pub const Preimage: Option = Some(10); +} +/// Used the compare the privilege of an origin inside the scheduler. +pub struct OriginPrivilegeCmp; +impl PrivilegeCmp for OriginPrivilegeCmp { + fn cmp_privilege(_left: &OriginCaller, _right: &OriginCaller) -> Option { + Some(Ordering::Equal) + } +} + +impl pallet_unq_scheduler::Config for Runtime { + type Event = Event; + type Origin = Origin; + type Currency = Balances; + type PalletsOrigin = OriginCaller; + type Call = Call; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureSigned; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type WeightInfo = (); + type CallExecutor = SchedulerPaymentExecutor; + type OriginPrivilegeCmp = OriginPrivilegeCmp; + type PreimageProvider = (); + type NoPreimagePostponement = NoPreimagePostponement; +} + +type ChargeTransactionPayment = pallet_charge_transaction::ChargeTransactionPayment; +use frame_support::traits::NamedReservableCurrency; + +fn get_signed_extras(from: ::AccountId) -> 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(), + // sponsoring transaction logic + // pallet_charge_transaction::ChargeTransactionPayment::::new(0), + ) +} + +pub struct SchedulerPaymentExecutor; +impl + DispatchCall for SchedulerPaymentExecutor +where + ::Call: Member + + Dispatchable + + SelfContainedCall + + GetDispatchInfo + + From>, + SelfContainedSignedInfo: Send + Sync + 'static, + Call: From<::Call> + + From<::Call> + + SelfContainedCall, + sp_runtime::AccountId32: From<::AccountId>, +{ + fn dispatch_call( + signer: ::AccountId, + call: ::Call, + ) -> Result< + Result>, + TransactionValidityError, + > { + let dispatch_info = call.get_dispatch_info(); + let extrinsic = fp_self_contained::CheckedExtrinsic::< + AccountId, + Call, + SignedExtraScheduler, + SelfContainedSignedInfo, + > { + signed: + CheckedSignature::::Signed( + signer.clone().into(), + get_signed_extras(signer.into()), + ), + function: call.into(), + }; + + extrinsic.apply::(&dispatch_info, 0) + } + + fn reserve_balance( + id: [u8; 16], + sponsor: ::AccountId, + call: ::Call, + count: u32, + ) -> Result<(), DispatchError> { + let dispatch_info = call.get_dispatch_info(); + let weight: Balance = ChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0) + .saturating_mul(count.into()); + + >::reserve_named( + &id, + &(sponsor.into()), + weight.into(), + ) + } + + fn pay_for_call( + id: [u8; 16], + sponsor: ::AccountId, + call: ::Call, + ) -> Result { + let dispatch_info = call.get_dispatch_info(); + let weight: Balance = ChargeTransactionPayment::traditional_fee(0, &dispatch_info, 0); + Ok( + >::unreserve_named( + &id, + &(sponsor.into()), + weight.into(), + ), + ) + } + + fn cancel_reserve( + id: [u8; 16], + sponsor: ::AccountId, + ) -> Result { + Ok( + >::unreserve_named( + &id, + &(sponsor.into()), + u128::MAX, + ), + ) + } +} + type EvmSponsorshipHandler = ( UniqueEthSponsorshipHandler, pallet_evm_contract_helpers::HelpersContractSponsoring, @@ -933,18 +1072,6 @@ //pallet_contract_helpers::ContractSponsorshipHandler, pallet_evm_transaction_payment::BridgeSponsorshipHandler, ); - -// impl pallet_unq_scheduler::Config for Runtime { -// type Event = Event; -// type Origin = Origin; -// type PalletsOrigin = OriginCaller; -// type Call = Call; -// type MaximumWeight = MaximumSchedulerWeight; -// type ScheduleOrigin = EnsureSigned; -// type MaxScheduledPerBlock = MaxScheduledPerBlock; -// type SponsorshipHandler = SponsorshipHandler; -// type WeightInfo = (); -// } impl pallet_evm_transaction_payment::Config for Runtime { type EvmSponsorshipHandler = EvmSponsorshipHandler; @@ -1008,7 +1135,7 @@ // Unique Pallets Inflation: pallet_inflation::{Pallet, Call, Storage} = 60, Unique: pallet_unique::{Pallet, Call, Storage, Event} = 61, - // Scheduler: pallet_unq_scheduler::{Pallet, Call, Storage, Event} = 62, + Scheduler: pallet_unq_scheduler::{Pallet, Call, Storage, Event} = 62, // free = 63 Charging: pallet_charge_transaction::{Pallet, Call, Storage } = 64, // ContractHelpers: pallet_contract_helpers::{Pallet, Call, Storage} = 65, @@ -1078,6 +1205,14 @@ //pallet_contract_helpers::ContractHelpersExtension, pallet_ethereum::FakeTransactionFinalizer, ); +pub type SignedExtraScheduler = ( + frame_system::CheckSpecVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, + // pallet_charge_transaction::ChargeTransactionPayment, +); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = fp_self_contained::UncheckedExtrinsic; --- a/tests/package.json +++ b/tests/package.json @@ -68,6 +68,8 @@ "testOverflow": "mocha --timeout 9999999 -r ts-node/register ./**/overflow.test.ts", "testSetVariableMetadataSponsoringRateLimit": "mocha --timeout 9999999 -r ts-node/register ./**/setVariableMetadataSponsoringRateLimit.test.ts", "testInflation": "mocha --timeout 9999999 -r ts-node/register ./**/inflation.test.ts", + "testScheduler": "mocha --timeout 9999999 -r ts-node/register ./**/scheduler.test.ts", + "testSchedulingEVM": "mocha --timeout 9999999 -r ts-node/register ./**/eth/scheduling.test.ts", "testXcmTransfer": "mocha --timeout 9999999 -r ts-node/register ./**/xcmTransfer.test.ts", "testPalletPresence": "mocha --timeout 9999999 -r ts-node/register ./**/pallet-presence.test.ts", "testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts", --- /dev/null +++ b/tests/src/eth/scheduling.test.ts @@ -0,0 +1,55 @@ +// 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 . + +import {expect} from 'chai'; +import {createEthAccountWithBalance, deployFlipper, GAS_ARGS, itWeb3, subToEth, transferBalanceToEth} from './util/helpers'; +import {scheduleExpectSuccess, waitNewBlocks} from '../util/helpers'; +import privateKey from '../substrate/privateKey'; + +describe('Scheduing EVM smart contracts', () => { + itWeb3('Successfully schedules and periodically executes an EVM contract', async ({api, web3}) => { + const deployer = await createEthAccountWithBalance(api, web3); + const flipper = await deployFlipper(web3, deployer); + const initialValue = await flipper.methods.getValue().call(); + const alice = privateKey('//Alice'); + await transferBalanceToEth(api, alice, subToEth(alice.address)); + + { + const tx = api.tx.evm.call( + subToEth(alice.address), + flipper.options.address, + flipper.methods.flip().encodeABI(), + '0', + GAS_ARGS.gas, + await web3.eth.getGasPrice(), + null, + null, + [], + ); + const waitForBlocks = 4; + const periodBlocks = 2; + + await scheduleExpectSuccess(tx, alice, waitForBlocks, '0x' + '0'.repeat(32), periodBlocks, 2); + expect(await flipper.methods.getValue().call()).to.be.equal(initialValue); + + await waitNewBlocks(waitForBlocks - 1); + expect(await flipper.methods.getValue().call()).to.be.not.equal(initialValue); + + await waitNewBlocks(periodBlocks); + expect(await flipper.methods.getValue().call()).to.be.equal(initialValue); + } + }); +}); \ No newline at end of file --- a/tests/src/pallet-presence.test.ts +++ b/tests/src/pallet-presence.test.ts @@ -50,7 +50,7 @@ 'unique', 'nonfungible', 'refungible', - //'scheduler', + 'scheduler', 'charging', ]; --- a/tests/src/scheduler.test.ts +++ b/tests/src/scheduler.test.ts @@ -14,32 +14,197 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import chai from 'chai'; +import chai, {expect} from 'chai'; import chaiAsPromised from 'chai-as-promised'; import privateKey from './substrate/privateKey'; -import usingApi from './substrate/substrate-api'; import { + default as usingApi, + submitTransactionAsync, +} from './substrate/substrate-api'; +import { createItemExpectSuccess, createCollectionExpectSuccess, scheduleTransferExpectSuccess, + scheduleTransferAndWaitExpectSuccess, setCollectionSponsorExpectSuccess, confirmSponsorshipExpectSuccess, + findUnusedAddress, + UNIQUE, + enablePublicMintingExpectSuccess, + addToAllowListExpectSuccess, + waitNewBlocks, + normalizeAccountId, + getTokenOwner, + getGenericResult, + scheduleTransferFundsPeriodicExpectSuccess, + getFreeBalance, + confirmSponsorshipByKeyExpectSuccess, + scheduleExpectFailure, } from './util/helpers'; +import {IKeyringPair} from '@polkadot/types/types'; chai.use(chaiAsPromised); -describe.skip('Integration Test scheduler base transaction', () => { - it('User can transfer owned token with delay (scheduler)', async () => { +describe.skip('Scheduling token and balance transfers', () => { + let alice: IKeyringPair; + let bob: IKeyringPair; + let scheduledIdBase: string; + let scheduledIdSlider: number; + + before(async() => { await usingApi(async () => { - const alice = privateKey('//Alice'); - const bob = privateKey('//Bob'); - // nft + alice = privateKey('//Alice'); + bob = privateKey('//Bob'); + }); + + scheduledIdBase = '0x' + '0'.repeat(31); + scheduledIdSlider = 0; + }); + + // Loop scheduledId around 10. Unless there are concurrent tasks with long periods/repetitions, tests' tasks' ids shouldn't ovelap. + function makeScheduledId(): string { + return scheduledIdBase + ((scheduledIdSlider++) % 10); + } + + it('Can schedule a transfer of an owned token with delay', async () => { + await usingApi(async () => { const nftCollectionId = await createCollectionExpectSuccess(); const newNftTokenId = await createItemExpectSuccess(alice, nftCollectionId, 'NFT'); await setCollectionSponsorExpectSuccess(nftCollectionId, alice.address); await confirmSponsorshipExpectSuccess(nftCollectionId); - await scheduleTransferExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4); + await scheduleTransferAndWaitExpectSuccess(nftCollectionId, newNftTokenId, alice, bob, 1, 4, makeScheduledId()); + }); + }); + + it('Can transfer funds periodically', async () => { + await usingApi(async () => { + const waitForBlocks = 4; + const period = 2; + await scheduleTransferFundsPeriodicExpectSuccess(1n * UNIQUE, alice, bob, waitForBlocks, makeScheduledId(), period, 2); + const bobsBalanceBefore = await getFreeBalance(bob); + + // discounting already waited-for operations + await waitNewBlocks(waitForBlocks - 2); + const bobsBalanceAfterFirst = await getFreeBalance(bob); + expect(bobsBalanceAfterFirst > bobsBalanceBefore).to.be.true; + + await waitNewBlocks(period); + const bobsBalanceAfterSecond = await getFreeBalance(bob); + expect(bobsBalanceAfterSecond > bobsBalanceAfterFirst).to.be.true; + }); + }); + + it('Can sponsor scheduling a transaction', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async () => { + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); + + const bobBalanceBefore = await getFreeBalance(bob); + const waitForBlocks = 4; + // no need to wait to check, fees must be deducted on scheduling, immediately + await scheduleTransferExpectSuccess(collectionId, tokenId, alice, bob, 0, waitForBlocks, makeScheduledId()); + const bobBalanceAfter = await getFreeBalance(bob); + // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true; + expect(bobBalanceAfter < bobBalanceBefore).to.be.true; + // wait for sequentiality matters + await waitNewBlocks(waitForBlocks - 1); + }); + }); + + it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => { + await usingApi(async (api) => { + // Find an empty, unused account + const zeroBalance = await findUnusedAddress(api); + + const collectionId = await createCollectionExpectSuccess(); + + // Add zeroBalance address to allow list + await enablePublicMintingExpectSuccess(alice, collectionId); + await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); + + // Grace zeroBalance with money, enough to cover future transactions + const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); + await submitTransactionAsync(alice, balanceTx); + + // Mint a fresh NFT + const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT'); + + // Schedule transfer of the NFT a few blocks ahead + const waitForBlocks = 5; + await scheduleTransferExpectSuccess(collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, makeScheduledId()); + + // Get rid of the account's funds before the scheduled transaction takes place + const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n); + const events = await submitTransactionAsync(zeroBalance, balanceTx2); + expect(getGenericResult(events).success).to.be.true; + /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved? + const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any); + const events = await submitTransactionAsync(alice, sudoTx); + expect(getGenericResult(events).success).to.be.true;*/ + + // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions + await waitNewBlocks(waitForBlocks - 3); + + expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address)); + }); + }); + + it('Sponsor going bankrupt does not impact a scheduled transaction', async () => { + const collectionId = await createCollectionExpectSuccess(); + + await usingApi(async (api) => { + const zeroBalance = await findUnusedAddress(api); + const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE); + await submitTransactionAsync(alice, balanceTx); + + await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address); + await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance); + + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address); + + const waitForBlocks = 5; + await scheduleTransferExpectSuccess(collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, makeScheduledId()); + + const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); + const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any); + const events = await submitTransactionAsync(alice, sudoTx); + expect(getGenericResult(events).success).to.be.true; + + // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions + await waitNewBlocks(waitForBlocks - 3); + + expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address)); + }); + }); + + it.skip('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => { + const collectionId = await createCollectionExpectSuccess(); + await setCollectionSponsorExpectSuccess(collectionId, bob.address); + await confirmSponsorshipExpectSuccess(collectionId, '//Bob'); + + await usingApi(async (api) => { + const zeroBalance = await findUnusedAddress(api); + + await enablePublicMintingExpectSuccess(alice, collectionId); + await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address); + + const bobBalanceBefore = await getFreeBalance(bob); + + const createData = {nft: {const_data: [], variable_data: []}}; + const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any); + + /*const badTransaction = async function () { + await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice); + }; + await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/ + + await scheduleExpectFailure(creationTx, zeroBalance, 3, makeScheduledId(), 1, 3); + + expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore); }); }); }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -630,10 +630,16 @@ } export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed = '//Alice') { + await usingApi(async () => { + const sender = privateKey(senderSeed); + await confirmSponsorshipByKeyExpectSuccess(collectionId, sender); + }); +} + +export async function confirmSponsorshipByKeyExpectSuccess(collectionId: number, sender: IKeyringPair) { await usingApi(async (api) => { // Run the transaction - const sender = privateKey(senderSeed); const tx = api.tx.unique.confirmSponsorship(collectionId); const events = await submitTransactionAsync(sender, tx); const result = getGenericResult(events); @@ -899,7 +905,7 @@ } /* eslint no-async-promise-executor: "off" */ -async function getBlockNumber(api: ApiPromise): Promise { +export async function getBlockNumber(api: ApiPromise): Promise { return new Promise(async (resolve) => { const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => { unsubscribe(); @@ -935,29 +941,76 @@ } export async function -scheduleTransferExpectSuccess( - collectionId: number, - tokenId: number, +scheduleExpectSuccess( + operationTx: any, sender: IKeyringPair, - recipient: IKeyringPair, - value: number | bigint = 1, blockSchedule: number, + scheduledId: string, + period = 1, + repetitions = 1, ) { await usingApi(async (api: ApiPromise) => { const blockNumber: number | undefined = await getBlockNumber(api); const expectedBlockNumber = blockNumber + blockSchedule; expect(blockNumber).to.be.greaterThan(0); - const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, value); - const scheduleTx = api.tx.scheduler.schedule(expectedBlockNumber, null, 0, transferTx as any); + const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule + scheduledId, + expectedBlockNumber, + repetitions > 1 ? [period, repetitions] : null, + 0, + {value: operationTx as any}, + ); + + const events = await submitTransactionAsync(sender, scheduleTx); + expect(getGenericResult(events).success).to.be.true; + }); +} + +export async function +scheduleExpectFailure( + operationTx: any, + sender: IKeyringPair, + blockSchedule: number, + scheduledId: string, + period = 1, + repetitions = 1, +) { + await usingApi(async (api: ApiPromise) => { + const blockNumber: number | undefined = await getBlockNumber(api); + const expectedBlockNumber = blockNumber + blockSchedule; + + expect(blockNumber).to.be.greaterThan(0); + const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule + scheduledId, + expectedBlockNumber, + repetitions <= 1 ? null : [period, repetitions], + 0, + {value: operationTx as any}, + ); + + //const events = + await expect(submitTransactionExpectFailAsync(sender, scheduleTx)).to.be.rejected; + //expect(getGenericResult(events).success).to.be.false; + }); +} - await submitTransactionAsync(sender, scheduleTx); +export async function +scheduleTransferAndWaitExpectSuccess( + collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair, + value: number | bigint = 1, + blockSchedule: number, + scheduledId: string, +) { + await usingApi(async (api: ApiPromise) => { + await scheduleTransferExpectSuccess(collectionId, tokenId, sender, recipient, value, blockSchedule, scheduledId); const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt(); - expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address)); - - // sleep for 4 blocks + // sleep for n + 1 blocks await waitNewBlocks(blockSchedule + 1); const recipientBalanceAfter = (await api.query.system.account(recipient.address)).data.free.toBigInt(); @@ -967,6 +1020,45 @@ }); } +export async function +scheduleTransferExpectSuccess( + collectionId: number, + tokenId: number, + sender: IKeyringPair, + recipient: IKeyringPair, + value: number | bigint = 1, + blockSchedule: number, + scheduledId: string, +) { + await usingApi(async (api: ApiPromise) => { + const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, value); + + await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId); + + expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address)); + }); +} + +export async function +scheduleTransferFundsPeriodicExpectSuccess( + amount: bigint, + sender: IKeyringPair, + recipient: IKeyringPair, + blockSchedule: number, + scheduledId: string, + period: number, + repetitions: number, +) { + await usingApi(async (api: ApiPromise) => { + const transferTx = api.tx.balances.transfer(recipient.address, amount); + + const balanceBefore = await getFreeBalance(recipient); + + await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions); + + expect(await getFreeBalance(recipient)).to.be.equal(balanceBefore); + }); +} export async function transferExpectSuccess(