Offchain Worker (Medium)

In this section, we will share some cases to show how offchain is used in actual business scenarios, mainly involving storage and http requests. In the last section, we will use offchain workers to perform some transaction operations

Case 1: Introduce Offchain Worker

Target

  1. Add offchain worker hook to pallet
  2. Print the log information in the offchain worker, and view the log on the terminal

All codes will be built on the substrate-node-template, and the substrate-node-template can be obtained as follows

git clone https://github.com/substrate-developer-hub/substrate-node-template

Write the code after clone repo, there are two specific places, one is lib.rs, and the other is Cargo.toml, as follows:

substrate-node-template/pallets/template/src/lib.rs

#[frame_support::pallet]
pub mod pallet {
--
// offchain workers 通过hooks实现
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(block_number: T::BlockNumber) {
log::info!("Hello World from offchain workers!: {:?}", block_number);
}
}
}

substrate-node-template/pallets/template/Cargo.toml

[dependencies]
--
log = { version = "0.4.17", default-features = false } // 引入log依赖,打印offchain worker信息

Ok, the introduction of Offchain worker is very simple, now you can compile and run

cargo build // 编译 (在这里也可以使用cargo build --release)
./target/debug/node-template --dev // 运行 (也可以使用./target/release/node-template --dev)dev代表单节点出块

view terminal

focus

  1. Offchain workers are executed after each block import
  2. Every time a block is imported, it will be executed once

Case 2 : Offchain worker lifecycle (1)

Target

  1. Open multiple hooks at the same time, on_initialize, on_finalize, on_idle
  2. Print information in each hooks, observe the timing of log appearance, and understand the execution of offchain worker

Now we add more functions on the basis of case1

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(block_number: T::BlockNumber) {
log::info!("Hello World from offchain workers!: {:?}", block_number);
}

fn on_initialize(_n: T::BlockNumber) -> Weight {
log::info!("in on_initialize!");
let weight: Weight = Default::default();
weight
}
fn on_finalize(_n: T::BlockNumber) {
log::info!("in on_finalize!");
}

fn on_idle(_n: T::BlockNumber, _remaining_weight: Weight) -> Weight {
log::info!("in on_idle!");
let weight: Weight = Default::default();
weight
}
}

Continue to compile and run

focus

  1. The execution order of the above four hooks functions is determined, and the offchain worker is executed last
  2. These four hooks functions are related to the consensus of the substrate

Case 3 : Offchain worker lifecycle (2)

Target

  1. Sleep for a period of time in the offchain worker, and observe the cross-block execution of the offchain worker

The result of cross-block execution is that multiple offchain workers will run in the entire substrate to achieve concurrency. In fact, the bottom layer is implemented by tokio task

substrate-node-template/pallets/template/src/lib.rs

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(block_number: T::BlockNumber) {
log::info!("Hello World from offchain workers!: {:?}", block_number);

let timeout =
sp_io::offchain::timestamp().add(sp_runtime::offchain::Duration::from_millis(8000));

sp_io::offchain::sleep_until(timeout);

log::info!("Leave from offchain workers!: {:?}", block_number);
}
}
}

substrate-node-template/pallets/template/Cargo.toml

[dependencies]
--
sp-io = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" }
sp-runtime = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" }

Continue to compile and run

We can see that the offchain worker introduced in the first block exits only in the second block, and the same offchain worker introduced in the second block exits in the third block. As we said before, offchain workers are very useful for executing some business needs that require uncertain computing time

focus

  1. The programming mode of the offchain worker is the flow mode of the timer (timed reentry)
  2. Unlike the reactive Reaction mode, Noed.js is this mode

Case 4: Write and read data to Local Storage

Functions of Offchain Storage (Local storage)

  1. Offchain Worker can directly read and write Local Storage
  2. The code on the chain can directly write data to Local Storage through the Indexing function, but cannot read
  3. It can be used for communication and coordination between Offchain Workers tasks. Note that since multiple Offchain workers may exist at the same time, locks are required for concurrent access

