Substrate 总览(3) - 入门参考

  • 金晓
  • 更新于 2019-11-05 19:42
  • 阅读 7658

Substrate设计总览入门介绍系列第三篇

已经鸽了好久了,现在开始补文章。

本文承接Substrate设计总览入门介绍系列第三篇,介绍Substrate的入门参考。三篇文章为:

本文重点从运行Substrate的node节点介绍如何入门。

目前Substrate的文档十分缺乏,本文的介绍相当于是Substrate的一种文档。

自本系列第一篇文章至本文半年的时间,Substrate虽然整体框架上变动不大,但是其中很多细节已经有了很大的变化,因此对于入学者来说需要对照起前两篇文章介绍时的substrate与当前的substrate,否则在一些概念上无法联系起来。

对于新的Substrate,本文定master分支上的提交ec7c6cf1779b88e75137ef6f6f7bf67ecd0f75a5(11月2日),后文简称为new-S

对于前两篇文章提到的Substrate,本文定master分支上的提交d6eba14a55be26e1a4e24a882ff574aa0190aff6 (5月22日),后文简称为old-S

运行node

根据(二)中介绍的项目结构,Substrate自身已经提供了一个node节点用于运行Substrate。也就是说node就是使用Substrate框架的模板。

因此若使用Substrate开发区块链,就是仿照node进行项目组织

准备

因为Substrate在第一次启动的时候需要加载一些环境,而且若加载初始环境时间过久会导致无法出块,因此建议大家选用cpu性能好一些的电脑,或者根据后文更改出块间隔时间

另一方面若能够灵活使用gdb或者IDE的debug工具将会对学习substrate节省相当大量的时间,因此建议大家首先想办法配置并熟悉如何debug一个rust项目,再继续后续文章。

Substrate项目当前已经十分复杂了,很多情况下仅仅看可能无法在脑中记住代码逻辑,因此推荐大家采用一些IDE进行辅助。这里推荐IntelliJ系列的IDE:

  1. IntelliJ IDEA ,安装上rust插件后即可使用。但是无法与gdb进行联通,进行debug。不过这个有社区版,免费。
  2. Clion,安装上rust插件后即可使用,可以使用gdb进行联通调试,不过这个收费。

个人推荐Clion。

至于vscode加上rust插件和rls,个人不是很推荐,因为substrate太庞大了,经常让rls崩溃。。而IntelliJ 这边的rust插件的智能化是重新写的,没有用rls。

对于Rust而言,需要事先熟悉好“关联属性”相关的部分。

运行node

这里建议不用参照substrate的README进行操作,而是自己编译substrate的源码进行调试。

new-S

对于new-S而言,首先参照README中的6.1章节,根据自己的操作系统配置好环境

切换到Substrate的根目录下,执行以下命令:

# 建议首先设置下面这个环境变量(到当前shell环境,到.bashrc 等等,总之就是在执行cargo build/run 的时候,上下文要有这个环境变量)
export WASM_BUILD_TYPE=release

cargo run -- --dev -d .sub --execution=NativeElseWasm
# 若电脑性能不够,建议编译成release,否则无法出块。
# cargo run --release -- --dev -d .sub --execution=NativeElseWasm

即可直接运行node节点。这里--dev是指定为dev模式,并配置好默认的Alice私钥运行单节点。

命令行参数说明如下:

  • --dev差不多等价为---chain=dev --validator --key=//Alice,再加上其他的一些rpc,telemtry相关的。这里只是强调会以Alice的私钥启动验证者模式。
  • -d .sub 用于指定该链生成数据(区块,状态等)的数据根目录,我一般习惯用.sub,大家可以设置成自己希望的路径。
  • --execution=Native将会指定执行方式为NativeElseWasm,因为只有在Native环境下才可下断点调试Runtime,否则默认以wasm执行是无法调试Runtime的。
2019-11-03 16:32:26 Running in --dev mode, RPC CORS has been disabled.
2019-11-03 16:32:26 Substrate Node
2019-11-03 16:32:26   version 2.0.0-ec7c6cf17-x86_64-linux-gnu
2019-11-03 16:32:26   by Parity Technologies, 2017-2019
2019-11-03 16:32:26 Chain specification: Development
2019-11-03 16:32:26 Node name: delightful-planes-8797
2019-11-03 16:32:26 Roles: AUTHORITY
2019-11-03 16:32:29 Initializing Genesis block/state (state: 0x0ec9…c1dd, header-hash: 0x8762…41c3)
2019-11-03 16:32:29 Loading GRANDPA authority set from genesis on what appears to be first startup.
2019-11-03 16:32:40 Loaded block-time = BabeConfiguration { slot_duration: 3000, epoch_length: 200, c: (1, 4), genesis_authorities: [(Public(d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d (5GrwvaEF...)), 1)], randomness: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], secondary_slots: true } seconds from genesis on first-launch
2019-11-03 16:32:40 Creating empty BABE epoch changes on what appears to be first startup.
2019-11-03 16:32:43 Highest known block at #0
2019-11-03 16:32:43 Using default protocol ID "sup" because none is configured in the chain specs
2019-11-03 16:32:43 Local node identity is: QmRX26NAABDXQHGxVESibyrAnciBQdpgwdX2ZRngh5vCUt
2019-11-03 16:32:43 Starting BABE Authorship worker
2019-11-03 16:32:45 Starting consensus session on top of parent 0x8762ac86a1f1723f4b6659c2f5b0c848c1f1ec3f65f1fb6ef37e903a72ec41c3

