peer节点启动的时候根据配置文件core.yaml文件配置项peer.validator.consensus.plugin选择采用哪种共识算法。目前Fabric实现了两种共识算法NOOPS和PBFT,默认是NOOPS:
// ExecutionConsumer allows callbacks from asycnhronous execution and statetransfer
type ExecutionConsumer interface {
Executed(tag interface{}) // Called whenever Execute completes
Committed(tag interface{}, target *pb.BlockchainInfo) // Called whenever Commit completes
RolledBack(tag interface{}) // Called whenever a Rollback completes
StateUpdated(tag interface{}, target *pb.BlockchainInfo) // Called when state transfer completes, if target is nil, this indicates a failure and a new target should be supplied
}
// Consenter is used to receive messages from the network
// Every consensus plugin needs to implement this interface
type Consenter interface {
RecvMsg(msg *pb.Message, senderHandle *pb.PeerID) error // Called serially with incoming messages from gRPC
ExecutionConsumer
}
每个共识插件都需要实现Consenter接口,包括RecvMsg函数和ExecutionConsumer接口里的函数(可以直接返回)。
Consenter是EngineImpl的一个成员,EngineImpl是接口Engine的一个实例,是在peer启动的时候创建的,连同Impl的其他成员一起注册到gRPC服务中。当通过gRPC收到ProcessTransaction消息时,最终会调用Consenter的RecvMsg处理交易信息。
ExecutionConsumer接口是专门处理事件消息的,它是Stack的成员Executor的一个接口。coordinatorImpl是Executor的一个实例,在实例化coordinatorImpl的时候同时设置自身为成员变量Manager的事件消息接收者,然后启动一个协程循环处理接收到的事件,根据不同的事件类型,调用ExecutionConsumer的不同函数。特别说明一下,事件在内部是channel实现的生产者/消费者模型,只有一个缓冲区,如果处理不及时会出现消息等待的情况,在实际产品化过程中需要进行优化。
// Stack is the set of stack-facing methods available to the consensus plugin
type Stack interface {
NetworkStack // 网络消息发送和接收接口
SecurityUtils // Sign和Verify接口
Executor // 事件消息处理接口
LegacyExecutor // 交易处理接口
LedgerManager // 控制ledger的状态
ReadOnlyLedger // 操作blockchain
StatePersistor // 操作共识状态
}
这个接口的实现都是在helper中实现的,这里只是统一的抽象出来便于实现共识算法插件的时候调用。
// timerStart is used to deliver the start request to the eventTimer thread
type timerStart struct {
hard bool // Whether to reset the timer if it is running
event Event // What event to push onto the event queue
duration time.Duration // How long to wait before sending the event
}
// timerImpl is an implementation of Timer
type timerImpl struct {
threaded // Gives us the exit chan
timerChan <-chan time.Time // When non-nil, counts down to preparing to do the event
startChan chan *timerStart // Channel to deliver the timer start events to the service go routine
stopChan chan struct{} // Channel to deliver the timer stop events to the service go routine
manager Manager // The event manager to deliver the event to after timer expiration
}
timerStart指定了几个参数:timer还未到时间前是否可以重置、消息队列、超时时间。timerImpl在初始化的时候,启动一个协程,循环检测startChan、stopChan、timerChan、event、exit等是否消息,对timer进行操作,比如停止、重启等。
// Message encapsulates an OpenchainMessage with sender information
type Message struct {
Msg *pb.Message
Sender *pb.PeerID
}
// MessageFan contains the reference to the peer's MessageHandlerCoordinator
type MessageFan struct {
ins map[*pb.PeerID]<-chan *Message
out chan *Message
lock sync.Mutex
}
MessageFan类似风扇,把不同PeerID的消息汇聚到一个通道统一输出。
NOOPS是为了演示共识算法的,要实现一个共识算法的插件,可以看看它都实现了哪些功能:
GetNoops返回一个插件对象 NOOPS和PBFT都是单例模式,输入参数是Stack接口:// GetNoops returns a singleton of NOOPS func GetNoops(c consensus.Stack) consensus.Consenter { if iNoops != nil { iNoops = newNoops(c) } return iNoops }
实现Consenter接口 包括RecvMsg、Executed、Committed、RolledBack、StateUpdated等。
NOOPS只实现了RecvMsg接口,处理了Message_CHAIN_TRANSACTION和Message_CONSENSUS消息。对Message_CHAIN_TRANSACTION消息的处理就是把消息类型修改成Message_CONSENSUS再广播出去,对Message_CONSENSUS消息的处理就是保存下来。
#### 前提假设 #### * 分布式节点通过网络是连接在一起的 * 网络节点发送的消息可能会丢,可能会延迟到达,也可能会重复,到达顺序也可能是乱的
从primary收到消息开始,每个消息都会有view的编号,每个节点都会检查是否和自己的view是相同的,代表是哪个节点发送出来的消息,源头在哪里,client收到消息也会检查该请求返回的所有消息是否是相同的view。如果过程中发现view不相同,消息就不会被处理。除了检查view之外,每个节点收到消息的时候都会检查对应的序列号n是否匹配,还会检查相同view和n的PRE-PREPARE、PREPARE消息是否匹配,从协议的连续性上提供了一定程度的安全。
每个节点收到其他节点发送的消息,能够验证其签名确认发送来源,但并不能确认发送节点是否伪造了消息,PBFT采用的办法就是数数,看有多少节点发送了相同的消息,在有问题的节点数有限的情况下,就能判断哪些节点发送的消息是真实的。REQUEST和PRE-PREPARE阶段还不涉及到消息的真实性,只是独立的生成或者确认view和序列号n,所以收到消息判断来源后就广播出去了。PREPARE阶段开始会汇总消息,通过数数判断消息的真实性。PREPARE消息是收到PRE-PREPARE消息的节点发送出来的,primary收到REQUEST消息后不会给自己发送PRE-PREPARE消息,也不会发送PRE-PREPARE消息,所以一个节点收到的消息数满足2f+1-1=2f个就能满足没问题的节点数比有问题节点多了(包括自身节点)。COMMIT阶段primary节点也会在收到PREPARE消息后发送COMMIT消息,所以收到的消息数满足2f+1个就能满足没问题的节点数比有问题节点多了(包括自身节点)。
PRE-PREPARE和PREPARE阶段保证了所有正常的节点对请求的处理顺序达成一致,它能够保证如果PREPARE(m, v, n, i) 是真的话,PREPARE(m’, v, n, j) 就一定是假的,其中j是任意一个正常节点的编号,只要D(m) != D(m’)。因为如果有3f+1个节点,至少有f+1个正常的节点发送了PRE-PREPARE和PREPARE消息,所以如果PREPARE(m’, v, n, j) 是真的话,这些节点中就至少有一个节点发了不同的PRE-PREPARE或者PREPARE消息,这和它是正常的节点不一致。当然,还有一个假设是安全强度是足够的,能够保证m != m’时,D(m) != D(m’),D(m) 是消息m的摘要。
确定好了每个请求的处理顺序,怎么能保证按照顺序执行呢?网络消息都是无序到达的,每个节点达成一致的顺序也是不一样的,有可能在某个节点上n比n-1先达成一致。其实每个节点都会把PRE-PREPARE、PREPARE和COMMIT消息缓存起来,它们都会有一个状态来标识现在处理的情况,然后再按顺序处理。而且序列号n在不同view中也是连续的,所以n-1处理完了,处理n就好了。
上图是发生VIEW-CHANGE的一种情况,就是节点正常收到PRE-PREPARE消息以后都会启动一个定时器,如果在设置的时间内都没有收到回复,就会触发VIEW-CHANGE,该节点就不会再接收除CHECKPOINT 、VIEW-CHANGE和NEW-VIEW等消息外的其他消息了。NEW-VIEW是由新一轮的primary节点发送的,O是不包含捎带的REQUEST的PRE-PREPARE消息集合,计算方法如下: * primary节点确定V中最新的稳定检查点序列号min-s和PRE-PREPARE消息中最大的序列号max-s * 对min-s和max-s之间每个序列号n都生成一个PRE-PREPARE消息。这可能有两种情况: - P的VIEW-CHANGE消息中至少存在一个集合,序列号是n - 不存在上面的集合
第一种情况,会生成新的PRE-PREPARE消息