offchaincb.rs
(in runtime/src
)
Offchain Worker Callback Example
This is a minimal example module to show case how the runtime can and should interact with an offchain worker asynchronously.
This example plays simple ping-pong with authenticated off-chain workers:
Once a signed transaction to ping
is submitted, the runtime store Ping
request.
After every block the offchain worker is triggered. If it sees a Ping
request
in the current block, it reacts by sending a signed transaction to call
pong
. When pong
is called, it emits an Ack
event so it easy to track
with existing UIs whether the Ping-Pong-Ack happened.
However, because the pong
contains trusted information (the nonce
) the runtime
can't verify by itself - the key reason why we have the offchain worker in the
first place - we can't allow just anyone to call pong
. Instead the runtime has a
local list of authorities
-keys that allowed to evoke pong
. In this simple example
this list can only be extended via a root call (e.g. sudo
). In practice more
complex management models and session based key rotations should be considered, but
this is out of the scope of this example
Ensure we're no_std
when compiling for Wasm. Otherwise our Vec
and operations
on it will fail with invalid
.
#![cfg_attr(not(feature = "std"), no_std)]
We have to import a few things
use rstd::prelude::*;
use app_crypto::RuntimeAppPublic;
use support::{decl_module, decl_event, decl_storage, StorageValue, dispatch::Result};
use system::{ensure_signed, ensure_root};
use system::offchain::SubmitSignedTransaction;
use codec::{Encode, Decode};
Our local KeyType.
For security reasons the offchain worker doesn't have direct access to the keys
but only to app-specific subkeys, which are defined and grouped by their KeyTypeId
.
We define it here as ofcb
(for offchain callback
). Yours should be specific to
the module you are actually building.
pub const KEY_TYPE: app_crypto::KeyTypeId = app_crypto::KeyTypeId(*b"ofcb");
The module's main configuration trait.
pub trait Trait: system::Trait {
The regular events type, we use to emit the Ack
type Event:From<Event<Self>> + Into<<Self as system::Trait>::Event>;
A dispatchable call type. We need to define it for the offchain worker to
reference the pong
function it wants to call.
type Call: From<Call<Self>>;
Let's define the helper we use to create signed transactions with
type SubmitTransaction: SubmitSignedTransaction<Self, <Self as Trait>::Call>;
The local keytype
type KeyType: RuntimeAppPublic + From<Self::AccountId> + Into<Self::AccountId> + Clone;
}
The type of requests we can send to the offchain worker
#[derive(Encode, Decode)]
pub enum OffchainRequest<T: system::Trait> {
If an authorised offchain worker sees this ping, it shall respond with a pong
call
Ping(u8, <T as system::Trait>::AccountId)
}
We use the regular Event type to sent the final ack for the nonce
decl_event!(
pub enum Event<T> where AccountId = <T as system::Trait>::AccountId {
When we received a Pong, we also Ack it.
Ack(u8, AccountId),
}
);
We use storage in two important ways here:
- we have a local list of
OcRequests
, which are cleared at the beginning and then collected throughout a block - we store the list of authorities, from whom we accept
pong
calls.
decl_storage! {
trait Store for Module<T: Trait> as OffchainCb {
Requests made within this block execution
OcRequests: Vec<OffchainRequest<T>>;
The current set of keys that may submit pongs
Authorities get(authorities): Vec<T::AccountId>;
}
}
The actual Module definition. This is where we create the callable functions
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
Initializing events
fn deposit_event() = default;
Clean the state on initialisation of a block
fn on_initialize(_now: T::BlockNumber) {
At the beginning of each block execution, system triggers all
on_initialize
functions, which allows us to set up some temprorary
state or - like in this case - clean up other states
<Self as Store>::OcRequests::kill();
}
The entry point function: storing a Ping
offchain request with the given nonce
.
pub fn ping(origin, nonce: u8) -> Result {
It first ensures the function was signed, then it store the Ping
request
with our nonce and author. Finally it results with Ok
.
let who = ensure_signed(origin)?;
<Self as Store>::OcRequests::mutate(|v| v.push(OffchainRequest::Ping(nonce, who)));
Ok(())
}
Called from the offchain worker to respond to a ping
pub fn pong(origin, nonce: u8) -> Result {
We don't allow anyone to pong
but only those authorised in the authorities
set at this point. Therefore after ensuring this is singed, we check whether
that given author is allowed to pong
is. If so, we emit the Ack
event,
otherwise we've just consumed their fee.
let author = ensure_signed(origin)?;
if Self::is_authority(&author) {
Self::deposit_event(RawEvent::Ack(nonce, author));
}
Ok(())
}
Runs after every block within the context and current state of said block.
fn offchain_worker(_now: T::BlockNumber) {
As pongs
are only accepted by authorities, we only run this code,
if a valid local key is found, we could submit them with.
if let Some(key) = Self::authority_id() {
Self::offchain(&key);
}
}
Simple authority management: add a new authority to the set of keys that
are allowed to respond with pong
.
pub fn add_authority(origin, who: T::AccountId) -> Result {
In practice this should be a bit cleverer, but for this example it is enough
that this is protected by a root-call (e.g. through governance like sudo
).
let _me = ensure_root(origin)?;
if !Self::is_authority(&who){
<Authorities<T>>::mutate(|l| l.push(who));
}
Ok(())
}
}
}
We've moved the helper functions outside of the main decleration for briefety.
impl<T: Trait> Module<T> {
The main entry point, called with account we are supposed to sign with
fn offchain(key: &T::AccountId) {
Let's iterat through the locally stored requests and react to them.
At the moment, only knows of one request to respond to: ping
.
Once a ping is found, we respond by calling pong
as a transaction
signed with the given key.
This would be the place, where a regular offchain worker would go off
and do its actual thing before reponding async at a later point in time.
Note, that even though this is run directly on the same block, as we are creating a new transaction, this will only react in the following block.
for e in <Self as Store>::OcRequests::get() {
match e {
OffchainRequest::Ping(nonce, _who) => {
Self::respond(key, nonce)
}
there would be potential other calls
}
}
}
Respondong to as the given account to a given nonce by calling pong
as a
newly signed and submitted trasnaction
fn respond(key: &T::AccountId, nonce: u8) {
runtime_io::print_utf8(b"Received ping, sending pong");
let call = Call::pong(nonce);
let _ = T::SubmitTransaction::sign_and_submit(call, key.clone().into());
}
Helper that confirms whether the given AccountId
can sign pong
transactions
fn is_authority(who: &T::AccountId) -> bool {
Self::authorities().into_iter().find(|i| i == who).is_some()
}
Find a local AccountId
we can sign with, that is allowed to pong
fn authority_id() -> Option<T::AccountId> {
Find all local keys accessible to this app through the localised KeyType.
Then go through all keys currently stored on chain and check them against
the list of local keys until a match is found, otherwise return None
.
let local_keys = T::KeyType::all().iter().map(
|i| (*i).clone().into()
).collect::<Vec<T::AccountId>>();
Self::authorities().into_iter().find_map(|authority| {
if local_keys.contains(&authority) {
Some(authority)
} else {
None
}
})
}
}
lib.rs
(in runtime/src
)
Based off the regular Substrate Node Template runtime.
#![cfg_attr(not(feature = "std"), no_std)]
#![recursion_limit="256"]
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
use rstd::prelude::*;
use primitives::{OpaqueMetadata, crypto::key_types};
use sr_primitives::{
ApplyResult, transaction_validity::TransactionValidity, generic, create_runtime_str,
impl_opaque_keys, AnySignature
};
use sr_primitives::traits::{NumberFor, BlakeTwo256, Block as BlockT, DigestFor, StaticLookup,
Verify, ConvertInto, SaturatedConversion};
use sr_primitives::weights::Weight;
use babe::{AuthorityId as BabeId};
use grandpa::{AuthorityId as GrandpaId, AuthorityWeight as GrandpaWeight};
use grandpa::fg_primitives::{self, ScheduledChange};
use client::{
block_builder::api::{CheckInherentsResult, InherentData, self as block_builder_api},
runtime_api as client_api, impl_runtime_apis
};
use version::RuntimeVersion;
#[cfg(feature = "std")]
use version::NativeVersion;
#[cfg(any(feature = "std", test))]
pub use sr_primitives::BuildStorage;
pub use timestamp::Call as TimestampCall;
pub use balances::Call as BalancesCall;
pub use sr_primitives::{Permill, Perbill};
pub use support::{StorageValue, construct_runtime, parameter_types};
Additionally, we need system
here
use system::offchain::TransactionSubmitter;
Everything else is as usual
pub type BlockNumber = u32;
pub type Signature = AnySignature;
pub type AccountId = <Signature as Verify>::Signer;
pub type AccountIndex = u32;
pub type Balance = u128;
pub type Index = u32;
pub type Hash = primitives::H256;
pub type DigestItem = generic::DigestItem<Hash>;
We import our own module here.`
mod offchaincb;
pub mod opaque {
use super::*;
pub use sr_primitives::OpaqueExtrinsic as UncheckedExtrinsic;
pub type Header = generic::Header<BlockNumber, BlakeTwo256>;
pub type Block = generic::Block<Header, UncheckedExtrinsic>;
pub type BlockId = generic::BlockId<Block>;
pub type SessionHandlers = (Grandpa, Babe);
impl_opaque_keys! {
pub struct SessionKeys {
#[id(key_types::GRANDPA)]
pub grandpa: GrandpaId,
#[id(key_types::BABE)]
pub babe: BabeId,
}
}
}
pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("offchain-cb"),
impl_name: create_runtime_str!("offchain-cb"),
authoring_version: 3,
spec_version: 4,
impl_version: 4,
apis: RUNTIME_API_VERSIONS,
};
pub const MILLISECS_PER_BLOCK: u64 = 6000;
pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK;
pub const EPOCH_DURATION_IN_BLOCKS: u32 = 10 * MINUTES;
pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber);
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4);
#[cfg(feature = "std")]
pub fn native_version() -> NativeVersion {
NativeVersion {
runtime_version: VERSION,
can_author_with: Default::default(),
}
}
parameter_types! {
pub const BlockHashCount: BlockNumber = 250;
pub const MaximumBlockWeight: Weight = 1_000_000;
pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75);
pub const MaximumBlockLength: u32 = 5 * 1024 * 1024;
pub const Version: RuntimeVersion = VERSION;
}
impl system::Trait for Runtime {
type AccountId = AccountId;
type Call = Call;
type Lookup = Indices;
type Index = Index;
type BlockNumber = BlockNumber;
type Hash = Hash;
type Hashing = BlakeTwo256;
type Header = generic::Header<BlockNumber, BlakeTwo256>;
type Event = Event;
type WeightMultiplierUpdate = ();
type Origin = Origin;
type BlockHashCount = BlockHashCount;
type MaximumBlockWeight = MaximumBlockWeight;
type MaximumBlockLength = MaximumBlockLength;
type AvailableBlockRatio = AvailableBlockRatio;
type Version = Version;
}
parameter_types! {
pub const EpochDuration: u64 = EPOCH_DURATION_IN_BLOCKS as u64;
pub const ExpectedBlockTime: u64 = MILLISECS_PER_BLOCK;
}
impl babe::Trait for Runtime {
type EpochDuration = EpochDuration;
type ExpectedBlockTime = ExpectedBlockTime;
}
impl grandpa::Trait for Runtime {
type Event = Event;
}
impl indices::Trait for Runtime {
type AccountIndex = u32;
type ResolveHint = indices::SimpleResolveHint<Self::AccountId, Self::AccountIndex>;
type IsDeadAccount = Balances;
type Event = Event;
}
parameter_types! {
pub const MinimumPeriod: u64 = 5000;
}
impl timestamp::Trait for Runtime {
type Moment = u64;
type OnTimestampSet = Babe;
type MinimumPeriod = MinimumPeriod;
}
parameter_types! {
pub const ExistentialDeposit: u128 = 500;
pub const TransferFee: u128 = 0;
pub const CreationFee: u128 = 0;
pub const TransactionBaseFee: u128 = 0;
pub const TransactionByteFee: u128 = 1;
}
impl balances::Trait for Runtime {
type Balance = Balance;
type OnFreeBalanceZero = ();
type OnNewAccount = Indices;
type Event = Event;
type TransactionPayment = ();
type DustRemoval = ();
type TransferPayment = ();
type ExistentialDeposit = ExistentialDeposit;
type TransferFee = TransferFee;
type CreationFee = CreationFee;
type TransactionBaseFee = TransactionBaseFee;
type TransactionByteFee = TransactionByteFee;
type WeightToFee = ConvertInto;
}
impl sudo::Trait for Runtime {
type Event = Event;
type Proposal = Call;
}
We need to define the AppCrypto for the keys that are authorized
to pong
pub mod offchaincb_crypto {
pub use crate::offchaincb::KEY_TYPE;
use primitives::sr25519;
app_crypto::app_crypto!(sr25519, KEY_TYPE);
impl From<Signature> for super::Signature {
fn from(a: Signature) -> Self {
sr25519::Signature::from(a).into()
}
}
}
We need to define the Transaction signer for that using the Key definition
type OffchainCbAccount = offchaincb_crypto::Public;
type SubmitTransaction = TransactionSubmitter<OffchainCbAccount, Runtime, UncheckedExtrinsic>;
Now we configure our Trait usng the previously defined primitives
impl offchaincb::Trait for Runtime {
type Call = Call;
type Event = Event;
type SubmitTransaction = SubmitTransaction;
type KeyType = OffchainCbAccount;
}
Lastly we also need to implement the CreateTransaction signer for the runtime
impl system::offchain::CreateTransaction<Runtime, UncheckedExtrinsic> for Runtime {
type Signature = Signature;
fn create_transaction<F: system::offchain::Signer<AccountId, Self::Signature>>(
call: Call,
account: AccountId,
index: Index,
) -> Option<(Call, <UncheckedExtrinsic as sr_primitives::traits::Extrinsic>::SignaturePayload)> {
let period = 1 << 8;
let current_block = System::block_number().saturated_into::<u64>();
let tip = 0;
let extra: SignedExtra = (
system::CheckVersion::<Runtime>::new(),
system::CheckGenesis::<Runtime>::new(),
system::CheckEra::<Runtime>::from(generic::Era::mortal(period, current_block)),
system::CheckNonce::<Runtime>::from(index),
system::CheckWeight::<Runtime>::new(),
balances::TakeFees::<Runtime>::from(tip),
);
let raw_payload = SignedPayload::new(call, extra).ok()?;
let signature = F::sign(account.clone(), &raw_payload)?;
let address = Indices::unlookup(account);
let (call, extra, _) = raw_payload.deconstruct();
Some((call, (address, signature, extra)))
}
}
Then all this can be put together
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: system::{Module, Call, Storage, Config, Event},
Timestamp: timestamp::{Module, Call, Storage, Inherent},
Babe: babe::{Module, Call, Storage, Config, Inherent(Timestamp)},
Grandpa: grandpa::{Module, Call, Storage, Config, Event},
Indices: indices::{default, Config<T>},
Balances: balances,
Sudo: sudo,
Nothing special here.
OffchainCB: offchaincb::{Module, Call, Event<T>, Storage},
}
);
pub type Address = <Indices as StaticLookup>::Source;
pub type Header = generic::Header<BlockNumber, BlakeTwo256>;
pub type Block = generic::Block<Header, UncheckedExtrinsic>;
pub type SignedBlock = generic::SignedBlock<Block>;
pub type BlockId = generic::BlockId<Block>;
pub type SignedExtra = (
system::CheckVersion<Runtime>,
system::CheckGenesis<Runtime>,
system::CheckEra<Runtime>,
system::CheckNonce<Runtime>,
system::CheckWeight<Runtime>,
balances::TakeFees<Runtime>
);
pub type UncheckedExtrinsic = generic::UncheckedExtrinsic<Address, Call, Signature, SignedExtra>;
Just that the Signature Signer needs this aditional definition as well
pub type SignedPayload = generic::SignedPayload<Call, SignedExtra>;
pub type CheckedExtrinsic = generic::CheckedExtrinsic<AccountId, Call, SignedExtra>;
pub type Executive = executive::Executive<Runtime, Block, system::ChainContext<Runtime>, Runtime, AllModules>;
impl_runtime_apis! {
impl client_api::Core<Block> for Runtime {
fn version() -> RuntimeVersion {
VERSION
}
fn execute_block(block: Block) {
Executive::execute_block(block)
}
fn initialize_block(header: &<Block as BlockT>::Header) {
Executive::initialize_block(header)
}
}
impl client_api::Metadata<Block> for Runtime {
fn metadata() -> OpaqueMetadata {
Runtime::metadata().into()
}
}
impl block_builder_api::BlockBuilder<Block> for Runtime {
fn apply_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> ApplyResult {
Executive::apply_extrinsic(extrinsic)
}
fn finalize_block() -> <Block as BlockT>::Header {
Executive::finalize_block()
}
fn inherent_extrinsics(data: InherentData) -> Vec<<Block as BlockT>::Extrinsic> {
data.create_extrinsics()
}
fn check_inherents(block: Block, data: InherentData) -> CheckInherentsResult {
data.check_extrinsics(&block)
}
fn random_seed() -> <Block as BlockT>::Hash {
System::random_seed()
}
}
impl client_api::TaggedTransactionQueue<Block> for Runtime {
fn validate_transaction(tx: <Block as BlockT>::Extrinsic) -> TransactionValidity {
Executive::validate_transaction(tx)
}
}
This comes with new templates now, if you don't have it, you have to implement this trait in order for the Offchain Worker to be triggerd.
impl offchain_primitives::OffchainWorkerApi<Block> for Runtime {
fn offchain_worker(number: NumberFor<Block>) {
Executive::offchain_worker(number)
}
}
impl fg_primitives::GrandpaApi<Block> for Runtime {
fn grandpa_pending_change(digest: &DigestFor<Block>)
-> Option<ScheduledChange<NumberFor<Block>>>
{
Grandpa::pending_change(digest)
}
fn grandpa_forced_change(digest: &DigestFor<Block>)
-> Option<(NumberFor<Block>, ScheduledChange<NumberFor<Block>>)>
{
Grandpa::forced_change(digest)
}
fn grandpa_authorities() -> Vec<(GrandpaId, GrandpaWeight)> {
Grandpa::grandpa_authorities()
}
}
impl babe_primitives::BabeApi<Block> for Runtime {
fn startup_data() -> babe_primitives::BabeConfiguration {
babe_primitives::BabeConfiguration {
median_required_blocks: 1000,
slot_duration: Babe::slot_duration(),
c: PRIMARY_PROBABILITY,
}
}
fn epoch() -> babe_primitives::Epoch {
babe_primitives::Epoch {
start_slot: Babe::epoch_start_slot(),
authorities: Babe::authorities(),
epoch_index: Babe::epoch_index(),
randomness: Babe::randomness(),
duration: EpochDuration::get(),
secondary_slots: Babe::secondary_slots().0,
}
}
}
impl substrate_session::SessionKeys<Block> for Runtime {
fn generate_session_keys(seed: Option<Vec<u8>>) -> Vec<u8> {
let seed = seed.as_ref().map(|s| rstd::str::from_utf8(&s).expect("Seed is an utf8 string"));
opaque::SessionKeys::generate(seed)
}
}
}