- A+
之前写的很多文章的模块代码都是基于substrate-node-template开发的,它是一个节点模板程序,这篇文章介绍substrate-node-template的代码结构和各个代码块的功能。
目录结构
定位到substrate-node-template目录,使用tree -I target命令,可以查看substrate-node-template项目的目录结构:
这里使用-I选项省略掉了target目录,下面依次分析这些目录和文件。
Cargo.lock & Cargo.toml
Cargo是rust的包管理器,相当于nodejs的npm或yarn,但cargo具有更多功能,还充当rust的代码组织管理工具,cargo提供了从项目的建立、构建到测试、运行直至部署的一系列工具,为rust项目的管理提供尽可能完整的手段。
Cargo.lock包含依赖项的确切信息,由Cargo自动生成,无需手动编辑,而Cargo.toml需要手动配置依赖。
Cargo.toml存放项目信息[package]和依赖库[dependencies]等,相当于cargo构建项目的指南。
根目录下的Cargo.toml内容如下:
[profile.release]
panic = 'unwind'
[workspace]
members = [
'node',
'pallets/template',
'runtime',
]
substrate-node-template是一个Rust workspace项目,可以清晰地管理组件库(library)和可执行程序(binary)。
这个[workspace]的成员有:
- node:可执行程序,在node/src/main.rs中有可执行的main函数入口;
- pallets/template:模块代码,在pallets/template/src/lib.rs中定义了可被外部调用的函数和数据结构;
- runtime:组件库,在runtime/src/lib.rs中定义了运行时逻辑;
scripts目录
scripts目录下包含两个Shell脚本:
- docker_run.sh:使用Docker启动substrate-node-template的脚本;
- init.sh:初始化WASM构建环境的脚本;
init.sh脚本的内容包括升级Rust版本:
rustup update nightly
rustup update stable
和添加构建WebAssembly工具链:
rustup target add wasm32-unknown-unknown --toolchain nightly
node目录
node目录包含以下文件:
- build.rs:自定义的构建脚本;
- Cargo.toml:node包构建指南;
- src/chain_spec.rs:构造ChainSpec(链规范文件);
- src/cli.rs:声明客户端结构体和子命令;
- src/command.rs:提供客户端相关命令的实现函数;
- src/lib.rs:引入库模块;
- src/main.rs:substrate-node-template编译成可执行程序的入口文件;
- src/rpc.rs:节点指定的RPC方法的集合;
- src/service.rs:提供构造Substrate服务的工具方法;
1、Cargo.toml是node包构建指南,使用[[bin]]表示这个包是可执行的:
[[bin]]
name = 'node-template'
通过[build-dependencies]引入编译时的依赖,在build.rs中使用:
[build-dependencies]
substrate-build-script-utils = '2.0.0'
其他信息还有[package]、[package.metadata.docs.rs]、[dependencies]、[features]等,和一般Cargo.toml相同。
2、 build.rs是自定义的构建脚本,内容如下:
use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed};
fn main() {
generate_cargo_keys();
rerun_if_git_head_changed();
}
作用是让Cargo编译和执行该脚本。
3、src/main.rs是substrate-node-template编译成可执行程序的入口文件,内容如下:
#![warn(missing_docs)]
mod chain_spec;
#[macro_use]
mod service;
mod cli;
mod command;
mod rpc;
fn main() -> sc_cli::Result<()> {
command::run()
}
#![warn(missing_docs)]注解表示在编译时,如果模块缺少文档会打印警告信息。
mod chain_spec、mod service、mod cli、mod command、mod rpc引入当前目录下的其他模块。
#[macro_use]加载引入的模块下的所有宏。
main()函数是应用程序入口,返回的sc_cli::Result<()>是一个自定义Result类型:
pub type Result<T> = std::result::Result<T, Error>;
main()函数内部执行command模块提供的run()函数。
4、src/command.rs提供客户端相关命令的实现函数,创建了Cli的实现SubstrateCli,定义了run()函数。
run()函数内部先通过from_args()解析命令行参数,返回一个Cli结构体,该结构体在cli.rs中定义,然后匹配参数中的子命令(subcommand),如果存在子命令则执行它。
执行子命令时先调用cli.create_runner(cmd)?创建runner,再调用async_run()异步执行该子命令。
如果命令行参数中没有子命令,则调用run_node_until_exit()启动节点。
5、src/cli.rs声明客户端结构体和子命令。
Cli结构体声明如下:
#[derive(Debug, StructOpt)]
pub struct Cli {
#[structopt(subcommand)]
pub subcommand: Option<Subcommand>,
#[structopt(flatten)]
pub run: RunCmd,
}
包含可选的子命令和命令行选项,编译后的substrate-node-template可以通过
./target/release/node-template -h
获得可用的子命令和选项的使用说明。
具体的子命令使用枚举声明:
#[derive(Debug, StructOpt)]
pub enum Subcommand {
BuildSpec(sc_cli::BuildSpecCmd),
CheckBlock(sc_cli::CheckBlockCmd),
ExportBlocks(sc_cli::ExportBlocksCmd),
ExportState(sc_cli::ExportStateCmd),
ImportBlocks(sc_cli::ImportBlocksCmd),
PurgeChain(sc_cli::PurgeChainCmd),
Revert(sc_cli::RevertCmd),
#[structopt(name = "benchmark", about = "Benchmark runtime pallets.")]
Benchmark(frame_benchmarking_cli::BenchmarkCmd),
}
6、src/chain_spec.rs构造ChainSpec(链规范文件),ChainSpec定义了链的可用配置,用来构造初始区块。
src/chain_spec.rs中定义了两个函数:
- pub fn development_config() -> Result<ChainSpec, String>
- pub fn local_testnet_config() -> Result<ChainSpec, String>
表示substrate-node-template提供的两种模式:
- 通过命令行选项--dev指定的开发网络(development),只有一个验证人Alice;
- 本地测试网络(local_testnet),有两个验证人Alice和Bob;
接下来调用ChainSpec::from_genesis创建链规范文件。
7、src/service.rs提供构造Substrate服务的工具方法。
src/service.rs先使用native_executor_instance!宏定义了一个结构体Executor,并实现NativeExecutionDispatch接口,即可以通过函数名来调用该函数。
src/service.rs的工具方法包括:
- new_partial:构建一个局部节点服务;
- new_full:构建一个全节点服务;
- new_light:构建一个轻节点服务;
8、src/rpc.rs提供节点指定的RPC方法的集合。
rpc.rs提供create_full()方法,用于实例化所有完整的RPC扩展。
9、src/lib.rs用于引入库模块,内容如下:
pub mod chain_spec;
pub mod service;
pub mod rpc;
runtime目录
runtime目录包含以下文件:
- build.rs:自定义的构建脚本;
- Cargo.toml:runtime包构建指南;
- src/lib.rs:链上runtime入口文件;
1、Cargo.toml是runtime包的构建指南,除了常见的配置项外,还有[build-dependencies]:
[build-dependencies]
wasm-builder-runner = { package = 'substrate-wasm-builder-runner', version = '2.0.0' }
添加了构建脚本build.rs所依赖的wasm-builder-runner。
2、build.rs是自定义的构建脚本,内容如下:
use wasm_builder_runner::WasmBuilder;
fn main() {
WasmBuilder::new()
.with_current_project()
.with_wasm_builder_from_crates("2.0.0")
.export_heap_base()
.import_memory()
.build()
}
使用wasm-builder-runner将当前的runtime项目编译为Wasm二进制文件,该文件位于target/release/wbuild/node-template-runtime/node_template_runtime.compact.wasm。
3、src/lib.rs是链上runtime入口文件。
#![cfg_attr(not(feature = "std"), no_std)]表示编译时如果feature不是std(Rust标准库),那么必须是no_std(编译为Wasm)。
#![recursion_limit="256"]设置编译时可能出现的无限递归操作的最大数量。
下面的代码:
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
表示当使用Rust标准库编译时,将生成的Wasm二进制内容通过常量的方式引入到当前runtime代码中。
引入依赖模块和template模块:
pub use pallet_template;
接下来是一些runtime所需的基础类型的别名,和模块中的相关类型名称一致。
opaque模块封装了一些用于CLI初始化时的类型。
定义区块时间相关的常量:
pub const MILLISECS_PER_BLOCK: u64 = 6000;
即每个区块的生产时间是6秒,可以根据需要修改配置。
接下来使用parameter_types!宏生成一些功能模块所需的满足Get接口的数据类型。
然后为runtime实现各个功能模块的接口:
impl frame_system::Trait for Runtime {...}
impl pallet_aura::Trait for Runtime {...}
impl pallet_grandpa::Trait for Runtime {...}
impl pallet_timestamp::Trait for Runtime {...}
impl pallet_balances::Trait for Runtime {...}
impl pallet_transaction_payment::Trait for Runtime {...}
impl pallet_sudo::Trait for Runtime {...}
impl pallet_template::Trait for Runtime {...}
runtime由construct_runtime!宏构建:
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: frame_system::{Module, Call, Config, Storage, Event<T>},
RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage},
Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent},
Aura: pallet_aura::{Module, Config<T>, Inherent},
Grandpa: pallet_grandpa::{Module, Call, Storage, Config, Event},
Balances: pallet_balances::{Module, Call, Storage, Config<T>, Event<T>},
TransactionPayment: pallet_transaction_payment::{Module, Storage},
Sudo: pallet_sudo::{Module, Call, Config<T>, Storage, Event<T>},
// Include the custom logic from the template pallet in the runtime.
TemplateModule: pallet_template::{Module, Call, Storage, Event<T>},
}
);
construct_runtime!宏根据模块名称和所用的模块内的组件来构造runtime,构造时按照顺序加载初始存储,所以当B模块依赖A模块时,应当将A模块放在B之前。
最后使用impl_runtime_apis!宏实现runtime api定义的接口,这些接口通过decl_runtime_apis!宏进行定义。
pallets/template目录
pallets目录下可以包含多个pallet(模块),template就是一个pallet。
pallets/template目录包含以下文件:
- Cargo.toml:template模块构建指南;
- src/lib.rs:模块的具体功能实现代码;
- src/mock.rs:测试用例服务代码;
- src/tests.rs:测试用例;
1、Cargo.toml是template模块的构建指南,根据需求主要配置[dependencies]和[features]。
[dependencies]是模块的依赖库,[features]默认使用std feature,保证runtime既可以编译为native版本(使用std feature),也可以编译为wasm版本(使用no_std feature)。2、src/lib.rs是模块的具体功能实现代码。
mock和tests只在运行测试时进行编译。
然后是类型声明:
pub trait Trait: frame_system::Trait {
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
}
以及和业务逻辑代码相关的四个宏:
- decl_storage!:定义存储;
- decl_event!:定义事件;
- decl_error!:定义错误处理机制;
- decl_module!:定义业务逻辑代码;
- 我的微信
- 这是我的微信扫一扫
- 我的电报
- 这是我的电报扫一扫