作者: songtianyi create@2022-07-09, WIP
Substrate 是 Ethereum 创始人之一 Gravin Wood 另起炉灶开发的一个基于 Rust 的区块链开发框架。基于 Substrate 框架,普通开发者能够快速开发出以太坊级别(技术方面)的区块链。比较知名的是 Parity 的 Polkadot. 在 Polkadot 出现之前,区块链有两种玩法,一种是基于 Ethereum EVM 去做合约上的创新,一种是把 Ethereum 复制过来,改改代码做共识或者其它方面的技术创新,甚至去构建自己的生态。而 Polkadot 的出现让链与链之间的互操作变得更加方便,你可以在 Polkadot 上玩合约,像 Ethereum 生态一样;也可以自己基于 Substrate 去快速开发出自己的新链,引入自己的共识,甚至虚拟机; 可以自己单玩,也可以以 parachain 的形式接入到 Polkadot 的大生态里面。从技术的角度讲,Polkadot 比 Ethereum 更加先进,但生态方面,Polkadot 还不足以挑战 Ethereum,目前仍然是强者恒强的局面。
Substrate 有着自己的显著特征:
很容易想到,Substrate 作为链的开发框架,至少得有两部分,公共部分和自定义逻辑部分。公共部分用来解决网络、通信、存储、共识、监控等基本问题,自定义逻辑部分用来方便开发者编写和发布自己的应用逻辑。
用术语来讲的话,这两部分分别是 Outer node 和 WebAssembly Runtime. Outer node 除了图中所列的几个核心功能外,还有:
Substrate 框架的代码结构分为三个主要层次:
Runtime 包含了所有的业务逻辑,包括校验和执行 transaction, 和 Outer node 交互等等。Runtime 可以编译成 WebAssembly, 它能够带来以下好处:
和其它的 blockchain 一样,基于 Substrate 的 blocchain 也是一个分布式的账本,或者说是一个分布式数据库,Runtime 相当于是 state transition function, 负责改变和存储状态。
在 Substrate 中, Outer node 是通过 Runtime 提供的 API 来获取一些信息的。 sp_api
crate 提供了一个 interface
让大家可以借助 impl_runtime_apis
macro 实现自己的 API.
大部分基于 Substate 的链都实现了下面几个 API(interface):
除了这些, Core
和 Metadata
interface 是必须要实现的。
我们可以自行编码实现自己的 Runtime, 也可以借助 FRAME. FRAME 提供了很多实用的 pallet. 我们从这些内置的 pallet 中挑选一些出来就可以构造出特定场景使用的区块链。
FRAME 还提供了一些基础库和 pallet, 我们开发的时候都会用到:
frame_system
provides low-level types, storage, and functions for the runtime.frame_support
is a collection of Rust macros, types, traits, and modules that simplify the development of Substrate pallets.frame_executive
orchestrates the execution of incoming function calls to the respective pallets in the runtime.node-template 是 Parity 官方提供的基于 Substrate 的一个可用的区块链样例。 根据指引 可以编译启动 template 节点。在启动之前你需要详细看下 node-template 的子命令及启动选项。 这里主要强调下节点的几种运行模式
archive node
会保存所有的 block, 方便 block explorer 等场景的使用,启动方式如下:
./target/release/node-template --pruning archive
full node 是经过精简的,它只保存固定数量的区块数据以及创世区块(genesis block), 默认是保存 256 个区块。通过 validator 选项可以启动一个默认保存 256 个区块的 node:
./target/release/node-template --validator
也可以指定保存的区块的数量:
./target/release/node-template --validator --pruning 10
通过 --light
选项,你可以让节点以 light client 的模式运行。light client node 主要的作用是向外暴露接口,方便用户读取 block headers, 提交 transaction 等,不负责出块。你可以理解为它是 read-only 的,不会修改区块链的状态, 因此 light client node 只需要保存当前的状态。
./target/release/node-template --light
打开 node-template 的源码,可以看到源码中有三个主要目录
/// Import the template pallet.
pub use pallet_template;
/// ...
/// Configure the pallet-template in pallets/template.
impl pallet_template::Config for Runtime {
type Event = Event;
}
// Create the runtime by composing the FRAME pallets that were previously configured.
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: frame_system,
RandomnessCollectiveFlip: pallet_randomness_collective_flip,
Timestamp: pallet_timestamp,
Aura: pallet_aura,
Grandpa: pallet_grandpa,
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
Sudo: pallet_sudo,
// Include the custom logic from the pallet-template in the runtime.
TemplateModule: pallet_template,
}
);
在动手开发自己的 pallet 之前,我们需要了解并熟悉 substrate 提供的已有的 pallet.
前面提到过,这里也不单独再做介绍,System pallets 更多的是在我们的开发过程中起一个辅助的作用。
比较通用的功能型的 pallet, 开箱即用,还有人帮你维护。
用来管理代币, 包括挖矿,转账,冻结,销毁等一系列操作。
负责管理账户和余额。这里要讲几个术语
负责创建和执行基于 WebAssembly 的智能合约。 值得注意的一点,合约中如果出现失败,并不会向上一直传播。比如 contact A call contract B 的时候出现错误,contract A 可以决定如何处理,可以只回退 call B 时发生的状态变化,也可以回退所有。
pallet_transaction_payment 负责处理交易费用。这里提一下交易费用的构成:
inclusion_fee = base_fee + length_fee + [targeted_fee_adjustment * weight_fee];
final_fee = inclusion_fee + tip;
final_fee 为最终需要支付的交易费用。
pallet_aura 和 pallet_babe 都是共识算法,会用单独的文章介绍
pallet_grandpa 主要是用来确认块(finality)的,并不是用来做共识的
pallet_sudo 是一个单一功能的 pallet, 并不和其它 pallet 相互配合。它负责暴露一些特权接口,只有特权账户才能调用,方便做链上维护。
一个 pallet 开发模板, 展示相关概念,API, 数据结构,文档等。
我们前面提到,基于 Substrate 开发的 blockchain 是可以作为 parachain 接入到 relaychain 里的,因此会有一些预制的 pallet 来完成这些工作。 这些 pallet 都是在 relaychain 中实现。
我们在了解完平行链相关的概念之后再来补充这部分的内容
如果你看了 pallet 的 example basic 或者 node-template 中的 template 代码,你会发现 Substrate 还是比较难懂的,虽然它把需要些逻辑的地方都预留好了。但你要弄明白为什么还是比较吃力的。一方面是因为 Rust 的学习曲线本身会比较陡峭,另一方面,这里面大量使用了宏,你也知道,宏这个东西本身就是写着舒服,看着蛋疼的玩意儿,你要有能力在脑袋里展开它才会比较好懂。可以先看下我关于 Rust Macro 的一篇文章。另外,可以使用 cargo expand
来展开 macro.
FRAME v2 对 pallet 中使用的 macro 进行了升级,从 declarative macros
替换到了 attribute-like macros
. 这个升级就是为了让大家能看懂 macro 的实现,否则写什么都是稀里糊涂的 copy&paste, 如果官方写了什么 bug, 你也很难去看。
我们挑一段 quote!
里的样例来看看
let tokens = quote! {
struct SerializeWith #generics #where_clause {
value: &'a #field_ty,
phantom: core::marker::PhantomData<#item_ty>,
}
impl #generics serde::Serialize for SerializeWith #generics #where_clause {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#path(self.value, serializer)
}
}
SerializeWith {
value: #value,
phantom: core::marker::PhantomData::<#item_ty>,
}
};
也挺容易读懂的,和我们最终的编码结果相差不大,只是用变量代替了最终值。
在开发 pallet 的过程中经常需要用到的 macro 可以查看这里。这些 Substrate runtime macros 需要都看一遍。