其他命令具体执行--help看描述就好。

old-S

这里同样不建议参照这个版本下的README,而是按照以下操作:

首先参照该版本README的6.1章节配置好环境,注意这个版本的下的windows似乎支持还不完善(不确定)。

然后切换以下目录执行:

cd node/runtime/wasm
./build.sh

这个行为会编译node的节点的Runtime的WASM文件于路径下

<substrate>/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm

简单浏览build.sh文件可知,实际上这个步骤是把Runtime编译了一份--target=wasm32-unknown-unknown目标的wasm文件,然后使用wasm-gc对生成的wasm进行压缩。

注意这个文件后续将要在其他文件中获取到(主要是genesis),编译进入node的二进制中。

然后切换回substrate根目录上,执行

cargo run -- --dev -d .sub --block-construction-execution=NativeElseWasm --other-execution=NativeElseWasm

即可和new-S一样运行起以Alice为私钥启动的单验证者节点。

这里提一下,在这个版本的substrate里面存在一个私钥推断的bug,如果一定要严格按照Alice私钥生成规则生成的话(如涉及到subkey)建议切换到提交498452517f95d399ed1b422ea5097d2aa984fd02,或者把这个提交cherry pick过来,否则只要把Alice私钥导出来使用即可,其他部分不影响。

new-S相比execution部分在old-S中还没有统一成一个指令,而另两个execution不重要,因此只需要指定--block-construction-execution--other-execution是native即可。

运行起来后可看到

2019-11-03 21:37:04 Running in --dev mode, RPC CORS has been disabled.
2019-11-03 21:37:04 Substrate Node
2019-11-03 21:37:04   version 2.0.0-d6eba14a5-x86_64-linux-gnu
2019-11-03 21:37:04   by Parity Technologies, 2017-2019
2019-11-03 21:37:04 Chain specification: Development
2019-11-03 21:37:04 Node name: able-thumb-3759
2019-11-03 21:37:04 Roles: AUTHORITY
2019-11-03 21:37:07 Initializing Genesis block/state (state: 0xd4a3…d6cd, header-hash: 0x9dd9…1463)
2019-11-03 21:37:07 Loaded block-time = 4 seconds from genesis on first-launch
2019-11-03 21:37:07 Loading GRANDPA authority set from genesis on what appears to be first startup.
2019-11-03 21:37:08 Highest known block at #0
2019-11-03 21:37:08 Using default protocol ID "sup" because none is configured in the chain specs
2019-11-03 21:37:08 Local node identity is: QmbeniX5UtetYpk8i6NdLvuvtWhnymz6R5tCdK91pgaL4d
2019-11-03 21:37:08 Libp2p => Random Kademlia query has yielded empty results
2019-11-03 21:37:08 Listening for new connections on 127.0.0.1:9944.
2019-11-03 21:37:08 Using authority key 5CLFVjc5C8A5vwwuHaYgxHDsamotvEsaWE7D7jJR7SsDbeWm
2019-11-03 21:37:08 Running Grandpa session as Authority 5CLFVjc5C8A5vwwuHaYgxHDsamotvEsaWE7D7jJR7SsDbeWm
2019-11-03 21:37:13 Idle (0 peers), best: #0 (0x9dd9…1463), finalized #0 (0x9dd9…1463), ⬇ 0 ⬆ 0
2019-11-03 21:37:13 Prepared block for proposing at 1 [hash: 0x8df244b6e236c6b224a2b0399151455ac34a5a6498f47cae6ea2b9282d9abf45; parent_hash: 0x9dd9…1463; extrinsics: [0x6a28…491a]]
2019-11-03 21:37:13 Pre-sealed block for proposal at 1. Hash now 0x6c70e1e55e2f44962b3ddbac078c7c7d7dbd3a0c108a9af011585de55580041a, previously 0x8df244b6e236c6b224a2b0399151455ac34a5a6498f47cae6ea2b9282d9abf45.

new-S一样,当看到Pre-sealed block for proposal时,表示已经在正常出块了。

