在当前版本中,共内置了三种共识机制:solo、kafka和raft,每一种共识机制都需要实现fabric/orderer/consensus/consensus.go文件中的Consenter接口和Chain接口,其中Consenter接口代表着共识机制,需要实现HandleChain函数,通过调用HandleChain函数可以产生Chain接口,Chain接口代表了在这一共识机制下的区块链,共识算法的实现直接依赖于Chain接口而非Consenter接口,Chain接口的运行又依赖于Consenter接口,Chain接口实现了对交易信息的处理,区块的生成,区块的共识,区块的上链等一系列操作,启动Chain后,会循环运行一个go协程。
在fabric/orderer/consensus/consensus.go文件中还有另外一个接口ConsenterSupport,其实现为结构体ChainSupport,该接口用于帮助开发者实现共识算法,提供了一些用于操作区块链的基本函数,如使用blockcutter产生交易batch,通过交易batch生成新的区块,将区块写入区块链等,这些函数的实现均在结构体ChainSupport中,我们在实现自己的共识算法时,可以直接使用ConsenterSupport接口中的函数。
内容分散,叙述略长,所以最好能对照着源码来看@_@
回忆“Orderer模块启动”博客中,在创建“大杂烩”管理器的时候,会将所有共识算法的Consenter结构体存放在consenters映射中,因此会有如下代码:
consenters["solo"] = solo.New()
因此我们将从solo.New函数切入到solo共识模块来进行梳理。
func New() consensus.Consenter {
return &consenter{}
}
上述代码为solo.New函数,会创建一个本地的consenter结构体返回,consenter结构体如下:
type consenter struct{}
func (solo *consenter) HandleChain(support consensus.ConsenterSupport, metadata *cb.Metadata) (consensus.Chain, error) {
return newChain(support), nil
}
func newChain(support consensus.ConsenterSupport) *chain {
return &chain{
support: support,
sendChan: make(chan *message),
exitChan: make(chan struct{}),
}
}
solo共识中的consenter结构体实现了consensus.Consenter接口,其本身并不包含任何变量。
通过调用consenter结构体中的HandleChain函数可以创建chain结构体,chain结构体结构如下:
type chain struct {
support consensus.ConsenterSupport
sendChan chan *message
exitChan chan struct{}
}
chain结构体包括一个consensus.ConsenterSupport接口和两个通道,实现了consensus.Chain接口。
在solo共识机制中,没有涉及到Metadata,Metadata为存储在区块中的元数据,包含多种类型,在kafka和raft中都有涉及,在这里先不做讨论。
在“Orderer模块启动”博客中,我们了解到在生成共识机制的Consenter之后,会通过Consenter产生相应的ChainSupport结构体,在产生ChainSupport结构体的过程中,会调用Consenter的HandleChain函数产生Chain保存在ChainSupport结构体的Chain变量中,最终会调用ChainSupport结构体的start函数,如下代码:
type ChainSupport struct {
*ledgerResources
msgprocessor.Processor
*BlockWriter
consensus.Chain
cutter blockcutter.Receiver
crypto.LocalSigner
}
func (cs *ChainSupport) start() {
cs.Chain.Start()
}
从上面代码可知,ChainSupport的start函数,实质为Chain接口的Start函数。
因此我们现在将视角转向共识机制中Chain接口的Start函数。
func (ch *chain) Start() { // ChainSupport初始化完成后,会启动共识机制
go ch.main()
}
上述代码为solo共识中chain结构体的Start函数,在调用后,会启动一个go协程运行chain.main函数,如下:
func (ch *chain) main() {
var timer <-chan time.Time
var err error
for {
seq := ch.support.Sequence() // 返回当前的配置序号,嵌套了很多层结构体
err = nil
select {
case msg := <-ch.sendChan:
if msg.configMsg == nil {
// NormalMsg
if msg.configSeq < seq {
_, err = ch.support.ProcessNormalMsg(msg.normalMsg) //基于当前的配置对传入的消息进行有效性验证,返回配置序号以及验证结果
if err != nil {
logger.Warningf("Discarding bad normal message: %s", err)
continue
}
}
batches, pending := ch.support.BlockCutter().Ordered(msg.normalMsg)
for _, batch := range batches {
block := ch.support.CreateNextBlock(batch) // 创建区块
ch.support.WriteBlock(block, nil)
}
switch {
case timer != nil && !pending:
// Timer is already running but there are no messages pending, stop the timer
timer = nil
case timer == nil && pending:
// Timer is not already running and there are messages pending, so start it
timer = time.After(ch.support.SharedConfig().BatchTimeout())
logger.Debugf("Just began %s batch timer", ch.support.SharedConfig().BatchTimeout().String())
default:
// Do nothing when:
// 1. Timer is already running and there are messages pending
// 2. Timer is not set and there are no messages pending
}
} else {
// ConfigMsg
if msg.configSeq < seq { // 消息中的配置序号小于通道最新配置序号,那么该消息就有可能会存在失效的问题
msg.configMsg, _, err = ch.support.ProcessConfigMsg(msg.configMsg)
if err != nil {
logger.Warningf("Discarding bad config message: %s", err)
continue
}
}
batch := ch.support.BlockCutter().Cut()
if batch != nil {
block := ch.support.CreateNextBlock(batch)
ch.support.WriteBlock(block, nil)
}
block := ch.support.CreateNextBlock([]*cb.Envelope{msg.configMsg})
ch.support.WriteConfigBlock(block, nil)
timer = nil
}
case <-timer:
//clear the timer
timer = nil
batch := ch.support.BlockCutter().Cut()
if len(batch) == 0 {
logger.Warningf("Batch timer expired with no pending requests, this might indicate a bug")
continue
}
logger.Debugf("Batch timer expired, creating block")
block := ch.support.CreateNextBlock(batch)
ch.support.WriteBlock(block, nil)
case <-ch.exitChan:
logger.Debugf("Exiting")
return
}
}
}
通过go协程循环运行一个for死循环,来对交易信息进行处理。
在main函数中,首先通过ChainSupport的Sequence函数返回当前的配置区块序号seq,然后是一个select选择语句,共有三个分支:
当没有条件满足时,就一直滞留在这里,直到有一个条件满足。
我们首先看sendChan通道中所传输的数据,是message类型,如下所示:
type message struct {
configSeq uint64
normalMsg *cb.Envelope
configMsg *cb.Envelope
}
通过上面的结构体可以看到,在message结构体中会包含有交易信息cb.Envelope,因此,main函数中的第一个分支即为处理提交到Orderer节点的交易信息。
我们现在假设进入了第一个分支,即启动之后,在sendChan通道中有新的message传输过来了,这时,会进入第一个分支语句,首先会判断message中的configMsg是否为空:
当消息为正常的数据信息时,会首先对该消息的有效性进行检测,当消息中的配置序号msg.configSeq小于当前通道配置序号seq时,那么该消息就有可能会存在失效的问题,这时就需要调用ChainSupport的ProcessNormalMsg函数来对消息进行有效性验证,并返回验证结果,当返回有错误时,就continue,忽视掉这个消息,否则就对该消息进行下一步的处理。
调用ChainSupport的BlockCutter函数,会返回之前在ChainSupport中创建的blockcutter.receiver结构体(该结构体由batchSize控制,用于对交易信息进行区块化处理),然后调用blockcutter.receiver结构体的Ordered函数,根据batchSize对传入的交易消息进行处理,在blockcutter.receiver结构体中会有一个pendingBatch变量,用于临时存储交易消息,Ordered函数会根据pendingBatch变量中保存的交易消息以及传入的交易消息,结合batchSize来返回需要产生区块的交易(并非一个交易信息就产生一个区块,而是由batchSize控制),Ordered函数共有两个返回值messageBatches和pending,这里除去错误情况之外,共分为四种情况,其中messageBatches是一个二维切片,它的长度最多为2,表示需要产生两个区块,pending为布尔值,当其为true时,表示在pendingBatch中还存有交易信息,当其为false时,表明pendingBatch当前为空。
在产生messageBatches之后,会先后调用ChainSupport的CreateNextBlock函数和WriteBlock函数,由交易batch产生区块并加入到区块链中,在这之后需要判断计时器和pending变量的值,当pending为true并且计时器未开启时,需要开启一个由BatchTimeout控制的计时器timer,timer倒计时结束后,会令之前的select语句进入第二个分支,表明到点需要将现有保存在pendingBatch变量中的交易信息切割生成区块了,具体如何切割生块将在下文中详述。
由timer和blockcutter.receiver结构体共同控制生成区块,可以体现出区块生成是由BatchTimeout和BatchSize共同控制的。
此时,Chain区块链对正常交易信息的处理就完成了,共涉及到,“信息有效性的检验”、“使用blockCutter产生区块batches”、“由区块batches生成区块”和“将区块加入到区块链中”这四个步骤。
当消息为配置消息时,同样也是先对消息的有效性进行检测,合格则进一步处理,否则就忽视这一配置消息,与正常消息不同的是,配置消息不能传入blockcutter.receiver结构体的Ordered函数中由BatchSize控制来进行切块,因为配置消息需要独占一个区块,因此直接调用blockcutter.receiver结构体的Cut函数,将pendingBatch变量中的交易信息返回保存在batch变量中,然后由batch变量中的交易信息产生区块,并将区块加入到区块链中,这时,因为配置信息前面不再有其他未上账的交易信息了,所以直接通过该配置信息构造出新的区块,并调用ChainSupport的WriteConfigBlock函数,将配置区块写入区块链,并应用新的配置,因为此时pendingBatch变量中没有保存的交易信息了,所以将timer清空。
至此,select的第一个分支的分析就完成了,该分支即为solo共识对接收到的交易信息的处理过程。
当timer没有被取消,并倒计时结束时,会进入第二个分支,此时由于BatchTimeout的控制,需要将
pendingBatch变量中保存的交易信息进行切块上账,调用blockcutter.receiver结构体的Cut函数,将pendingBatch变量中的交易信息返回,然后调用ChainSupport的CreateNextBlock函数生成交易区块,最后调用WriteBlock函数将区块加入到区块链中。
该分支表明区块的产生不仅仅是受配置文件中的BlockSize所控制,同时也受到BlockTimeout的控制,timer只有在上文中的pending为true时才会开启。
func (ch *chain) Order(env *cb.Envelope, configSeq uint64) error {
select {
case ch.sendChan <- &message{
configSeq: configSeq,
normalMsg: env,
}:
return nil
case <-ch.exitChan:
return fmt.Errorf("Exiting")
}
}
solo共识机制chain结构体的Order函数是在grpcServer接收到来自于peer节点的Broadcast数据流时所调用的函数之一,当Broadcast数据流传输过来的数据为正常的交易信息,最终就会调用Order函数;当传输过来的数据为配置信息,则会调用Configure函数。
在Order函数中,会利用接收到的交易信息cb.Envelope构造message结构体,并将其输入到sendChan通道。
因此,Order函数可以唤醒chain.main函数中的第一个select分支。
同理,当grpcServer接收到配置信息,则会调用Configure函数,构造交易message,同样也可以唤醒第一个select分支。
Configure函数代码如下所示。
func (ch *chain) Configure(config *cb.Envelope, configSeq uint64) error {
select {
case ch.sendChan <- &message{
configSeq: configSeq,
configMsg: config,
}:
return nil
case <-ch.exitChan:
return fmt.Errorf("Exiting")
}
}
solo共识机制只能用于单节点模式,即只能有一个Orderer节点,因此,其共识过程很简单,每接收到一个交易信息,就在BatchSize和BatchTimeout的控制下产生区块并上账,在源代码中的体现如下:
solo共识的实现是最简单的,但“麻雀虽小五脏俱全”,通过分析solo共识的源码实现,我们可以了解到Fabric中的共识模块在实现时的基本接口以及数据的基本流向,这对我们后续研究raft源码以及实现自己的共识机制均有很大的意义。