深入浅出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>) whereBlock = 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!
宏展开后生成的RawEvent
和Event
类型。
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
。
签名的Extrinsic -
ensure_signed(origin)?
固有的Extrinsic -
ensure_inherent(origin)?
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!
中,初始化存储项的四种方式:
硬编码默认值:使用
config()
,并将初始值置于行末尾。部署时赋值:仅使用
config()
,部署时赋值:1)在Rust代码中src/chain_spec.rs
;2)在配置文件中chainspec.json
。单值计算:使用
build(<closure>)
,通过闭包返回想要的初始值。多值计算:使用
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链接。
相关阅读
如何用Substrate构建自己的区块链?
深入浅出Substrate:剖析运行时Runtime相关推荐
- Deep Learning部署TVM Golang运行时Runtime
Deep Learning部署TVM Golang运行时Runtime 介绍 TVM是一个开放式深度学习编译器堆栈,用于编译从不同框架到CPU,GPU或专用加速器的各种深度学习模型.TVM支持来自Te ...
- “ compiler-rt”运行时runtime库
" compiler-rt"运行时runtime库 编译器-rt项目包括: • Builtins-一个简单的库,提供了代码生成和其他运行时runtime组件所需的特定于目标的低级接 ...
- CUDA运行时 Runtime(四)
CUDA运行时 Runtime(四) 一. 图 图为CUDA中的工作提交提供了一种新的模型.图是一系列操作,如内核启动,由依赖项连接,依赖项与执行分开定义.这允许定义一次图形,然后重复启动.将图的定义 ...
- CUDA运行时Runtime(三)
CUDA运行时Runtime(三) 一.异步并发执行 CUDA将以下操作公开为可以彼此并发操作的独立任务: 主机计算: 设备计算: 从主机到设备的内存传输: 从设备到主机的存储器传输: 在给定设备的存 ...
- CUDA运行时 Runtime(二)
CUDA运行时 Runtime(二) 一. 概述 下面的代码示例是利用共享内存的矩阵乘法的实现.在这个实现中,每个线程块负责计算C的一个方子矩阵C sub,块内的每个线程负责计算Csub的一个元素.如 ...
- CUDA运行时 Runtime(一)
CUDA运行时 Runtime(一) 一. 概述 运行时在cudart库中实现,该库通过静态方式链接到应用程序库cudart.lib和 libcudart.a,或动态通过cudart.dll或者lib ...
- iOS运行时Runtime浅析
运行时是iOS中一个很重要的概念,iOS运行过程中都会被转化为runtime的C代码执行.例如[target doSomething];会被转化成objc)msgSend(target,@select ...
- ios runtime重要性_iOS运行时RunTime详解
Objective-C语言是扩展于C语言的一种面向对象的编程语言,然而其方法的调用方式又和大多数面向对象语言大有不同,其采用消息传递.转发的方式进行方法的调用.因此在OC中,对象的真正行为往往发生在运 ...
- HotSpot VM运行时01---命令行选项解析
HotSpot VM有3个主要组件:VM运行时(Runtime).JIT编译器(JIT Compiler)以及内存管理器(Memory Manager). HotSpot VM运行时担当许多职责:命令 ...
最新文章
- 通往SQL Server复制的阶梯:一级- SQL服务器复制介绍
- ARM NEON 优化
- JavaScript编程知识
- Android Studio使用Gradle上传AAR至Maven
- javap(反汇编命令)详解
- Java可移动性不强_java地位无可撼动的原因
- 在IT行业对专业知识的掌握能力
- Introduction to Chinese natural language processing
- 推荐一个程序员阅读文章资料时的辅助神器
- 【jquery调用ajax老是进error,不进success】 bug命名:小雨
- itunes卸载工具_iTunes卸载麻烦
- 计算机2级选择题及答案,计算机二级Office模拟试题及答案
- 【ansible】如何将ansible jinja2的双花括号转义?
- c++中 int、long、long long等取值范围
- 部署 redis 和基本操作
- Proxy SwitchyOmega
- 在vue中用openlayers调取天地图服务并动态选择各个省份的中心,及行政边界
- 天津理工大学 信息论与编码实验3 离散信源编码-香农编码
- linux convert 命令 把gif转成jpg或png格式的问题
- 干货!教您使用工业树莓派结合CODESYS配置EtherCAT主站
热门文章
- t20天正插件5.0怎么安装
- 微信公众号二次开发流程
- 为debian linux配置upsd不间断电源守护
- R语言中的函数17:as.Date()和as.POSIXlt()
- R语言中的函数3:curve()
- 用了都说好的Python专属无广告视频播放器,良心到想为它疯狂打call
- C++ Primer Plus 学习记录(第五章节-包含练习题答案)
- Altium Designer 实用操作笔记
- 瞬变电磁matlab,基于Matlab的矿井瞬变电磁超前探测三维显示技术
- PLC通信协议【三菱Q系列】MC协议