专栏首页Rust语言学习交流深入浅出Substrate:剖析运行时Runtime

深入浅出Substrate:剖析运行时Runtime

基于Substrate开发自己的运行时模块,会遇到一个比较大的挑战,就是理解Substrate运行时(Runtime)。本文首先介绍了Runtime的架构,类型,常用宏,并结合一个实际的演示项目,做了具体代码分析,以帮助大家更好地理解在Substrate中它们是如何一起工作的。

Runtime架构

Runtime的类型

  • Module,是个结构体类型
  • Call,是个枚举类型
  • Event,是个结构体类型

Runtime的宏

  • construct_runtime!
  • decl_module!
  • decl_storage!
  • decl_event!

SRML架构:

有四个主要框架组件支持运行时模块:

  • System模块,它为其他模块提供底层级别的API和实用工具集。可以将其视为SRML的“std”(标准)库。特别是,系统模块定义了Substrate运行时的所有核心类型。
  • Executive模块,它充当运行时的业务流程层。它将传入的外部调用分派给运行时中的各个模块。
  • 常见宏,它帮助实现模块的常见组件。这些宏在运行时扩展以生成类型(Module, Call, Store, Event等),运行时使用这些类型与模块进行通信。常见的宏是decl_module!decl_storage!decl_event!等。
  • Runtime,汇集了所有组件和模块。它扩展了宏以获取每个模块的类型和特征实现。它还调用Executive模块来分派各个模块的调用。

SRML(Substrate Runtime Module Library,运行时模块库),包含了一组预定义的模块,这些模块可以作为独立的功能在运行时重用。例如,SRML中的Balances模块可用于跟踪帐户和余额,Assets模块可用于创建和管理可替换资产等等。

可以通过派生System模块,或者选择其它预定义SRML模块(Assets, Aura, Balances, ..., Executive, Support)构建自己的自定义模块。

Module结构体

Module结构体是每个Substrate运行时模块的主干,由Substrate提供的宏decl_module!生成。同时开发人员在编写自己的运行时模块时,可以为Module定义跟自己业务相关的函数和实现。

在宏decl_module!中定义Module结构体:

decl_module! {
    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
        ...
    }
}

友情提醒 熟悉Rust语言中宏概念的,都应该知道:在decl_module!宏中的代码,有些不是标准的Rust语法,而是Substrate扩展后的语法。

这个宏展开后,最终生成标准Rust语法的Module结构体定义如下:

pub struct Module<T: Trait>(::std::marker::PhantomData<(T)>);

如果您对模块完全展开后的代码感兴趣,可以尝试使用cargo expand

在这个结构体的基础上,Substrate实现了以下函数和特性,如:

  • 可调用函数,为自己的运行时模块,提供维护操作区块链状态的逻辑。
  • trait Store,包含模块公开的所有运行时存储项,每个存储项都有一个结构体,其中定义了所有存储API。
  • trait OnInitialize / OnFinalize,包含在块执行的开始或结束时运行的函数。
  • trait OffchainWorker
  • ...

此外Module结构体可用于实现各种模块的内部函数,在decl_module!宏中定义一个名为init的函数,示例代码如下:

fn init(origin) -> Result {
    let sender = ensure_signed(origin)?;
    <BalanceOf<T>>::insert(sender, Self::total_supply());
    Ok(())
}

宏展开后的代码如下:

impl <T: Trait> Module<T> {
    fn init(origin: T::Origin) -> Result {
        let sender = ensure_signed(origin)?;
        {
            if !(Self::is_init() == false) {
                { return Err("Already initialized."); };
            }
        };
        <BalanceOf<T>>::insert(sender, Self::total_supply());
        <Init<T>>::put(true);
        Ok(())
    }
}       

如示例中定义的init函数,我们可以使用Self::init(...)在整个模块中访问这些函数。

为了确保可以通过外部extrinsic调用函数,Module结构体同时通过连接到模块的Call枚举实现了Callable特性。所有可调用的函数都将通过Call枚举暴露给外部。下面会具体介绍Call

impl <T: Trait> ::srml_support::dispatch::Callable for Module<T> {
    type Call = Call<T>;
}

