Adds ability to trigger tasks via unsigned transactions (#4075)

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 <git@kchr.de>
This commit is contained in:
gupnik
2024-04-24 11:25:54 +05:30
committed by GitHub
parent ffbce2a817
commit 0a56d071c7
7 changed files with 126 additions and 5 deletions
+36 -2
View File
@@ -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::*;
@@ -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> {
@@ -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;
}
@@ -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;
@@ -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());
}
}
@@ -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")]
@@ -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
View File
@@ -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:
@@ -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.
+13 -3
View File
@@ -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())
}
@@ -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())
}
}