本文关于tendermint的共识部分,主要参考如下:
1. Ethan Buchman:Tendermint: Byzantine Fault Tolerance in the Age of
Blockchains
2. tendermint中文文档
3.tendermint的一个论文
4. 万向区块链对参考0中论文的翻译
5. tendermint官方doc
本文着重介绍tendermint的共识方面的细节。
tendermint共识状态机解析
上图中每个step,其实都是共识状态机中的一个状态,状态转换周期如下:
NewHeight -> (Propose -> Prevote -> Precommit)+ -> Commit -> NewHeight ->...
gossip 都广播哪些内容?
我们知道在驱动状态机最重要两个因素,一个是通信(tendermint中的P2P是以gossip协议和reactor模式为基础);另一个就是event(设计上任何异步通讯的过程都离不开event),这两部分的内容后面有机会的话会单独写文章解析,现在我们先来看看gossip都广播哪些内容,以使节点都能跟进当前的共识状态。
以下引用自官方文档,逐条说明本人结合源码对其的解析:
- Nodes gossip
PartSet
parts of the current round's proposer's proposed block. A LibSwift inspired algorithm is used to quickly broadcast blocks across the gossip network.
说明:partset代表block的一些简要信息,其结构如下:
type PartSet struct {
total int
hash []byte
mtx sync.Mutex
parts []*Part
partsBitArray *bits.BitArray
count int
}
- Nodes gossip prevote/precommit votes. A node
NODE_A
that is ahead ofNODE_B
can sendNODE_B
prevotes or precommits forNODE_B
's current (or future) round to enable it to progress forward.
说明:这条要结合下面状态机的prevote、precommit状态解析中的step ends一起理解。因为prevote,precommit都需要2/3+的票数通过,才能进行状态转换。
- Nodes gossip prevotes for the proposed PoLC (proof-of-lock-change) round if one is proposed.
说明:gossip prevotes的另一个用处是polka unlock.
- Nodes gossip to nodes lagging in blockchain height with block commits for older blocks.
说明:gossip block,落后的节点同步新block。
- Nodes opportunistically gossip
HasVote
messages to hint peers what votes it already has.
说明:节点偶尔gossip HasVote消息
- Nodes broadcast their current state to all neighboring peers. (but is not gossiped further)
说明:节点gossip 当前所在状态(未来版本不再gossip此部分)
共识状态解析
接下来让我们逐条分析各个共识状态。
1. propose step(height:H,round:R)
客户端发起一笔交易gentx,通过rpc发到tendermint节点后进入mempool cache,tm调用checktx的abci接口验证交易,如果验证成功就进入mempool,proposer节点选择打包交易到区块中,并将区块附到proposalMsg中,对此msg进行签名,然后通过gossip把此msg广播到区块链网络中。
一个proposal包含一个(H,R)的block,和一个可选的最近的PoLC-Round < R,当然前提是proposer知道此可选参数(PoLC:proof of lock change,指的2/3+ prevote某个block),这样适当保住了共识网络的liveness。
Upon entering Propose:
指定的proposer 提议了一个block(H,R).
The Propose step ends:
- timeoutProposeR 超时,则进入-->Prevote(H,R)
- 当收到了proposal block,并且改validator的所有prevote step 都在PoLC-Round,则进入-->Prevote(H,R)。(要理解这点,重点要理解某个validator有可能involve在若干个round,相当于说你其他轮中都过了prevote的step,才能进入新一轮的prevote step?但如果有锁的情况下即使进入了prevote轮,你也不能prevote了)
- After common exit conditions
Note:
注意每轮共识开始时,validator节点在本地会有一个本地同步时钟,用来记时ProPose timeout, 如果在timeout时间周期内没有收到proposer节点的提议,对该提议的预投票为空(nil),validator节点会投票决定是否进入下一轮共识,validator也可以通过治理模块投票移出或者替换拜占庭验证者。而propose timeout也会随着每轮共识而增加。所以可以理解为此过程是同步过程。
但是在其后的prevote和precommit的两阶段共识是完全异步的,从而减少对时钟同步的依赖,或者网络延迟。但这也同样存在一个问题,如果这两阶段共识中就是得不到1/3以上validator的投票,整个区块链网络将瘫痪(此处其实有待看源码仔细研究,因为这两个阶段也是有timeout定时器的,重点关注step end的条件,以此来分析可能引起网络hang住的场景)。这个问题的本质其实就是FLP不可能定理。FLP 原理实际上说明对于允许节点失效情况下,纯粹异步系统无法确保一致性在有限时间内完成。
所以tendermint其实在一定程度是假定节点中大部分都是正常节点。
2. prevote step (height:H,round:R)
每个节点收到propose消息后,会先对区块进行验证,如果验证通过才开始prevote签名,然后广播到网络中。
Upon entering Prevote, each validator broadcasts its prevote vote.
- First, if the validator is locked on a block since LastLockRound but now has a PoLC for something else at round PoLC-Round where LastLockRound < PoLC-Round < R, then it unlocks.(这条好理解,就是解锁条件,重点还是要理解异步)。
- If the validator is still locked on a block, it prevotes that. (这条是因为锁的规则,他只能prevote自己lock住的block,但是会引起重复prevote?这个还得结合源码研究)。
- Else, if the proposed block from Propose(H,R) is good, it prevotes that.(没啥好解释的,就是验证了proposal后正常prevote)
- Else, if the proposal is invalid or wasn't received on time, it prevotes <nil>.(proposal无效,或者定时器超时,就prevote<nil>)
The Prevote
step ends:
- After +2/3 prevotes for a particular block or
<nil>
. -->; gotoPrecommit(H,R)
- After
timeoutPrevote
after receiving any +2/3 prevotes. --> gotoPrecommit(H,R)
- After common exit conditions
3. Precommit Step (height:H,round:R)
Upon entering Precommit, each validator broadcasts its precommit vote.
- If the validator has a PoLC at (H,R) for a particular block B, it (re)locks (or changes lock to) and precommits B and sets LastLockRound = R.(说明了什么时候上锁)
- Else, if the validator has a PoLC at (H,R) for <nil>, it unlocks and precommits <nil>.(说明了什么时候precommit nil,简言之,就是获得了POLC的<nil>)
3.Else, it keeps the lock unchanged and precommits <nil>.(最后剩下的情况也就是超时了吧?)
A precommit for <nil> means "I didn’t see a PoLC for this round, but I did get +2/3 prevotes and waited a bit".(这句可以理解为没有获得非nil的POLC,而且等待了timeout时间段)
The Precommit step ends:
- After +2/3 precommits for
<nil>
. --> gotoPropose(H,R+1)
(注意,并没有commit nil。prevote nil、precommit nil进入下一轮,因此tendermint中没有类似PBFT中的viewchange ) - After
timeoutPrecommit
after receiving any +2/3 precommits. --> gotoPropose(H,R+1)
(我理解此处+2/3 precommits是笔误?应该是+2/3 prevotes?) - After common exit conditions
Common exit conditions
- After +2/3 precommits for a particular block. --> goto Commit(H)
- After any +2/3 prevotes received at (H,R+x). --> goto Prevote(H,R+x)
- After any +2/3 precommits received at (H,R+x). --> goto Precommit(H,R+x)
第一点,可理解为在R内要追赶最新状态,尽快commit。
后两点可理解为要追赶更新的R。
4. Commit Step (height:H)
- Set CommitTime = now()
- Wait until block is received. --> goto NewHeight(H+1) //注意此处要等到本轮的块到了才会进入NewHeight step.
5. NewHeight Step (height:H)
Move Precommits to LastCommit and increment height.
Set StartTime = CommitTime+timeoutCommit
Wait until StartTime to receive straggler commits. --> goto Propose(H,0)
小结
锁
在异步网络中,一个最大的问题就是同高度上,在不同的round提交了不同的block。tendermint引入了锁的机制。
什么时候锁?
每个validator锁定在他获得PoLC的block中,然后只能对该block precommit,在后面的R中,如果没有unlock也只能prevote此block。
什么时候解锁?
提交了一个当前锁定的块后
在下一轮(准确说应该是PoLC round)看到PoLC(proof of lock change,在新的一轮收到大于2/3 prevote的block)时获得解锁, 解锁后不会在PoLC round 投票了,因为在该R中已经获得了polka,也就是说已经结束了prevote state。
锁定规则
验证者只能预投票(pre-vote)他们被锁定的区块(因为他在某Round中prevote的block未必和precommit的block是相同的)。这样就阻止验证者在同一个高度,上一轮中预提交(pre-commit)一个区块,之后又预投票了下一轮的另一个区块。
Proof of Safety
假设有-1/3的Byzantine voting power(-1/3指不到1/3), 在R轮时,validator commit一个block B。这就意味着有1/3+的诚实节点lock在了R' > R, 他们直到获得在R`>R 的POLC才能unlock。但是这种unlock的情况不会发生,因为剩余的-2/3个节点能投票给B以外的block,无法形成POLC。(这不是会导致诚实节点一直锁吗?从而网络hang住?)