最后,Substrate使用construct_runtime!宏将整个Module结构体导入区块链的运行时。这个宏将自定义的模块和所有其他模块包含在一个名为AllModules的元组中。运行时的Executive模块,使用此元组来处理执行这些模块的编排。

Call枚举

Substrate中,Call枚举列出运行时模块公开的可分派函数。每个模块都有自己的Call枚举,其中包含该模块的函数名称和参数。然后,在构造运行时,会生成一个外部Call枚举,作为每个模块特定Call的聚合。

在之前的示例中,在decl_module!宏中定义函数init,宏展开后会生成一个Call枚举。代码如下:

pub enum Call<T: Trait> {
    ...
    #[allow(non_camel_case_types)]
    init(),
    ...
}

运行时中每个模块生成的Call枚举,Substrate会将此枚举传递给construct_runtime!宏用于生成外部Call枚举,该枚举列出所有运行时模块并引用了它们各自的Call对象。示例如下:

construct_runtime!(
    pub enum Runtime with Log(InternalLog: DigestItem<Hash, AuthorityId, AuthoritySignature>) where
        Block = Block,
        NodeBlock = opaque::Block,
        UncheckedExtrinsic = UncheckedExtrinsic
    {
        System: system::{default, Log(ChangesTrieRoot)},
        Timestamp: timestamp::{Module, Call, Storage, Config<T>, Inherent},
        Consensus: consensus::{Module, Call, Storage, Config<T>, Log(AuthoritiesChange), Inherent},
        Aura: aura::{Module},
        Indices: indices,
        Balances: balances,
        Sudo: sudo,
        // Used for the module template in `./template.rs`
        TemplateModule: template::{Module, Call, Storage, Event<T>},
    }
);

construct_runtime!宏展开后,生成以下外部Call枚举:

pub enum Call {
    Timestamp(::srml_support::dispatch::CallableCallFor<Timestamp>),
    Consensus(::srml_support::dispatch::CallableCallFor<Consensus>),
    Indices(::srml_support::dispatch::CallableCallFor<Indices>),
    Balances(::srml_support::dispatch::CallableCallFor<Balances>),
    Sudo(::srml_support::dispatch::CallableCallFor<Sudo>),
    TemplateModule(::srml_support::dispatch::CallableCallFor<TemplateModule>),
}

外部Call枚举收集了construct_runtime!宏中的所有模块暴露的Call枚举,因此,它定义了区块链中完整的公开可调度函数集。

最后,当运行Substrate节点时,它将自动生成一个getMetadata API,其中包含运行时生成的对象。这可以用于生成JavaScript函数,允许将调用分派给运行时。

Event枚举

Substrate中,Event枚举用作终端用户和客户端间通信。

声明Events

使用decl_event!宏来声明events。示例定义了如下event

decl_event!(
    pub enum Event<T> where AccountId = <T as system::Trait>::AccountId {
        Transfer(AccountId, AccountId, u64),
    }
);

decl_event!宏展开

在编译时,decl_event!宏展开,会为每个模块生成RawEvent枚举。然后使用宏中指定的特征trait将事件event类型生成为RawEvent的具体实现。

示例中decl_event!宏展开后生成的RawEventEvent类型。

pub type Event<T> = RawEvent<<T as system::Trait>::AccountId>;
pub enum RawEvent<AccountId> { Transfer(AccountId, AccountId, u64), }

Module类似,除了每个模块的Event类型之外,还有一个使用construct_runtime!宏,为整个运行时生成的外部Event类型。此类型是合并了所有运行时模块的Event枚举。

为了订阅相关事件,客户端和应用程序需要知道哪些事件是运行时中每个模块的一部分。为此,Substrate的RPC API具有getMetadata,它公开有关事件(和其他元数据)的信息。

construct_runtime!

可以在宏中声明要包含在区块链运行时中的所有运行时模块,包括SRML中的任何模块,以及自定义模块。

支持的类型:Module, Call, Storage, Config, Event, Origin, Log, 默认类型defalut相当于包含前五个类型。

decl_module!

