这块内容主要是一个拜占庭的过程。本文掐头去尾,只讲一下在 quorum
中是如何实现拜占庭的,也就是共识接口 Seal()
下向拜占庭发送了一个区块的请求事件开始。请求事件如下:
// post block into Istanbul engine
go sb.EventMux().Post(istanbul.RequestEvent{
Proposal: block,
})
前言
在说正文之前,先讲四个变量:sequence round state Code
,因为这四个变量覆盖了 BFT
的整个流程,可以说 BFT
玩的就是这几个变量。
// 视图包括round和sequence两个数字
//sequence是我们即将提交的区块号
//每个round都有一个数字,而且包含三步:preprepare, prepare and commit
// 如果给定一个区块没有被验证者接收,round将会改变,并且验证者将使用 round+1 这个新的round开始新的验证周期
type View struct {
Round *big.Int
Sequence *big.Int
}
state
表述了每个 round
周期中,进行到了哪一步,是每个 round
周期的状态。
type State uint64
const (
StateAcceptRequest State = iota
StatePreprepared
StatePrepared
StateCommitted
)
标明发送消息的类型
const (
msgPreprepare uint64 = iota
msgPrepare
msgCommit
msgRoundChange
msgAll // 未使用
)
type message struct {
Code uint64 // 消息的类型
Msg []byte // 消息的内容
Address common.Address // 发送消息的人
Signature []byte // 消息的签名
CommittedSeal []byte // 提交消息到区块额外字段时用到
}
BFT正文
从这里开始正式对 quorum
的 istanbul
共识的 BFT
流程进行说明,开始的文件是 consensus/istanbul/core/handler.go
。
启动BFT
在启动挖矿的时候会启动 BFT
,此时开始一个新的 BFT
轮次,然后订阅事件并启动对事件的监听及处理。
func (c *core) Start() error {
// Start a new round from last sequence + 1
// 开始一个新的轮次
c.startNewRound(common.Big0)
// Tests will handle events itself, so we have to make subscribeEvents()
// be able to call in test.
// 订阅事件,事件包括:
// |-- 普通拜占庭事件
// |-- 区块提案请求事件
// |-- 消息事件,又分为四种:
// |--preprepare
// |--prepare
// |--commit
// |--roundChange
// |-- 积压消息事件
// |-- round周期超时事件
// |--区块头提交事件
c.subscribeEvents()
// 启动一个协程来处理不同的事件
go c.handleEvents()
return nil
}
处理不同的事件
要处理的事件可分为三大类:普通拜占庭事件、round周期超时事件、区块头提交事件
。普通拜占庭事件
监听的是 BFT
一个轮次的流程。round周期超时事件
则是监听每个 BFT
轮次的时间,只要超时就会结束本次 BFT
流程,然后开始一个新的 BFT
流程。区块头提交事件
则是只要矿工收到区块头,就结束本次 BFT
,然后开始一个新块的 BFT
。
func (c *core) handleEvents() {
// Clear state
defer func() {
c.current = nil
c.handlerWg.Done()
}()
c.handlerWg.Add(1)
for {
select {
case event, ok := <-c.events.Chan():
if !ok {
return
}
// A real event arrived, process interesting content
// 一个真正人事件到达,处理有趣的内容
switch ev := event.Data.(type) {
case istanbul.RequestEvent: // 普通拜占庭事件
r := &istanbul.Request{ // 区块提案请求事件
Proposal: ev.Proposal,
}
// 处理区块提案请求
err := c.handleRequest(r)
// 如果是未来的请求则保存
if err == errFutureMessage {
c.storeRequestMsg(r)
}
case istanbul.MessageEvent: // 各自消息事件
if err := c.handleMsg(ev.Payload); err == nil {
// 如果没错误,通过gossip协议发送给其它验证器(不包括自己)
c.backend.Gossip(c.valSet, ev.Payload)
}
case backlogEvent: // 积压消息事件
// No need to check signature for internal messages
// 没必要检查内部消息的签名,因为消息事件中已检查过了
if err := c.handleCheckedMsg(ev.msg, ev.src); err == nil {
p, err := ev.msg.Payload()
if err != nil {
c.logger.Warn("Get message payload failed", "err", err)
continue
}
c.backend.Gossip(c.valSet, p)
}
}
case _, ok := <-c.timeoutSub.Chan():// 拜占庭round周期超时事件
if !ok {
return
}
c.handleTimeoutMsg()
case event, ok := <-c.finalCommittedSub.Chan(): //区块头提交事件
if !ok {
return
}
switch event.Data.(type) {
case istanbul.FinalCommittedEvent:
c.handleFinalCommitted()
}
}
}
}
后两个比较简单,这里贴一下代码和注释。
round周期超时事件
// round 超时后处理方式
func (c *core) handleTimeoutMsg() {
// If we're not waiting for round change yet, we can try to catch up
// the max round with F+1 round change message. We only need to catch up
// if the max round is larger than current round.
// 如果不需要等待round值修改
if !c.waitingForRoundChange {
//消息数量大于等于 f+1 的round中, 取round值最大的那个round值,
maxRound := c.roundChangeSet.MaxRound(c.valSet.F() + 1)
if maxRound != nil && maxRound.Cmp(c.current.Round()) > 0 {
//修改 round 轮次
c.sendRoundChange(maxRound)
return
}
}
// 拿到最后一次申请的提案
lastProposal, _ := c.backend.LastProposal()
// 如果提案不是空的,而且提案不小于当前的区块号,则开始生产新块的轮次
if lastProposal != nil && lastProposal.Number().Cmp(c.current.Sequence()) >= 0 {
c.logger.Trace("round change timeout, catch up latest sequence", "number", lastProposal.Number().Uint64())
// 开始一个全新round轮次
c.startNewRound(common.Big0)
} else {
// 否则,进入下一个round轮次,也就是round+1
c.sendNextRoundChange()
}
}
区块头提交事件
func (c *core) handleFinalCommitted() error {
logger := c.logger.New("state", c.state)
logger.Trace("Received a final committed proposal")
// 开始一个新的轮次
c.startNewRound(common.Big0)
return nil
}
然后开始重头戏,BFT的正常流程。
先简单介绍一下BFT的正常流程:
——开始一个新的 round
轮次,round=0
,sequence=当前区块链高度+1
,state状态置为StateAcceptRequest
,启动 round
的计时器。
——区块提案申请人提交提案。
——收到请求后判断,如果是未来消息,则保存该请求到待处理请求的优先队列中。否则发送 preprepare
消息。
——把当前 View(round sequence)
和请求提案(区块)包装成 preprepare
消息,广播出去,广播前会对消息进行签名处理。
——收到preprepare消息后处理消息,如果验证区块提案时发现这是一个未来区块,则把它放到积压消息的优先队列中,到期处理。如果 区块Hash
被锁定且一致,则发送一个 commit消息
。如果是正常流程,则切换 state状态
为 StatePreparepared
并发送 prepare消息
。
——把 View(round sequence)
和 Hash
包装成 prepare消息
,广播出去,广播前会对消息进行签名处理。
——收到 prepare消息
后处理消息,检查 round、sequence
来确保消息的有效性,验证消息签名的正确性,当收到 2f+1
条消息后,锁定 Hash
、切换 state
状态为 StatePrepared
并发送 commit消息
。
——把 View(round sequence)
和 Hash
包装成 commit消息
,广播出去,广播前会对消息进行签名处理。
——收到 commit消息
后处理消息,检查 round、sequence
来确保消息的有效性,验证消息签名的正确性,当收到 2f+1
条消息后,锁定 <