每轮共识试图对某个区块达成共识。一轮起始于 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 两次投票:

  1. 第一次投票(pre-vote)
    • 作用:
      1. 告知其它节点自己收到了区块提案且认可提案的内容;
      2. 为提案投票,对区块提案的哈希签名,其它节点用来核验身份,用于计算是否达成 polka。
    • 从单个节点的视角,达成 polka 的含义:
      1. 知道了 +2/3 节点收到并认可了同一个区块提案;(第一次共识达成)
      2. 区块链网络已经准备好提交该区块;
      3. 可以发起 pre-commit 投票。
  2. 第二次投票(pre-commit)
    • 作用:
      1. 对实际提交该区块提案这一动作进行投票;
      2. 签名,其它节点用来核验身份,用于计算是否达成 +2/3 pre-commit。
    • 从单个节点的视角,达成 +2/3 pre-commit 的含义:
      1. 其它节点也在对真正提交该区块进行提案,且 +2/3 节点对真正提交该区块提案达成共识;(第二次共识达成)
      2. 自己可以放心地真正提交该区块。

如果只有单阶段,节点 1 只根据它收到的其它节点的 pre-vote 消息就作出决策(更新状态、commit 区块等),而不知道集群是否对这一决策达成了共识,可能造成节点间状态不一致。所以节点 1 需要在作出决策之前将自己的决策广播出去(pre-commit),获得集群 +2/3 成员认可后,表示该决策在集群范围内达成共识,此时再去真正执行决策(commit)。

Tendermint 锁机制:

  1. 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 给 blockYblockY 无法被共识通过。
    • 锁机制避免了节点在前一轮 pre-commit 了区块 A,却又在下一轮 pre-vote 区块 B、促成区块 B 的 polka。
    • 同时它带来的潜在问题是某个轮次中一旦出现对某个区块 +2/3 的 pre-commit,网络中的节点就都锁定在该区块,无法在更高轮次中的就新区块达成 polka,出现共识不可用的情况(见下面的问题场景描述),因此需要有解锁机制,让节点在满足条件的情况下可以放弃锁定着的区块,进入到更高轮次。
  2. 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
      }
      
    • RoundStepPrevoteWaitRoundStepPrecommitWait 阶段表示收到了对 block 和对 nil 的 +2/3 的投票,但对单独的 block 或 nil 都还不能凑足 +2/3。

validator 发送给与它连接的 peer 的消息类型:

  1. ProposalMessageVoteMessageBlockPartMessage:对区块进行共识;
    • 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.
  2. NewRoundStepMessageNewValidBlockMessage:告知 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 round r, i.e., there is a Proposal for block B and 2/3+ prevotes for the block B in the round r. In case the block is also committed, then IsCommit flag is set to true.
  3. HasVoteMessageVoteSetMaj23MessageVoteSetBitsMessage:告诉其他 peers 自己见到的投票类型。
    • HasVoteMessage is sent to indicate that a particular vote has been received.
    • VoteSetMaj23Message is sent to indicate that a given BlockID has seen +2/3 votes.
    • VoteSetBitsMessage is sent to communicate the bit-array of votes seen for the BlockID.

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