首页 > 技术 > 【区块链是什么通俗解释】Substrate 链下工作机是什么?如何用?
一块Plus社区  

【区块链是什么通俗解释】Substrate 链下工作机是什么?如何用?

摘要:关于Substrate Off-Chain Workers的介绍,本文翻译了 Substrate Developer Hub 中的两篇文章。

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

免责声明
世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。