查看原文
其他

Substrate Off-Chian Workers 是什么?如何用?

洋芋 一块Plus社区 2020-11-11
作者:洋芋,《Substrate快速入门与开发实战》开发课第2期助教

关于Substrate Off-Chain Workers的介绍,本文翻译了 Substrate Developer Hub 中的两篇文章。
  • 概念部分的 Substrate 核心:Off-Chain Workers

  • 开发部分的 Runtime 模块:Off-Chian Workers




1



概念:Off-Chain Workers 链下工作机


Overview 概览


通常,我们需要先查询和(或)处理链外数据,然后才将其包含在链上的状态中。常规的做法是通过预言机(Oracle)。

预言机是一种外部服务,通常用于监听区块链事件,并根据条件触发任务。当任务执行完毕,执行结果会以交易的形式提交至区块链。虽然这种方法可行,但在安全性、可扩展性和基础设施效率方面仍然存在一些缺陷。

为了使链外数据集成更加安全和高效,Substrate提供了链下工作机机制。

链下工作机子系统允许执行长时间运行且可能非确定性的任务(例如 web 请求、数据的加解密和签名、随机数生成、 CPU 密集型计算、对链上数据的枚举 / 聚合等) ,而这些任务可能需要比区块执行时间更长的时间。

链下工作机在Substrate runtime之外,拥有自己的Wasm运行环境。这种分割是为了确保区块生成不会受到长时间运行的任务的影响。

但是,由于声明链下工作机,使用了与 runtime 相同的代码,因此它们可以轻松地访问链上状态进行计算。

APIs 应用程序接口


为与外部世界进行通信,链下工作机可以访问的扩展应用程序接口(APIs)包括:

  • 能够向链提交交易(已签名或未签名)以发布计算结果。

  • 一个功能齐全的HTTP客户端,允许链下工作机从外部服务中访问和获取数据。

  • 访问本地密钥库以签署和验证声明(statements)或交易。

  • 另一个本地键值(key-value)数据库,在所有链下工作机之间共享。

  • 一个安全的本地熵源(entropy),用于生成随机数。

  • 访问节点的精确本地时间,以及休眠和恢复工作的功能。


链下工作机可以在 runtime 实现模块的一个特定函数fn offchain_worker(block: T::BlockNumber)中进行初始化。该函数在每次区块导入后执行。

为了将结果传递回链,链下工作机可以提交已签名或未签名的交易,这些交易会被打包进后续的区块中。


请注意,来自链下工作机的结果不受常规交易验证的约束。应该实施验证机制(例如投票,取平均,检查发件人签名或简单地“信任”),以确定哪些信息进入链中。

关于如何在下一个 runtime 开发项目中使用链下工作机的更多信息,请参阅开发指南(译者注:本文的下节内容)。




2



开发部分的 Runtime 模块:Off-Chian Workers


本文介绍在 Substrate runtime 中使用链下工作机的技术开发方面。有关链下工作机的概念概述,请参阅概念指南(译者注:本文的上节内容)。

在 Runtime 中使用链下工作


创建一个链下工作机的逻辑,可以将其放在它自己的 pallet 中。在本示例中,我们将此 pallet 称为 myoffchainworker。它属于 runtime ,所以源文件目录为:runtime/src/myoffchainworker.rs。

首先,包括以下模块:
// 为了更好地调试(打印)支持
use support::{ debug, dispatch };
use system::offchain;
use sp_runtime::transaction_validity::{
TransactionValidity, TransactionLongevity, ValidTransaction, InvalidTransaction
};
在 pallet 的配置 trait 中包括以下关联类型,用于从链下工作机发送已签名和未签名的交易。
pub trait Trait: timestamp::Trait + system::Trait {
/// 总的事件类型
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
type Call: From<Call<Self>>;

type SubmitSignedTransaction: offchain::SubmitSignedTransaction<Self, <Self as Trait>::Call>;
type SubmitUnsignedTransaction: offchain::SubmitUnsignedTransaction<Self, <Self as Trait>::Call>;
}
在宏 declmodule! 模块中,定义 offchainworker 函数。此函数作为链下工作机的入口点,并在每次导入区块后运行。
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {

// --snip--

fn offchain_worker(block: T::BlockNumber) {
debug::info!("Hello World.");
}
}
}
默认情况下,链下工作机无法直接访问用户密钥(即使在开发环境中),由于安全原因,只能访问应用特定的子密钥(subkeys)。需要在 runtime 顶部定义 KeyTypeId 用于将应用特定的子密钥分组,如下所示:
// 密钥类型ID可以是任何4个字符的字符串
pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"abcd");

// --snip--