Target

  1. Write data to Local Storage in odd blocks, read data in even blocks, and check
  2. Learning: Get off-chain random numbers; perform mathematical operations on BlockNumber types; get off-chain time; generate storage keys; write off-chain storage, read off-chain storage; clean up storage keys

substrate-node-template/pallets/template/src/lib.rs

Import related types and traits (note that it can be imported outside or inside the mod pallet, but if it is imported again, it must be imported into the module using use super::* internally)

use sp_runtime::{offchain::storage::StorageValueRef, traits::Zero};
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::inherent::Vec;
--
}

Continue writing in the mod pallet

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(block_number: T::BlockNumber) {
log::info!("Hello World from offchain workers!: {:?}", block_number);

// odd
if block_number % 2u32.into() != Zero::zero() {
let key = Self::derive_key(block_number);
let val_ref = StorageValueRef::persistent(&key);

// get local random value
let random_slice = sp_io::offchain::random_seed();

// get a local timestamp
let timestamp_u64 = sp_io::offchain::timestamp().unix_millis();

// combine to a tuple and print it
let value = (random_slice, timestamp_u64);
log::info!("in odd block, value to write {:?}", value);

// write or mutate tuple content to key
val_ref.set(&value);
} else {
// even
let key = Self::derive_key(block_number - 1u32.into());
let mut val_ref = StorageValueRef::persistent(&key);

// get from db by key
if let Ok(Some(value)) = val_ref.get::<([u8; 32],u64)>() {
// print values
log::info!("in even block, value read:{:?}", value);
// delete key
val_ref.clear();
}
}
log::info!("Leave from offchain workers!: {:?}", block_number);
}
}

impl<T: Config> Pallet<T> {
#[deny(clippy::clone_double_ref)]
// 由 drive_key 获取key
fn derive_key(block_number: T::BlockNumber) -> Vec<u8> {
block_number.using_encoded(|encoded_bn| {
b"node-template::storage::"
.iter()
.chain(encoded_bn)
.copied()
.collect::<Vec<u8>>()
})
}
}

Compile and run

Through the terminal, we can see that we have successfully stored random data in the odd block, and read the data accurately in the even block. All learning objectives are also completed!

Case 5: Use the mutate method to write data to Local Storage

Target

  1. On the basis of case 4, use the mutate method to make atomic changes to the data
  2. Learn the new atomic operation modification method (no longer use the previous manual lock method), and learn the supporting error handling mode

substrate-node-template/pallets/template/src/lib.rs

Added two new error types

use sp_runtime::{offchain::storage::{StorageValueRef,MutateStorageError,StorageRetrievalError}, traits::Zero};

Use mutate to change data

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(block_number: T::BlockNumber) {
log::info!("Hello World from offchain workers!: {:?}", block_number);

// odd
if block_number % 2u32.into() != Zero::zero() {
let key = Self::derive_key(block_number);
let val_ref = StorageValueRef::persistent(&key);

// get local random value
let random_slice = sp_io::offchain::random_seed();

// get a local timestamp
let timestamp_u64 = sp_io::offchain::timestamp().unix_millis();

// combine to a tuple and print it
let value = (random_slice, timestamp_u64);
log::info!("in odd block, value to write {:?}", value);

struct StateError;

// 通过一种方式实现get and set操作
let res = val_ref.mutate(|val:Result<Option<([u8;32],u64)>,StorageRetrievalError>| -> Result<_,StateError> {
match val {
Ok(Some(_)) => Ok(value),
_ => Ok(value),
}
});
match res {
Ok(value) => {
log::info!("in odd block, mutate successfully {:?}", value);
},
Err(MutateStorageError::ValueFunctionFailed(_)) => (),
Err(MutateStorageError::ConcurrentModification(_)) => (),
}
} else {
// even
let key = Self::derive_key(block_number - 1u32.into());
let mut val_ref = StorageValueRef::persistent(&key);

// get from db by key
if let Ok(Some(value)) = val_ref.get::<([u8; 32], u64)>() {
// print values
log::info!("in even block, value read:{:?}", value);
// delete key
val_ref.clear();
}
}
log::info!("Leave from offchain workers!: {:?}", block_number);
}
}

