Skip to content

Commit

Permalink
Adds ability to trigger tasks via unsigned transactions (#4075)
Browse files Browse the repository at this point in the history
This PR updates the `validate_unsigned` hook for `frame_system` to allow
valid tasks to be submitted as unsigned transactions. It also updates
the task example to be able to submit such transactions via an off-chain
worker.

---------

Co-authored-by: Bastian Köcher <[email protected]>
  • Loading branch information
codekitz and bkchr authored Apr 24, 2024
1 parent ffbce2a commit 0a56d07
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 5 deletions.
19 changes: 19 additions & 0 deletions prdoc/pr_4075.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
title: Adds ability to trigger tasks via unsigned transactions

doc:
- audience: Runtime Dev
description: |
This PR updates the `validate_unsigned` hook for `frame_system` to allow valid tasks
to be submitted as unsigned transactions. It also updates the task example to be able to
submit such transactions via an off-chain worker.

Note that `is_valid` call on a task MUST be cheap with minimal to no storage reads.
Else, it can make the blockchain vulnerable to DoS attacks.

Further, these tasks will be executed in a random order.

crates:
- name: frame-system
bump: patch
- name: pallet-example-tasks
bump: minor
38 changes: 36 additions & 2 deletions substrate/frame/examples/tasks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
#![cfg_attr(not(feature = "std"), no_std)]

use frame_support::dispatch::DispatchResult;
use frame_system::offchain::SendTransactionTypes;
#[cfg(feature = "experimental")]
use frame_system::offchain::SubmitTransaction;
// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;

Expand All @@ -31,10 +34,14 @@ mod benchmarking;
pub mod weights;
pub use weights::*;

#[cfg(feature = "experimental")]
const LOG_TARGET: &str = "pallet-example-tasks";

#[frame_support::pallet(dev_mode)]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;

#[pallet::error]
pub enum Error<T> {
Expand All @@ -59,9 +66,36 @@ pub mod pallet {
}
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "experimental")]
fn offchain_worker(_block_number: BlockNumberFor<T>) {
if let Some(key) = Numbers::<T>::iter_keys().next() {
// Create a valid task
let task = Task::<T>::AddNumberIntoTotal { i: key };
let runtime_task = <T as Config>::RuntimeTask::from(task);
let call = frame_system::Call::<T>::do_task { task: runtime_task.into() };

// Submit the task as an unsigned transaction
let res =
SubmitTransaction::<T, frame_system::Call<T>>::submit_unsigned_transaction(
call.into(),
);
match res {
Ok(_) => log::info!(target: LOG_TARGET, "Submitted the task."),
Err(e) => log::error!(target: LOG_TARGET, "Error submitting task: {:?}", e),
}
}
}
}

#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeTask: frame_support::traits::Task;
pub trait Config:
SendTransactionTypes<frame_system::Call<Self>> + frame_system::Config
{
type RuntimeTask: frame_support::traits::Task
+ IsType<<Self as frame_system::Config>::RuntimeTask>
+ From<Task<Self>>;
type WeightInfo: WeightInfo;
}

Expand Down
21 changes: 21 additions & 0 deletions substrate/frame/examples/tasks/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

use crate::{self as tasks_example};
use frame_support::derive_impl;
use sp_runtime::testing::TestXt;

pub type AccountId = u32;
pub type Balance = u32;
Expand All @@ -32,12 +33,32 @@ frame_support::construct_runtime!(
}
);

pub type Extrinsic = TestXt<RuntimeCall, ()>;

#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type Block = Block;
}

impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
where
RuntimeCall: From<LocalCall>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = Extrinsic;
}

impl tasks_example::Config for Runtime {
type RuntimeTask = RuntimeTask;
type WeightInfo = ();
}

