Offchain Worker (中)

本节内容我们将会分享一些案例,以展示offchain 在实际业务场景中到底是如何使用,主要是涉及存储以及http请求。在最后一节,我们将会使用offchain worker 执行一些交易操作

Case 1 : 引入 Offchain Worker

目标

  1. 给pallet 添加 offchain worker hook
  2. 在offchain worker中打印日志信息,并在终端查看日志

所有代码都会在substrate-node-template上构建,可以通过如下方式获取substrate-node-template

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

clone repo 后编写代码,具体有两处,一是lib.rs,二是Cargo.toml, 如下:

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,Offchain worker引入非常简单,现在可以编译并运行

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

查看终端

重点

  1. Offchain worker 在每次块导入之后执行
  2. 每导入一个区块,就会执行一次

Case 2 : Offchain worker lifecycle (1)

目标

  1. 同时打开多个hooks, on_initialize, on_finalize,on_idle
  2. 在各个hooks中打印信息,观察日志出现的时机,理解offchain worker的执行

现在我们在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
}
}

继续编译运行

重点

  1. 以上四个hooks函数执行顺序确定,其中offchain worker最后执行
  2. 这四个hooks函数和substrate的共识相关

Case 3 : Offchain worker lifecycle (2)

目标

  1. 在offchain worker 中sleep一段时间,观察offchain worker 跨块执行

跨块执行的结果是会在整个substrate中跑多个offchain worker,实现并发。实际上底层使用的是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" }

继续编译运行

我们可以看到,第一个块中引入的offchain worker在第二个块中才退出,同样的第二个块引入的offchain worker在第三个块中退出。正如我们之前所言,offchain worker对于执行一些需要不确定计算时间的业务需求非常有用

重点

  1. offchain worker的编程模式是定时器的流模式(定时重入)
  2. 与响应式Reaction模式不同,Noed.js就是这种模式

Case 4 : 向Local Storage 写入并读取数据

Offchain Storagte(Local storage)的功能

  1. Offchain Worker可以直接读写 Local Storage
  2. 链上代码可以通过Indexing功能直接向Local Storage写数据,但是不能读
  3. 可用于Offchain Workers tasks 之间的通信和协调,注意由于可能同时存在多个Offchain worker,因此并发访问时需要加锁

目标

  1. 在奇数块向Local Storage写数据,偶数块读取数据,并检查
  2. 学习:获取链下随机数;对BlockNumber类型进行数学运算;获取链下时间;生成存储key;写链下存储,读链下存储;清理存储key

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

引入相关的类型和Trait(注意在mod pallet外或者内引入都可,但是再外引入的话,内部要再使用use super::* 引入到模块内)

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

在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>>()
})
}
}

编译运行

通过终端可以看到,在奇数块,我们成功的存储了随机数据,并且在偶数块准确的读到了数据。所有的学习目标也都完成了!

Case 5 : 使用mutate 方法向Local Storage 写入数据

目标

  1. 在 case 4的基础上使用mutate 方法对数据进行原子更改
  2. 学习新的原子操作修改方法(不再使用之前手动锁的方式),学习配套的错误处理模式

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

新增加两种错误类型

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

使用mutate更改数据

#[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>>()
})
}
}

编译并运行

通过日志我们可以看到,使用mutate方法成功的存储了数据,这种方式的好处是应付并发处理

Case 6 : 获取外部Http 接口的数据

目标

  1. 在Offchain Worker中发起https请求,获取数据
  2. 使用serde_json解析获取到的json数据
  3. 学习serde的类型转换和调试相关的知识

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

引入crate

[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" }

引入相关的类型和Trait

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

定义数据类型和方法

这种逻辑和我们日常处理http请求是一样的,也就是先定义类型以及解析方法,另外,注意为了打印结构体,我们手动为其实现了Debug Trait

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);
}
}
}

辅助函数

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)
}
}
}

编译并运行

OK,我们成功的实现了通过offchain worker 从http请求获取数据

今天的内容就分享到这里,其实蛮多的,我们下期再会

猜你喜欢

转载自blog.csdn.net/weixin_51487151/article/details/127477486
今日推荐