pub mod crypto {
pub use super::KEY_TYPE;
use sp_runtime::app_crypto::{app_crypto, sr25519};
app_crypto!(sr25519, KEY_TYPE);
}
和任何其他 pallet 一样,runtime 必须实现 pallet 的配置 trait。进入位于 runtime/src/lib.rs 的 runtime lib.rs。
// 定义交易签名人
type SubmitTransaction = system::offchain::TransactionSubmitter<
offchain_pallet::crypto::Public, Runtime, UncheckedExtrinsic>;

impl runtime::Trait for Runtime {
type Event = Event;
type Call = Call;

// 在 runtime 中使用签名的交易
type SubmitSignedTransaction = SubmitTransaction;

// 在 runtime 中使用未签名的交易
type SubmitUnsignedTransaction = SubmitTransaction;
}
然后为 runtime 实现 system::offchain::CreateTransaction trait。仍然在 lib.rs 中:
use sp_runtime::transaction_validity;

// --snip--

impl system::offchain::CreateTransaction<Runtime, UncheckedExtrinsic> for Runtime {
type Public = <Signature as Verify>::Signer;
type Signature = Signature;

fn create_transaction<TSigner: system::offchain::Signer<Self::Public, Self::Signature>> (
call: Call,
public: Self::Public,
account: AccountId,
index: Index,
) -> Option<(Call, <UncheckedExtrinsic as sp_runtime::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(),
transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
);
let raw_payload = SignedPayload::new(call, extra).ok()?;
let signature = TSigner::sign(public, &raw_payload)?;
let address = Indices::unlookup(account);
let (call, extra, _) = raw_payload.deconstruct();
Some((call, (address, signature, extra)))
}
}
在宏 contrast_runtime! 中,将所有不同的 pallet 作为 runtime 的一部分。 如果在链下工作机中使用未签名的交易,则添加另外一个参数 ValidateUnsigned。需要为此编写自定义验证逻辑。
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
// --snip--

// 使用未签名交易
OffchainPallet: offchain_pallet::{ Module, Call, Storage, Event<T>, transaction_validity::ValidateUnsigned }

// 使用签名交易
// OffchainPallet: offchain_pallet::{ Module, Call, Storage, Event<T> }
}
);

在 service.rs 中添加密钥(Keys)


使用KeyTypeId指定本地密钥库来存储特定于应用的密钥,链下工作机可以访问这些密钥来签署交易。需要通过以下两种方式之一添加密钥。

 选项 1(开发阶段):添加第一个用户密钥作为应用的子密钥 
在开发环境中,可以添加第一个用户的密钥作为应用的子密钥。更新 node/src/service.rs 如下所示。
pub fn new_full<C: Send + Default + 'static>(config: Configuration<C, GenesisConfig>)
-> Result<impl AbstractService, ServiceError>
{
// --snip--

// 给Alice clone密钥
let dev_seed = config.dev_key_seed.clone();

// --snip--

let service = builder.with_network_protocol(|_| Ok(NodeProtocol::new()))?
.with_finality_proof_provider(|client, backend|
Ok(Arc::new(GrandpaFinalityProofProvider::new(backend, client)) as _)
)?
.build()?;

// 添加以下部分以将密钥添加到keystore
if let Some(seed) = dev_seed {
service
.keystore()
.write()
.insert_ephemeral_from_seed_by_type::<runtime::offchain_pallet::crypto::Pair>(
&seed,
runtime::offchain_pallet::KEY_TYPE,
)
.expect("Dev Seed should always succeed.");
}
}
这样就可以签名交易了。这仅对 开发阶段 有利。

选项2:通过 CLI 添加应用的子密钥 
在更实际的环境中,在设置 Substrate 节点后,可以通过命令行接口添加一个新的应用子密钥。如下所示:
# 生成一个新帐户
$ subkey -s generate

# 通过RPC提交一个新密钥
$ curl -X POST -vk 'http://localhost:9933' -H "Content-Type:application/json;charset=utf-8" \
-d '{
"jsonrpc":2.0,
"id":1,
"method":"author_insertKey",
"params": [
"<YourKeyTypeId>",
"<YourSeedPhrase>",
"<YourPublicKey>"
]
}'
新密钥已添加到本地密钥库(keystore)中。

签名交易


现在已经准备好与链下工作进行签名交易。返回 
pallet my_offchain_worker.rs。
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
// --snip--

pub fn onchain_callback(origin, _block: T::BlockNumber, input: Vec<u8>) -> dispatch::Result {
let who = ensure_signed(origin)?;
debug::info!("{:?}", core::str::from_utf8(&input).unwrap());
Ok(())
}