impl<T: Config> Pallet<T> {
#[deny(clippy::clone_double_ref)]
// 由 drive_key 获取key
fn derive_key(block_number: T::BlockNumber) -> Vec<u8> {
block_number.using_encoded(|encoded_bn| {
b"node-template::storage::"
.iter()
.chain(encoded_bn)
.copied()
.collect::<Vec<u8>>()
})
}
}

compile and run

Through the log, we can see that the data is successfully stored using the mutate method. The advantage of this method is to cope with concurrent processing

Case 6: Get the data of the external Http interface

Target

  1. Initiate https request in Offchain Worker to obtain data
  2. Use serde_json to parse the obtained json data
  3. Learn about serde type conversion and debugging

substrate-node-template/pallets/template/src/lib.rs

import crates

[dependencies]
--
serde = {version = "1.0.147", default-features = false, features = ['derive']}
serde_json = {version = "1.0.87",default-features = false, features = ['alloc']}
sp-std = {version ="4.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" }

Introduce related types and Trait

use sp_runtime::{
offchain::{http,Duration}
};
use serde::{Deserialize,Deserializer}

Define data types and methods

This logic is the same as our daily processing of http requests, that is, first define the type and analysis method. In addition, note that in order to print the structure, we manually implemented the Debug Trait for it

pub mod pallet {
--
#[derive(Deserialize, Encode, Decode)]
struct GithubInfo {
#[serde(deserialize_with = "de_string_to_bytes")] // 使用指定的方法解析
login: Vec<u8>,
#[serde(deserialize_with = "de_string_to_bytes")] // 使用指定的方法解析
blog: Vec<u8>,
public_repos: u32,
}

pub fn de_string_to_bytes<'de, D>(de: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s: &str = Deserialize::deserialize(de)?;
Ok(s.as_bytes().to_vec())
}

use core::{convert::TryInto, fmt};
impl fmt::Debug for GithubInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{
   
   { login: {}, blog: {}, public_repos: {} }}",
sp_std::str::from_utf8(&self.login).map_err(|_| fmt::Error)?,
sp_std::str::from_utf8(&self.blog).map_err(|_| fmt::Error)?,
&self.public_repos
)
}
}
}

offchain worker

pub mod pallet {
--
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(block_number: T::BlockNumber) {
log::info!("Hello World from offchain workers!: {:?}", block_number);

if let Ok(info) = Self::fetch_github_info() {
log::info!("Github Info: {:?}", info);
} else {
log::info!("Error while fetch github Info");
}
log::info!("Leave from offchain workers!: {:?}", block_number);
}
}
}

helper function

pub mod pallet {
--
// 辅助函数
impl<T: Config> Pallet<T> {
fn fetch_github_info() -> Result<GithubInfo, http::Error> {
// prepare for send request
let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(8_000));
let request = http::Request::get("https://api.github.com/orgs/substrate-developer-hub");
let pending = request
.add_header("User-Agent", "Substrate-Offchain-Worker")
.deadline(deadline)
.send()
.map_err(|_| http::Error::IoError)?;
let response =
pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??;
if response.code != 200 {
log::warn!("Unexpected status code: {}", response.code);
return Err(http::Error::Unknown)
}
let body = response.body().collect::<Vec<u8>>();
let body_str = sp_std::str::from_utf8(&body).map_err(|_| {
log::warn!("No UTF8 body");
http::Error::Unknown
})?;

// parse the response str
let gh_info: GithubInfo =
serde_json::from_str(body_str).map_err(|_| http::Error::Unknown)?;

Ok(gh_info)
}
}
}

compile and run

OK, we successfully obtained data from http requests through offchain worker

Today's content is shared here, in fact, quite a lot, we will see you next time

Guess you like

Origin blog.csdn.net/weixin_51487151/article/details/127477486