概述
Tendermint项目通过分层的理念将区块链应用构建划分成3层: P2P网络层, 共识层以及应用层, 项目本身提供了P2P网络层以及Tendermint共识协议层的实现, 并且定义了通用的ABCI接口来支持与上层应用的交互. ABCI接口的定义支持应用层的深度定制, Tendermint项目本身只负责P2P网络通信以及共识过程, 而交易检查和执行, 存储状态更新, PoS中的奖惩与验证者集合更新以及链上治理等都可以由应用层根据需要进行定制
通过ABCI接口进行交互时, 根据传统的C/S模型划分, 则Tendermint本身是客户端, 而上层应用App则是服务器, 客户端发起ABCI请求, 而服务器端则根据请求作出做出响应. 在允许应用层逻辑的深度定制之外, Tendermint也支持上层应用的多种实现方式, 只要遵循了ABCI接口的规范, Tendermint和应用层之间可以通过以下三种方式进行交互:
- 进程内交互: 用Golang开发的App与Tendermint一起编译生成单独的二进制文件
- GRPC交互: 用任意语言开发的App均可以与Tendermint通过GPRC进行交互
- TSP交互: 用任意语言开发的App均可以与Tendermint通过Tendermint Socket Protocol进行交互
值得指出的是, 当通过GRPC或者TSP交互时, 上层App可以用任意编程语言实现, 达到了与Tendermint项目解耦的效果.
在介绍Tendermint的架构设计时, 有讨论到mempool.Reactor
收到一笔交易时, 需要检查交易是否合法, 而这需要上层应用的协助, 而执行区块时也需要上层应用的协助, 另外Tendermint本身也需要知道上层应用的一些信息, 由此Tendermint的ABCI接口按照功能划分为以下三大类:
- Mempool connection:
CheckTx
- Consensus connection:
InitChain
,BeginBlock
,DeliverTx
,EndBlock
,Commit
- Info connection:
Info
,SetOption
,Query
第一类方法与Mempool相关。Tendermint项目中维护了一个Mempool,用来存放接收到的有效交易。在网络接收到交易之后,需要通过CheckTx
方法将交易发送给上层App进行基本的有效性验证,验证通过之后才会将交易加入到Mempool中。
第二类方法与区块执行相关, 在接收到一个合法区块之后,需要将区块提交给App来执行。为了最大限度的支持上层应用的可定制化,Tendermint将一个区块的执行拆分成 BeginBlock
,DeliverTx
,EndBlock
, Commit
这四步来完成。InitChain
方法用来初始化链状的,只在链刚刚启动的时候被调用。
+----------+ +---------+ ... +---------+ +--------+ +------+
|BeginBlock|----> |DeliverTx| -----> |DeliverTx|----->|EndBlock|----->|Commit|
+----------+ +---------+ +---------+ +--------+ +------+
^ |
| |
+---------------------------------------------------------------------+
第三类方法主要用来查询上层App的信息,如Info
方法会返回App的版本号、上个区块高度和上个区块的Apphash
。Query
可以用来查询某个存储状态。SetOption
用来配置一些与共识无关的选项,如设置节点所能接受的最小gasPrice等。
这三类方法在与上层App交互的时候,均会涉及到App存储状态的读写。这三类方法逻辑相互独立,也因此可以并发执行. App方面需要为每一类方法分别维护状态信息,而同一类方法则共用同一个状态信息。下图中展示了通过ABCI接口进行交易检查, 区块构建以及区块执行的基本逻辑.
-
Mempool
通过CheckTx
让上层App检查交易有效性, 有效性检查结果在TxResult
中返回,Mempool
将有效的交易保存在交易池中. - 当一个节点被选中作为新区块的提议者时, 从
Mempool
中抓取交易构建区块(Proposal Txs), 并通过共识协议在全网达成共识. 当全网就新的区块达成共识后, 网络中的节点都会更新自己Mempool
中的交易, 移除因为新区块执行而不再有效的交易(Reap) - 通过共识协议确定的新的区块通过ABCI接口提交给App执行, 交易执行结果通过
TxResult
返回给共识引擎, 共识引擎基于这些结果更新本地状态并开始下一个区块的构建.
Node
结构体中的proxyApp proxy.AppConns
用来完成ABCI交互, 接口proxy.AppConns
中封装了Mempool
, Consensus
以及Query
三类分别处理三类不同的ABCI接口, 其类型分别为接口AppConnMempool
, 接口AppConnConsensus
以及接口AppConnQuery
.接下来介绍这三类接口中定义的方法与涉及的数据结构.
// tendermint/proxy/multi_app_conn.go 11-18
// Tendermint's interface to the application consists of multiple connections
type AppConns interface {
service.Service
Mempool() AppConnMempool
Consensus() AppConnConsensus
Query() AppConnQuery
}
AppConnMempool
Tendermint底层维护了一个Mempool
,用来存储接收到的有效交易,以便在节点被选为区块提案者时从中获取交易并构建区块。Tendermint自身没有办法检查交易的有效性,只能借助上层App完成对交易的有效性检查。需要指出的是,判定是否允许交易进入Mempool
的检查通常只会执行比较基本的检查, 也因此可能会交易进行Mempool
并最终被打包进区块, 但是最终在链上执行时失败的情况. 这是因为不同交易之间的执行可能会相互影响, 也因此一笔交易最终是否能够执行成功取决于执行交易时刻的链上状态, 而这些状态在判定是否允许交易进入Mempool
时无法确定. 也即Tendermint打包的区块中可能包含最终会执行失败的交易, 这并不影响区块的合法性.
为了保证三类ABCI接口可以并行, 上层App需要为Mempool
类的ABCI单独方法维护一个状态。对应每类ABCI接口的状态在一个新区块被提交之后都会更新各自的状态当前状态。由于前述的先执行的交易可能会影响后续的交易的执行结果, 也因此ABCI接口的设计也需要保证, 在每一类方法中, 一个方法的前一次调用能够在状态上影响后一次调用。例如,对Mempool
连接来说,CheckTx
检查的一笔交易可能会引起某种状态变更,后面的交易检查所基于的状态是此次变更之后的新状态,以此类推。
CheckTx
Tendermint需要向App发送的CheckTx
请求定义如下:
type RequestCheckTx struct {
Tx []byte
Type CheckTxType
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
Tendermint与App之间所有的请求和回应数据格式都通过一个Protobuf文件定义,数据格式编码方式的统一可以为上层App的开发提供更多选择以及更多灵活性.RequestCheckTx
结构体中以XXX_
开头的字段是Protobuf库用来存储未知域的数据,本章节不不做介绍,后续会自动将这些字段省略。在RequestCheckTx
结构中,Tx
的类型为字节数组, 也即Tendermint层面不关心交易的具体格式. 上层App则可以根据业务逻辑尝试将字节数组解析为一笔标准交易. RequestCheckTx
结构中另一个字段为CheckTxType
类型的Type
的字段,这个字段可以有两种取值.
type CheckTxType int32
const (
CheckTxType_New CheckTxType = 0
CheckTxType_Recheck CheckTxType = 1
)
值为CheckTxType_New
时,表示这是从网络中收到的全新交易,在将这笔交易放入Mempool
之前需要对它进行一次完整的合法性检查。CheckTxType_Recheck
则与新区块执行导致的Mempool
的更新相关. 在一个区块被提交之后,Tendermint需要重新遍历自己Mempool
中的交易,以删除那些因为区块执行而变得不合法的交易。当App收到Type=CheckTxType_Recheck
的请求时,就知道这笔交易之前已经检查过一次,就可以跳过那些不受区块执行状态更新影响的有效性检查步骤, 从而提高执行效率。
App处理完收到CheckTx
的请求后, 会返回ResponseCheckTx
结构体类型的结果, 其中的Code
字段表示这笔交易是否通过了检查, Code
字段为0时, 表示交易有效. 区块链世界中, 为了防止链上资源的滥用, 链上的任何资源消耗都需要消耗一定的Gas. 交易本身可以指定自身提供的最大的Gas值(也即为了执行这笔交易愿意付出的最大成本), 而在真正处理这笔交易时可能并不需要交易指定的最大的gas值, ResponseCheckTx
结构体的GasWanted
字段表示这笔交易中附带的最大Gas, 而GasUsed
字段表示本次交易验证过程中消耗的Gas.
之后我们会看到,有许多ABCI方法的返回结果Response*
中都包含了Events
字段,每一个Event
都包含了Type
表示事件类型,以及一个Attributes
类型用来存储一些键值对,来标识在方法执行过程中发生了什么特定行为。根据上层App返回的Events
,Tendermint可以提供交易、区块等的索引服务。同时,外部服务商如钱包、区块浏览器等也可以向Tendermint订阅其感兴趣的事件,以便在这些事件发生时向外推送。
ResponseCheckTx
与ResponseDeliverTx
结构基本一致,其他字段留作以后解释。
// tendermint/abci/types/types.pb.go 1596-1608
type ResponseCheckTx struct {
Code uint32
Data []byte
Log string
Info string
GasWanted int64
GasUsed int64
Events []Event
Codespace string
// 省略 XXX_ 开头的字段
}
AppConnConsensus
InitChain
InitChain
方法用来初始化链的状态,只在链刚刚启动的时候被调用一次,链的起始状态被写在一个genesis.json
文件中,Tendermint解析这个文件的字段,将相应信息发给App进行初始化。RequestInitChain
定义如下,该结构体是InitChain
方法的主要参数:
// cosmos-sdk/abci/types/types.pb.go 476-485
type RequestInitChain struct {
Time time.Time
ChainId string
ConsensusParams *ConsensusParams
Validators []ValidatorUpdate
AppStateBytes []byte
}
Tendermint从genesis.json
文件中解析链的初始时间Time
、链标识ChainId
、共识参数ConsensusParams
、验证者集合等参数Validators
。由于Tendermint不关心App状态相关的数据,所以AppStateBytes
类型为字节数组, 上层传给App收到RequestInitChain
之后负责对AppStateBytes
进行解析并设置应用初始状态.
// cosmos-sdk/abci/types/types.pb.go 1915-1922
// ConsensusParams contains all consensus-relevant parameters
// that can be adjusted by the abci app
type ConsensusParams struct {
Block *BlockParams
Evidence *EvidenceParams
Validator *ValidatorParams
// 省略 XXX_ 开头的字段
}
ConsensusParams
中包含了会影响到共识执行的各类参数, 其中BlockParams
指定了允许的区块最大字节数、最大Gas数(当设置为-1时,表示没有限制),EvidenceParams
指定了与双签证据有效期相关的举证参数而ValidatorParams
则指定了与验证者公钥相关的参数.
由于Tendermint层和上层App需要共享共识参数和验证者集合,而App链上治理的相关业务逻辑有可能会导致这共识参数和验证者集合的变化。因此在ResponseInitChain
和随后会讲到的ResponseEndBlock
中都这两个字段,用来通知Tendermint相关参数的更新,保持App和Tendermint的一致性。
// cosmos-sdk/abci/types/types.pb.go 1382-1388
type ResponseInitChain struct {
ConsensusParams *ConsensusParams
Validators []ValidatorUpdate
// 省略 XXX_ 开头的字段
}
BeginBlock
当根据Tendermint共识协议就新区块的内容达成共识之后, 就需要将该区块提交给App执行. 整个执行过程分为四步. 第一步是BeginBlock
,该方法用来通知App一个新高度上区块的到来,App接收到相应请求后,可以在交易执行前进行预处理, 例如Cosmos Hub中为了激励共识过程参与者而设计的通过通胀进行铸币的过程就发生在这一步。RequestBeginBlock
中包含的字段包括:
-
Hash
: 新区块的区块头的哈希值 -
Header
: 新区块的区块头, 包含区块高度, 时间, 上一区块的标识, 本区块提议者等信息 -
LastCommitInfo
: 验证者对新区块的投票信息 -
ByzantineValidators
: 新区块中包含的验证者的作恶信息, 目前仅实现了双签作恶
// cosmos-sdk/abci/types/types.pb.go 626-634
type RequestBeginBlock struct {
Hash []byte
Header Header
LastCommitInfo LastCommitInfo
ByzantineValidators []Evidence
// 省略 XXX_ 开头的字段
}
在收到RequestBeginBlock
之后,App需要为新区块的执行做一些准备:设置好区块高度、执行上下文等。另外,App还需根据CommitInfo
和ByzantineValidators
来执行链上惩罚措施。Cosmos-SDK的slashing
模块根据CommitInfo
推断验证者的稳定性并对稳定性很差的验证者进行惩罚, 而evidence
模块则根据ByzantineValidators
的信息对在共识中作恶的验证者进行惩罚.
上层App处理完RequestBeginBlock
请求之后, 返回给Tendermint的ResponseBeginBlock
中包含了在处理过程中发生的事件集合Events
(比如对哪个验证者执行了链上惩罚等事件). Tendermint层会存储这些事件信息, 供对链上事件感兴趣的区块链浏览器, 钱包等服务商等查询.
// cosmos-sdk/abci/types/types.pb.go 1549-1554
type ResponseBeginBlock struct {
Events []Event
// 省略 XXX_ 开头的字段
}
DeliverTx
在完成BeginBlock
的调用之后,Tendermint层需要按序将区块中的交易传给App进行处理。由于交易在Tendermint看来只是一个字节序列,因此RequestDeliverTx
也只包含了这一个字段:
// cosmos-sdk/abci/types/types.pb.go 752-757
type RequestDeliverTx struct {
Tx []byte
// 省略 XXX_ 开头的字段
}
虽然App的业务逻辑千差万别, 但是不论业务逻辑如何变化, 每一笔交易的执行都可以分为以下三步:
- 尝试将交易解码为App定义的标准交易。
- 对交易执行预检查,即交易的合法性检查。
- 根据App的实现,执行解码后的交易。
值得指出的是,DeliverTx
和CheckTx
的处理逻辑类似,两者都需要进行第一步的预检查。但CheckTx
通常并不会真正执行交易,这取决于上层App的具体实现方式. 也因此, 被打包进区块的交易有可能在真正执行的时候失败,但这并不影响交易所在区块的合法性。这种方式所带来的一个好处是,能够降低CheckTx
对资源的占用,提高链的交易处理能力。
App每处理完一个交易的执行,就会向Tendermint返回处理结果,ResponseDeliverTx
中包含的字段与ResponseCheckTx
类似:
-
Code
字段标志着交易执行是否成功,下一区块的LastResultsHash
计算涉及到该字段 -
CodeSpace
字段指定了Code
的命名空间 -
GasWanted
字段记录了本交易所提供的gas总量 -
GasUsed
指在实际执行过程中真实消耗的gas总量. 如果GasUsed>GasWanted
,就表明交易因为gas不够而执行失败。 -
Events
包含了本次交易执行过程中所发生的事件集合,Tendermint存储后供外部订阅者查询。 -
Data
字段可以存储从App返回的任何确定性的数据,该字段的值对全网来说必须是一致的, 下一区块的LastResultsHash
计算涉及到该字段 -
Log
字段可以包含任意的日志信息, 该字段是共识无关的 -
Info
字段则可以包含除日志之外的其他任意信息
Tendermint在执行一个区块时, 会根据接收到的所有的ResponseDeliverTx
中的Code
字段来统计区块中执行成功的交易数量。
// cosmos-sdk/abci/types/types.pb.go 1699-1711
type ResponseDeliverTx struct {
Code uint32
Data []byte
Log string
Info string
GasWanted int64
GasUsed int64
Events []Event
Codespace string
// 省略 XXX_ 开头的字段
}
EndBlock
待执行区块中所有的交易均通过DeliverTx
执行完毕之后, Tendermint需要发送RequestEndBlock
类型的请求通知App当前区块的交易已经发送完毕,该请求里只包含了当前的区块高度Height
. 在收到该请求后,App可以根据区块中交易的执行结果做进一步的处理.
// cosmos-sdk/abci/types/types.pb.go 799-804
type RequestEndBlock struct {
Height int64
// 省略 XXX_ 开头的字段
}
App返回给Tendermint的ResponseEndBlock
里包含本次区块执行后活跃验证者集合的更新ValidatorUpdates
,共识参数的更新ConsensusParamUpdates
和发生的事件Events
。在BeginBlock
方法中,我们提到了App可能需要对恶意验证者进行惩罚。此外,在交易执行过程中各个验证者的投票权重也会因为相关交易的执行而发生变化,因此EndBlock
方法的一个重要用途就是App重新计算验证者投票权重的变化(Cosmos-SDK的staking
模块实现了这一操作),将这个集合返回给Tendermint, Tendermint根据这一信息更新活跃验证者集合并使用带权重的轮换算法选择下一个区块的区块提案者. 在当前的实现中, 交易执行引发的验证者集合的更新反映到底层Tendermint会有一个区块的延迟(参见Cosmos-SDK的PoS实现中参数ValidatorUpdateDelay
的介绍). 也即在区块高度H
的EndBlock
中产生的新的验证者集合首次参与共识投票是针对区块高度为H+2
的区块, 并且其投票信息包含在区块高度为H+3
的区块中. 然而共识参数的更新会直接影响下一个区块。需要说明的是,如果App返回的共识参数不为空,Tendermint会完全使用该参数来替换原来的参数,因此App返回的参数更新中,同一类参数(如BlockParams
类型的参数)必须全部设置,即使是那些未更新的。
// cosmos-sdk/abci/types/types.pb.go 1802-1809
type ResponseEndBlock struct {
ValidatorUpdates []ValidatorUpdate
ConsensusParamUpdates *ConsensusParams
Events []Event
// 省略 XXX_ 开头的字段
}
ValidatorUpdate
结构体中的字段PubKey
代表验证者的共识公钥, Power
字段则表示更新后的验证者的投票权重,该值必须是是一个非负类型的值,并且不能超过MaxTotalVotingPower
的上限. 在接收到App返回的结果后,Tendermint会据此更新自己的验证者集合信息和共识参数信息,并将Events
存储后供外部订阅者查询。
// cosmos-sdk/abci/types/types.pb.go 2629-2635
type ValidatorUpdate struct {
PubKey PubKey
Power int64
// 省略 XXX_ 开头的字段
}
Commit
在执行DeliverTx
的时候,App并没有将交易造成的状态更新持久化到数据库,Tendermint需要主动发起一个Commit
调用来通知App将本次区块的状态更新持久化存储下来。在这一过程中,Tendermint无需向App传送任何字段,因此RequestCommit
被定义为一个空的结构体。
Tendermint的当前区块中需要包含上一个区块的AppHash
,因此App在持久化存储状态之后,还需计算一个Apphash
返回给Tendermint。ResponseCommit
结构体中包含了一个字节数组Data
,Apphash
就存储在这里,但理论上App可以在此处返回任何确定性的值给Tendermint。
// cosmos-sdk/abci/types/types.pb.go 1865-1871
type ResponseCommit struct {
Data []byte
// 省略 XXX_ 开头的字段
}
AppConnQuery
Info
Info
接口主要用来维护在Tendermint与App之间的一致性。在正常情况下,App与Tendermint之间可以按照上面所述的流程来完成新区块的执行. 但在实际执行时,App和Tendermint有可能在中间的任意一步崩溃,从而导致两者的状态不一致。因此,在重新启动Tendermint时,需要通过发送RequestInfo
来向上层App请求其最新状态,验证两者是否同步。在不一致的时候,Tendermint需要根据App落后的区块数来重放这些区块。
// cosmos-sdk/abci/types/types.pb.go 357-364
type RequestInfo struct {
Version string
BlockVersion uint64
P2PVersion uint64
// 省略 XXX_ 开头的字段
}
RequestInfo
结构体里包含了Tendermint当前的版本号Version
、区块版本号BlockVersion
和P2P协议版本号。App返回给Tendermint的ResponseInfo
结构体定义如下, Data
可以包含任意的信息,Version
代表App的语义版本号,AppVersion
表示App的协议版本号,但这几个字段都不是必须的。 LastBlockHeight
和LastBlockAppHash
字段是App必须返回的,其用途就是同步Tendermint与App的状态。
// cosmos-sdk/abci/types/types.pb.go 1238-1247
type ResponseInfo struct {
Data string
Version string
AppVersion uint64
LastBlockHeight int64
LastBlockAppHash []byte
// 省略 XXX_ 开头的字段
}
Query
由于Tendermint完全不掌握上层App的业务逻辑和数据存储,但是外部应用只能通过Tendermint来与上层App进行交互。因此,在外部查询者需要查询一些上层状态时,需要Tendermint将查询转发给App,这就是Query
方法的用途。
// cosmos-sdk/abci/types/types.pb.go 555-563
type RequestQuery struct {
Data []byte
Path string
Height int64
Prove bool
// 省略 XXX_ 开头的字段
}
RequestQuery
结构体中包含了需要查询的路径Path
、参数Data
、查询的状态高度Height
和是否需要App对查询的结果进行证明Prove
。App一方面需要定义可供查询的数据和参数格式,另一方面需要引入可认证数据结构进行键值对存储,以支持查询的证明。该证明对于实现高效的轻客户端来说很重要,它可以允许客户端在无需跟踪所有链上数据的情况下验证返回的结果的真实性。
// cosmos-sdk/abci/types/types.pb.go 1437-1451
type ResponseQuery struct {
Code uint32
// bytes data = 2; // use "value" instead.
Log string
Info string
Index int64
Key []byte
Value []byte
Proof *merkle.Proof
Height int64
Codespace string
// 省略 XXX_ 开头的字段
}
App返回的结果类型为ResponseQuery
结构体, 其中Code
和CodeSpace
用来标识查询是否成功。如果成功的话,Key
和Value
字段存储了查询的结果,Proof
是App返回的证明,Index
代表了查询的Key
在树中的索引值。 Tendermint在拿到查询结果后直接将其返回给外部查询者.
Proof
是指Merkle树的存在性或者不存在性证明, 而为了支持证明, 就需要在上层App的存储结构中使用可认证的数据结构, 如以太坊的Merkle Patricia Trie或者Tendermint项目开发的IAVL+树. Cosmos-SDK采用模块化设计的理念, 每个应用的模块都利用IAVL+树维护本模块相关的状态, 而不同模块的IAVL+树的树根又参照RFC6962中的Merkle树规范, 共同组成了一棵简单Merkle树, 其树根就是AppHash
. 证明某个模块中的某个值确实存在时, 首先需要提供从AppHash
到某个模块的IAVL+树根的证明, 然后提供从该IAVL+树根到具体值的证明. 由此也解释了为何Proof
结构体中需要包含一组ProofOp
.每个ProofOp
结构结构包含一个Merkle树证明. 其中的Type
字段表示Merkle树的类型, Key
字段根据Type
的不同有不同的含义,对于IAVL+
树类型的证明,该值表示查询的key
,对于Merkle树类型的证明,该值表示状态查询的模块名称。 Data
字段则包含了实际的证明.
// cosmos-sdk/abci/types/types.pb.go 93-99
type Proof struct {
Ops []ProofOp
// 省略 XXX_ 开头的字段
}
// cosmos-sdk/abci/types/types.pb.go 30-37
type ProofOp struct {
Type string
Key []byte
Data []byte
// 省略 XXX_ 开头的字段
}
SetOption
// cosmos-sdk/abci/types/types.pb.go 421-427
type RequestSetOption struct {
Key string
Value string
}
SetOption
用来从Tendermint向App发起与共识无关的键值对设置。比如说,Key="min-fee"
,value="100utom"
,可以设置上层App在CheckTx
时用到的最小gasPrice
。
Client
与Application
接下来介绍Tendermint为了支持ABCI接口而采用的整体设计, 其中包含两个最主要的主体结构:ABCI
客户端Client
和 ABCI服务器Application
. 在ABCI的交互中,Tendermint作为ABCI客户端向作为服务器的App发起请求,App处理请求并及时响应。为了能够支持App的多种实现方式,Tendermint中的ABCI客户端Client
被定义为Go语言中的接口类型, Tendermint自身提供了三种ABCI Client
的实现:localClient
, grpcClient
, socketClient
. 与此相对应,App可以根据自己的实现来创建出任意一种 ABCI Application
: localserver
,grpcserver
,socketserver
.
三类ABCI方法Mempool
,Consensus
,Query
从功能上来讲是相互独立的,为了能够使Tendermint底层在使用这些功能时逻辑更加清晰,Tendermint将ABCI中划分为三类接口类型AppConnMempool
,AppConnConsensus
,AppConnQuery
来供底层不同模块调用. 其中每一类接口方法都是ABCI接口方法的一个子集。因此,实现了ABCIClient
接口也就自然满足了这三类接口的要求。
接下来介绍ABCI Client
接口的定义以及三类连接接口AppConnMempool
,AppConnConsensus
,AppConnQuery
,并以localClient
和localserver
为例介绍以进程内交互方式实现的ABCI客户端与服务器, 以socketClient
和socketserver
为例介绍基于TSP实现的ABCI客户端与服务器.
ABCI Client
接口
ABCI Client
接口定义如下,可以看到,Client
集合了之前介绍的三类ABCI方法,并且针对每一类方法都分别有一个同步版本和异步版本。同步版的方法是Tendermint打包好请求发送给App处理,并等待App返回的处理结果。异步版的方法则可以在发送完请求后去做别的事情,等收到响应之后再回来处理。因此,异步版的方法返回的结果是一个指向ReqRes
结构体的指针, 里面包含了发送的请求和收到的响应, 以及对类请求/响应的回调函数, 稍后再具体介绍。此外,Client
中还内嵌了一个Service
的接口类型,该类型作为一个基本的服务类型接口多次出现在Tendermint的底层实现中,它是一个包含start()
、stop()
、reset()
等方法的接口, 在Tendermint的整体架构一章已经有详细介绍,在此不再赘述。
// tendermint/abci/client/client.go 21-50
type Client interface {
service.Service
SetResponseCallback(Callback)
Error() error
FlushAsync() *ReqRes
EchoAsync(msg string) *ReqRes
InfoAsync(types.RequestInfo) *ReqRes
SetOptionAsync(types.RequestSetOption) *ReqRes
DeliverTxAsync(types.RequestDeliverTx) *ReqRes
CheckTxAsync(types.RequestCheckTx) *ReqRes
QueryAsync(types.RequestQuery) *ReqRes
CommitAsync() *ReqRes
InitChainAsync(types.RequestInitChain) *ReqRes
BeginBlockAsync(types.RequestBeginBlock) *ReqRes
EndBlockAsync(types.RequestEndBlock) *ReqRes
FlushSync() error
EchoSync(msg string) (*types.ResponseEcho, error)
InfoSync(types.RequestInfo) (*types.ResponseInfo, error)
SetOptionSync(types.RequestSetOption) (*types.ResponseSetOption, error)
DeliverTxSync(types.RequestDeliverTx) (*types.ResponseDeliverTx, error)
CheckTxSync(types.RequestCheckTx) (*types.ResponseCheckTx, error)
QuerySync(types.RequestQuery) (*types.ResponseQuery, error)
CommitSync() (*types.ResponseCommit, error)
InitChainSync(types.RequestInitChain) (*types.ResponseInitChain, error)
BeginBlockSync(types.RequestBeginBlock) (*types.ResponseBeginBlock, error)
EndBlockSync(types.RequestEndBlock) (*types.ResponseEndBlock, error)
}
作为ABCI接口的一个子集,AppConnMempool
接口定义了mempool
相关的方法:
// tendermint/proxy/app_conn.go 23-31
type AppConnMempool interface {
SetResponseCallback(abcicli.Callback)
Error() error
CheckTxAsync(types.RequestCheckTx) *abcicli.ReqRes
FlushAsync() *abcicli.ReqRes
FlushSync() error
}
CheckTxAsync
实现了异步的CheckTx
传输,ABCI服务器要提供有序的异步消息传输机制,来允许Tendermint在App处理完上一个CheckTx
请求之前继续向App发送下一个请求。由于处理是异步的,CheckTxAsync
方法返回的结果中既包含了响应结果,也包含了请求信息,以及本请求是否已经处理完毕的标记等。Tendermint可以在创建ReqRes
结构体类型的变量时通过SetResponseCallback
方法来设置响应的回调函数cb
,以便在App返回响应之后根据结果来做进一步处理。针对CheckTx
而言,这个回调函数即是通过判断交易是否通过了App的有效性检查,并将交易加入mempool
中,同时通知其他模块mempool
中已经有新交易等待打包。
//tendermint/abci/client/client.go 74-82
type ReqRes struct {
*types.Request
*sync.WaitGroup
*types.Response // Not set atomically, so be sure to use WaitGroup.
mtx sync.Mutex
done bool // Gets set to true once *after* WaitGroup.Done().
cb func(*types.Response) // A single callback that may be set.
}
具体实现ABCI 客户端时,在调用一些异步的ABCI方法时可能只是将这些请求保存到一个待发送的队列里面,并未真正发送出去。而Flush()
方法会定时自动触发,用来将尚未发送给服务器的请求发送出去,以确保异步请求真正发送至ABCI服务器。
作为ABCI接口的一个子集,AppConnConsensus
接口定义了区块执行相关的方法:
// tendermint/proxy/app_conn.go 11-21
type AppConnConsensus interface {
SetResponseCallback(abcicli.Callback)
Error() error
InitChainSync(types.RequestInitChain) (*types.ResponseInitChain, error)
BeginBlockSync(types.RequestBeginBlock) (*types.ResponseBeginBlock, error)
DeliverTxAsync(types.RequestDeliverTx) *abcicli.ReqRes
EndBlockSync(types.RequestEndBlock) (*types.ResponseEndBlock, error)
CommitSync() (*types.ResponseCommit, error)
}
与AppConnMempool
的CheckTx
相类似,DeliverTx
的实现也是异步的。除此之外,其他方法都只需要同步的实现。异步方法的返回值都是*abcicli.ReqRes
类型的,ABCI客户端需要为已经发送的请求保存相应的ReqRes
,以便在收到服务器的响应时根据请求结果执行相应的回调函数。同步方法的返回结果则直接对应了本类型的Response
,如:ResponseInitChain
, ResponseBeginBlock
等.
作为ABCI功能的一个子集,AppConnQuery
接口定义了与Query
相关的方法集合,这些接口方法的主要功能在前面小节中已经介绍过, 此处不再重复.
// tendermint/proxy/app_conn.go 33-41
type AppConnQuery interface {
Error() error
EchoSync(string) (*types.ResponseEcho, error)
InfoSync(types.RequestInfo) (*types.ResponseInfo, error)
QuerySync(types.RequestQuery) (*types.ResponseQuery, error)
}
尽管在实现时,这三类连接的实现都包含了同一个ABCIClient
接口类型的变量,将ABCI Client
功能拆分成这三类子接口能够使得Tendermint底层各个模块的功能划分看起来更加清晰:Mempool
模块只需要依赖AppConnMempool
的接口类型,而无需依赖共识相关的ABCI方法.
进程内交互
在App采用Go语言开发并且和Tendermint编译成一个二进制文件时,Tendermint和App采用的是进程内的通信方式,两者分别对应localClient
& localserver
。localClient
的实现方式比较简单,在初始化的时候,需要将App作为Application
类型的变量传入:
// tendermint/abci/types/local_client.go 16-21
type localClient struct {
service.BaseService
mtx *sync.Mutex
types.Application
Callback
}
由于Tendermint对ABCI方法的调用是并发进行的,但App的实现并不要求是并发安全的。因此,需要在Client
中加一把锁来控制与App的交互。Application
同样被定义为一个接口类型,其中包含了Tendermint所期望上层App提供的ABCI方法,以供ABCI 客户端调用,从而驱动App的状态更新。
由于Tendermint和App被编译在一个二进制文件里,localClient
可以直接调用Application
暴露的接口,也不需要再额外定义Server
类型。由于两者采用进程内通信方式进行交互,localClient
所有的异步方法内部实际上都是按照同步的方式来进行的,只是在处理响应结果时,异步方法相比同步方法多了一个回调函数的执行,可以由Tendermint底层各组件选择使用同步的或异步的方法。模块在使用时可以选择异步的方法调用,并且为同类请求设置一个回调函数:如Mempool
模块为AppConnMempool
连接类型设置的globalCb
函数,该函数作为一个全局的回调函数,处理对于任何AppConnMempool
类的响应时都会被调用。
// tendermint/abci/types/application.go 11-26
type Application interface {
// Info/Query Connection
Info(RequestInfo) ResponseInfo // Return application info
SetOption(RequestSetOption) ResponseSetOption // Set application option
Query(RequestQuery) ResponseQuery // Query for state
// Mempool Connection
CheckTx(RequestCheckTx) ResponseCheckTx // Validate a tx for the mempool
// Consensus Connection
InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain w validators/other info from TendermintCore
BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block
DeliverTx(RequestDeliverTx) ResponseDeliverTx // Deliver a tx for full processing
EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set
Commit() ResponseCommit // Commit the state and return the application Merkle root hash
}
TSP交互
当采用Java, C++, Rust等语言开发上层App时, 无法与Go语言实现的Tendermint项目编译成一个二进制文件时,可以选择使用Tendermint Socket Protocol (TSP)来进行交互通信. TSP默认是建立在TCP连接之上的,因此继承了TCP连接的可靠性和和稳定性。SocketClient
的实现原理如下:在socketClient
被启动时,会同时开启两个go-routine:用来发送请求的sendRoutine
和 用来接收响应的recvRoutine
。与localClient
的实现一个很大的不同在于,socketClient
真正实现了异步方法的“异步”逻辑,即每次调用一个异步方法CheckTxAsync
时,socketClient
并不会把该请求立即发送出去,而是存储在自己的一个待发送队列里,同时设置一个定时器,在定时器被自动触发时,才会真正把待发送队列里的所有请求发送出去。
// tendermint v0.33.3 abci/client/socket_client.go:29-44
type socketClient struct {
service.BaseService
addr string
mustConnect bool
conn net.Conn
reqQueue chan *ReqRes
flushTimer *timer.ThrottleTimer
mtx sync.Mutex
err error
reqSent *list.List // list of requests sent, waiting for response
resCb func(*types.Request, *types.Response) // called on all requests, if set.
}
socketClient
结构体的定义如上, 其中各个字段的含义解释如下:
-
BaseService
封装了一个通用的服务类型,包含了服务的名字、是否已经启动/停止、日志器以及具体的实现 -
addr
,mustConnect
,conn
字段记录了网络连接相关的信息 -
flushTimer
字段是关于前面提到的定时器, 以便定期将待发送的请求发送给服务器, 并同时给服务器发送Flush
请求让服务器将已经处理完毕的请求结果按序返回 -
reqQueue
字段表示待发送队列,在每个异步方法被调用时,会将相应请求放入该队列 -
reqSent
字段存储了待发送的请求的ReqRes
变量,用来在收到请求时对响应结果进行处理 -
resCb
字段与localClient
中的类似,可以对同一类连接请求设置一个共同的回调函数
在socketClient
通过socket
连接接收到App的响应时,会从reqSent
中按序取出相应的ReqRes
,将其请求状态标记为已完成,并从reqSent
中移除,然后调用为socketClient
设置的全局的回调函数,如果该ReqRes
变量也设置了回调函数的话,还会再调用为每一个ReqRes
设置的回调函数,至此完成请求的发送和响应的处理。
对比socketClient
的功能,可以想到socketserver
需要实现的逻辑就是接收并处理请求,以及发送对请求的响应。socketserver
的结构体定义如下:
//tendermint v0.33.3 abci/client/socket_server.go:17-30
type SocketServer struct {
service.BaseService
proto string
addr string
listener net.Listener
connsMtx sync.Mutex
conns map[int]net.Conn
nextConnID int
appMtx sync.Mutex
app types.Application
}
与socketClient
不同的是,Tendermint底层不同组件(如共识组件、mempool
组件等)可能都与App有一个ABCI连接,因此socketserver
端需要维护一个conns
的map
,以便管理所有连接。在启动socketServer
时,会开启一个acceptConnectionsRoutine
的goroutine来接收来自各个socketClient
的连接请求,对每一个接收到的连接,都会再启动两个goroutine:用来处理请求的handleRequestsRoutine
和用来返回结果的handleResponsesRoutine
。在socketserver
接收到Flush
类型的请求时,会将已经处理好的响应结果按序返回给socketClient
,至此完成请求的处理和响应的返回.
总结
Tendermint与App之间通过ABCI接口进行交互,从功能上ABCI连接可以分为三类:AppConnMempool
相关的主要用来做交易的有效性检查。AppConnConsensus
允许Tendermint将新生成的区块分阶段提交给App执行。AppConnQuery
连接用来同步Tendermint与App的状态同步,并处理状态查询。ABCI接口使得App的业务逻辑可以基于Tendermint之上独立开发,负责独立功能的模块逻辑更加清晰。
**本文由CoinEx Chain开发团队成员Jia、longcpp撰写。CoinEx Chain是全球首条基于Tendermint共识协议和Cosmos SDK开发的DEX专用公链,借助IBC来实现DEX公链、智能合约链、隐私链三条链合一的方式去解决可扩展性(Scalability)、去中心化(Decentralization)、安全性(security)区块链不可能三角的问题,能够高性能的支持数字资产的交易以及基于智能合约的Defi应用。
有疑问加站长微信联系(非本文作者)