LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 区块链资讯 > Substrate 链下工作机是什么?如何用?

Substrate 链下工作机是什么?如何用?

2019-12-31 一块Plus社区 来源:区块链网络

关于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,使用链下工作机通知其他节点,网络中的验证人在线。

—-

编译者/作者:一块Plus社区

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

LOADING...
LOADING...