Substrate Introduction

作者: songtianyi create@2022-07-09, WIP

什么是 Substrate

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

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):

除了这些, CoreMetadata interface 是必须要实现的。

我们可以自行编码实现自己的 Runtime, 也可以借助 FRAME. FRAME 提供了很多实用的 pallet. 我们从这些内置的 pallet 中挑选一些出来就可以构造出特定场景使用的区块链。

FRAME 还提供了一些基础库和 pallet, 我们开发的时候都会用到:

node-template

node-template 是 Parity 官方提供的基于 Substrate 的一个可用的区块链样例。 根据指引 可以编译启动 template 节点。在启动之前你需要详细看下 node-template 的子命令及启动选项。 这里主要强调下节点的几种运行模式

archive nodes

archive node 会保存所有的 block, 方便 block explorer 等场景的使用,启动方式如下:

./target/release/node-template --pruning archive

full nodes

full node 是经过精简的,它只保存固定数量的区块数据以及创世区块(genesis block), 默认是保存 256 个区块。通过 validator 选项可以启动一个默认保存 256 个区块的 node:

./target/release/node-template --validator

也可以指定保存的区块的数量:

./target/release/node-template --validator --pruning 10

light client nodes

通过 --light 选项,你可以让节点以 light client 的模式运行。light client node 主要的作用是向外暴露接口,方便用户读取 block headers, 提交 transaction 等,不负责出块。你可以理解为它是 read-only 的,不会修改区块链的状态, 因此 light client node 只需要保存当前的状态。

./target/release/node-template --light

node-template codebase

打开 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, } );

pallets

在动手开发自己的 pallet 之前,我们需要了解并熟悉 substrate 提供的已有的 pallet.

System pallets

前面提到过,这里也不单独再做介绍,System pallets 更多的是在我们的开发过程中起一个辅助的作用。

Functional pallets

比较通用的功能型的 pallet, 开箱即用,还有人帮你维护。

pallet_assets

用来管理代币, 包括挖矿,转账,冻结,销毁等一系列操作。

pallet_balances

负责管理账户和余额。这里要讲几个术语

pallet_contracts

负责创建和执行基于 WebAssembly 的智能合约。 值得注意的一点,合约中如果出现失败,并不会向上一直传播。比如 contact A call contract B 的时候出现错误,contract A 可以决定如何处理,可以只回退 call B 时发生的状态变化,也可以回退所有。

pallet_transaction_payment

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_aura 和 pallet_babe 都是共识算法,会用单独的文章介绍

pallet_grandpa

pallet_grandpa 主要是用来确认块(finality)的,并不是用来做共识的

pallet_sudo

pallet_sudo 是一个单一功能的 pallet, 并不和其它 pallet 相互配合。它负责暴露一些特权接口,只有特权账户才能调用,方便做链上维护。

pallet_example_basic

一个 pallet 开发模板, 展示相关概念,API, 数据结构,文档等。

Parachain pallets

我们前面提到,基于 Substrate 开发的 blockchain 是可以作为 parachain 接入到 relaychain 里的,因此会有一些预制的 pallet 来完成这些工作。 这些 pallet 都是在 relaychain 中实现。

我们在了解完平行链相关的概念之后再来补充这部分的内容

Frame macros

如果你看了 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 需要都看一遍。

参考资料