作者:Mike Tang
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 提供的基礎設施進行獨立鏈的開發。
限于篇幅,很多地方只能點到而止,文末附上一些鏈接,可供大家擴展閱讀。
_參考資料 _
- Introducing Gateway
- Gateway Document
- 《 Compound’s Gateway: a deep dive into setting up a validator node. 》
- https://github.com/compound-finance/gateway
- Substrate.dev
- 《Rust 再次成為最受歡迎語言》