通过宏decl_module!,定义模块公开的公共函数,它们充当访问运行时的入口点。这些特性和功能最终将包含在区块链的运行时中。

Substrate运行时模块库中的每个不同组件都是运行时模块的示例。

我们从最简单的形式开始,看如何使用decl_module!

decl_module! {
  pub struct Module<T: Trait> for enum Call where origin: T::Origin {
    fn set_value(origin, value: u32) -> Result {
      let _sender = ensure_signed(origin)?;
      <Value<T>>::put(value);
      Ok(())
    }
  }
}

Module类型的声明

通常在decl_module!宏中的第一行,通过以下代码,定义Module类型:

pub struct Module<T: Trait> for enum Call where origin: T::Origin

对于大多数模块开发,这段代码不需要修改。

定义模块Module为泛型T表示的Trait类型。模块内的函数可以使用此泛型来访问自定义类型。

Call枚举是construct_runtime!宏所需要的。将decl_module中定义的函数分派到此枚举中,并明确定义函数名称和参数。由运行时公开,以允许API和前端轻松交互。

最后origin: T::Origin是为简化decl_module中函数的参数定义而进行的优化。它的意思就是,函数中使用了origin变量,它的类型是由System模块定义的Trait::Origin

实现函数时的要求

为确保模块按预期运行,在开发模块功能时需要遵循这些规则。

  • 绝不能panic。它可能导致潜在的拒绝服务(DoS)攻击。应该提前检查可能的错误情况并优雅地处理它们。
  • 没有副作用的错误Error。函数必须完全完成,并返回Ok(()),或者它返回对存储没有副作用的Err('Some reason')。基于Substrate开发,你必须知道如何设计运行时逻辑,对区块链状态所做的任何更改,确保遵循“先验证,后写入”的模式。它跟在以太坊平台上开发智能合约不一样。在以太坊,如果交易在任何时候失败(错误,没有汽油等),智能合约的状态将不受影响。但是,在Substrate上并非如此。一旦交易开始修改区块链的存储,这些更改就是永久性的,即使交易在运行时执行期间失败也是如此。
  • 函数返回。模块中的函数无法返回一个值。它只能返回一个Result,当一切成功完成时返回Ok(()),或者如果出现错误则返回Err(&'static str)。如果没有明确指定Result作为返回值,decl_module!宏将自动添加它,将在最后返回Ok(())
  • 计算成本。
  • 检查origin。所有函数都使用origin来确定调用的来源。模块支持三种origin类型的检查: 应该总是使用其中一个作为在函数中做的第一件事,否则链可能是可攻击的。decl_module!宏会自动转换没有origin的函数,并在函数中增加一行ensure_root(origin)?,来检查origin是否为Root
    1. 签名的Extrinsic - ensure_signed(origin)?
    2. 固有的Extrinsic - ensure_inherent(origin)?
    3. Root - ensure_root(origin)?

保留函数

有一些函数名称是保留的,可以在自己的模块中使用它们。

  • deposit_event()

如果想要模块发送事件,则需要定义deposit_event()函数,该函数处理在decl_events!宏中定义的事件。事件可以包含泛型,在这种情况下,应该定义deposit_event<T>()函数。

decl_module!宏为deposit_event()函数提供了一个默认实现,可以通过简单地定义函数来访问它:

fn deposit_event() = default;
// 或者使用泛型事件
// fn deposit_event<T>() = default;
  • on_initialise()on_finalise()

他们是特殊的函数,每个块执行一次。可以不带参数调用这些函数,也可以接受一个区块号的参数。

可以使用on_initalise(),在运行时的任何逻辑执行之前,运行需要运行的任务。可以使用on_finalise(),清理任何不需要的存储项或为下一个块重置某些值。

特权函数

特权函数是只能在调用来源是Root时调用的函数。可以在Consensus模块中找到特权函数的示例,进行运行时升级:

pub fn set_code(new: Vec<u8>) {
    storage::unhashed::put_raw(well_known_keys::CODE, &new);
}

decl_storage!

大多数运行时模块包含存储项,它在区块链运行时,用户与模块交互时被更改。

在宏decl_storage!中,初始化存储项的四种方式:

  1. 硬编码默认值:使用config(),并将初始值置于行末尾。
  2. 部署时赋值:仅使用config(),部署时赋值:1)在Rust代码中src/chain_spec.rs;2)在配置文件中chainspec.json
  3. 单值计算:使用build(<closure>),通过闭包返回想要的初始值。
  4. 多值计算:使用add-extra-genesis