new-S和old-S 启动变化的原因

从前文可以看出,old-S需要手动执行build.sh再编译节点,而new-S只需要设置一个环境变量,直接编译就好。

这是因为实际上执行build.sh的时候,就是在把Runtime部分编译成wasm的过程,而在new-S已经把这个集成到了编译命令里,因此直接编辑即可。

这两者是有显著区别的:

对于old-S而言,需要显式的存在wasm的这个包(crate),即位于/node/runtime/wasm/,因此

|-- node/runtime  # 代表整个Runtime部分
            |--/wasm  # 代表对上级目录的Runtime编译成WASM形式

我们查看/wasm目录下的lib.rsCargo.toml可以看到:

[package]
name = "node-runtime-wasm"
version = "2.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"

[lib]
name = "node_runtime"
crate-type = ["cdylib"]

[dependencies]
node-runtime = { path = "..", default-features = false }  # 注意这一行,表示对于编译WASM而言,引用的源文件为上级的Runtime

node/wasm/src/lib.rs文件中只有一行:

#![cfg_attr(not(feature = "std"), no_std)]

pub use node_runtime::*;

即表示引用Runtime的所有东西。

而编译出来的wasm文件即显式的位于:/node/runtime/wasm/target/wasm32-unknown-unknown/release/node_runtime.compact.wasm

对于new-S而言,这个wasm包已经不显式的存在了,它的整个存在及编译产物都融合在了一个编译指令中(这是由于cargo可以自定义编译过程,类似cmake一些脚本)。

new-S中,在编译中直接生成了类似old-S中的wasm包,位于目录:<substrate>/target/debug(或者release)/wbuild,注意target目录即是编译产物。所以实际上在new-S中,wasm包成变为了自动生成而不需要显示存在了。

wbuild目录下,我们可以看到一个目录node-runtime,这个目录下的的Cargo.toml文件是这样的:

[package]
                name = "node-runtime-wasm"
                version = "1.0.0"
                edition = "2018"

                [lib]
                name = "node_runtime"
                crate-type = ["cdylib"]

                [dependencies]
                wasm_project = { package = "node-runtime", path = "/你的路径/substrate/node/runtime", default-features = false }

将其和old-SCargo.toml一比较就很明显了,这个目录即是原来old-S那个需要显式存在的wasm目录。

而编译所产生的wasm文件也位于该目录下:<substrate>/target/debug(或者release)/wbuild/node-runtime/node_runtime.compact.wasm

而在old-S中,这个wasm文件根据build.sh脚本只能编译release,而在new-S中这个文件就是根据环境变量WASM_BUILD_TYPE来决定是release或者debug。虽然我觉得wasm编译成debug一点用都没有。

node项目结构

介绍了old-Snew-SRuntime wasm的差别后,现在介绍node的项目结构,即应该如何使用Substrat框架。

由前2篇的文章可知,实际上使用Substrate构建的链分为2部分:

  • Runtime 层,实际上代表链的业务逻辑
  • 除Runtime以外的其他,代表了链应该具备的基础功能,如共识,交易池,p2p等。

其中前者就是链的开发者需要做的,而后者Substrate已经做了绝大部分并留出了接口,因此node的作用就是去调用这些接口,并和Runtime层结合起来。

对于new-S而言,项目结构是:

  • cli,该目录实际上是连接Substrate的核心,并且是项目的入口,包含了以下3个核心

    • chain_spec.rs:表示链的描述(名字,网络协议号等),genesis的配置与生成
    • cli.rs:启动入口,配置自有的命令参数
    • service.rs:启动服务,也就是一个完整区块链中网络服务,交易池,执行线程,等等服务线程的配置与启动点。
  • executor:提供执行器宏native_executor_instance!的配置,自己的项目照抄即可

  • primitive:node项目中一些类型信息的原语,比如定义区块头,定义交易,定义签名类型,定义区块高度等等,自己的项目的通用类型可以定义于此,注意这些类型定义是对于区块链基础层的,不是Runtime层的基础类型

  • rpcrpc-client:在new-S留出了rpc的扩展接口,在old-S没有。如果需要添加针对自己项目的rpc,就可以参照rpc定义自己需要的rpc接口。rpc-client只是一个客户端,不重要

  • runtime:链的Runtime,即本链的核心,后文进行介绍。

  • testing(不做介绍)

对于old-S而言,项目结构与new-S基本一致。只是由于rpc没有扩展接口,所以old-S除非更改Substrate源码,否则无法扩展rpc。

这里重点介绍一下cli

新老在管理线程方式有比较大的区别,在new-S已经全套用了Future管理,在old-S中主要还是使用Signal管理。不过这些其实并不重要,若不是需要把老Substrate移植到新的Substrate上保持兼容,这些直接选用新的Substrate的管理方式即可。因此这里只简单介绍一下new-S

