动手写条自己的链(2):Cosmos SDK 入门

动手写条自己的链 系列文章:

  1. Hello Cosmos
  2. Cosmos 技术架构解析
  3. Cosmos SDK 入门

本文主要是对官方文档进行翻译,如后续内容发生变化,请以英文原文为准。

目录

Cosmos SDK 介绍

英文原文:SDK Intro

Cosmos SDK 是一个用于构建多资产权益证明(Proof-of-Stake,PoS)共识区块链的框架,例如 Cosmos Hub,以及权威证明(Proof-of-Authority,PoA)共识的区块链。

Cosmos SDK 的目标是允许开发人员可以轻松的创建自定义区块链,并且这些链天生就可以和其他区块链交互。我们将 SDK 设想为类似于 npm 的框架,能够在 Tendermint 之上构建安全的区块链应用程序。

它基于两个主要原则:

  • 可组合性:任何人都可以为 Cosmos SDK 创建模块,并且集成已经构建的模块就像将它们导入区块链应用程序一样简单
  • 功能:Cosmos SDK 受到基于功能的安全性的启发,以及多年来研究区块链状态机得到的经验。大多数开发人员在构建自己的模块时需要访问其他第三方模块。鉴于 Cosmos SDK 是一个开放框架,一些模块可能是恶意的,这意味着需要安全原则来推动模块间的交互。这些原则基于对象能力。实际上,这意味着不是让每个模块保留其他模块的访问控制列表,而是每个模块都实现称为守护者(keepers)的特殊对象,这些对象可以传递给其他模块以授予预定义的一组功能。例如,如果模块A的守护者(keepers)的实例被传递给模块B,则后者将能够调用一组受限制的模块A的功能。每个管理器(keeper)的功能由模块的开发人员定义,开发人员的工作是根据它们传递到每个第三方模块的功能来理解和审计来自第三方模块的外部代码的安全性。要深入了解功能,请跳转到本节

Cosmos SDK 程序架构

英文原文:SDK Application Architecture

状态机

区块链应用程序的核心是可复制的确定性状态机(State machine replication)

状态机是计算机科学概念,一台机器可以具有多个状态,但在任何给定时间只有一个状态。它描述了系统当前的状态和触发状态转换的事务。

给定一个状态S交易T,状态机会返回一个新的状态S'

1
2
3
4
5
+--------+                 +--------+
| | | |
| S +---------------->+ S' |
| | apply(T) | |
+--------+ +--------+

实际上,交易以区块的形式打包在一起可以提高处理效率。给定状态S和包含交易的区块B,状态机将返回新状态S'

1
2
3
4
5
+--------+                              +--------+
| | | |
| S +----------------------------> | S' |
| | For each T in B: apply(T) | |
+--------+ +--------+

在区块链上下文中,状态机是确定性的。这意味着如果从一个给定的状态开始并重放相同顺序的事务,将始终以相同的最终状态结束。

Cosmos SDK 为您提供了最大的灵活性,可以定义应用程序的状态、事务类型和状态转换功能。使用 SDK 构建状态机的过程将在接下来的内容中进行更深入的描述。但首先,让我们看看状态机是如何使用 Tendermint 进行复制的。

Tendermint

作为开发人员,你只需使用 Cosmos SDK 定义状态机,Tendermint 将为你处理网络复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
                ^  +-------------------------------+  ^
| | | | Built with Cosmos SDK
| | State-machine = Application | |
| | | v
| +-------------------------------+
| | | ^
Blockchain node | | Consensus | |
| | | |
| +-------------------------------+ | Tendermint Core
| | | |
| | Networking | |
| | | |
v +-------------------------------+ v

Tendermint 是一个与应用程序无关的引擎,负责处理区块链的网络层和共识层。实际上,这意味着 Tendermint 负责传播和排序交易字节。Tendermint Core 依赖于拜占庭容错(BFT)算法来达成交易顺序的共识。有关 Tendermint 的更多信息,请单击此处

Tendermint 一致性算法与一组称为验证人(Validators)的特殊节点一起运作。验证人负责向区块链添加交易区块。对于任何给定的区块,有一组验证人V。通过算法选择 V 中的验证人A作为下一个区块的提议人。如果 V 中超过三分之二的节点签署了 prevoteprecommit,并且区块包含的所有交易都是有效的,则该区块被认为是有效的。验证人集合可以通过状态机中编写的规则进行更改。要深入了解算法,请点击此处

Cosmos SDK 应用程序的主要部分是一个区块链守护程序,它由本地网络中的每个节点运行。如果验证器集中少于三分之一是拜占庭(即恶意),则每个节点在同时查询状态时应获得相同的结果。

ABCI

