Substrate 总览(3) – 入门参考

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

已经鸽了好久了,现在开始补文章。 本文承接Substrate设计总览入门介绍系列第三篇,介绍Substrate的入门参考。三篇文章为: * [Substrate的总体设计](https://learnblockchain.cn/2019/03/02/substrate-project/ "Substrate的总体设计") * [Substrate的项目结构](https://learnblockchain.cn/2019/03/10/substrate-structure/ "Substrate的项目结构") * [入门参考](https://learnblockchain.cn/2019/11/05/substrate-start/ "入门参考") 本文重点从运行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.rs`和`Cargo.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-S`的`Cargo.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-S`和`new-S`Runtime 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层的基础类型 * `rpc`和`rpc-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-S`和`old-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_block`,`execute_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 *本文首发于知乎专栏* *[金狗喵喵喵的区块链研习](https://zhuanlan.zhihu.com/c_74315572),版权属于* [@金晓](https://www.zhihu.com/people/cb3b97fbae2e613fba0f76e7bd23fc76) *如需转载,需取得同意并标明出处,并涵盖版权信息!*

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

本文承接Substrate设计总览入门介绍系列第三篇,介绍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文件于路径下

&lt;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 &lt;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包,位于目录:&lt;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文件也位于该目录下:&lt;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

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

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

区块链技术网。

  • 发表于 2019-11-05 19:42
  • 阅读 ( 3770 )
  • 学分 ( 27 )
  • 分类:Polkadot

评论