无论运行新老,它们的主要入口都是run_until_exit。该处就是启动所有服务的地方。在new-S中,其方式为:

  1. 首先在parse_and_prepare中,解析各种输入参数,成为config
  2. 根据config对Service进行配置(service::new_light/service::new_full),生成一个实例,因此Service实例,也就是core/service/src/lib.rs中的pub struct Service,持有了所有关键服务的引用。在new_full中启动了这些服务。
  3. 把Service实例传入run_until_exit中运行,并block住,等待退出的kill信号。

具体需要更改就依据源码即可,这里就不详细介绍了。

node的Runtime

这里就是核心部分。这个部分new-Sold-S差别也不是很大。Runtime的核心其实就是node/runtime/src/lib.rs文件,这个文件大体可分为一下几部分:

  1. VERSION的定义。这个很关键,其控制着Runtime的版本(Runtime版本和链版本不是一个东西),这个版本将会控制者执行代码的时候Native版本和Wasm版本的比较,从而比如在NativeElseWasm模式下选择正确版本的代码执行
  2. 很大一串 Runtime Module 的 trait的实现,这里的Runtime Module 即为实现Runtime的模块,比如balances控制资产模块,staking控制权益模块等等。而Substrate预先实现的一系列模块叫做 Substrate Runtime Module Libary,其缩写也就是srml,也就是Substrate目录下的srml目录下的文件。使用Substrate的链的实现者可以引用这些Substrate提供的库,也可以自己重新编写符合自己业务需求的。
  3. construct_runtime!宏,这个宏就是构建Runtime最核心的部分,也就是说这条链的Runtime由那些模块组成。在srml中编写的模块,或者开发者自己编写的Runtime Module,最后需要写到这个宏里才会真正在这条链里存在并生效,也就是Runtime中的各个模块的总开关。
  4. 地址,区块头,区块,交易体等的定义,注意这里和Runtime的定义,和前文提到的primitives中的定义不一样。
  5. impl_runtime_apis!宏,Runtime层的api的实现宏,通过这个宏可以实现一些让Runtime层对外暴露的接口,大部分用于重新组织对外暴露的数据,少部分如initialize_blockexecute_block会在一些关键地方被调用去执行Runtime。可以简单的理解为这个宏实现的部分就是外界和Runtime层范围的接口。

开发者开发自己的Runtime层时,应自己研究该部分的组成形式。以后的文章再细说

Runtime Module 的构成

Substrate提供了一个Runtime Module构成的example:位于srml/example

这个模块中文档也很长,写清楚了一个模块的基本构成。总体来说说,一个模块由3个宏构成:

  1. decl_module!。这个宏即是这个模块对外的Call,也就是交易中能够调用的function。在这个宏中每定义一个函数,最后在宏展开的结果都是一个对外的Call,也是发送到链上的交易能够调用入口,类似于以太坊合约中写成external的对外接口,供用户发送交易调用。注意这个宏同时对模块内部生成一个Module的数据结构,对外及对内都代表了这个模块实例

  2. decl_storage!。这个宏即是这个模块定义的k-v存储。注意该定义的存储最后将会进入状态树,也就是说定义在这里的存储即是“链上数据”。相对应的,由于区块链的特性,这里定义的存储应该经过仔细权衡和设计,否则后续会带来很多的坑!今后的文章会说一些。Substrate提供了2中基础的存储定义模式:

    1. value
    2. map/linked_map
  3. decl_event!。这个宏即是类似以太坊合约中的event的概念,用于记录一些关键信息供链外进行解析查看。这个event也是进入状态树的,因此也需要小心设计避免给状态树带来过大的负担,也要考虑兼容问题。个人认为这种设计不好。

  4. trait。这个trait是这个模块在runtime/src/lib.rs中需要实现的接口。一般情况下这个trait有2个作用:

    1. 用于类型的通用化
    2. 用于在Runtime Module 继承关系中,父模块调用子模块的接口(类似于多态的接口定义,实现在对应子模块中,而runtime/src/lib.rs的配置类似虚函数表的指向)

Runtime Module 就是链的开发者需要做的事情,这块部分讲深了需要花费大量的篇幅,后续的文章会简单进行剖析

总结

以上即是Substrate的入门参考,简单的说能做到以下就可以进行代码剖析了:

  1. 以native的形式运行起node,并正常出块
  2. 参考以上入门介绍
  3. 能够下断点debug

本文首发于知乎专栏 金狗喵喵喵的区块链研习,版权属于 @金晓

如需转载,需取得同意并标明出处,并涵盖版权信息!

点赞 2
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
金晓
金晓
江湖只有他的大名,没有他的介绍。