Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ability to trigger tasks via unsigned transactions #4075

Merged
merged 13 commits into from
Apr 24, 2024
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.
gupnik marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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())
ggwpez marked this conversation as resolved.
Show resolved Hide resolved
}
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() {
gupnik marked this conversation as resolved.
Show resolved Hide resolved
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
Loading