Add offchain workers
Please read Substrate to Polkadot SDK page first.
This tutorial illustrates how to modify a pallet to include an offchain worker and configure the pallet and runtime to enable the offchain worker to submit transactions that update the on-chain state.
Using offchain workers
If you use offchain workers to perform long-running computations or fetch data from offline sources, it's likely that you'll want to store the results of those operations on-chain. However, offchain storage is separate from on-chain resources and you can't save data processed by offchain workers directly to on-chain storage. To store any data processed by offchain workers as part of the on-chain state, you must create transactions to send the data from the offchain worker storage to the on-chain storage system.
This tutorial illustrates how to create offchain workers with the ability to send signed or unsigned transactions to store offchain data on-chain. In general, signed transactions are more secure, but require the calling account to handle transaction fees. For example:
- Use signed transactions if you want to record the associated transaction caller account and deduct the transaction fee from the caller account.
- Use unsigned transactions with signed payload if you want to record the associated transaction caller, but do not want the caller be responsible for the transaction fee payment.
Working with unsigned transactions
It's also possible to submit unsigned transactions without a signed payload—for example, because you don't want to record the associated transaction caller at all.
However, there's significant risk in allowing unsigned transactions to modify the chain state.
Unsigned transactions represent a potential attack vector that a malicious user could exploit.
If you are going to allow offchain workers to send unsigned transactions, you should include logic that ensures the transaction is authorized.
For an example of how unsigned transactions can be verified using on-chain state, see the ValidateUnsigned
implementation in the enact_authorized_upgrade
call.
In that example, the call validates the unsigned transaction by verifying that the given code hash was previously authorized.
It is also important to consider that even an unsigned transaction with a signed payload could be exploited because offchain workers can't be assumed to be a reliable source unless you implement strict logic to check the validity of the transaction. In most cases, checking whether a transaction was submitted by an offchain worker before writing to storage isn't sufficient to protect the network. Instead of assuming that the offchain worker can be trusted without safeguards, you should intentionally set restrictive permissions that limit access to the process and what it can do.
Remember that unsigned transactions are essentially an open door into your runtime. You should only use them after careful consideration of the conditions under which they should be allowed to execute. Without safeguards, malicious actors could impersonate offchain workers and access runtime storage.
Before you begin
Before you begin, verify the following:
- You have configured your environment for Substrate development by installing Rust and the Rust toolchain.
- You have completed the Build a local blockchain tutorial and have the Substrate node template from the Developer Hub installed locally.
- You are familiar with how to use FRAME macros and edit the logic for a pallet.
- You are familiar with how to modify the configuration trait for a pallet in the runtime.
Tutorial objectives
By completing this tutorial, you will be able to:
- Identify the risks involved in using unsigned transactions.
- Add an offchain worker function to a pallet.
- Configure the pallet and the runtime to enable the offchain worker to submit signed transactions.
- Configure the pallet and the runtime to enable the offchain worker to submit unsigned transactions.
- Configure the pallet and the runtime to enable the offchain worker to submit unsigned transactions with a signed payload.
Signed transactions
To submit signed transactions, you must configure your pallet and the runtime to enable at least one account for offchain workers to use. At a high level, configuring a pallet to use an office chain worker and submit signed transactions involves the following steps:
- Configure the offchain worker in the pallet.
- Implement the pallet and required traits in the runtime.
- Add an account for signing transactions.
Configure the offchain worker in the pallet
To enable offchain workers to send signed transactions:
- Open the
src/lib.rs
file for your pallet in a text editor. -
Add the
#[pallet::hooks]
macro and the entry point for offchain workers to the code.For example:
#[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { /// Offchain worker entry point. /// /// By implementing `fn offchain_worker` you declare a new offchain worker. /// This function will be called when the node is fully synced and a new best block is /// successfully imported. /// Note that it's not guaranteed for offchain workers to run on EVERY block, there might /// be cases where some blocks are skipped, or for some the worker runs twice (re-orgs), /// so the code should be able to handle that. fn offchain_worker(block_number: T::BlockNumber) { log::info!("Hello from pallet-ocw."); // The entry point of your code called by offchain worker } // ... }
- Add the logic for the
offchain_worker
function. -
Add
CreateSignedTransaction
to theConfig
trait for your pallet. For example, your palletConfig
trait should look similar to this:/// This pallet's configuration trait #[pallet::config] pub trait Config: CreateSignedTransaction<Call<Self>> + frame_system::Config { // ... }
-
Add an
AuthorityId
type to the palletConfig
trait:#[pallet::config] pub trait Config: CreateSignedTransaction<Call<Self>> + frame_system::Config { // ... type AuthorityId: AppCrypto<Self::Public, Self::Signature>; }
-
Add a
crypto
module with ansr25519
signature key to ensure that your pallet owns an account that can be used for signing transactions.use sp_core::{crypto::KeyTypeId}; // ... pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"demo"); // ... pub mod crypto { use super::KEY_TYPE; use sp_core::sr25519::Signature as Sr25519Signature; use sp_runtime::{ app_crypto::{app_crypto, sr25519}, traits::Verify, MultiSignature, MultiSigner }; app_crypto!(sr25519, KEY_TYPE); pub struct TestAuthId; // implemented for runtime impl frame_system::offchain::AppCrypto<MultiSigner, MultiSignature> for TestAuthId { type RuntimeAppPublic = Public; type GenericSignature = sp_core::sr25519::Signature; type GenericPublic = sp_core::sr25519::Public; } }
The
app_crypto
macro declares an account with ansr25519
signature that is identified byKEY_TYPE
. In this example, theKEY_TYPE
isdemo
. Note that this macro doesn't create a new account. The macro simply declares that acrypto
account is available for this pallet to use. -
Initialize an account for the offchain worker to use to send a signed transaction to on-chain storage.
fn offchain_worker(block_number: T::BlockNumber) { let signer = Signer::<T, T::AuthorityId>::all_accounts(); // ... }
This code enables you to retrieve all signers that this pallet owns.
-
Use
send_signed_transaction()
to create a signed transaction call:fn offchain_worker(block_number: T::BlockNumber) { let signer = Signer::<T, T::AuthorityId>::all_accounts(); // Using `send_signed_transaction` associated type we create and submit a transaction // representing the call we've just created. // `send_signed_transaction()` return type is `Option<(Account<T>, Result<(), ()>)>`. It is: // - `None`: no account is available for sending transaction // - `Some((account, Ok(())))`: transaction is successfully sent // - `Some((account, Err(())))`: error occurred when sending the transaction let results = signer.send_signed_transaction(|_account| { Call::on_chain_call { key: val } }); // ... }
-
Check if the transaction is successfully submitted on-chain and perform proper error handling by checking the returned
results
.fn offchain_worker(block_number: T::BlockNumber) { // ... for (acc, res) in &results { match res { Ok(()) => log::info!("[{:?}]: submit transaction success.", acc.id), Err(e) => log::error!("[{:?}]: submit transaction failure. Reason: {:?}", acc.id, e), } } Ok(()) }
Implement the pallet in the runtime
- Open the
runtime/src/lib.rs
file for the node template in a text editor. -
Add the
AuthorityId
to the configuration for your pallet and make sure it uses theTestAuthId
from thecrypto
module:impl pallet_your_ocw_pallet::Config for Runtime { // ... type AuthorityId = pallet_your_ocw_pallet::crypto::TestAuthId; }
-
Implement the
CreateSignedTransaction
trait in the runtime.Because you configured your pallet to implement the
CreateSignedTransaction
trait, you also need to implement that trait for the runtime.By looking at
CreateSignedTransaction
, you can see that you only need to implement the functioncreate_transaction()
for the runtime. For example:use codec::Encode; use sp_runtime::{generic::Era, SaturatedConversion}; // ... impl<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> for Runtime where RuntimeCall: From<LocalCall>, { fn create_transaction<C: frame_system::offchain::AppCrypto<Self::Public, Self::Signature>>( call: RuntimeCall, public: <Signature as Verify>::Signer, account: AccountId, nonce: Nonce, ) -> Option<(RuntimeCall, <UncheckedExtrinsic as traits::Extrinsic>::SignaturePayload)> { let tip = 0; // take the biggest period possible. let period = BlockHashCount::get().checked_next_power_of_two().map(|c| c / 2).unwrap_or(2) as u64; let current_block = System::block_number() .saturated_into::<u64>() // The `System::block_number` is initialized with `n+1`, // so the actual block number is `n`. .saturating_sub(1); let era = Era::mortal(period, current_block); let extra = ( frame_system::CheckNonZeroSender::<Runtime>::new(), frame_system::CheckSpecVersion::<Runtime>::new(), frame_system::CheckTxVersion::<Runtime>::new(), frame_system::CheckGenesis::<Runtime>::new(), frame_system::CheckEra::<Runtime>::from(era), frame_system::CheckNonce::<Runtime>::from(nonce), frame_system::CheckWeight::<Runtime>::new(), pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip), ); let raw_payload = SignedPayload::new(call, extra) .map_err(|e| { log::warn!("Unable to create signed payload: {:?}", e); }) .ok()?; let signature = raw_payload.using_encoded(|payload| C::sign(payload, public))?; let address = account; let (call, extra, _) = raw_payload.deconstruct(); Some((call, (sp_runtime::MultiAddress::Id(address), signature, extra))) } }
This code snippet is long, but, in essence, it illustrates the following main steps:
- Create and prepare
extra
ofSignedExtra
type, and put various checkers in place. - Create a raw payload based on the passed in
call
andextra
. - Sign the raw payload with the account public key.
- Bundle all data up and return a tuple of the call, the caller, its signature, and any signed extension data.
You can see an example of this code in the Substrate code base.
- Create and prepare
-
Implement
SigningTypes
andSendTransactionTypes
in the runtime to support submitting transactions, whether they are signed or unsigned.impl frame_system::offchain::SigningTypes for Runtime { type Public = <Signature as traits::Verify>::Signer; type Signature = Signature; } impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime where RuntimeCall: From<C>, { type Extrinsic = UncheckedExtrinsic; type OverarchingCall = RuntimeCall; }
You can see an example of this implementation in the Substrate code base.
Add an account for signing transactions
At this point, you have prepared your pallet to use offchain workers. Preparing the pallet involved the following steps:
- Adding the
offchain_worker
function and related logic for sending signed transactions. - Adding
CreateSignedTransaction
andAuthorityId
to theConfig
trait for your pallet. - Adding the
crypto
module to describe the account the pallet will use to sign transaction.
You have also updated the runtime with the code to support offchain workers and sending signed transactions. Updating the runtime involved the following steps:
- Adding the
AuthorityId
to the runtime configuration for your pallet. - Implementing the
CreateSignedTransaction
trait andcreate_transaction()
function. - Implementing
SigningTypes
andSendTransactionTypes
for offchain workers from theframe_system
pallet.
However, before your pallet offchain workers can submit signed transactions, you must specify at least one account for the offchain worker to use. To enable the offchain worker to sign transactions, you must generate the account key for the pallet to own and add that key to the node keystore.
There are several ways to accomplish this final step and the method you choose might vary depending on whether you are running a node in development mode for testing, using a custom chain specification, or deploying into a production environment.
Using a development account
If you are running a node in development mode—with --dev
command-line option—you can manually generate and insert the account key for a development account by modifying the node/src/service.rs
file as follows:
pub fn new_partial(config: &Configuration) -> Result <SomeStruct, SomeError> {
//...
if config.offchain_worker.enabled {
// Initialize seed for signing transaction using offchain workers. This is a convenience
// so learners can see the transactions submitted simply running the node.
// Typically these keys should be inserted with RPC calls to `author_insertKey`.
sp_keystore::SyncCryptoStore::sr25519_generate_new(
&*keystore,
node_template_runtime::pallet_your_ocw_pallet::KEY_TYPE,
Some("//Alice"),
).expect("Creating key with account Alice should succeed.");
}
}
This example manually adds the key for the Alice
account to the keystore identified by the KEY_TYPE
defined in your pallet.
For a working example, see this sample service.rs file.
Using other accounts
In a production environment, you can use other tools—such as subkey
—to generate keys that are specifically for offchain workers to use.
After you generate one or more keys for offchain workers to own, you can add them to the node keystore by:
- Modifying the configuration of your chain specification file.
- Passing parameters using the
author_insertKey
RPC method.
For example, you can use the Polkadot/Substrate Portal, Polkadot-JS API, or a curl
command to select the author_insertKey
method and specify the key type, secret phrase, and public key parameters for the account to use:
Note that the keyType parameter demo
in this example matches the KEY_TYPE
declared in the offchain worker pallet.
Now, your pallet is ready to send signed transactions on-chain from offchain workers.
Unsigned transactions
By default, all unsigned transactions are rejected in Substrate.
To enable Substrate to accept certain unsigned transactions, you must implement the ValidateUnsigned
trait for the pallet.
Although you must implement the ValidateUnsigned
trait to send unsigned transactions, this check doesn't guarantee that only offchain workers are able to send the transaction.
You should always consider the consequences of malicious actors sending these transactions as an attempt to tamper with the state of your chain.
Unsigned transactions always represent a potential attack vector that a malicious user could exploit and offchain workers can't be assumed to be a reliable source without additional safeguards.
You should never assume that unsigned transactions can only be submitted by an offchain worker. By definition, anyone can submit them.
Configure the pallet
To enable offchain workers to send unsigned transactions:
- Open the
src/lib.rs
file for your pallet in a text editor. -
Add the
validate_unsigned
macro.For example:
#[pallet::validate_unsigned] impl<T: Config> ValidateUnsigned for Pallet<T> { type Call = Call<T>; /// Validate unsigned call to this module. /// /// By default unsigned transactions are disallowed, but implementing the validator /// here we make sure that some particular calls (the ones produced by offchain worker) /// are being whitelisted and marked as valid. fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { //... } }
-
Implement the trait as follows:
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { let valid_tx = |provide| ValidTransaction::with_tag_prefix("my-pallet") .priority(UNSIGNED_TXS_PRIORITY) // please define `UNSIGNED_TXS_PRIORITY` before this line .and_provides([&provide]) .longevity(3) .propagate(true) .build(); // ... }
-
Check the calling extrinsics to determine if the call is allowed and return
ValidTransaction
if the call is allowed orTransactionValidityError
if the call is not allowed.For example:
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { // ... match call { RuntimeCall::my_unsigned_tx { key: value } => valid_tx(b"my_unsigned_tx".to_vec()), _ => InvalidTransaction::Call.into(), } }
In this example, users can only call the specific
my_unsigned_tx
function without a signature. If there are other functions, calling them would require a signed transaction.For an example of how
ValidateUnsigned
is implemented in a pallet, see the code for the offchain-worker. -
Add the
#[pallet::hooks]
macro and theoffchain_worker
function to send unsigned transactions as follows:#[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { /// Offchain worker entry point. fn offchain_worker(block_number: T::BlockNumber) { let value: u64 = 10; // This is your call to on-chain extrinsic together with any necessary parameters. let call = RuntimeCall::unsigned_extrinsic1 { key: value }; // `submit_unsigned_transaction` returns a type of `Result<(), ()>` // ref: https://paritytech.github.io/substrate/master/frame_system/offchain/struct.SubmitTransaction.html SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) .map_err(|_| { log::error!("Failed in offchain_unsigned_tx"); }); } }
This code prepares the call in the
let call = ...
line, submits the transaction usingSubmitTransaction::submit_unsigned_transaction
, and performs any necessary error handling in the callback function passed in.
Configure the runtime
-
Enable the
ValidateUnsigned
trait for the pallet in the runtime by adding theValidateUnsigned
type to theconstruct_runtime
macro.For example:
construct_runtime!( pub enum Runtime where Block = Block, NodeBlock = opaque::Block, UncheckedExtrinsic = UncheckedExtrinsic { // ... OcwPallet: pallet_ocw::{Pallet, Call, Storage, Event<T>, ValidateUnsigned}, } );
-
Implement the
SendTransactionTypes
trait for the runtime as described in sending signed transactions.For a full example, see the [offchain-worker](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/examples/offchain-worker examples pallet.
Signed payloads
Sending unsigned transactions with signed payloads is similar to sending unsigned transactions. You need to:
- Implement the
ValidateUnsigned
trait for the pallet. - Add the
ValidateUnsigned
type to the runtime when using this pallet. - Prepare the data structure to be signed—the signed payload—by implementing the
SignedPayload
trait. - Send the transaction with the signed payload.
You can refer to the section on sending unsigned transactions for more information about implementing the ValidateUnsigned
trait and adding the ValidateUnsigned
type to the runtime.
Keep in mind that unsigned transactions always represent a potential attack vector and that offchain workers can't be assumed to be a reliable source without additional safeguards. In most cases, you should implement restrictive permissions or additional logic to verify the transaction submitted by an offchain worker is valid.
The differences between sending unsigned transactions and sending unsigned transactions with signed payload are illustrated in the following code examples.
To make your data structure signable:
-
Implement
SignedPayload
.For example:
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, scale_info::TypeInfo)] pub struct Payload<Public> { number: u64, public: Public, } impl<T: SigningTypes> SignedPayload<T> for Payload<T::Public> { fn public(&self) -> T::Public { self.public.clone() } }
For an example of a signed payload, see the code for the offchain-worker.
-
In the
offchain_worker
function, call the signer, then the function to send the transaction:#[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { /// Offchain worker entry point. fn offchain_worker(block_number: T::BlockNumber) { let value: u64 = 10; // Retrieve the signer to sign the payload let signer = Signer::<T, T::AuthorityId>::any_account(); // `send_unsigned_transaction` is returning a type of `Option<(Account<T>, Result<(), ()>)>`. // The returned result means: // - `None`: no account is available for sending transaction // - `Some((account, Ok(())))`: transaction is successfully sent // - `Some((account, Err(())))`: error occurred when sending the transaction if let Some((_, res)) = signer.send_unsigned_transaction( // this line is to prepare and return payload |acct| Payload { number, public: acct.public.clone() }, |payload, signature| RuntimeCall::some_extrinsics { payload, signature }, ) { match res { Ok(()) => log::info!("unsigned tx with signed payload successfully sent."); Err(()) => log::error!("sending unsigned tx with signed payload failed."); }; } else { // The case of `None`: no account is available for sending log::error!("No local account available"); } } }
This code retrieves the
signer
then callssend_unsigned_transaction()
with two function closures. The first function closure returns the payload to be used. The second function closure returns the on-chain call with payload and signature passed in. This call returns anOption<(Account<T>, Result<(), ()>)>
result type to allow for the following results:None
if no account is available for sending the transaction.Some((account, Ok(())))
if the transaction is successfully sent.Some((account, Err(())))
if an error occurs when sending the transaction.
-
Check whether a provided signature matches the public key used to sign the payload:
#[pallet::validate_unsigned] impl<T: Config> ValidateUnsigned for Pallet<T> { type Call = Call<T>; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { let valid_tx = |provide| ValidTransaction::with_tag_prefix("ocw-demo") .priority(UNSIGNED_TXS_PRIORITY) .and_provides([&provide]) .longevity(3) .propagate(true) .build(); match call { RuntimeCall::unsigned_extrinsic_with_signed_payload { ref payload, ref signature } => { if !SignedPayload::<T>::verify::<T::AuthorityId>(payload, signature.clone()) { return InvalidTransaction::BadProof.into(); } valid_tx(b"unsigned_extrinsic_with_signed_payload".to_vec()) }, _ => InvalidTransaction::Call.into(), } } }
This example uses
SignedPayload
to verify that the public key in the payload has the same signature as the one provided. However, you should note that the code in the example only checks whether the providedsignature
is valid for thepublic
key contained insidepayload
. This check doesn't validate whether the signer is an offchain worker or authorized to call the specified function. This simple check wouldn't prevent an unauthorized actor from using the signed payload to modify state.For working examples of this code, see the offchain function call and the implementation of
ValidateUnsigned
.
Where to go next
This tutorial provides simple examples of how you can use offchain workers to send transactions for on-chain storage. To learn more, explore the following resources: