通过本文,你会了解到,1\Substrate node-template的组成部分,及各部分的功能简介;2\参数如何配置
通过本文,你会了解到,
Substrate作为一个标准的区块链开发框架,不仅提供了必备的底层公共组件(如数据库、共识、P2P、交易池)和通用的runtime模块(如资产相关的balances,治理相关的democracy等),还提供了将各个功能组件连接起来的节点模板程序(node-template)和节点程序(node)。本文主要介绍node-template中各个代码块的功能。
在使用类Unix操作系统的情况下,进入node-template文件目录,执行tree -I target
命令,获取详细的文件信息如下:
这里我们忽略了target文件目录下的内容,来较少干扰性的输出。
node-template是一个标准的Rust workspace项目,当项目比较复杂时,使用workspace可以清晰地管理组件库(library)和可执行程序(binary)。在项目根目录的cargo.toml文件里有:
[workspace]
members = [
'node',
'pallets/template',
'runtime',
]
这个workspace的成员有node、pallets/template、runtime,其中node是可执行程序,在对应的src/main.rs文件内拥有一个可执行的main函数入口;pallets/template和runtime是组件库,在src/lib.rs定义了可被外部使用的函数和数据结构。
细心的同学会注意到cargo.toml里还有下面两行配置:
[profile.release]
panic = 'unwind'
它和catch_unwind
一起使用可以捕获某个线程内panic抛出的异常,常用的场景有:
具体请参考Controlling panics with std::panic。
自定义的构建脚本放置在项目的build.rs文件内,可以在编译构建项目之前,让Cargo去编译和执行该脚本,使用场景有:
node-template根目录下的build.rs的具体功能,参考下面的注释,
use vergen::{ConstantsFlags, generate_cargo_keys};
const ERROR_MSG: &str = "Failed to generate metadata files";
fn main() {
// 使用vergen生成环境变量,供项目中的env!宏获取
// 这里设置了VERGEN_SHA_SHORT为Git最新的commit id的缩写
generate_cargo_keys(ConstantsFlags::SHA_SHORT).expect(ERROR_MSG);
// 当.git/HEAD文件改变即切换Git分支时,重新执行这一脚本
build_script_utils::rerun_if_git_head_changed();
}
初始化编译环境,包括升级Rust的版本,包括nightly和stable两个发布渠道:
rustup update nightly
rustup update stable
并且添加构建WebAssembly的支持工具:
rustup target add wasm32-unknown-unknown --toolchain nightly
定期执行本脚本,可以解决一些常见的编译问题如某个依赖安装失败。
包含了自定义的runtime模块,默认只有一个template模块,以此模块为例:
cargo.toml包含:
package的基本信息如name,version,authors等。
package所依赖的第三方库,以frame-support
为例,来源是Github上该代码仓库的某个commit id,并将default-features设置为false(即不使用默认的feature进行编译)。
[dependencies.frame-support]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
rev = '013c1ee167354a08283fb69915fda56a62fee943'
version = '2.0.0-alpha.3'
[features]
default = ['std']
std = [
'codec/std',
'frame-support/std',
'safe-mix/std',
'system/std',
]
说明:Substrate为了保证应用的安全和稳定,对runtime有意地添加了一个约束,也就是在runtime代码里只能使用Rust的核心库及一些辅助库,而不能使用标准库。使用标准库会导致Wasm执行版本编译失败。
src/lib.rs 是runtime模块的具体功能实现:
#![cfg_attr(not(feature = "std"), no_std)]
表示编译时如果feature不是std,那么必须是no_std。
mock和test模块只在运行测试时进行编译;
定义了模块的接口,继承自system模块的接口,并添加了一个关联类型Event,这个Event类型可以转换成system模块下的Event,也可以由当前的template模块定义的的Event转换而来;
pub trait Trait: system::Trait {
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
}
src/mock.rs 是为测试用例服务的,
pub struct Test;
创建了一个测试用的runtime结构体;impl_outer_origin
宏为runtime构造了一个Origin类型,用来标识交易的来源;parameter_types
宏生成一些后面功能模块所需的满足Get
接口的数据类型;()
来mock不关心的数据类型;impl Trait for Test {
type Event = ();
}
pub type TemplateModule = Module<Test>;
定义别名,使测试代码更简洁;new_test_ext
来初始化GenesisConfig,并返回一个基于HashMap的TestExternalities,用于存储的读写和其它扩展操作。src/tests.rs 包含了所有的测试用例,
#[test]
来标识测试函数;new_test_ext
,并通过execute_with
执行closure内的代码;TemplateModule::do_something(Origin::signed(1)
。assert_ok
断言结果是Ok,assert_eq
断言结果等于预期,assert_noop
断言结果为Error并且不修改链上存储状态。cargo.toml 除了上面提到的某个pallet对应的cargo.toml里的类似内容外,还添加了,
构建脚本即build.rs的依赖wasm-builder-runner
;
Substrate内置和开发者自定义的runtime模块(也叫做pallet),
[dependencies.sudo]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
package = 'pallet-sudo'
rev = '013c1ee167354a08283fb69915fda56a62fee943'
version = '2.0.0-alpha.3'
[dependencies.template]
default-features = false
package = 'pallet-template'
path = '../pallets/template'
version = '2.0.0-alpha.3'
build.rs 使用wasm-builder-runner
将当前的runtime项目编译为Wasm,编译后的文件位于target/release/wbuild/node-template-runtime/node_template_runtime.compact.wasm
。
src/lib.rs 就是构造我们链上runtime的入口,
#![cfg_attr(not(feature = "std"), no_std)]
表示编译时如果feature不是std,那么必须是no_std;#![recursion_limit="256"]
用来设置编译期间可能出现无限递归操作(如宏展开)的最大阈值,默认是128,这里我们提高到256,来满足consturct_runtime
宏的需求;#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
pub use balances::Call as BalancesCall
;pub use template
;pub type BlockNumber = u32
;spec_version
加1;impl_version
是协议的实现版本,用来表示节点运行的代码是不同的,仅当非共识相关的优化发生时才可能修改这个值;RUNTIME_API_VERSIONS
包含已实现的runtime api的所有版本信息,由impl_runtime_apis
宏生成,后面会进一步介绍。pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("node-template"),
impl_name: create_runtime_str!("node-template"),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
};
定义区块时间相关的常量,如pub const MILLISECS_PER_BLOCK: u64 = 6000
,即每个区块是6秒,可以根据需要修改配置;
指定当前的NativeVersion,在执行交易时会把NativeVersion和链上的RuntimeVersion进行比较,如果不一致,通常情况下会使用Wasm执行交易。
使用parameter_types
宏生成一些后面功能模块所需的满足Get
接口的数据类型;
为runtime的实现各个功能模块的接口,runtime由construct_runtime
宏生成,
impl template::Trait for Runtime {
type Event = Event;
}
construct_runtime
宏根据名称(如TemplateModule
)和所用的模块内的组件(如template::{Module, Call, Storage, Event<T>}
)来构造runtime,从而使模块中的信息通过metadata暴露出来,并且使该模块在runtime中可用。构造时,是按照顺序加载初始存储的,所以当B模块依赖A模块时,应当将A模块放在B之前。
通过impl_runtime_apis
宏实现runtime api定义的接口,这些接口需要通过decl_runtime_apis
宏进行定义。
cargo.toml 使用[[bin]]
表示这个包是可执行的,通过build-dependencies
引入编译时的依赖,在build.rs中使用,其它内容在前面的章节已经介绍过了。
build.rs 的内容和workspace根目录下的build.rs相同。
src/main.rs 是node-template编译成可执行程序的入口文件,
#![warn(missing_docs)]
在编译时,当模块缺少文档时会打印warnning;
引入了当前目录下的其它代码模块,如mod chain_spec
;
#[macro_use]
会加载引入的模块下的所有宏;
main函数是程序的入口,它返回一个自定义的Result类型,在函数内首先构造了一个VersionInfo
的结构体,用来保存可执行程序的版本信息,其中VERGEN_SHA_SHORT
是在编译时由build.rs产生的,然后执行command模块提供的run函数。
src/command.rs 提供了main所需的run函数,
通过from_args
解析命令行的执行参数,返回一个Cli
结构体,具体参考下面src/cli.rs的内容;
创建一个默认的Substrate服务配置,这些服务包含启动线程运行网络、客户端和交易池等;
如果返回的Cli
实例里存在子命令,则执行子命令,执行子命令时,
首先进行初始化,如设置panic的异常处理机制,日志等;
通过chain_spec::load_spec
获取chain的配置,来更新前面构造的Substrate服务配置;
调用子命令的run
函数来执行该命令,run函数依赖src/service.rs模块里的new_full_start
宏来返回ServiceBuilder,包含了构建Substrate服务的多种组件。
如果返回的Cli
实例里没有子命令,则执行当前命令,
首先初始化,和子命令的初始化功能一样;
更新Substrate服务配置,比子命令的更新操作更全面,配置的所有属性都会更新;
调用run
来启动节点,需要传入全节点客户端的服务实例和轻节点客户端的服务实例,根据服务配置中的节点角色进行选择,启动完成后,保持运行直到接收到退出信号SIGINT
(即Ctrl+C)。
src/cli.rs 借助StructOpt库将命令行参数解析为Cli结构体,包含:
purge-chain
清空本地存储,build-spec
创建一个spec.json的初始文件,revert
回滚链上状态等;--validator
开启验证人模式,--light
以轻客户端方式运行,--ws-port 9944
指定WebSocket监听的TCP端口,等等。编译node-template之后,可以通过./target/release/node-template -h
获取所有可用的子命令和参数,及其帮助信息。#[derive(Debug, StructOpt)]
pub struct Cli {
#[structopt(subcommand)]
pub subcommand: Option<Subcommand>,
#[structopt(flatten)]
pub run: RunCmd,
}
chain_spec.rs 构造了ChainSpec
,它定义了链的可用配置,用来构造初始区块,
--dev
指定开发者网络(Development),只有Alice是验证人;--local
指定本地测试网络(LocalTestnet),Alice和Bob是验证人;ChainSpec::from_genesis
创建硬编码的ChainSpec;testnet_genesis
函数,传入验证人列表、root账户、存有余额的账户列表,构造出GenesisConfig。service.rs 提供了构造Substrate服务的帮助方法,
使用native_executor_instance
宏定义了一个结构体Executor
,并且实现了NativeExecutionDispatch
接口,即可以通过函数名称来调用该函数;
new_full_start
宏构建了一个ServiceBuilder,用来构造全节点服务,过程如下:
调用with_select_chain
设置链的生成策略,也就是当链出现分叉的时候,选择哪个链继续工作,这里使用最长链原则;
调用with_transaction_pool
设置交易池类型,这里使用BasicPool;
调用with_import_queue
设置了导入区块所需的队列,这里使用BasicQueue,可以顺序地导入block。
使用new_full
构建一个全节点服务,
使用new_full_start
宏构建了一个ServiceBuilder;
调用with_finality_proof_provider
设置使用何种策略提供最终性验证;
调用build
构建真正的Substrate Service;
如果是验证人并且不是哨兵模式,调用service的spawn_essential_task
函数,启动用于生成区块的后台任务,使用的是Aura算法;
如果GRANDPA功能没有关闭,调用spawn_essential_task
开启后台运行的投票任务。
使用new_light
构建一个轻节点服务,
调用with_select_chain
设置跟随最长链;
调用with_transaction_pool
设置BasicPool交易池类型;
调用with_import_queue_and_fprb
设置区块导入时所用的队列,以及用来构建最终性验证请求的FinalityProofRequestBuilder
;
调用with_finality_proof_provider
设置使用何种策略提供最终性验证;
调用build
构建真正的Substrate Service。
通过本文,我相信你已经对node-template项目的代码有了比较深入的了解,现在你可以试着调节runtime代码中的一些配置参数如出块时间,自定义一条区块链。
Substrate官方文档: https://substrate.dev/substrate.dev
Parity介绍: https://www.parity.io/www.parity.io
Substrate源码: https://github.com/paritytech/substrategithub.com
Polkadot源码: https://github.com/paritytech/polkadotgithub.com
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!