Tendermint 通过名为 ABCI 的接口将事务从网络层传递到应用程序层,应用程序必须要实现该接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+---------------------+
| |
| Application |
| |
+--------+---+--------+
^ |
| | ABCI
| v
+--------+---+--------+
| |
| |
| Tendermint |
| |
| |
+---------------------+

注意,Tendermint 仅处理交易字节。它不知道这些字节究竟是什么意思。Tendermint 所做的只是对交易确定性地排序。赋予这些字节含义是应用程序该做的工作。Tendermint 通过 ABCI 将交易字节传递给应用程序,并期望返回结果代码以知晓消息是否传递成功。

以下是 ABCI 中最重要的信息类型:

  • CheckTx: 当 Tendermint Core 收到交易时,会将其传递给应用程序以检查其有效性。一个名为“Ante Handler”的特殊 handler 用于执行一系列验证步骤,例如检查手续费用是否足够、验证签名是否合法。如果交易有效,则将交易添加到 mempool 中并广播到对等节点。注意, CheckTx 不会处理交易(即不会对状态进行修改),因为它们尚未包含在区块中
  • DeliverTx: 当 Tendermint Core 接收到有效区块时,其中的每条交易都将通过DeliverTx传递给应用程序进行处理。正是在这一阶段发生了状态转换。“Ante Handler”将再次执行,连同对交易中每条消息进行处理的真实 handler 一起执行
  • BeginBlock/EndBlock: 无论区块是否包含交易,这两个消息都将在每个区块的开头和结尾执行。触发自动的逻辑执行是很有用的。过程中要足够小心,因为计算成本高昂的循环运算可能会减慢区块链的速度,甚至在无限循环中使区块链本身停滞

有关 ABCI 方法和类型的详细介绍请点击此处

在 Tendermint 上构建的任何应用程序都需要实现 ABCI 接口,以便与底层的本地 Tendermint 引擎进行通信。幸运的是,你不必自己实现 ABCI 接口。Cosmos SDK 以 BaseApp 的形式提供了标准实现。

Cosmos SDK 设计总览

英文原文:Cosmos SDK Design Overview

Cosmos SDK 是一个框架,可以在 Tendermint 之上优化安全状态机的开发。Cosmos SDK 的核心是基于 Golang 的 ABCI 标准实现。它带有一个用于持久化保存数据的multistore和一个用于处理事务的路由router

