Tendermint 的区块构成
Tendermint 的区块构成
Tendermint 定义了区块的格式,任何基于 Tendermint 所开发的区块链,都会遵循此格式:
Header:区块头(下文详述)
Data:交易列表
Evidence:Validator 作恶的证据列表
LastCommit:上一个区块所获得的若干 Validator 的签名
其中,交易列表在 Tendermint 看来,就是无意义的字节串,上层应用负责解释和执行这些交易。
Evidence 和 LastCommit 是在 PoS 链中才会出现的数据。不同于 Bitcoin 用 PoW 的 nonce 来确认一个区块,PoS 链用 Validator 的投票来确认一个区块,大多数 PoS 共识算法,包括 Tendermint,要求 2/3 以上的 Validator 投票才能确认一个区块。LastCommit 即为 Validator 们对上一个区块所作出的投票。
为何是 “上一个区块” 而不是 “当前区块”?因为 PoS 链要求先由某一个 Validator 提出(propose)下一个区块的候选(candidate),然后对它进行广播,请其他 Validator 对此节点进行投票,投票通过后区块得到确认,才能提交(commit)到链上。新生成的区块在被广播的时候,尚未得到投票确认,因此无法把“当前区块” 的投票信息(即各个 Validator 的签名)包括在区块中。
对恶意的 Validator 进行惩罚(slash)是保证 PoS 链安全的必要手段,目前,Tendermint 只惩罚一种恶意行为——双签(还有一种对可用性差的惩罚,但那并不属于恶意)。Validator 必须抵押一笔资金才能获得投票确认区块的权力,当它在一个区块高度上对两个不同的区块都进行了签名时,就犯了 “双签” 的错误,它抵押的资金会被罚掉很大一个比例。当某个节点发现某 Validator 进行双签的证据时,它把证据进行全网广播,最终由某个出块的 Validator 将证据包含在区块中,这就是区块中的 Evidence。
在介绍区块头之前,我们先介绍一下 Tendermint 是如何保存全节点状态的。全节点的状态分两部分,一部分是上层应用的状态,一般会表现为一系列键值对的集合,Tendermint 并不关心其细节,只要它们可以构成 Merkle 树,最终得到一个 Merkle Root 即可(下文中我们将看到,这一 Merkle Root 被命名为 AppHash);另一部分是共识引擎的状态,即由 Tendermint 自己维护的状态。Tendermint 会确保全网所有的全节点的状态都是一致的。
共识引擎的状态定义如下:
Version 包括两个部分,一个是底层共识的版本号,一个是上层应用的版本号,二者都是 int64 类型。
ChainID 用来唯一标志一条链,当链进行硬分叉升级时,ChainID 需要被更换。
LastBlockHeight、LastBlockTotalTx、LastBlockID、LastBlockTime 分别表示上一个区块的高度、累积交易总数(从创世块到上一个区块总共执行了多少交易),ID 和时间戳。其中,BlockID 的定义如下:
其中包含两个部分,一个是 Block 整体形成的 Merkle 树的根 Hash,另一个是各个 Part 的 Hash。之所以要这样设计 BlockID,是因为 Tendermint 借鉴了 LibSwift,把区块拆分成很多个 Part,每个 Part 各自在 P2P 网络上进行广播,其它全节点需要逐个接收和验证这些 Part,然后再把它们拼合起来。这样可以达到更快的区块传播速度。
Tendermint 依赖上层应用来决定 Validator 集合应该如何变动,而且每个区块都可以更改 Validator 集合。但是,当前区块对 Validator 集合所作出的修改,要隔一个区块,到下下个区块才能生效。因此,Tendermint 使用了三个变量来跟踪 Validator 集合的变动:NextValidators、Validators、LastValidators,它们有效的时间分别是下一个区块,当前正在投票决策的区块和上一个区块。比如执行高度为 100 的区块时,修改了 Validator 集合,那么要在决策高度为 102 的区块时才会按照更新后的 Validator 集合进行投票。当高度为 100 的区块被执行完毕后,新的 Validator 集合被保存在 NextValidators 变量中;当高度为 101 的区块被执行完毕后,这一集合被保存在 Validator 中;当高度为 101 的区块被执行完毕,这一集合被保存在 LastValidator 中,而且,对高度为 102 的区块投票,将来自于这个集合中的 Validator。LastHeightValidatorsChanged 表示上一个发生了 Validator 集合变动的区块的高度,在上述的例子中,此变量取值为 102。
ConsensusParams 包含一些和共识相关的参数。这些参数同样可以被上层逻辑所修改。LastHeightConsensusParamsChanged 表示上一次修改这些参数的区块高度。
上层应用在执行完一个交易后,向 Tendermint 引擎返回一些 Results。LastResultsHash 是上个区块执行过程中所返回的所有 Results 所形成的 Merkle 树的根 Hash。
上层应用在执行完上一区块之后的内部状态所构成 Merkle 树的根 Hash,被保存在 AppHash 中。
以上,我们详细介绍 State 这个结构体所包含的各个成员,有了这些知识,理解区块头的定义就比较容易了。区块头的定义如下:
其中,Version、ChainID、LastBlockID、AppHash 都直接拷贝自 State 这个结构体的成员。
有 7 个 Hash 值,是通过计算得来的。DataHash、EvidenceHash 和 LastCommitHash,分别从区块的 Data、Evidence 和 LastCommit 三部分数据计算得来,即数据构成 Merkle Tree 后的根 Hash。而 ValidatorsHash、NextValidatorsHash、ConsensusHash 和 LastResultsHash 分别由 State 结构体的 Validators、NextValidators、ConsensusParams 和 LastResults 计算得来。
剩下的几个成员的含义非常容易理解:Height 是区块高度,Time 是区块的时间戳,NumTxs 是区块中的交易数,TotalTxs 是累积的交易总数(TotalTxs=LastBlockTotalTx+NumTxs),ProposerAddress 是打包并且广播此 Block 的 Validator 的地址。
剩下的几个成员的含义非常容易理解:Height 是区块高度,Time 是区块的时间戳,NumTxs 是区块中的交易数,TotalTxs 是累积的交易总数(TotalTxs=LastBlockTotalTx+NumTxs),ProposerAddress 是打包并且广播此 Block 的 Validator 的地址。
最后更新于