Tendermint 共识算法
每轮共识试图对某个区块达成共识。一轮起始于 ProposalMessage
,表明该节点认为下一个区块中应该包含哪些内容,其它节点响应 VoteMessage
(2 种,pre-vote 和 pre-commit),两种消息都需要签名。
Tendermint 对 BlockID(区块哈希)进行共识,区块本身通过 P2P 协议传递(proposer 将区块拆分成小块,通过 BlockPartMessage
消息进行 gossip)。
pol 指 proof of lock,就是 polka,是对一个区块超过 2/3 的 pre-vote 的集合;对 nil 的超过 2/3 的 pre-vote 的集合称为 nil-polka。
Tendermint 两次投票:
- 第一次投票(pre-vote)
- 作用:
- 告知其它节点自己收到了区块提案且认可提案的内容;
- 为提案投票,对区块提案的哈希签名,其它节点用来核验身份,用于计算是否达成 polka。
- 从单个节点的视角,达成 polka 的含义:
- 知道了 +2/3 节点收到并认可了同一个区块提案;(第一次共识达成)
- 区块链网络已经准备好提交该区块;
- 可以发起 pre-commit 投票。
- 作用:
- 第二次投票(pre-commit)
- 作用:
- 对实际提交该区块提案这一动作进行投票;
- 签名,其它节点用来核验身份,用于计算是否达成 +2/3 pre-commit。
- 从单个节点的视角,达成 +2/3 pre-commit 的含义:
- 其它节点也在对真正提交该区块进行提案,且 +2/3 节点对真正提交该区块提案达成共识;(第二次共识达成)
- 自己可以放心地真正提交该区块。
- 作用:
如果只有单阶段,节点 1 只根据它收到的其它节点的 pre-vote 消息就作出决策(更新状态、commit 区块等),而不知道集群是否对这一决策达成了共识,可能造成节点间状态不一致。所以节点 1 需要在作出决策之前将自己的决策广播出去(pre-commit),获得集群 +2/3 成员认可后,表示该决策在集群范围内达成共识,此时再去真正执行决策(commit)。
Tendermint 锁机制:
-
Prevote-the-Lock
- 锁机制:节点记录(维护在节点自身的共识状态中)每个轮次中自己 pre-commit 过的区块(也就是收到了 polka 的区块),并锁定在这样的区块中;若节点锁定在某个区块,在更高的轮次中节点作为普通节点只能 pre-vote 该区块,作为主节点只能提案该区块(参见下面的源码)。
- “在更高的轮次中节点作为普通节点只能 pre-vote 该区块”,但仍可以为收到的其它区块的 pre-vote 消息执行 pre-vote,但自己不会主动发送对自己 polka 区块之外的区块的 pre-vote 消息。
- 没有锁机制下的问题场景描述:
- 4 个节点分别 A,B,C,D 在 R 轮对区块 blockX 达成 polka;
- A 由于网络原因没有知晓该 polka,于是 pre-commit nil,B、C、D 则 pre-commit blockX;
- 只有 D 收到了足数的 pre-commit,于是 D 提交 blockX,A、B、C 进入 R+1 轮;
- R+1 轮中 A、B、C 提交了新区块 blockY;
- 结果是仅仅因为网络问题,在没有拜占庭节点的情况下就发生分叉;引入锁机制后,B、C 锁定在 blockX,在 R+1 轮就不能 pre-vote 给 blockY,blockY 无法被共识通过。
- 锁机制避免了节点在前一轮 pre-commit 了区块 A,却又在下一轮 pre-vote 区块 B、促成区块 B 的 polka。
- 同时它带来的潜在问题是某个轮次中一旦出现对某个区块 +2/3 的 pre-commit,网络中的节点就都锁定在该区块,无法在更高轮次中的就新区块达成 polka,出现共识不可用的情况(见下面的问题场景描述),因此需要有解锁机制,让节点在满足条件的情况下可以放弃锁定着的区块,进入到更高轮次。
- 锁机制:节点记录(维护在节点自身的共识状态中)每个轮次中自己 pre-commit 过的区块(也就是收到了 polka 的区块),并锁定在这样的区块中;若节点锁定在某个区块,在更高的轮次中节点作为普通节点只能 pre-vote 该区块,作为主节点只能提案该区块(参见下面的源码)。
-
Unlock-on-Polka
-
解锁机制:节点在看到比自己锁定着的轮次更高的 polka/nil-polka 时,可以将自己解锁,并为该更高轮次的 polka 进行 pre-commit。
-
只有锁定而没有解锁机制下的问题场景描述:
- R 轮中 A、B pre-commit 给 blockX(A、B 锁定在 blockX),C、D pre-commit 给 nil,无法达成共识,进入 R+1 轮;
- R+1 轮中提案的区块是 blockY,C、D pre-vote 给 blockY;
- A 是拜占庭节点,尽管锁定在 blockX,但也恶意 pre-vote 给 blockY,使得 blockY 达成 polka;
- B 没有知晓 blockY 的 polka,pre-commit 给 nil;
- A 下线;
- C、D pre-commit blockY;
- 进入 R+2 轮,此时 B 仍锁定在 R 轮的 blockX,C、D 锁定在 R+1 轮 blockY(较高轮次),A 离线;
- 后续轮次再也无法达成 polka,拜占庭节点少于 1/3 的情况下就出现了这种局面。
-
有了解锁机制,不考虑拜占庭节点 A,C、D pre-vote blockY,B pre-vote blockX;B 在 pre-vote 给 blockY 后会促成 blockY polka 的达成,B 解除对 blockX 的锁定。gossip 机制也可以同步节点间的
VoteSet
。cs.handleMsg(mi) // 处理 internalMsgQueue 和 peerMsgQueue 消息 func (cs *State) handleMsg(mi msgInfo) { // ... switch msg := msg.(type) { // ... case *VoteMessage: added, err = cs.tryAddVote(msg.Vote, peerID) // ... // ... } // ... } func (cs *State) tryAddVote(vote *types.Vote, peerID types.NodeID) (bool, error) { added, err := cs.addVote(vote, peerID) // ... } func (cs *State) addVote(vote *types.Vote, peerID types.NodeID) (added bool, err error) { // ... added, err = cs.Votes.AddVote(vote, peerID) // 直接 AddVote,不论轮次高低 if !added { // Either duplicate, or error upon cs.Votes.AddByIndex() return } // ... switch vote.Type { case tmproto.PrevoteType: prevotes := cs.Votes.Prevotes(vote.Round) // If +2/3 prevotes for a block or nil for *any* round: // 见到任意轮次的 polka if blockID, ok := prevotes.TwoThirdsMajority(); ok { // There was a polka! // If we're locked but this is a recent polka, unlock. // If it matches our ProposalBlock, update the ValidBlock // Unlock if `cs.LockedRound < vote.Round <= cs.Round` // NOTE: If vote.Round > cs.Round, we'll deal with it when we get to vote.Round if (cs.LockedBlock != nil) && (cs.LockedRound < vote.Round) && (vote.Round <= cs.Round) && !cs.LockedBlock.HashesTo(blockID.Hash) { cs.Logger.Debug("unlocking because of POL", "locked_round", cs.LockedRound, "pol_round", vote.Round) cs.LockedRound = -1 cs.LockedBlock = nil cs.LockedBlockParts = nil if err := cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()); err != nil { return added, err } } // Update Valid* if we can. // NOTE: our proposal block may be nil or not what received a polka.. if len(blockID.Hash) != 0 && (cs.ValidRound < vote.Round) && (vote.Round == cs.Round) { if cs.ProposalBlock.HashesTo(blockID.Hash) { cs.Logger.Debug("updating valid block because of POL", "valid_round", cs.ValidRound, "pol_round", vote.Round) cs.ValidRound = vote.Round cs.ValidBlock = cs.ProposalBlock cs.ValidBlockParts = cs.ProposalBlockParts } else { cs.Logger.Debug( "valid block we do not know about; set ProposalBlock=nil", "proposal", cs.ProposalBlock.Hash(), "block_id", blockID.Hash, ) // we're getting the wrong block cs.ProposalBlock = nil } if !cs.ProposalBlockParts.HasHeader(blockID.PartSetHeader) { cs.metrics.MarkBlockGossipStarted() cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartSetHeader) } cs.evsw.FireEvent(types.EventValidBlockValue, &cs.RoundState) if err := cs.eventBus.PublishEventValidBlock(cs.RoundStateEvent()); err != nil { return added, err } } } // If +2/3 prevotes for *anything* for future round: switch { // 节点处于滞后 round,且未来轮已经有 2/3 个 prevote 投票表决(可以是对 block 或 nil 的投票),说明集群中 2/3 节点都在新的一轮,节点没理由继续留在滞后的 round。 case cs.Round < vote.Round && prevotes.HasTwoThirdsAny(): // Round-skip if there is any 2/3+ of votes ahead of us cs.enterNewRound(height, vote.Round) case cs.Round == vote.Round && cstypes.RoundStepPrevote <= cs.Step: // current round blockID, ok := prevotes.TwoThirdsMajority() if ok && (cs.isProposalComplete() || len(blockID.Hash) == 0) { cs.enterPrecommit(height, vote.Round) } else if prevotes.HasTwoThirdsAny() { // +2/3 了但是 proposal 没集齐 cs.enterPrevoteWait(height, vote.Round) } case cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round: // If the proposal is now complete, enter prevote of cs.Round. if cs.isProposalComplete() { cs.enterPrevote(height, cs.Round) } } // ... } return added, err }
-
RoundStepPrevoteWait
和RoundStepPrecommitWait
阶段表示收到了对 block 和对 nil 的 +2/3 的投票,但对单独的 block 或 nil 都还不能凑足 +2/3。
-
validator 发送给与它连接的 peer 的消息类型:
ProposalMessage
,VoteMessage
,BlockPartMessage
:对区块进行共识;ProposalMessage
is sent when a new block is proposed.VoteMessage
is sent when voting for a proposal (or lack thereof).BlockPartMessage
is sent when gossipping a piece of the proposed block.
NewRoundStepMessage
,NewValidBlockMessage
:告知 peers 共识算法执行到哪一步了;NewRoundStepMessage
is sent for every step taken in the ConsensusState for every height/round/step transition.NewValidBlockMessage
is sent when a validator observes a valid block B in some roundr
, i.e., there is a Proposal for block B and 2/3+ prevotes for the block B in the roundr
. In case the block is also committed, thenIsCommit
flag is set to true.
HasVoteMessage
,VoteSetMaj23Message
,VoteSetBitsMessage
:告诉其他 peers 自己见到的投票类型。HasVoteMessage
is sent to indicate that a particular vote has been received.VoteSetMaj23Message
is sent to indicate that a givenBlockID
has seen +2/3 votes.VoteSetBitsMessage
is sent to communicate the bit-array of votes seen for theBlockID
.
ProposalPOLMessage
is sent when a previous proposal is re-proposed.
这些消息共同决定一个进程该发送给它的 peers 什么消息。
Consensus State 负责共识算法的执行,包括 Timeout Ticker(TT) 和 Receive Routine(RR)这两个执行单元。TT 会根据 height/round/step 设定计时器,定时器由 Receive Routine 处理。
状态机输入的 3 个来源包括其它 peers,内部模块的 validator,timer。
RR 处理会引起内部共识状态(internal consensus state transitions)转移的消息,它是唯一会更新 RoundState 的协程。
状态转移发生的时机:timeouts, complete proposals, and 2/3 majorities。
RR 收到状态机的输入后会调用相应的执行器,执行的结果可能引起 RoundState 的更新。
gossip 协程依赖 RoundState 来确定给 peers 发送什么消息。
RoundState defines the internal consensus state. It contains height, round, round step, a current validator set, a proposal and proposal block for the current round, locked round and block (if some block is being locked), set of received votes and last commit and last validators set.
References