pub Ante get(ante) config(): u32 = 5;
pub Ante get(ante) config(): u32;
pub MinRaise get(min_raise) build(|config: &GenesisConfig<T>| config.ante * 2): u32;
add_extra_genesis {
  config(atom): u32;
}

示例中的存储项定义如下:

decl_storage! {
    trait Store for Module<T: Trait> as TemplateModule {
        pub TotalSupply get(total_supply): u64 = 21000000;
        pub BalanceOf get(balance_of): map T::AccountId => u64;
        Init get(is_init): bool;
    }
}

GenesisConfig结构体

创世配置用于在Substrate运行时的第一个块初始化存储项的状态。

decl_storage!宏展开时,它生成GenesisConfig类型,其中包含使用config()参数声明的每个存储项的引用。支持创世配置的每个模块也将其GenesisConfig类型别名为<ModuleName>Config,作construct_runtime!宏展开的一部分。

启动节点时,将使用外部GenesisConfig将初始值设置到存储中。

结语

到此为止,我们大致明白了Substrate运行时的主要组件及其使用。可点击阅读原文获取示例代码的Github链接。

本文分享自微信公众号 - Rust语言学习交流(rust-china)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Rust 视界 | async-std 团队发布 Async Http 套件

    本文是对Yoshua Wuyts 博客文章的摘录,以及一些私人观点。原文地址:https://blog.yoshuawuyts.com/async-http/ ...

    MikeLoveRust
  • 【Rust每周一库】smol - 异步rumtime

    smol是一个轻量而高效的异步runtime。它采用了对标准库进行扩展的方式,整个runtime只有大约1500行代码。作者stjepang大神是大名鼎鼎cro...

    MikeLoveRust
  • 从 RUST 库中公开 FFI

    Wikipedia 将 FFI 定义为一种机制,通过这种机制,用一种编程语言编写的程序可以调用或使用用另一种编程语言编写的服务。

    MikeLoveRust
  • 开板冲刺,科创板交易系统将于5月底全部准备就绪丨科创板块

    昨日,上海证券交易所透露,在中国证监会的统筹指导下,当前科创板已经进入开板冲刺阶段,预计到今年3月底交易系统便可进行全行业全市场联调联试,到5月底便将全部准备就...

    镁客网
  • Python基础学习-函数

    一:定义函数: ① 函数是带名字的代码块,用于完成具体的工作。 ② 函数使用关键字def来定义,最后,定义以冒号结尾。 ③ 每个函数后面都应紧跟一个文档字符串,...

    爱吃西瓜的番茄酱
  • 浏览器启动外部软件

    晓晨
  • 代价函数

    代价函数,度量【假设集】的准确性。 机器学习中常用的代价函数,总结如下: 1 误差平方和函数 ? 说明:yi 是模型预测值,oi是样本实际值 2 交叉熵函数...

    陆勤_数据人网
  • 我是如何发现Google服务器上的LFI漏洞的

    本文将介绍如何利用本地文件包含漏洞读取Google某服务器上的任意文件。漏洞存在于Google的Feedburner中,在提交漏洞后,Google安全团队迅速修...

    FB客服
  • Android ActionBar完全解析,使用官方推荐的最佳导航栏(下)

    本篇文章主要内容来自于Android Doc,我翻译之后又做了些加工,英文好的朋友也可以直接去读原文。 限于篇幅的原因,在上篇文章中我们只学习了ActionBa...

    用户1158055
  • CVPR2020 夜间目标检测挑战赛冠军方案解读

    在 CVPR 2020 Workshop 举办的 NightOwls Detection Challenge 中,来自国内团队深兰科技的 DeepBlueAI ...

    小白学视觉

扫码关注云+社区

领取腾讯云代金券