< 返回版块

洋芋 发表于 2019-12-27 19:49

Tags:substrate, Off-Chian Workers

关于Substrate Off-Chain Workers的介绍,本文翻译了Substrate Developer Hub中的两篇文章。欢迎指正和互相交流。

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

Overview 概览

通常,我们需要先查询和(或)处理链下数据,然后才将其包含在链上的状态中。常规的做法是通过预言机(Oracle)。预言机是一种外部服务,通常用于监听区块链事件,并根据条件触发任务。当任务执行完毕,执行结果会以交易的形式提交至区块链。虽然这种方法可行,但在安全性、可扩展性和基础设施效率方面仍然存在一些缺陷。

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

链下工作机在Substrate runtime之外,拥有自己的Wasm运行环境。这种分割是为了确保区块生成不会受到长时间运行的任务的影响。但是,由于声明链下工作机,使用了与 runtime 相同的代码,因此它们可以轻松地访问链上状态进行计算。

off-chain workers

APIs 应用程序接口

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

  • 能够向链提交交易(已签名或未签名)以发布计算结果。
  • 一个功能齐全的HTTP客户端,允许链下工作机从外部服务中访问和获取数据。
  • 访问本地密钥库以签署和验证声明(statements)或交易。
  • 另一个本地键值(key-value)数据库,在所有链下工作机之间共享。
  • 一个安全的本地熵源(entropy),用于生成随机数。
  • 访问节点的精确本地时间,以及休眠和恢复工作的功能。

链下工作机可以在 runtime 实现模块的一个特定函数fn offchain_worker(block: T::BlockNumber)中进行初始化。该函数在每次区块导入后执行。为了将结果传递回链,链下工作机可以提交已签名或未签名的交易,这些交易会被打包进后续的区块中。

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

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

开发:Off-Chain Workers 链下工作机

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

在 Runtime 中使用链下工作机

创建一个链下工作机的逻辑,可以将其放在它自己的 pallet 中。在本示例中,我们将此 pallet 称为 my_offchain_worker。它属于 runtime ,所以源文件目录为:runtime/src/my_offchain_worker.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>;
}

在宏 decl_module! 模块中,定义 offchain_worker 函数。此函数作为链下工作机的入口点,并在每次导入区块后运行。

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获取外部数据,请在 my_offchain_worker.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的 示例。

示例

参考文档

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

原文链接:https://zhuanlan.zhihu.com/p/99809960

评论区

写评论

还没有评论

1 共 0 条评论, 1 页