Tendermint 导读|乌托邦周报 #20
如果说 Cosmos SDK 是 Layer 1 的黄埔军校,那么 Tendermint 就是黄埔军校的奠基石。
自 2017 年问世以来,Cosmos SDK 是当今使用最为广泛的 L1 框架。从自家的 Cosmos Hub,到新晋金融协议 Injective,无不是出自 Cosmos SDK 之手。而 Tendermint,作为 Cosmos SDK 背后的核心模块,更是驱动了所有这些 L1 项目。
今天我们就来解构一下 Tendermint 的底层逻辑。
本文要点:
什么是 Tendermint
从头到尾走一遍 Tendermint
什么是 Tendermint
Tendermint 由 3 部分组成:
网络层:负责节点间的 P2P 通信。
共识层:拜占庭容错(BFT)的 PoS 共识机制。
应用层接口:暴露给上层应用的接口,所谓 ABCI(Application BlockChain Interface)。
其中,网络层和共识层整体上又被称为 Tendermint Core。
Cosmos SDK 做的,其实是在 Tendermint 的基础上,进一步为大家提供了应用层的逻辑模板,包括如何治理、如何质押、干坏事怎么 slash,以及如何跨链通信。
总而言之,Tendermint 把 L1 最共性的部分抽了出来,做成公共组件,让大家专心写应用逻辑就好。这就是 Tendermint 的价值所在。
从头到尾走一遍 Tendermint
理解 Tendermint,目前最好的资料便是官方文档里的这张流程图了。它从用户提交交易开始,到打包入块,到节点间达成共识,到执行交易,改变状态,完整地呈现了 Tendermint 的主线逻辑。
不过除此以外,似乎也没有其他补充性资料展开说明其中的逻辑细节。怎么办?我们不妨从头到尾扒代码看看。
用户提交 Tx
这一步骤对应流程图的左上区域。
Tendermint 在 Mempool Cache 介入,检查 Cache 中的 Tx 是否合法,合法的话才会将其加入 Mempool。
对应的逻辑在 Mempool 的 Reactor 中。Reactor 结构体用于响应 P2P 网络层的事件,最关键的是 Receive(),即收到其他节点发过来的消息后如何处理。
看看 Mempool Reactor 的实现。收到消息后首先反序列化,然后检查每一条 Tx 是否合法。
合法性主要看 Tx 的大小是否在配置的范围内,此处也可以加入自定义的检查逻辑。最终如果 Tx 合法,就加入到 Mempool 中。
Proposer 将 Tx 打包入块
Mempool 中的 Tx 打包由 CreateProposalBlock() 完成,后者会在 Mempool 中按 Tx 优先级从高到低,获取一组 Tx,放入区块中,并填充区块头。
节点间 3 阶段通信,达成共识
在区块链的语境下,共识机制讲的是所有节点按相同的顺序,执行同样的一组 Tx(状态转移),最终达到相同状态(State)的方法。
Tendermint 的共识机制和 PBFT 很相似,都有 3 个阶段,只不过命名不同。
PBFT 的三阶段为 Pre-prepare、Prepare、Commit。
其中,Pre-prepare 和 Prepare 阶段需要至少 2f+1
(f
为拜占庭节点数)的节点投票,才能进入下一阶段,原因是最坏情况下,所有拜占庭节点(f
个)同时广播假消息,那么合法节点数至少要超过它们(≥f+1
),收到投票的节点才能判断哪边消息是真,哪边是假。拜占庭节点和合法节点数相加便得到2f+1
。
而在 Commit 后的 Reply 环节,客户端则只需要f+1
个确认来判断是否 OK,因为最坏情况下,所有拜占庭节点(f
个)同时作恶,只要有 1 个合法节点与其反馈不一,就可以判断有问题。
最后,考虑所有拜占庭节点(f
个)还可以选择不广播、不回应,因此要让这个机制能运行下去,总的节点数至少得是2f+1+f=3f+1
。
在 Tendermint 里,三阶段对应 Pre-vote、Pre-commit、Commit。
其中,Pre-vote 和 Pre-commit 阶段都需要至少 2f+1
的节点投票,Commit 后需要至少f+1
个确认来开启下一轮。由于 Tendermint 基于 PoS,这里的节点看的不再是节点数量,而是节点的投票权(如按照质押额分配)。此外,由于节点多,公网环境复杂,且谁都可以加入,Tendermint 加入了超时机制,每个阶段只要超过 Δ 时间,就会跳过当前阶段。
实现层面上,可以分步来看。
首先,Proposer 签名并广播 Proposal。
其他节点在收到 Proposer 广播的区块后,验证区块的合法性,包括区块的高度、时间戳、关联的上一个区块的 ID 等。
若区块没问题,则签名并广播 Pre-vote,其中包含区块的 ID,否则不包含(nil)。
投票结构体的定义也可以简单看一眼。
接下来,等待一段时间,接收其他节点的 Pre-vote,直到收到三分之二节点的 Pre-vote(这种情形又称作 Polka)或超时。
Pre-vote 阶段过后,进入到 Pre-commit 阶段。检查之前有没有收到三分之二的 Pre-vote,有且自己也见着 Pre-vote 对应的区块的话,就签名并广播 Pre-commit,其中包含该区块的 ID,否则不包含(nil)。
接下来,如前一样,进入 Pre-commit 阶段的等待期。这里不再赘述。
最后,终于走到 Commit 阶段,看大家有没有达成一致(Pre-commit 阶段达成 Polka),有的话就 Commit,没有的话从头再来。
执行 Tx
执行 Commit 的区块,包括其中的所有 Tx,并更新状态。
选举下一区块的新 Proposer
Tendermint 的 Leader Election 是基于 Weighted Round-Robin 算法进行的。
一轮走完后,先给所有节点增加优先级。然后,选择优先级最高的作为 Proposer,循环往复。
注意,被选为 Proposer 的节点在下一轮的 Priority 会排到队尾(被减去 VotingPower 之和)。
值得一提的是,上一轮的 Proposer 节点在下一轮的优先级会排到末尾。狡猾一点的话,他可以选择退出,再重新加入,这样又会按照他的质押额来赋予投票权,从而插到队伍中间。为了尽可能避免这种情况,新加入的节点的优先级会有一个惩罚系数。
以上便是 Tendermint 的全流程玩法了。后面我们将看看,构建在 Tendermint 之上的 Cosmos SDK 是如何对其进行扩展的。