pub fn advance_to(b: u64) {
#[cfg(feature = "experimental")]
use frame_support::traits::Hooks;
while System::block_number() < b {
System::set_block_number(System::block_number() + 1);
#[cfg(feature = "experimental")]
TasksExample::offchain_worker(System::block_number());
}
}
30 changes: 30 additions & 0 deletions substrate/frame/examples/tasks/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
#![cfg(test)]

use crate::{mock::*, Numbers};
#[cfg(feature = "experimental")]
use codec::Decode;
use frame_support::traits::Task;
#[cfg(feature = "experimental")]
use sp_core::offchain::{testing, OffchainWorkerExt, TransactionPoolExt};
use sp_runtime::BuildStorage;

#[cfg(feature = "experimental")]
Expand Down Expand Up @@ -130,3 +134,29 @@ fn task_execution_fails_for_invalid_task() {
);
});
}

#[cfg(feature = "experimental")]
#[test]
fn task_with_offchain_worker() {
let (offchain, _offchain_state) = testing::TestOffchainExt::new();
let (pool, pool_state) = testing::TestTransactionPoolExt::new();

let mut t = sp_io::TestExternalities::default();
t.register_extension(OffchainWorkerExt::new(offchain));
t.register_extension(TransactionPoolExt::new(pool));

t.execute_with(|| {
advance_to(1);
assert!(pool_state.read().transactions.is_empty());

Numbers::<Runtime>::insert(0, 10);
assert_eq!(crate::Total::<Runtime>::get(), (0, 0));

advance_to(2);

let tx = pool_state.write().transactions.pop().unwrap();
assert!(pool_state.read().transactions.is_empty());
let tx = Extrinsic::decode(&mut &*tx).unwrap();
assert_eq!(tx.signature, None);
});
}
3 changes: 3 additions & 0 deletions substrate/frame/support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2465,6 +2465,9 @@ pub mod pallet_macros {
/// Finally, the `RuntimeTask` can then used by a script or off-chain worker to create and
/// submit such tasks via an extrinsic defined in `frame_system` called `do_task`.
///
/// When submitted as unsigned transactions (for example via an off-chain workder), note
/// that the tasks will be executed in a random order.
///
/// ## Example
#[doc = docify::embed!("src/tests/tasks.rs", tasks_example)]
/// Now, this can be executed as follows:
Expand Down
4 changes: 4 additions & 0 deletions substrate/frame/support/src/traits/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ pub trait Task: Sized + FullCodec + TypeInfo + Clone + Debug + PartialEq + Eq {
fn iter() -> Self::Enumeration;

/// Checks if a particular instance of this `Task` variant is a valid piece of work.
///
/// This is used to validate tasks for unsigned execution. Hence, it MUST be cheap
/// with minimal to no storage reads. Else, it can make the blockchain vulnerable
/// to DoS attacks.
fn is_valid(&self) -> bool;

/// Performs the work for this particular `Task` variant.
Expand Down
16 changes: 13 additions & 3 deletions substrate/frame/system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,9 +741,7 @@ pub mod pallet {
#[cfg(feature = "experimental")]
#[pallet::call_index(8)]
#[pallet::weight(task.weight())]
pub fn do_task(origin: OriginFor<T>, task: T::RuntimeTask) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;

pub fn do_task(_origin: OriginFor<T>, task: T::RuntimeTask) -> DispatchResultWithPostInfo {
if !task.is_valid() {
return Err(Error::<T>::InvalidTask.into())
}
Expand Down Expand Up @@ -1032,6 +1030,18 @@ pub mod pallet {
})
}
}
#[cfg(feature = "experimental")]
if let Call::do_task { ref task } = call {
if task.is_valid() {
return Ok(ValidTransaction {
priority: u64::max_value(),
requires: Vec::new(),
provides: vec![T::Hashing::hash_of(&task.encode()).as_ref().to_vec()],
longevity: TransactionLongevity::max_value(),
propagate: true,
})
}
}
Err(InvalidTransaction::Call.into())
}
}
Expand Down

0 comments on commit 0a56d07

Please sign in to comment.