以下是一个基于 Cosmos SDK 的应用程序在通过DeliverTx从 Tendermint 传输时如何处理事务的简化视图(CheckTx过程是相同的,没有强制执行状态更改):

  1. 解码从 Tendermint 共识引擎收到的事务(记住 Tendermint 只处理字节数据[]bytes
  2. 从事务中提取消息并执行基本的完整性检查
  3. 将每条消息路由到对应的模块,以便可以处理它
  4. 提交状态更改

该应用程序还能够生成事务,对它们进行编码并将它们传递给底层的 Tendermint 引擎以进行广播。

BaseApp

BaseApp 是 Cosmos SDK 中 ABCI 的标准实现。它带有一个路由(router),可以将事务路由到它们各自的模块。你的应用程序的主文件app.go需要自定义一个基于 BaseApp 的app类型。这样,你自定义的app将自动继承 BaseApp 的所有 ABCI 方法。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type nameServiceApp struct {
*bam.BaseApp
cdc *codec.Codec

keyMain *sdk.KVStoreKey
keyAccount *sdk.KVStoreKey
keyNS *sdk.KVStoreKey
keyFeeCollection *sdk.KVStoreKey
keyParams *sdk.KVStoreKey
tkeyParams *sdk.TransientStoreKey

accountKeeper auth.AccountKeeper
bankKeeper bank.Keeper
feeCollectionKeeper auth.FeeCollectionKeeper
paramsKeeper params.Keeper
nsKeeper nameservice.Keeper
}

BaseApp 的目标是在存储和可扩展状态机之间提供安全的接口,同时尽可能少地定义该状态机(保持 ABCI 的原汁原味)。

有关 BaseApp 的详细介绍,请点击此处

Multistore

Cosmos SDK 为状态持久化提供了multistore,其允许开发人员声明任意数量的 KVStore。这些KVStore只接受[]byte类型的数据作为值,因此任何自定义的结构都需要在存储之前使用 go-amino 进行序列化。

multistore抽象地将状态划分为不同的分区,每个分区由其自己的模块管理。有关multistore的详细介绍,请点击此处

Modules

Cosmos SDK 的强大之处在于其模块化。 SDK 应用是通过聚合一组可互操作的模块构建的。每个模块定义一份状态的子集并包含它自己的消息/事务处理器,而 SDK 负责将每个消息路由到其各自的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
                                      +
|
| Transaction relayed from Tendermint
| via DeliverTx
|
|
+---------------------v--------------------------+
| APPLICATION |
| |
| Using baseapp's methods: Decode the Tx, |
| extract and route the message(s) |
| |
+---------------------+--------------------------+
|
|
|
+---------------------------+
|
|
|
| Message routed to the correct
| module to be processed
|
|
+----------------+ +---------------+ +----------------+ +------v----------+
| | | | | | | |
| AUTH MODULE | | BANK MODULE | | STAKING MODULE | | GOV MODULE |
| | | | | | | |
| | | | | | | Handles message,|
| | | | | | | Updates state |
| | | | | | | |
+----------------+ +---------------+ +----------------+ +------+----------+
|
|
|
|
+--------------------------+
|
| Return result to Tendermint
| (0=Ok, 1=Err)
v

每个模块都可以看作是一个小型的状态机,开发人员需要定义各个模块处理的状态子集,以及涉及修改状态的自定义消息类型(注意:消息是通过 BaseApp 方法从事务中提取的)。通常情况下,每个模块在multistore中声明其自己的KVStore以便持久化其定义的状态子集。大多数开发人员在构建自己的模块时都需要访问其他第三方模块。鉴于 Cosmos SDK 是一个开放框架,一些模块可能是恶意的,这意味着需要安全原则来推动模块间的交互,这些原则基于对象能力。实际上,这意味着不是让每个模块保留其他模块的访问控制列表,而是每个模块都实现称为守护者(keepers)的特殊对象,这些对象可以传递给其他模块以授予预定义的一组功能。

SDK 模块在 SDK 源码中的x/文件夹中定义。一些核心模块包括:

  • x/auth:用于管理帐户和签名
  • x/bank:用于实现 Token 和 Token 转账
  • x/staking + x/slashing:用于构建权益证明(Proof-Of-Stake,PoS)共识的区块链

除了使用x/中已有的模块,SDK 还支持构建自定义的模块。你可以查看示例教程

Cosmos SDK 对象能力模型

英文原文:Object-Capability Model

介绍

在考虑安全性时,最好从特定的威胁模型开始。我们的威胁模型如下:

我们假设蓬勃发展的 Cosmos SDK 模块生态中会包含错误或恶意的模块。

Cosmos SDK 旨在通过以对象能力系统作为基础来解决此威胁。

对象能力系统的结构特性有利于代码设计中的模块化,并确保代码实现中的可靠封装。

这些结构上的特性便于分析一个对象能力程序或操作系统的某些安全属性。其中一些 - 特别是信息流属性 - 可以在对象引用和连接级别进行分析,而不依赖于对决定对象行为的代码的任何了解或分析。

因此,可以在存在包含未知或恶意代码的新对象的情况下建立和维护这些安全属性。

这些结构属性源于管理对现有对象的访问的两个规则:

  1. 只要对象A持有对象B的引用,A可以向B发送一条消息,。
  2. 只要对象A收到了一条包含对象C引用的消息,A可以获得C的引用。根据这两条规则,一个对象只有通过一条先前存在的引用链获得另一个对象的引用,简而言之,“只有连接才能产生连接”。

查看关于 object-capabilities 的文章了解更多。

严格来说,Golang 由于几个问题没有完全实现 object-capabilities:

  • 普遍有引入原始模块(比如 unsafe、os)
  • 普遍重写模块的变量
  • 存在 2 个以上 goroutine 时的数据竞态漏洞可以创建非法的接口值

第一点很容易通过审计 import 和使用适当的依赖版本控制系统(如Dep)来捕获。但第二点和第三点就不容易了,需要成本进行代码审核。

实践中的对象能力模式

想法就是只暴露完成工作所需要的部分。

比如,下面的代码片段违反了对象能力原则:

1
2
3
4
5
6
type AppAccount struct {...}
var account := &AppAccount{
Address: pub.Address(),
Coins: sdk.Coins{sdk.NewInt64Coin("ATM", 100)},
}
var sumValue := externalModule.ComputeSumValue(account)

方法名ComputeSumValue暗示了这是一个不修改状态的纯函数,但传入指针值意味着函数可以修改其值。更好的函数定义是使用一个拷贝来替代:

1
var sumValue := externalModule.ComputeSumValue(*account)

在 Cosmos SDK 中,你可以看到 gaia app 中对该原则的实践。

1
2
3
4
5
6
7
// register message routes
app.Router().
AddRoute(bank.RouterKey, bank.NewHandler(app.bankKeeper)).
AddRoute(staking.RouterKey, staking.NewHandler(app.stakingKeeper)).
AddRoute(distr.RouterKey, distr.NewHandler(app.distrKeeper)).
AddRoute(slashing.RouterKey, slashing.NewHandler(app.slashingKeeper)).
AddRoute(gov.RouterKey, gov.NewHandler(app.govKeeper))