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
17 changes: 17 additions & 0 deletions prdoc/pr_4075.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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.

crates:
- name: frame-system
bump: patch
- name: pallet-example-tasks
bump: minor
32 changes: 30 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 @@ -35,6 +38,7 @@ pub use weights::*;
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 +63,33 @@ 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
SubmitTransaction::<T, frame_system::Call<T>>::submit_unsigned_transaction(
call.into(),
)
.map_err(|()| "Unable to submit unsigned transaction.")
.unwrap();
gupnik marked this conversation as resolved.
Show resolved Hide resolved
gupnik marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

#[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/traits/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ 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
10 changes: 7 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,12 @@ 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::default())
gupnik marked this conversation as resolved.
Show resolved Hide resolved
}
}
Err(InvalidTransaction::Call.into())
}
}
Expand Down
Loading