1、目录结构
使用命令行在工程目录下输入 tree -I target 命令,可以获取下详细的目录结构,makefile 是我自己创建的,为了方便调试
➜ substrate-drill git:(master) ✗ tree -I target
.
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── Makefile
├── README.md
├── node
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ ├── chain_spec.rs
│ ├── cli.rs
│ ├── command.rs
│ ├── main.rs
│ └── service.rs
├── pallets
│ ├── contracts
│ └── template
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ ├── mock.rs
│ └── tests.rs
├── runtime
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ └── lib.rs
└── scripts
└── init.sh
9
directories,
20
files
2
、Cargo
substrate-node-template 是一个Rust workspace 项目,workspace为了开发大型程序,分治crate用,一个workspace是一个共享公共依赖项解析 (具有共享 Cargo.lock),输出目录和各种设置,如配置文件,的一个或多个包的集合。
➜ substrate-drill git:(master) ✗ cat Cargo.toml
[profile.release]
panic =
'unwind'
[workspace]
members = [
'node',
'pallets/template',
'runtime',
]
这里指定了workspace有三个成员:node、pallets/template、runtime,其中node是可执行程序(node/src/main.rs), pallets/template和runtime是lib,在src/lib.rs定义了可被外部使用的函数和数据结构。
[profile.release]
panic =
'unwind'
cargo 可以通过配置profile 去配置rustc编译选项(Profiles provide a way to alter the compiler settings, influencing things like optimizations and debugging symbols)
profile内置了4种:
- dev
- release
- test
- bench
这里设定了当运行 cargo build --release 时 rustc会自动附上 -C panic flag , 'unwind'的意思是 Unwind the stack upon panic, 这样就可以用 std::panic::catch_unwind 捕捉 panic,类似Go里的recover
详细的文档可以查看这里:
- https://doc.rust-lang.org/beta/cargo/reference/profiles.html
- https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html
- https://doc.rust-lang.org/beta/rustc/codegen-options/index.html#panic
3、Build.rs
在node和runtime目录下都能发现有一个build.rs文件,这个是一个构建脚本,构建脚本只是另一个 Rust 文件,此文件将在编译包中的任何其他内容之前,优先进行编译和调用。
node/build.rs
// 主要是用了一个第三方lib -- vergen
// vergen是利用build scripts脚本,在编译期间生成环境变量的包。 我们可以通过env!宏使用vergen生成的如下变量:
// VERGEN_BUILD_TIMESTAMP
// VERGEN_BUILD_DATE
// VERGEN_SHA
// VERGEN_SHA_SHORT
// VERGEN_TARGET_TRIPLE
// VERGEN_SEMVER
// VERGEN_SEMVER_LIGHTWEIGHT
// 提供了git版本、编译时间、运行机器信息、软件版本
use vergen::{ConstantsFlags, generate_cargo_keys};
const
ERROR_MSG: &str =
"Failed to generate metadata files";
fn main() {
// 这里设置了VERGEN_SHA_SHORT为Git最新的commit id的缩写
generate_cargo_keys(ConstantsFlags::SHA_SHORT).expect(ERROR_MSG);
// 如果.git/HEAD文件发生改变(比如切分支),重新触发build.rs脚本
build_script_utils::rerun_if_git_head_changed();
}
这里比较好玩的是: 构建脚本可以通过打印到标准输出流(stdout)里是可以和cargo 进行“交流”
rerun_if_git_head_changed 源码:
pub fn rerun_if_git_head_changed() {
let mut manifest_dir = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is always set by cargo.")
);
let manifest_dir_copy = manifest_dir.clone();
while
manifest_dir.parent().is_some() {
if
manifest_dir.join(".git/HEAD").exists() {
println!("cargo:rerun-if-changed={}", manifest_dir.join(".git/HEAD").display());
// 这里会告诉cargo 如果.git/HEAD发生改变,就重新执行构建脚本
return
}
manifest_dir.pop();
}
println!(
"cargo:warning=Could not find `.git/HEAD` searching from `{}` upwards!",
manifest_dir_copy.display(),
);
// 这里告诉cargo 输出警告
}
当执行构建的时候会在 target/<profile>/build/<pkg>/output 保存这些“交流“
target/release/build/node-template-0cf8392394c631fa/output
cargo:rustc-env=VERGEN_SHA_SHORT=c18df68
cargo:rerun-if-changed=/Users/xib/Desktop/workspaces/rust/substrate-driil/.git/HEAD
cargo:rerun-if-changed:
cargo仅使用文件系统上次修改的“ mtime”时间戳来确定文件是否已更改,如果给定路径下的文件已更改,则重新运行构建脚本
相关文档可以可以查看:https://doc.rust-lang.org/beta/cargo/reference/build-scripts.html
runtime/build.rs
use wasm_builder_runner::WasmBuilder;
fn main() {
WasmBuilder::new()
.with_current_project()
.with_wasm_builder_from_crates("1.0.9")
.export_heap_base()
.import_memory()
.build()
}
将 runtime编译成 WASM binary
编译后会输出到 target/release/wbuild,wasm的文件会在target/release/wbuild/node-template-runtime/node_template_runtime.compact.wasm
4、pallets
这个文件夹下主要是放一些自定义的runtime module
Substrate runtime 是可以由好多runtime module组成,每一个runtime module都可以是独立个功能点
这里以pallets/template为例, 目录结构为:
➜ pallets git:(master) ✗ cd template
➜ template git:(master) ✗ tree -I target
.
├── Cargo.toml
└── src
├── lib.rs
├── mock.rs
└── tests.rs
1
directory,
4
files
查看下cargo会发现有一些依赖配置:
➜ template git:(master) ✗ cat Cargo.toml
# ....
[dependencies.frame-support]
default-features =
false
version =
'2.0.0-alpha.5'
# ....
[dev-dependencies.sp-runtime]
default-features =
false
#### 不使用默认的feature进行编译
version =
'2.0.0-alpha.5'
[features]
default
= ['std']
std = [
'codec/std',
'frame-support/std',
'safe-mix/std',
'system/std',
]
通过feature进行条件编译,当使用Cargo进行构建时,features default = ['std']配置表示默认使用std feature,当编译依赖库如frame-support也默认使用std feature。这样的配置保证了runtime模块既可以编译为Native执行版本(使用std feature),也可以编译为wasm执行版本(使用no_std feature,并由WasmBuilder进行编译)。
特别值得注意:Substrate为了保证应用的安全和稳定,对runtime有意地添加了一个约束,也就是在runtime代码里只能使用Rust的核心库及一些辅助库,而不能使用标准库(The Rust Standard Library)。使用标准库会导致Wasm执行版本编译失败。
5、Runtime简介
写一个runtime module可以分为6个步骤:
- Imports 导入外部依赖
- Pallet Configuration 定义Pallet trait
- Pallet Events 定义Pallet Event类型
- Pallet Errors 定义错误类型
- Pallet Storage Items 定义存储单元
- Callable Pallet Functions 定义可以调用的函数,初始化Error和Event
pallets/template/src/lib.rs 是runtime module组的具体功能实现:
Imports 导入外部依赖
#![cfg_attr(not(feature =
"std"), no_std)]
// 表示编译时如果feature不是std,那么必须是no_std。
// 1. Imports
use frame_support::{decl_module, decl_storage, decl_event, decl_error, dispatch};
use system::ensure_signed;
Pallet Configuration 定义Pallet trait
/// The pallet's configuration trait.
pub trait Trait: system::Trait {
// Add other types and constants required to configure this pallet.
/// The overarching event type.
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
}
Pallet Events 定义Pallet Event类型
// The pallet's events
decl_event!(
pub
enum
Event<T> where AccountId = <T as system::Trait>::AccountId {
/// Just a dummy event.
/// Event `Something` is declared with a parameter of the type `u32` and `AccountId`
/// To emit this event, we call the deposit function, from our runtime functions
SomethingStored(u32, AccountId),
}
);
熟悉Rust中宏的概念都应该知道:在这里decl_event!宏中的代码,有些不是标准的Rust语法,而是Substrade扩展后的语法, 我们可以展开这个宏,查看最终生成的标准生成的Rust代码,我们可以发现,会生成RawEvent枚举,然后使用宏中指定的特征trait将事件event类型生成为RawEvent的具体实现
这里宏展开后生成的RawEvent
和Event
类型:
pub type Event<T> = RawEvent<<T as system::Trait>::AccountId>;
/// Events for this module.
///
pub
enum
RawEvent<AccountId> {
#[doc = r" Just a dummy event."]
#[doc =
r" Event `Something` is declared with a parameter of the type `u32` and `AccountId`"]
#[doc =
r" To emit this event, we call the deposit function, from our runtime functions"]
SomethingStored(u32, AccountId),
}
Pallet Errors 定义错误类型
// The pallet's errors
decl_error! {
pub
enum
Error
for
Module<T: Trait> {
/// Value was None
NoneValue,
/// Value reached maximum and cannot be incremented further
StorageOverflow,
}
}
这个也是和上述Event类似,展开宏后可以看到:
pub
enum
Error<T: Trait> {
#[doc(hidden)]
__Ignore(::frame_support::sp_std::marker::PhantomData<(T,)>,
::frame_support::dispatch::Never),
#[doc = r" Value was None"]
NoneValue,
#[doc = r" Value reached maximum and cannot be incremented further"]
StorageOverflow,
}
Pallet Storage Items 定义存储单元
// This pallet's storage items.
decl_storage! {
// It is important to update your storage name so that your pallet's
// storage items are isolated from other pallets.
// ---------------------------------vvvvvvvvvvvvvv
trait Store
for
Module<T: Trait> as TemplateModule {
// Just a dummy storage item.
// Here we are declaring a StorageValue, `Something` as a Option<u32>
// `get(fn something)` is the default getter which returns either the stored `u32` or `None` if nothing stored
Something get(fn something): Option<u32>;
}
}
宏展开后:
#[doc(hidden)]
mod sp_api_hidden_includes_decl_storage {
pub extern crate frame_support as hidden_include;
// It is important to update your storage name so that your pallet's
// storage items are isolated from other pallets.
// ---------------------------------vvvvvvvvvvvvvv
// Just a dummy storage item.
// Here we are declaring a StorageValue, `Something` as a Option<u32>
// `get(fn something)` is the default getter which returns either the stored `u32` or `None` if nothing stored
}
trait Store {
type Something;
}
impl <T: Trait + 'static> Store
for
Module<T> {
type Something = Something<>;
}
impl <T: Trait + 'static> Module<T> {
pub fn something() -> Option<u32> {
<Something<> as
self::sp_api_hidden_includes_decl_storage::hidden_include::storage::StorageValue<u32>>::get()
}
}
6
、Storage 简介
在Substrate Runtime里主要 Storage Items 主要有四种:
- Storage Value ---- 单一值 Implements the StorageValue trait using the StorageValue generator
- Storage Map ---- KV hash map Implements the StorageMap trait using the StorageMap generator. And StoragePrefixedMap
- Storage Double Map ----- 有两个key的map Implements the StorageDoubleMap trait using the StorageDoubleMap generator. And StoragePrefixedMap
举个栗子:
decl_storage! {
trait Store
for
Module<T: Trait> as Example {
SomeValue get(fn something): Option<u32>;;
SomeMap: map hasher($hash) type => type;
SomeDoubleMap: double_map hasher($hash1) u32, hasher($hash2) u32 => u32;
Foo get(fn foo) config(): u32=12;
}
}
$hash 代表Hashable中可用的哈希算法选择,需要选择一种:
- blake2_128_concat
- twox_64_concat
- identity
上述的storage 可以像这样扩展:
#vis #name get(fn #getter) config(#field_name) build(#closure): #type = #default;
#vis
: Set the visibility of the structure.pub
or nothing.#name
: Name of the storage item, used as a prefix in storage.- [optional]
get(fn #getter)
: Implements the function #getter toModule
. - [optional]
config(#field_name)
:field_name
is optional if get is set. Will include the item inGenesisConfig
. - [optional]
build(#closure)
: Closure called with storage overlays. #type
: Storage type.- [optional]
#default
: Value returned when none
7、Module定义方法
Callable Pallet Functions
// The pallet's dispatchable functions.
decl_module! {
/// The module declaration.
pub struct Module<T: Trait>
for
enum
Call where origin: T::Origin {
// ....
}
}
Module结构体是每个Substrate runtime module 的主干,由Substrate提供的宏decl_module!生成。同时开发人员在编写自己的runtime module时,可以为Module定义跟自己业务相关的函数和实现。
宏展开后,最终生成标准Rust语法的Module
结构体定义如下:
#[doc = r" The module declaration."]
pub struct Module<T: Trait>(::frame_support::sp_std::marker::PhantomData<(T,)>);
最后,Substrate在runtime/lib.rs中使用construct_runtime!
宏将整个Module
结构体导入区块链的运行时。这个宏将自定义的模块和所有其他模块包含在一个名为AllModules
的元组中。运行时的Executive
模块,使用此元组来处理执行这些模块的编排
8、runtime
这个是链的运行时,集成pallets下的runtime module和外部runtime module
runtime/src/lib.rs就是构造我们链上runtime的入口
使用std featue编译时,将生成的Wasm二进制内容通过常量的方式引入到当前runtime代码中
// ...
// Make the WASM binary available.
#[cfg(feature =
"std")]
include!(concat!(env!("OUT_DIR"),
"/wasm_binary.rs"));
// ...
引入依赖的模块,以及为了下游模块方便调用而暴露上游模块的部分函数和数据类型
// ...
// A few exports that help ease life for downstream crates.
#[cfg(any(feature =
"std", test))]
pub use sp_runtime::BuildStorage;
pub use timestamp::Call as TimestampCall;
pub use balances::Call as BalancesCall;
pub use sp_runtime::{Permill, Perbill};
// -----------------
/// add Gas type
pub use contracts::Gas;
// 这个是我自己加的
// -----------------
// ....
/// Importing a template pallet //引入template模块
pub use template;
// ....
Substrate中,Call枚举列出运行时模块公开的可分派函数。每个模块都有自己的Call枚举,其中包含该模块的函数名称和参数。然后,在构造运行时,会生成一个外部Call枚举,作为每个模块特定Call的聚合
运行时中每个模块生成的Call枚举,Substrate会将此枚举传递给construct_runtime!宏用于生成外部Call枚举,该枚举列出所有运行时模块并引用了它们各自的Call对象。示例如下:
construct_runtime!(
pub
enum
Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: system::{Module, Call, Config, Storage, Event<T>},
RandomnessCollectiveFlip: randomness_collective_flip::{Module, Call, Storage},
Timestamp: timestamp::{Module, Call, Storage, Inherent},
Aura: aura::{Module, Config<T>, Inherent(Timestamp)},
Grandpa: grandpa::{Module, Call, Storage, Config, Event},
Balances: balances::{Module, Call, Storage, Config<T>, Event<T>},
TransactionPayment: transaction_payment::{Module, Storage},
Sudo: sudo::{Module, Call, Config<T>, Storage, Event<T>},
// Used for the module template in `./template.rs`
TemplateModule: template::{Module, Call, Storage, Event<T>},
/*** Add This Line ***/
Contracts: contracts::{Module, Call, Config<T>, Storage, Event<T>},
}
);
展开宏后会:
pub
enum
Call {
System(::frame_support::dispatch::CallableCallFor<System, Runtime>),
RandomnessCollectiveFlip(::frame_support::dispatch::CallableCallFor<RandomnessCollectiveFlip,
Runtime>),
Timestamp(::frame_support::dispatch::CallableCallFor<Timestamp, Runtime>),
Grandpa(::frame_support::dispatch::CallableCallFor<Grandpa, Runtime>),
Balances(::frame_support::dispatch::CallableCallFor<Balances, Runtime>),
Sudo(::frame_support::dispatch::CallableCallFor<Sudo, Runtime>),
TemplateModule(::frame_support::dispatch::CallableCallFor<TemplateModule,
Runtime>),
Contracts(::frame_support::dispatch::CallableCallFor<Contracts, Runtime>),
}
外部Call枚举收集了construct_runtime!宏中的所有模块暴露的Call枚举,因此,它定义了substrate中完整的公开可调度函数集
最后,当运行Substrate节点时,它将自动生成一个getMetadata API,其中包含运行时生成的对象。这可以用于生成JavaScript函数,允许将调用分派给运行时