diff --git a/authority/src/lib.rs b/authority/src/lib.rs index 3634907df..2502691fd 100644 --- a/authority/src/lib.rs +++ b/authority/src/lib.rs @@ -28,10 +28,10 @@ use frame_support::{ }, weights::GetDispatchInfo, }; -use frame_system::pallet_prelude::*; +use frame_system::{pallet_prelude::*, EnsureOneOf, EnsureRoot, EnsureSigned}; use sp_runtime::{ - traits::{CheckedSub, Dispatchable, Saturating}, - ArithmeticError, DispatchError, DispatchResult, RuntimeDebug, + traits::{CheckedSub, Dispatchable, Hash, Saturating}, + ArithmeticError, DispatchError, DispatchResult, Either, RuntimeDebug, }; use sp_std::prelude::*; @@ -172,6 +172,12 @@ pub mod module { FailedToFastTrack, /// Failed to delay a task. FailedToDelay, + /// Call is not authorized. + CallNotAuthorized, + /// Triggering the call is not permitted. + TriggerCallNotPermitted, + /// Call weight bound is wrong. + WrongCallWeightBound, } #[pallet::event] @@ -187,12 +193,22 @@ pub mod module { Delayed(T::PalletsOrigin, ScheduleTaskIndex, T::BlockNumber), /// A scheduled call is cancelled. [origin, index] Cancelled(T::PalletsOrigin, ScheduleTaskIndex), + /// A call is authorized. \[hash, caller\] + AuthorizedCall(T::Hash, Option), + /// An authorized call was removed. \[hash\] + RemovedAuthorizedCall(T::Hash), + /// An authorized call was triggered. \[hash, caller\] + TriggeredCallBy(T::Hash, T::AccountId), } #[pallet::storage] #[pallet::getter(fn next_task_index)] pub type NextTaskIndex = StorageValue<_, ScheduleTaskIndex, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn saved_calls)] + pub type SavedCalls = StorageMap<_, Identity, T::Hash, (CallOf, Option), OptionQuery>; + #[pallet::pallet] pub struct Pallet(_); @@ -325,5 +341,63 @@ pub mod module { Self::deposit_event(Event::Cancelled(*initial_origin, task_id)); Ok(()) } + + #[pallet::weight(T::WeightInfo::authorize_call())] + pub fn authorize_call( + origin: OriginFor, + call: Box>, + caller: Option, + ) -> DispatchResult { + ensure_root(origin)?; + let hash = T::Hashing::hash_of(&call); + SavedCalls::::insert(hash, (call, caller.clone())); + Self::deposit_event(Event::AuthorizedCall(hash, caller)); + Ok(()) + } + + #[pallet::weight(T::WeightInfo::remove_authorized_call())] + pub fn remove_authorized_call(origin: OriginFor, hash: T::Hash) -> DispatchResult { + let root_or_sigend = + EnsureOneOf::, EnsureSigned>::ensure_origin( + origin, + )?; + + SavedCalls::::try_mutate_exists(hash, |maybe_call| { + let (_, maybe_caller) = maybe_call.take().ok_or(Error::::CallNotAuthorized)?; + match root_or_sigend { + Either::Left(_) => {} // root, do nothing + Either::Right(who) => { + // signed, ensure it's the caller + let caller = maybe_caller.ok_or(Error::::CallNotAuthorized)?; + ensure!(who == caller, Error::::CallNotAuthorized); + } + } + Self::deposit_event(Event::RemovedAuthorizedCall(hash)); + Ok(()) + }) + } + + #[pallet::weight(T::WeightInfo::trigger_call().saturating_add(*call_weight_bound))] + pub fn trigger_call( + origin: OriginFor, + hash: T::Hash, + #[pallet::compact] call_weight_bound: Weight, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + SavedCalls::::try_mutate_exists(hash, |maybe_call| { + let (call, maybe_caller) = maybe_call.take().ok_or(Error::::CallNotAuthorized)?; + if let Some(caller) = maybe_caller { + ensure!(who == caller, Error::::TriggerCallNotPermitted); + } + ensure!( + call_weight_bound >= call.get_dispatch_info().weight, + Error::::WrongCallWeightBound + ); + let result = call.dispatch(OriginFor::::root()); + Self::deposit_event(Event::TriggeredCallBy(hash, who)); + Self::deposit_event(Event::Dispatched(result.map(|_| ()).map_err(|e| e.error))); + Ok(()) + }) + } } } diff --git a/authority/src/tests.rs b/authority/src/tests.rs index 593919af9..530089ac0 100644 --- a/authority/src/tests.rs +++ b/authority/src/tests.rs @@ -404,3 +404,132 @@ fn call_size_limit() { If the limit is too strong, maybe consider increasing the limit", ); } + +#[test] +fn authorize_call_works() { + ExtBuilder::default().build().execute_with(|| { + run_to_block(1); + let ensure_root_call = Call::System(frame_system::Call::fill_block(Perbill::one())); + let call = Call::Authority(authority::Call::dispatch_as( + MockAsOriginId::Root, + Box::new(ensure_root_call), + )); + let hash = ::Hashing::hash_of(&call); + + // works without account + assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None)); + assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), None))); + System::assert_last_event(mock::Event::Authority(Event::AuthorizedCall(hash, None))); + + // works with account + assert_ok!(Authority::authorize_call( + Origin::root(), + Box::new(call.clone()), + Some(1) + )); + assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), Some(1)))); + System::assert_last_event(mock::Event::Authority(Event::AuthorizedCall(hash, Some(1)))); + }); +} + +#[test] +fn trigger_call_works() { + ExtBuilder::default().build().execute_with(|| { + run_to_block(1); + let ensure_root_call = Call::System(frame_system::Call::fill_block(Perbill::one())); + let call = Call::Authority(authority::Call::dispatch_as( + MockAsOriginId::Root, + Box::new(ensure_root_call), + )); + let hash = ::Hashing::hash_of(&call); + + let call_weight_bound = call.get_dispatch_info().weight; + + // call not authorized yet + assert_noop!( + Authority::trigger_call(Origin::signed(1), hash, call_weight_bound), + Error::::CallNotAuthorized + ); + + assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None)); + + // wrong call weight bound + assert_noop!( + Authority::trigger_call(Origin::signed(1), hash, call_weight_bound - 1), + Error::::WrongCallWeightBound + ); + + // works without caller + assert_ok!(Authority::trigger_call(Origin::signed(1), hash, call_weight_bound)); + assert_eq!(Authority::saved_calls(&hash), None); + System::assert_has_event(mock::Event::Authority(Event::TriggeredCallBy(hash, 1))); + System::assert_last_event(mock::Event::Authority(Event::Dispatched(Ok(())))); + + // works with caller 1 + assert_ok!(Authority::authorize_call( + Origin::root(), + Box::new(call.clone()), + Some(1) + )); + // caller 2 is not permitted to trigger the call + assert_noop!( + Authority::trigger_call(Origin::signed(2), hash, call_weight_bound), + Error::::TriggerCallNotPermitted + ); + assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), Some(1)))); + + // caller 1 triggering the call + assert_ok!(Authority::trigger_call(Origin::signed(1), hash, call_weight_bound)); + assert_eq!(Authority::saved_calls(&hash), None); + System::assert_has_event(mock::Event::Authority(Event::TriggeredCallBy(hash, 1))); + System::assert_last_event(mock::Event::Authority(Event::Dispatched(Ok(())))); + }); +} + +#[test] +fn remove_authorized_call_works() { + ExtBuilder::default().build().execute_with(|| { + run_to_block(1); + let ensure_root_call = Call::System(frame_system::Call::fill_block(Perbill::one())); + let call = Call::Authority(authority::Call::dispatch_as( + MockAsOriginId::Root, + Box::new(ensure_root_call), + )); + let hash = ::Hashing::hash_of(&call); + + assert_noop!( + Authority::remove_authorized_call(Origin::root(), hash), + Error::::CallNotAuthorized + ); + + assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None)); + assert_noop!( + Authority::remove_authorized_call(Origin::signed(1), hash), + Error::::CallNotAuthorized + ); + assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), None))); + assert_ok!(Authority::remove_authorized_call(Origin::root(), hash)); + assert_eq!(Authority::saved_calls(&hash), None); + + assert_ok!(Authority::authorize_call( + Origin::root(), + Box::new(call.clone()), + Some(1) + )); + assert_ok!(Authority::remove_authorized_call(Origin::root(), hash)); + assert_eq!(Authority::saved_calls(&hash), None); + + assert_ok!(Authority::authorize_call( + Origin::root(), + Box::new(call.clone()), + Some(1) + )); + assert_noop!( + Authority::remove_authorized_call(Origin::signed(2), hash), + Error::::CallNotAuthorized + ); + assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), Some(1)))); + assert_ok!(Authority::remove_authorized_call(Origin::signed(1), hash)); + assert_eq!(Authority::saved_calls(&hash), None); + }); +} diff --git a/authority/src/weights.rs b/authority/src/weights.rs index 59c1354f1..8d595441e 100644 --- a/authority/src/weights.rs +++ b/authority/src/weights.rs @@ -1,11 +1,11 @@ //! Autogenerated weights for orml_authority //! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 -//! DATE: 2021-05-04, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2021-09-13, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 // Executed Command: -// /Users/xiliangchen/projects/acala/target/release/acala +// /Users/ermal/Acala/target/release/acala // benchmark // --chain=dev // --steps=50 @@ -15,9 +15,8 @@ // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 +// --template=../templates/orml-weight-template.hbs // --output=./authority/src/weights.rs -// --template -// ../templates/orml-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -36,36 +35,53 @@ pub trait WeightInfo { fn fast_track_scheduled_dispatch() -> Weight; fn delay_scheduled_dispatch() -> Weight; fn cancel_scheduled_dispatch() -> Weight; + fn authorize_call() -> Weight; + fn remove_authorized_call() -> Weight; + fn trigger_call() -> Weight; } /// Default weights. impl WeightInfo for () { fn dispatch_as() -> Weight { - (10_000_000 as Weight) + (12_000_000 as Weight) } fn schedule_dispatch_without_delay() -> Weight { - (28_000_000 as Weight) + (30_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } fn schedule_dispatch_with_delay() -> Weight { - (29_000_000 as Weight) + (32_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } fn fast_track_scheduled_dispatch() -> Weight { - (36_000_000 as Weight) + (42_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } fn delay_scheduled_dispatch() -> Weight { - (36_000_000 as Weight) + (42_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } fn cancel_scheduled_dispatch() -> Weight { - (24_000_000 as Weight) + (29_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } + fn authorize_call() -> Weight { + (14_000_000 as Weight) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn remove_authorized_call() -> Weight { + (16_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn trigger_call() -> Weight { + (29_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } }