Tendermint的区块构成(含代码示例)

Tendermint的区块构成

Tendermint定义了区块的格式,任何基于Tendermint所开发的区块链,都会遵循此格式:

  1. Header:区块头(下文详述)
  2. Data:交易列表
  3. Evidence:Validator作恶的证据列表
  4. 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会确保全网所有的全节点的状态都是一致的。

共识引擎的状态定义如下:

type State struct {
    Version Version

    // immutable
    ChainID string

    // LastBlockHeight=0 at genesis (ie. block(H=0) does not exist)
    LastBlockHeight  int64
    LastBlockTotalTx int64
    LastBlockID      types.BlockID
    LastBlockTime    time.Time

    // LastValidators is used to validate block.LastCommit.
    // Validators are persisted to the database separately every time they change,
    // so we can query for historical validator sets.
    // Note that if s.LastBlockHeight causes a valset change,
    // we set s.LastHeightValidatorsChanged = s.LastBlockHeight + 1 + 1
    // Extra +1 due to nextValSet delay.
    NextValidators              *types.ValidatorSet
    Validators                  *types.ValidatorSet
    LastValidators              *types.ValidatorSet
    LastHeightValidatorsChanged int64

    // Consensus parameters used for validating blocks.
    // Changes returned by EndBlock and updated after Commit.
    ConsensusParams                  types.ConsensusParams
    LastHeightConsensusParamsChanged int64

    // Merkle root of the results from executing prev block
    LastResultsHash []byte

    // the latest AppHash we've received from calling abci.Commit()
    AppHash []byte
}
type ConsensusParams struct {
    Block     BlockParams     `json:"block"`
    Evidence  EvidenceParams  `json:"evidence"`
    Validator ValidatorParams `json:"validator"`
}
type BlockParams struct {
    MaxBytes int64 `json:"max_bytes"`
    MaxGas   int64 `json:"max_gas"`
    // Minimum time increment between consecutive blocks (in milliseconds)
    // Not exposed to the application.
    TimeIotaMs int64 `json:"time_iota_ms"`
}
type EvidenceParams struct {
    MaxAge int64 `json:"max_age"` // only accept new evidence more recent than this
}
type ValidatorParams struct {
    PubKeyTypes []string `json:"pub_key_types"`
}

Version包括两个部分,一个是底层共识的版本号,一个是上层应用的版本号,二者都是int64类型。

ChainID用来唯一标志一条链,当链进行硬分叉升级时,ChainID需要被更换。

LastBlockHeight、LastBlockTotalTx、LastBlockID、LastBlockTime分别表示上一个区块的高度、累积交易总数(从创世块到上一个区块总共执行了多少交易),ID和时间戳。其中,BlockID的定义如下:

type BlockID struct {
    Hash        cmn.HexBytes  `json:"hash"`
    PartsHeader PartSetHeader `json:"parts"`
}
type PartSetHeader struct {
    Total int          `json:"total"`
    Hash  cmn.HexBytes `json:"hash"`
}

其中包含两个部分,一个是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这个结构体所包含的各个成员,有了这些知识,理解区块头的定义就比较容易了。区块头的定义如下:

type Header struct {
    // basic block info
    Version  version.Consensus `json:"version"`
    ChainID  string            `json:"chain_id"`
    Height   int64             `json:"height"`
    Time     time.Time         `json:"time"`
    NumTxs   int64             `json:"num_txs"`
    TotalTxs int64             `json:"total_txs"`

    // prev block info
    LastBlockID BlockID `json:"last_block_id"`

    // hashes of block data
    LastCommitHash cmn.HexBytes `json:"last_commit_hash"` // commit from validators from the last block
    DataHash       cmn.HexBytes `json:"data_hash"`        // transactions

    // hashes from the app output from the prev block
    ValidatorsHash     cmn.HexBytes `json:"validators_hash"`      // validators for the current block
    NextValidatorsHash cmn.HexBytes `json:"next_validators_hash"` // validators for the next block
    ConsensusHash      cmn.HexBytes `json:"consensus_hash"`       // consensus params for current block
    AppHash            cmn.HexBytes `json:"app_hash"`             // state after txs from the previous block
    LastResultsHash    cmn.HexBytes `json:"last_results_hash"`    // root hash of all results from the txs from the previous block

    // consensus info
    EvidenceHash    cmn.HexBytes `json:"evidence_hash"`    // evidence included in the block
    ProposerAddress Address      `json:"proposer_address"` // original proposer of the block
}

其中,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   int64             `json:"height"`
Time     time.Time         `json:"time"`
NumTxs   int64             `json:"num_txs"`
TotalTxs int64             `json:"total_txs"`

剩下的几个成员的含义非常容易理解:Height是区块高度,Time是区块的时间戳,NumTxs是区块中的交易数,TotalTxs是累积的交易总数(TotalTxs=LastBlockTotalTx+NumTxs),ProposerAddress是打包并且广播此Block的Validator的地址。


本文由 CoinEx Chain团队 Kui 写作,转载无需授权