Compound 独立链充分利用 Substrate 提供的基础设施,实现了链上代码与链下代码的集成及与其它链的交互。
原文标题:《为什么 Compound 选择 Substrate 进行独立链的开发》
撰文:Mike Tang
来源:Web3 Explorer
Compound 在 2021 年 3 月正式上线了其独立链网络 Gateway (原名 Compound Chain)。Gateway 是一条独立的链,它的目标是成为一个统一的 Defi 枢纽,实现跨链的 Defi——你可以质押链 A 上的资产,在链 B 上借款——从而将所有链的资产全部打通,提升全网络资产利用率和流动性。
Gateway 的出现基于如下的判断:未来世界一定是一个多链的世界,即没有一条区块链能解决所有问题。未来会有很多条链,很多种可能性。于是人们的资产会被分散到各个独立的平台。如何提升这些分散的资产的利用率,就是 Gateway 要研究和解决的问题。
Gateway 的系统架构
Gateway 架构图
如架构图所示,Gateway 通过针对各种链(Peer Chain)开发特定的 Starport (Peer Chain 上的一组合约或组件),来连接所有的链。
本文不会在 Gateway 的 Defi 相关事物上做过多叙述,本文主要就 Gateway 为何选择 Substrate 进行开发做一个简要的分析。
为什么要选择 Substate
Compound 官方的 说法 :
We chose Substrate so that we could focus on building application code, instead of inventing consensus algorithms; it』s a modern framework built on a modern language, Rust.
我们选择 Substrate 以便我们可以聚焦在构建应用代码上,而不是去发明共识算法;并且它是一个现代框架,构建在现代语言 Rust 之上。
Rust 语言是一门面向安全的现代编程语言。关于 Rust 语言的良好口碑,本文也不再赘述,感兴趣的朋友可以阅读《2020 开发者调查报告:Rust 再次成为最受欢迎的语言》。Rust 几乎已成为当今区块链开发的首选语言,比如:Polkadot, Near, Solana, Dfinity,FileCoin 的底层等等都是 Rust 实现的。Rust 语言有以下显著特性:
- 内存安全:其独有的所有权和生命期设计,理论上保证不会出现内存错误。解决了底层系统缺陷中的 70% 的问题
- 并发安全:在并发编程(多线程,多协程)中,能保证并发的安全性
- 高性能:与 C/C++是同一级的性能,目前主流语言中的顶级
- 零开销抽象:此特性让 Rust 能够无缝连接 C 语言的既有生态,而没有性能损失
- 强大的抽象表达能力:Rust 借鉴了很多函数式语言的表达力,让其表达更干练
- 现代的工程化设施:Cargo 和 crates.io 这种现代化的辅助系统,让 Rust 构建复杂工程时轻松自在
- 积极活跃的社区:Rust 从一开始就是以社区模式开发迭代的,这点其实非常有意思,与 go 这种集权式开发显著不同
了解 Rust 的这些特性后,我们再去理解「Rust 语言几乎已成为当今区块链开发的首选语言。」这句话就显得很自然了。区块链本身以安全性为第一位,这点与 Rust 的设计理念完全一致。区块链中要做大量的计算,也需要高性能,这点 Rust 也是当仁不让。区块链系统一般比较复杂,对工程相关基础设施的要求很高,而 Rust 强大的工程性设计,让其在团队、开源社区开发中,特别适合大型项目的协作。
以注重安全性闻名的 Paritytech 公司,用 Rust 语言开发了以太坊的 parity 客户端。然后继续用 Rust 开发了新项目 Polkadot,在开发 Polkadot 的过程中,逐渐形成了一个重大决定:将区块链的所有功能,拆解成抽象的设计,实现到一个开源的、通用的区块链框架中,并以此框架为工具,构建 Polkadot 产品。而这个区块链框架的重大成果,就是本文的主角——Substrate。
Substrate 是一个用 Rust 语言开发的以通用性为目标的区块链开发框架。它的设计元素,比如密码学算法、存储结构 MPT 树,账户体系等,大部分借鉴自有史以来最成功的以太坊的基础设施,(这个可以理解,Paritytech 最早就是做以太坊客户端起家的,Gavin Wood 也是以太坊的联合创始人之一)。一个框架,要做到通用,就需要高度抽象。而高度抽象的代价往往会显得结构复杂,不易于使用。所以 Substrate 也提供了很多 DSL (领域特定语言),方便新手学习使用。简单归纳一下,Substrate 具有如下特点:
面向通用。其设计面向通用领域,而不是专为某一条链做开发的 SDK。每个团队都可以使用 Substrate 开发出一条完全独立的不依赖于任何既有网络的链出来(比如,使用 Substrate 开发的区块链可以与 Polkadot 无关,这也是 Paritytech 的设计目标之一) 功能全面。能覆盖区块链几乎所有的场景,可以说是目前市面上功能最全面的区块链框架 Runtime 代码编译成 wasm 执行。Wasm 是当今区块链业界主流的 VM 字节码选择 可定制性超强。Substrate 本身是一堆分散的组件,可以在一套规范约束下,自由替换组件,自由组合。
工程设计就是做取舍,当它强调一方面的时候,在另一方面,就会有一定的妥协。Substrate 也有自己的不足:
- 新引入一些概念名词(比如:extrinsic),需要重新学习理解,上手有一定门槛
- 强调通用,导致在某些方面过于抽象,代码视觉比较复杂(比如泛型参数特别多)。而这些抽象将 Rust 的高级语法特性做了充分的展现,代码噪音较大
- 整个工程代码量较大,依赖的包非常多(目前有 1000 多个),导致编译时间比较长(普通笔记本几十分钟以上),对开发机性能要求较高
如上可以看到,这些不足主要是 Substrate 的设计目标——通用——本身带来的客观依赖复杂性造成的。
瑕不掩瑜,作为目前为止最强的区块链开发框架之一,Substrate 受到了越来越多创新团队的欢迎,Compound 团队选择 Substrate 进行 Gateway 的开发也就顺理成章了。
Substrate 的功能组件
Substrate 整体框架图
从架构图可以看到,Substrate 有如下几大组件:
- P2p Networking P2p 网络
- Runtime 运行时
- Storage 存储
- Consensus 共识
- RPC 远程过程调用
- Telemetry 客户端监测工具
P2p 网络,是区块链系统必备的子系统。多个节点通过 p2p 连接成组成同一个网络。在节点与节点之间发消息传递数据,网络中的消息传播有可能通过多个中继节点传播后到达。在 Substrate 中,使用的是 rust-libp2p ( https://github.com/libp2p/rust-libp2p ),此项目也主要是由 Paritytech 在负责维护。
Runtime,运行时,是区块链业务逻辑的实现部分。也就是说要用区块链干什么实际的事,就需要写在这里面。Substrate 支持将 Runtime 代码编译成 wasm 字节码或 native code 两种模式运行(随着 wasm vm 运行速度的提升,未来 native code 模式可能会被抛弃,这也能降低一些内部复杂性)。在 Runtime 中可以操作 Storage,实现状态的变更,因此 Runtime 整体也被称作状态转移函数(State Transition Function, STF)。
Storage,存储子系统,是区块链系统中不可缺少的组成部分。在 Substrate 中,Storage 用于持久化 Runtime 的逻辑对状态的变更,同时也支持对外的 RPC 状态读取接口,Runtime 中的 Event 在发出前,也会在 Storage 中做短暂的存储。共识系统的目标,也是对 Storage 中的状态达成一致。Storage 子系统在底层用的是 kv 数据库 rocksdb 或 paritydb。
Consensus,共识子系统,用于在网络的参与方之间就区块链的状态达成一致,也就是「共识」。由于是分布式系统,所谓区块链的状态,并没有一个宏观的上帝视角能看到一个统一的宏观状态。这个状态,其实就是各个节点的状态,每个节点有自己的本地状态视图,也有一个「局部「的全局视图。每个节点通过本地视图与全局视图的比对,做出决策。共识系统要在各个节点上达成一个一致的状态,从而推动系统往前运行。
RPC,远程过程调用,用于向节点外部提供访问的接口。一个 Substrate node 本身可以作为一个服务而存在,外界可以通过这些 rpc 接口访问 node 的本地状态信息或向 node 提交变更请求。Substrate 同时提供 HTTP 和 Websocket 两种 rpc 通道。
Telemetry,客户端监测工具,用于搜集 node 的运行信息,发送到远端的 Prometheus 服务器。
Substrate 的开发模式
Substrate 是一个通用开发框架。它为不同层次的开发者提供了三种开发风格。
直接使用 Substrate 自带的 node 起链
对于想快速起链,体验效果的开发者,可以直接使用 Substrate 预置的 node 的实现。只需要修改一个 JSON 配置文件,就可以跑起来,具体可定制以下内容:
- 创始区块的状态信息
- 账户 Account
- 余额 Balance
- Staking 比例等
Runtime FRAME pallets 的开发
这种开发模式通过写 Runtime 中的 pallet 代码,将业务逻辑实现到 pallet 中。然后将自定义的 pallets 和其依赖的 Substrate 自带的 pallets 一起编译成 wasm 字节码运行,这是大部分 Substrate 开发者的选择。
基于 Substrate Core 深度定制
Substrate 已实现成分散的组件,做了充分的抽象和解耦。对于一些高级开发者,在某些特定的场景下,可以完全从底层重新组合这些组件,实现深度的 node 的定制。比如,可以做到:
- 使用不同的密码曲线和哈希算法
- 使用不同的序列化方法
- 替换不同的共识算法
- 完全去除 FRAME 层代码,使用另一种语言编写业务,只要能保证编译到 wasm 且遵循 Substrate 的规范即可
- 等等
下图展示了三个层次的开发难度和技术灵活性之间的关系。
直接使用 Substrate Node 最简单,但是最不灵活。基于 Substrate Core 开发最灵活,但是最难。进行 FRAME pallets 开发处于中间位置。也是大部分 Substrate 开发者应该采用的方式。
Substrate 的优秀之处
Substrate 的设计有很多优秀之处,我们来了解一下。
可升级无分叉 Runtime
由于 Substrate 的 Runtime 代码编译为 wasm 运行,然后 wasm 字节码本身作为交易的数据直接提交到链上,再藉由链本身的 p2p 网络全网传播,实现业务逻辑的更新。每个节点在收到更新版本的 wasm 字节码后,将其更新到代码段,在某个块之后就使用新版本的 wasm 来执行逻辑。
有了这种热更新代码机制,业务代码的升级不再会引起分叉(软分叉或硬分叉)了,也就是说,不会因为是技术客观的原因,导致网络的分叉(人为主动分叉还是可以的)。
需要注意的是,这种升级仅限于 wasm 字节码能覆盖的部分——也即 Runtime 中的代码——的升级。如果改动了 node 代码本身(即 Runtime 外的部分),仍然需要通知所有节点进行手动或 devops 替换。
可替换的密码学库
Substrate 同时支持几种密码学曲线:
- ECDSA
- Ed25519
- SR25519
同时支持几种 Hash:
- Blake2
- xxHash
开发者可根据自己的需要选择使用。如果没有你想要的,按照它的架构为其添加新的曲线和 Hash 函数也不难。
层次丰富的 Account 系统
Substrate 的 Account Key 分三个层次:
- Stash Key
- Controller Key
- Session Keys
Stash Key 是用来存资金的账户,其私钥部分应该尽可能安全地存储在冷钱包中。Controller Key 用来控制 Validator 的参数,也是 Stash Key 的一个中间代理账户,在更新验证人集合时非常有用。Session Keys 用来对共识相关的消息进行签名。Session Keys 可以有多个,每一个都有自己专门的用途,一般可组合在一起使用,如:
impl_opaque_keys! {
pub struct SessionKeys {
pub grandpa: Grandpa,
pub babe: Babe,
pub im_online: ImOnline,
pub authority_discovery: AuthorityDiscovery,
}
}
上述代码将 4 个独立的 Session Keys: Grandpa session key, Babe session key, ImOnline session key, AuthorityDiscovery session key 组合在一起成为一个大的 SessionKeys。
可以将 Session Keys 理解成 Validator 运行过程中的唯一标识(Identification)。Session Keys 每过一个 Session 最好更换一次,这样能最大程度保证安全性。
Substrate 通过这种分层的 Account Key 的设计,既保证了安全性,又提供了充分的灵活性,基本能覆盖所有应用场景的需求。
抽象可切换的共识引擎
Substrate 设计了一套共识框架,这套共识框架非常了得。它将块的生产(proposal)与敲定(finalize)分离,同时容纳了 Nakamoto 类(只有概率性敲定,无确定性敲定)共识和 BFT 类(有确定性敲定)共识。
Substrate 为出块提供了以下几种算法:
- Aura:slot 模式,在一个已知的 authority set 中,使用 round robin 模式轮流出块
- Babe:slot 模式,在一个已知的 authority set 中,使用可验证随机函数 VRF 随机选择出块节点(每个 slot 可能不止一个出块人)
- Pow:工作量证明出块
这几种出块算法,如果没有 Finalize Gadget 配合,只能做到概率性敲定(finalization),而无法做到确定性敲定。Substrate 提供了 Grandpa 这个 Finalize Gadget,用于确定性敲定。
出块算法与敲定算法可以自由配合使用。于是有 Aura/Grandpa, Babe/Grandpa, 甚至 Pow/Grandpa 这几种组合。而在不需要确定性敲定的场景下,当然也可以不使用 Grandpa。Substrate 给了开发者充分的自由。
Substrate 共识框架还提供了其它一些基础设施:
- Fork Choince Rules: Longest Chain Rule 或 GHOST Rule,用于决定在链有分叉的情况下,如何选择一个最好的链的算法
- Import Queue:导入队列
- Block Import Trait:块导入接口
- Block Import Pipeline:块导入流水线
Substrate 还提供了在 Runtime 中对共识过程进行协调控制的功能。比如,在 Pow 运行过程中调整难度,在 PoA 中决定下一个是否轮转到,在 PoS 中动态修改 Stake 的权重等等。
在 Substrate 这一套完备的基础设施之上,如果他自带的共识引擎无法满足开发者的需求,开发者还可以按照他的规范开发自己的共识引擎,引入到框架中使用,并且可在 Runtime 中进行适当控制。
Off-Chain 特性
Off-Chain 特性是 Substrate 中提供的一套相当强大的基础设施。毕竟对区块链来说,链上的逻辑操作空间非常有限,有些事情必须通过链下来完成。在没有 Off-Chain Worker (OCW) 之前,这一类事情,通常是由预言机 Oracle 来完成。预言机是外部服务,通过区块链节点 RPC 接口向区块链提交交易从而把外界的信息传到链上去。这种方式虽然是可行的,但它在安全性、集成性、可扩展性和基础设施效率问题上面,仍然不够好。
为了让链下数据的集成更安全和有效,Substrate 提供了 off-chain 相关的特性。其架构图如下:
Off-chain Worker 架构图
Off-chain 特性包含三大组件:
- Off-Chain Worker
- Off-Chain Storage
- Off-Chain Indexing
Off-Chain Worker 用于实现链下逻辑。其代码与 Runtime 代码写在一起,并被编译到同一个 wasm 字节码字符串中,在同一个交易中被传播到全网络。但是在执行的时候,Off-Chain Worker 的代码是在独立的 VM 中执行的,即与 Runtime 逻辑的执行完全隔离开。具体来说,Off-Chain Worker 能够实现如下功能:
- 将计算的结果以交易形式提交到链上
- 包含一个全功能的 HTTP 客户端,能够访问外部服务的数据
- 可以访问本地 node 的 keystore,这样便可以验证和签名交易
- 可以访问本地的 KV 数据库,且在所有 off-chain worker 中共享这个数据库
- 本地的安全的熵源,用来产生随机数
- 可以访问本节点的本地时间
- 可以 sleep 和 resume 工作
Off-Chain Storage 是链下逻辑独立的存储空间,与链上的 Storage 是完全隔离开的。它具有如下特性:
- 能被 Off-Chain Worker 读取和写入
- 存储在 node 本地,不会传递到网络中其它节点去,不会参与网络共识
- 被所有同时运行的 Off-Chain Workers 共享访问(因此需要锁操作)。因此,可以利用其在不同的 Workers 之间通信
- 能被 Runtime 代码写入,但是不能读。因此,可基于其实现一定的链上链下交互功能
- 可被 wasm 环境外的 node 中的代码读取,因此能被 RPC 读取
Off-Chain Indexing 提供了在 Runtime 环境中,向 Off-Chain Storage 写入数据的能力。但是不能读取 Off-Chain Storage 中的数据。这为一些新的编程范式提供了可能性。
其它还有一些,比如,完善的 OCW 集成测试框架等等。
Substrate 的 Off-chain 特性非常强大,令人印象深刻。
完备灵活的 Gas 费计算机制
有以太坊开发经验的朋友都知道,Gas 费机制是非常成功的一个设计,对链的安全和平稳运行非常重要。几乎所有后来的区块链都直接借鉴了这种设计。而在 Substrate 中,提供了非常完整详尽的机制和配置参量来帮助开发者设计他们自己的 Gas 费算法。Substrate 中内置了如下 Gas 计算和配置参量:
- Includsion Fee: 包含 length_fee 和 weight_fee
- Fee Multiplier
- Additional Fees,包含 Bonds、Deposits、Burns 与 Limits
- Default Weight Annotations
- Dynamic Weights
- Post Dispatch Weight Correction
- Custom Fees
- Custom Inclusion Fee
我们这里不再对每个条目做详细解释,具体意义请参考这个 网站 。
可以看到,Substrate 对 Gas 费计算的设计非常全面,甚至稍显复杂。其目的仍然是实现通用区块链框架的目标——该有的都应该有,并且要能开箱即用。
Substrate 非常适合用于启动独立的链或者是面向 Web3.0 应用的 Appchain,一般这些链会在用户体验上下足功夫。比如对于应用的普通用户来讲,使用服务的过程中,可能会意识不到 Gas 费的存在,在这种场景下,Substrate 提供的上述丰富的 Gas 费机制,能做到 Gas 费置零或者设置为代付。这类特性便有机会让 Substrate 成为最适合 Web3.0 App 的开发框架之一。
Runtime API 与 RPC 集成
Substrate 提供了一套 RPC 扩展框架,让开发者可以(在 Substrate 默认提供的接口之外)扩展开发自己的 RPC 接口。由于 RPC 实现代码是在 node 中,Runtime 之外,所以理论上来说,可以在 Substrate 中开发全功能(做任何事情)的 RPC 服务。这就使得 Substrate 成为了一个强大的 RPC 开发框架。
而往往我们需要与 Runtime 中的状态进行交互,这时就需要用到 Runtime API 了。Runtime API 是 Runtime 内与外的桥梁,也可以说是链上与链下的桥梁。
于是 RPC 与 Runtime API 组合起来,就可以将外部请求发起到获取链上状态的流程全部打通。配合自定义的 Runtime API 功能和 RPC 扩展接口功能,给予了开发者巨大的灵活性和可能性。也为 Substrate 成为一个一体化集成式的 Web3.0 开发框架打下基础。
Gateway 如何使用 Substrate
为了搞清楚 Gateway 是如何使用 Substrate 的,我们直接拉它的 源代码 下来简要分析一番。
可以看到,有如下主要目录(我们加了简要注释)。
- ethereum/ 以太坊上的 Starport 合约代码
- ethereum-client/ 以太坊相关类型定义和基础工具函数
- gateway-crypto/ Gateway 用到的密码相关的基础工具函数
- integration/ 集成测试代码
- node/ Substrate 的自定义 node 代码
- our-std/ Gateway 的 std 代码,是对 sp_std 的简单封装,并添加了一点额外的东西
- pallets/ Gateway 独立链的主体业务逻辑代码
- runtime/ Substrate 的运行时构建代码
- types-derive/ Gateway 用到的类型相关过程宏
可以看到,目前 Gateway 只实现了到 ethereum 的 startport 连接,其它链的对接还在开发中,会逐渐加入。
主体业务代码在 pallets/ 目录下,此目录下有三个子目录。
- cash/ 主体业务逻辑
- oracle/ 从喂价机获取价格的代码
- runtime-interfaces/ 一些运行时接口
我们从 pallets/cash/src/lib.rs 看起。这是一个标准的 pallet 文件,其结构有
- trait Config 的定义
- decl_storage! 定义链上存储部分
- decl_event! 定义事件输出部分
- decl_module! 定义模块实现部分
在模块实现部分中,有 fn offchain_worker(block_number: T::BlockNumber) {} 这个函数。其实现主要是这两个函数调用:
-
internal::events::track_chain_events::
() -
internal::notices::process_notices::
(block_number)
我们知道,Substrate 的 offchain_worker 入口,是在每个块导入本地状态数据库时调用。也即 fn offchain_worker(block_number: T::BlockNumber) 这个函数会被每个块驱动执行,每个块执行一次这个函数。因此,每增长一个块,上面的代码会分别处理 events 和 notices 这两块内容,而这两块内容的代码在 internal 模块中定义。
继续追踪 pallets/cash/src/internal/events.rs 和 pallets/cash/src/internal/notices.rs 文件后,可以整理出大体的逻辑要点:
- Offchain_worker 每个块调用一次
- 在 offchain_worker 代码中,会调用到 ethereum-client/ 提供的以太坊基础类型和辅助工具,发 http 请求到以太坊的事件服务器,批量获取以太坊的 events,并分类处理
- 处理后的结果会以发交易的形式从 offchain_worker 中提交到链上
- 进入 Runtime 业务逻辑进行处理
pallets/oracle/ 里面的逻辑流程也类似:在 offchain_worker 中,用 http 请求从喂价机地址获取数据,然后提交给链上使用。
理解了上述流程后,也就能比较轻松地理解 Compound 在 Ethereum 上做 Compound Governance Proposal 是如何影响到 Gateway 链上的验证人节点集合更新的了。其仍然是通过以太坊的事件向外抛出信息,事件服务器搜集到事件后,缓存下来。Gateway 这边的 Offchain worker 每新增一个块就去事件服务器上批量取一次最近的事件。获取到最近的事件后,按对应的逻辑迭代处理就行了。而 Substrate 中要更新验证人集合,需要用到 Controler account,以及 SetKeys 等相关由 Substrate 提供的功能。
限于篇幅,我们这里的分析点到为止,这里只简要的分析了 Gateway Pallet 粗线条的逻辑流程,然后对 Offchain Worker 部分做了重点关注。
可以看到,Gateway 充分利用 Substrate 提供的基础设施,很方便地实现了业务逻辑以及链上代码与链下代码的集成,以及与其它链的交互。整个代码实现得非常清晰,值得我们学习借鉴。
总结
本篇,我们介绍了 Gateway 的总体架构,Substrate 的功能模块,Substrate 框架的特色之处,以及 Gateway 是如何充分使用 Substrate 提供的基础设施进行独立链的开发。
限于篇幅,很多地方只能点到而止,文末附上一些链接,可供大家扩展阅读。
参考资料
《 Compound’s Gateway: a deep dive into setting up a validator node. 》
来源链接: www.theblockbeats.com