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:

  1. we have a local list of OcRequests, which are cleared at the beginning and then collected throughout a block
  2. 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)
		}
	}
}