Hyperledger Fabric v1.4.3 Orderer模块部分源码分析(2)—solo共识机制

写在前面

  • 本系列博客主要用于记录总结自己在做一个区块链相关项目中所涉及到的源码知识,一方面加深自己的记忆,另一方面也可以锻炼自己的知识叙述能力,博客内容仅供参考,真诚的欢迎对Fabric源码感兴趣的dalao来与我讨论问题。
  • 因自己所做项目的原因,该系列博客侧重点在于Orderer模块中共识机制的分析以及修改,因此源码中的其他部分可能不会做特别深入的研究,使用的平台为Ubuntu16.04+Windows10双系统,源代码为Hyperledger Fabric项目的1.4.3版本。点击跳转源码。
  • 码字不易,转载请说明出处:)

Orderer模块中的共识机制

在当前版本中,共内置了三种共识机制: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接口中的函数。

solo共识机制

内容分散,叙述略长,所以最好能对照着源码来看@_@

回忆“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结构体的过程中,会调用ConsenterHandleChain函数产生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()
}

从上面代码可知,ChainSupportstart函数,实质为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函数中,首先通过ChainSupportSequence函数返回当前的配置区块序号seq,然后是一个select选择语句,共有三个分支:

  • msg := <-ch.sendChan,即chain结构体的sendChan通道中有数据传输过来
  • <-timer,计时器时间到
  • <-ch.exitChan,chain结构体的exitChan通道中有数据传输过来

当没有条件满足时,就一直滞留在这里,直到有一个条件满足。

我们首先看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时,那么该消息就有可能会存在失效的问题,这时就需要调用ChainSupportProcessNormalMsg函数来对消息进行有效性验证,并返回验证结果,当返回有错误时,就continue,忽视掉这个消息,否则就对该消息进行下一步的处理。
调用ChainSupportBlockCutter函数,会返回之前在ChainSupport中创建的blockcutter.receiver结构体(该结构体由batchSize控制,用于对交易信息进行区块化处理),然后调用blockcutter.receiver结构体的Ordered函数,根据batchSize对传入的交易消息进行处理,在blockcutter.receiver结构体中会有一个pendingBatch变量,用于临时存储交易消息,Ordered函数会根据pendingBatch变量中保存的交易消息以及传入的交易消息,结合batchSize来返回需要产生区块的交易(并非一个交易信息就产生一个区块,而是由batchSize控制),Ordered函数共有两个返回值messageBatchespending,这里除去错误情况之外,共分为四种情况,其中messageBatches是一个二维切片,它的长度最多为2,表示需要产生两个区块,pending为布尔值,当其为true时,表示在pendingBatch中还存有交易信息,当其为false时,表明pendingBatch当前为空。
在产生messageBatches之后,会先后调用ChainSupportCreateNextBlock函数和WriteBlock函数,由交易batch产生区块并加入到区块链中,在这之后需要判断计时器和pending变量的值,当pending为true并且计时器未开启时,需要开启一个由BatchTimeout控制的计时器timertimer倒计时结束后,会令之前的select语句进入第二个分支,表明到点需要将现有保存在pendingBatch变量中的交易信息切割生成区块了,具体如何切割生块将在下文中详述。

timerblockcutter.receiver结构体共同控制生成区块,可以体现出区块生成是由BatchTimeout和BatchSize共同控制的。

此时,Chain区块链对正常交易信息的处理就完成了,共涉及到,“信息有效性的检验”、“使用blockCutter产生区块batches”、“由区块batches生成区块”和“将区块加入到区块链中”这四个步骤。
当消息为配置消息时,同样也是先对消息的有效性进行检测,合格则进一步处理,否则就忽视这一配置消息,与正常消息不同的是,配置消息不能传入blockcutter.receiver结构体的Ordered函数中由BatchSize控制来进行切块,因为配置消息需要独占一个区块,因此直接调用blockcutter.receiver结构体的Cut函数,将pendingBatch变量中的交易信息返回保存在batch变量中,然后由batch变量中的交易信息产生区块,并将区块加入到区块链中,这时,因为配置信息前面不再有其他未上账的交易信息了,所以直接通过该配置信息构造出新的区块,并调用ChainSupportWriteConfigBlock函数,将配置区块写入区块链,并应用新的配置,因为此时pendingBatch变量中没有保存的交易信息了,所以将timer清空。
至此,select的第一个分支的分析就完成了,该分支即为solo共识对接收到的交易信息的处理过程。
timer没有被取消,并倒计时结束时,会进入第二个分支,此时由于BatchTimeout的控制,需要将
pendingBatch变量中保存的交易信息进行切块上账,调用blockcutter.receiver结构体的Cut函数,将pendingBatch变量中的交易信息返回,然后调用ChainSupportCreateNextBlock函数生成交易区块,最后调用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的控制下产生区块并上账,在源代码中的体现如下:

  1. grpcServer接收到来自peer节点的交易信息
  2. 最终调用chain.Order函数或chain.Configure函数进行响应
  3. 唤醒chain.main函数中的第一个select分支进行共识处理,共包含四个主要步骤
  4. 可能会唤醒chain.main函数的第二个select分支,在BatchTimeout的控制下进行区块的生成

solo共识的实现是最简单的,但“麻雀虽小五脏俱全”,通过分析solo共识的源码实现,我们可以了解到Fabric中的共识模块在实现时的基本接口以及数据的基本流向,这对我们后续研究raft源码以及实现自己的共识机制均有很大的意义。

你可能感兴趣的:(共识机制)