Skip to content
Merged
83 changes: 80 additions & 3 deletions authority/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down Expand Up @@ -172,6 +172,10 @@ pub mod module {
FailedToFastTrack,
/// Failed to delay a task.
FailedToDelay,
/// Call is not authorized.
CallNotAuthorized,
/// Triggering the call is not permitted.
TriggerCallNotPermitted,
}

#[pallet::event]
Expand All @@ -187,12 +191,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<T::AccountId>),
/// 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<T: Config> = StorageValue<_, ScheduleTaskIndex, ValueQuery>;

#[pallet::storage]
#[pallet::getter(fn saved_calls)]
pub type SavedCalls<T: Config> = StorageMap<_, Identity, T::Hash, (CallOf<T>, Option<T::AccountId>), OptionQuery>;

#[pallet::pallet]
pub struct Pallet<T>(_);

Expand Down Expand Up @@ -325,5 +339,68 @@ 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<T>,
call: Box<CallOf<T>>,
caller: Option<T::AccountId>,
) -> DispatchResult {
ensure_root(origin)?;
let hash = T::Hashing::hash_of(&call);
SavedCalls::<T>::insert(hash, (call, &caller));
Self::deposit_event(Event::AuthorizedCall(hash, caller));
Ok(())
}

#[pallet::weight(T::WeightInfo::remove_authorized_call())]
pub fn remove_authorized_call(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
let root_or_sigend =
EnsureOneOf::<T::AccountId, EnsureRoot<T::AccountId>, EnsureSigned<T::AccountId>>::ensure_origin(
origin,
)?;

SavedCalls::<T>::try_mutate_exists(hash, |maybe_call| {
let (_, maybe_caller) = maybe_call.take().ok_or(Error::<T>::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::<T>::CallNotAuthorized)?;
ensure!(who == caller, Error::<T>::CallNotAuthorized);
}
}
Self::deposit_event(Event::RemovedAuthorizedCall(hash));
Ok(())
})
}

#[pallet::weight(T::WeightInfo::trigger_call())]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to include the weight of the call. Add new #[pallet::compact] weight_bound: Weight and add it to the weight, and then ensure the call weight is lte the weight bound.
Something similar to this
https://github.com/paritytech/substrate/blob/ce1746ddf6588c8f9ecfb77411c3cac7a2c09db3/frame/collective/src/lib.rs#L662

pub fn trigger_call(origin: OriginFor<T>, hash: T::Hash) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
SavedCalls::<T>::try_mutate_exists(hash, |maybe_call| {
let (call, maybe_caller) = maybe_call.take().ok_or(Error::<T>::CallNotAuthorized)?;
if let Some(caller) = maybe_caller {
ensure!(who == caller, Error::<T>::TriggerCallNotPermitted);
}

Self::deposit_event(Event::TriggeredCallBy(hash, who));

let mut weight = T::WeightInfo::trigger_call();
let dispatch_info = call.get_dispatch_info();

let (post_info, result) = match call.dispatch(OriginFor::<T>::root()) {
Ok(post_info) => (post_info, Ok(())),
Err(e) => (e.post_info, Err(e.error)),
};

Self::deposit_event(Event::Dispatched(result));

if post_info.pays_fee(&dispatch_info) == Pays::Yes {
weight = weight.saturating_add(post_info.calc_actual_weight(&dispatch_info))
}
Ok(Some(weight).into())
})
}
}
}
132 changes: 132 additions & 0 deletions authority/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,135 @@ 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 = <Runtime as frame_system::Config>::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 = <Runtime as frame_system::Config>::Hashing::hash_of(&call);

// trigger_call_weight + call_weight
let mut weight = Call::Authority(authority::Call::trigger_call(hash))
.get_dispatch_info()
.weight;
weight = weight.saturating_add(call.get_dispatch_info().weight);

// call not authorized yet
assert_noop!(
Authority::trigger_call(Origin::signed(1), hash),
Error::<Runtime>::CallNotAuthorized
);

// works without caller
assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None));
assert_eq!(
Authority::trigger_call(Origin::signed(1), hash),
Ok(Some(weight).into())
);
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),
Error::<Runtime>::TriggerCallNotPermitted
);
assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), Some(1))));

// caller 1 triggering the call
assert_eq!(
Authority::trigger_call(Origin::signed(1), hash),
Ok(Some(weight).into())
);
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 = <Runtime as frame_system::Config>::Hashing::hash_of(&call);

assert_noop!(
Authority::remove_authorized_call(Origin::root(), hash),
Error::<Runtime>::CallNotAuthorized
);

assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None));
assert_noop!(
Authority::remove_authorized_call(Origin::signed(1), hash),
Error::<Runtime>::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::<Runtime>::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);
});
}
38 changes: 27 additions & 11 deletions authority/src/weights.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)]
Expand All @@ -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))
}
}