fn offchain_worker(block: T::BlockNumber) {
// 这里指定下一个区块导入阶段的链上回调函数。
let call = Call::onchain_callback(block, b"hello world!".to_vec());
T::SubmitSignedTransaction::submit_signed(call);
}
}
}

在链上回调函数 onchain_callback 定义之后,在链下工作机中,可以指定下一个区块导入阶段的链上回调函数。然后将签名的交易提交给节点。

如果在Substrate 代码库中查看 fn system::offchain::submit_signed 的实现,将看到它正在调用本地密钥库中每个密钥的链上回调函数。但由于在本地密钥库中只有一个密钥,因此只调用一次该函数。

未签名交易


使用以下代码,可以将未签名的交易发送回链。
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
// --snip--

pub fn onchain_callback(_origin, _block: T::BlockNumber, input: Vec<u8>) -> dispatch::Result {
debug::info!("{:?}", core::str::from_utf8(&input).unwrap());
Ok(())
}

fn offchain_worker(block: T::BlockNumber) {
// 这里指定下一个区块导入阶段的链上回调函数。
let call = Call::onchain_callback(block, b"hello world!".to_vec());
T::SubmitUnsignedTransaction::submit_unsigned(call);
}
}
}
默认情况下,所有未签名的交易都被视为无效交易。需要在my_offchain_worker.rs中添加以下代码段,以显式允许提交未签名的交易。
decl_module! {
// --snip--
}

impl<T: Trait> Module<T> {
// --snip--
}

#[allow(deprecated)]
impl<T: Trait> support::unsigned::ValidateUnsigned for Module<T> {
type Call = Call<T>;

fn validate_unsigned(call: &Self::Call) -> TransactionValidity {

match call {
Call::onchain_callback(block, input) => Ok(ValidTransaction {
priority: 0,
requires: vec![],
provides: vec![(block, input).encode()],
longevity: TransactionLongevity::max_value(),
propagate: true,
}),
_ => InvalidTransaction::Call.into()
}
}
}

添加 deprecated 属性,以防止显示警告消息。这是因为这一部分API仍然处于过渡阶段,并将在即将发布的 Substrate 版本中进行更新。请暂时谨慎使用。

链上回调函数中的参数


在进行链上回调时,我们的实现会将函数名称及其所有参数值一起哈希。回调将在下次区块导入时被存储和调用。如果我们发现哈希值存在,这意味着之前已经调用了具有相同参数集的函数。

那么对于签名交易,如果以更高的优先级调用该函数,则该函数将被替换;对于未签名交易,此回调将被忽略。

如果你的 pallet 定期进行链上回调,并希望它偶尔有重复的参数集,则始终可以从offchain_worker函数传入当前区块号外的其他参数。该数字只会增加,并且保证是唯一的。

获取外部数据


要从第三方API获取外部数据,请在 myoffchainworker.rs 中使用 offchain::http 库,如下所示。
use sp_runtime::{
offchain::http,
transaction_validity::{
TransactionValidity, TransactionLongevity, ValidTransaction, InvalidTransaction
}
};

// --snip--

decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
// --snip--
fn offchain_worker(block: T::BlockNumber) {
match Self::fetch_data() {
Ok(res) => debug::info!("Result: {}", core::str::from_utf8(&res).unwrap()),
Err(e) => debug::error!("Error fetch_data: {}", e),
};
}
}
}

impl<T: Trait> Module<T> {
fn fetch_data() -> Result<Vec<u8>, &'static str> {

// 指定请求
let pending = http::Request::get("https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD")
.send()
.map_err(|_| "Error in sending http GET request")?;

// 等待响应
let response = pending.wait()
.map_err(|_| "Error in waiting http response back")?;

// 检查HTTP响应是否正确
if response.code != 200 {
debug::warn!("Unexpected status code: {}", response.code);
return Err("Non-200 status code returned from http request");
}

// 以字节形式收集结果
Ok(response.body().collect::<Vec<u8>>())
}
}
之后可能需要将结果解析为JSON格式。我们这里有一个在 no_std 环境中,使用外部库解析JSON的 示例。



示例

  • Sub0 工作坊链下工作机的资料

  • 链下工作机价格获取

参考文档

  • Substrate im-online 模块, 一个 Substrate 内部的 pallet,使用链下工作机通知其他节点,网络中的验证人在线。




转载自:知行之录


更多阅读:
▎Subdev分享 ▏Substrate Staking代币经济系统讲解
▎Subdev 讨论|从命令频繁报错开始的第一个Substrate 项目
▎为什么Rust已成为最先进的主流通用语言之一?



扫码关注公众号,回复“1”加入开发者社群



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存