1、管道
原有的数据保密性是通过对交易中的敏感数据进行哈希操作,因此,就需要每个节点维护所有其他节点上的数据来对敏感数据进行安全保障,即使这些数据是我这个节点一点不关心的,特别是有多个联盟加入的时候。所以在fabric中引入了管道概念。管道的设计目的是为了让交易方在各自独立的管道内进行交易,保障交易的秘密性和私有性。一个管道内的交易对其他管道是不可见的,这样所有在交易中的敏感数据都不需要一个单独的数据库来维护。
Fabric管道的原理类似于订阅者发布者的消息队列,一个管道就像一个topic,每个授权通过的节点都可以订阅这个topic,成为这个管道的一个成员,而之后这个管道内的所有交易都会通知到这个节点,但是对于其他没有订阅这个管道的节点来说,这些交易是不可见的。
在之前的文章中,我们已经知道一比交易需要经过背书签名,排序分发以及验证三个过程。Ordering服务提供了基于消息订阅发布模式的消息格式。每个节点通过一个或是多个管道与ordering服务通信,通信的方式类似于客户端的订阅发布通信系统。经过排序后的交易发布到所有订阅了这个管道的节点上,保证每个节点接收到的交易是一样的,顺序也一致。这些消息在管道内是以块的形式传播。每个节点验证交易块并把交易块提交到账本,之后提供一些服务给应用,方便这些应用使用账本。Ordering服务会根据交易的参数,区分出不同管道的消息,并独立处理,现在的消息还不能跨管道通信。
一个网络中的部分节点可以创建一个管道用于交易,这些节点之间就共享所有发生在这个管道内的所有交易信息,也只有这些节点可以接收这个管道内的交易块。这些交易块对于其他管道的节点来说是完全不可见的。一个未授权订阅这个管道节点无法在这个管道内进行交易。
一个管道上可以有任意多的节点,关键是要节点可到达(可以理解为图论里的“顶点可到达”),也就是要求管道内的所有节点都是“在线”状态(“离线”为长时间不响应或是取消了订阅这个管道)。节点在订阅了一个管道,就维护一个交易账本,如果一个节点参与订阅了两个管道,那这个节点将有两个交易记录或是2本账本,如下图显示:
如上图显示,节点1,2,N订阅了红色管道,这几个节点之间维护了一份红色账本,节点1和N订阅了蓝色管道并维护了一份蓝色账本,注意,节点2由于没有订阅蓝色管道,所有没有蓝色账本。同样的,节点2,N订阅了黑色管道,所有维护了一份黑色账本。可以看到节点1,2,N每个节点都维护了不止一份账本。
看到这里也许有部分读者会有困惑,不是说区块链是去中心化,所有节点共享账本的吗?其实区块链是讲去中心化,但是没有说一定是完全去中心化,也没有说我的账本一定是全局共享的。对于公有链来说,它是一个完全去中心化的,并且对于整个公有链来说只有一条主链,所有的节点都维护一本账本。但是fabric属于联盟链,联盟链是部分去中心化,中心化的程度没有公有链这么高,并且数据是可以根据自己的规则来实现部分节点共享。应用根据自己的业务逻辑来决定一个交易是发送到1个或是多个管道,没有固定的限制。
有些区块链网络如果所有的交易都是公开的,就根本不需要多个管道,比如以太坊,这样,所有的交易对所有的节点可见,每个节点有且仅有一份账本。如果区块链网络中有部分数据具有一定的私有性和保密性,只是针对网络中的部分成员可见的,这种就可以创建一个独立的管道来专门处理这些数据。当然管道只是实现数据隔离访问的一种方法,对于只有一份账本来说,想要实现数据访问的隔离性也可以用非对称加密算法。
注意,ordering服务接收整个区块链网络中的所有管道交易,所以数据的保密性只是针对节点,而不是针对ordering服务。这种设计适合于业务要求数据对ordering服务可见这种情况,也就是说ordering服务对于整个区块链网络是可信的。另一方面,如果有数据需要对ordering服务不可见,就需要使用其他技术来隔离敏感数据,例如加密敏感数据或是再搭建一个ordering服务。
2、创建管道
管道可以通过发送一个管道配置交易到ordering 服务,交易内容包括参与的已授权节点列表,这些节点表示MSPs(成员服务提供者 Member Service Providers)。当然还有其他很多配置交易。我们这里只是描述管道配置交易。
应用可以决定为哪些节点创建一个管道。对于节点间的管道的协议,为了避免增加orderer的不必要复杂性,我们不会使用链码类型的交易来完成协议,因为一旦使用了链码交易,就需要使用orderer提供的ordering服务进行排序,封装,分发等操作,我们更多会根据需要在应用程序创建配置交易时完成的。对于其他的一些配置,我们还是需要使用链码来完成相关配置操作,我们称之为:配置系统链码(CSCC)。所谓的配置系统链码就是用来处理所有节点上配置交易。同时CSCC也提供了方法用于查询各种配置数据。
例如,有两个节点,就叫Alice和Bob吧(他俩最熟悉),他俩都有能力为这个区块链网络上的节点和orderer颁发注册证书。并假定在启动过程中配置了Alice和Bob可以创建管道,下面一个典型的过程:
- 应用程序发起一个为Alice和Bob背书的配置交易请求去创建一个名字为“foo” 的管道。之后通过RPC 广播发送把这个配置交易发送到ordering 服务。
- 应用之后在管道 foo 内调用Deliver RPC, 如果管道没有创建成功或是还正在创建,RPC会一直返回错误直到管道foo创建成功。一旦管道创建成功,RPC就返回管道数据流,在这个时候,管道才刚刚创建,所以只有一个包含了相关orderer的创世块和配置交易
-
应用调用Alice和Bob上的CSCC,通过发送管道foo的创世块到这两个节点上来使得这两个节点加入到管道。Alice和Bob检查接收到的创世块,包括检查配置交易中的背书签名。如果通过了管道中的参与者列表将会更新成最新的成员列表,ordering服务也会自动替换订阅者列表,并发送配置交易到新的节点上。之后就同步一个完整账本到本地并开始从管道上接收发过来的交易块
流程图如下:
那么有人可能会有疑惑,上面提到的创世块是怎么来的?我们通过上面的描述知道了管道是由ordering服务创建的。其实一个ordering服务包含一个或是多个orderer,每个orderer根据对应的创世块来配置,创世块是通过启动CLI命令来创建,启动CLI命令提供了一些必要数据,如可信根列表,排序证书列表,IP地址,一组指定的共识算法属性和访问控制策略,其中访问控制策略也可以通过管道来创建。一个orderer将使用创世块配置数据来启动。管道的创世块创建后,包含有一个配置交易信息,管道授权的节点信息都被编码到该配置交易中。在完成这个配置交易后,这些节点也就是自动订阅了这个管道。
如果一个节点想要加入到管道中就需要两种信息:一个可以用于加入到现有区块链网络的证书,可用于在管道外验证消息的可信根列表。我们可以通过CLI或是应用使用SDK API让一个节点订阅一个已有管道。当然不是所有的节点都是可以随意加入管道的,要加入一个管道,必须要求节点的证书必须是这个管道内某个节点签名的。
管道既然可以通过配置交易来创建,那么相应的,也可以通过类似的配置交易来终止一个管道。这个终止配置交易执行后,orderer将会销毁这个管道,那么之后在这个管道的交易都会失败。虽然管道销毁了,节点成了一个孤立节点(假如只有订阅了一个管道),但是节点内原本维持的账本不会自动销毁。
上面提到CSCC提供了管道内交易查找操作,这个查找操作也只仅限于已订阅了该管道的节点,其他未经过证书验证的节点无法查找。查找操作是通过发送一个查询交易到指定链码ID的CSCC中去查找,查找交易的返回内容是一个交易块,包括了成员证书和其他配置数据。
通过上面的整体描述,可以看出管道对于交易的重要性,所以一个交易必须包含一个对应的管道ID,一个管道ID代表了这个交易所在的管道,哪些成员节点可以接收同步这个交易。共识服务会把这个交易排序后发布到对应的管道,使得在这个管道的交易可以独立于其他的管道。最终会在管道内创建一个交易块,然后把这个交易块通过管道发送给订阅了这个管道的所有节点。需要注意的是,由于每个管道的操作都是独立和同步的,所以如果一个节点订阅了多个管道,那么这个节点可能同时接收并处理来自不同管道的交易块。
3、身份认证
3.1节点身份认证
在Gossip网络里每个节点都由MSP 提供一个唯一的身份,也就是节点的PKI-ID。通过gossip发送的每个消息必须包含
- 节点的PKI-ID
- 节点的签名
- 可以被其他节点认证的节点证书
节点间通过点对点方式而不是gossip传播发送的消息可以不签名,原因是在产品环境中,节点的TLS是工作的,能保证数据的安全性。唯一一种不需要节点签名并且不是点对点传播的消息,就是包含了账本块的消息,这类消息由ordering服务签名。
节点的成员关系的建立过程如下:
每个节点定期的gossip 一个称为 AliveMessage的特定消息,AliveMessage包括:
- 节点的PKI-ID,
- 节点的终端信息(主机:端口)
- Metadata, 节点时间(节点的启动时间,每次调用都会单调递增的计数器)
- 对上述消息的签名信息
- 节点证书(可选项, 在节点启动后一段时间内,必须带有)
当一个节点接收到网络中其他任何一个节点发来的消息,会通过节点的证书来验证,节点的证书是在这个节点收到消息前就已经拿到,也就是节点启动后的AliveMessage中获得。这就是为什么要求节点在启动后的一段时间内所发的AliveMessage必须带有节点证书,否则就会出现节点收到消息后无法验证的情况。
那如果因为一些原因导致节点没有拿到这个节点的证书,怎么办呢?对于这种情况,没有收到其他节点证书的节点通过定期传播机制获取到其他节点的证书,该机制负责向缺少这些证书的节点传播证书。
如果一个节点第一次收到另外一个节点的AliveMessage或是 以前收到过但是过了很长一段时间内都没有收到,认为另外一节点已经死亡或是离线的情况下,这两种情况下,再次收到这个节点的AliveMessage,这个节点会根据AliveMessage里的IP和端口号重新连接这个节点。
3.2节点鉴权
在产品环境中,假设各节点都使用了TLS,并且TLS连接都有一个有效的TLS证书。当一个节点和另外一个节点第一次通信时候,会进行一次握手来验证将要进行通信的远程节点有TLS证书的私钥,从而将TLS的会话和Fabric成员关系绑定。这种握手是对称,而且比较简单,过程如下:
本地节点发给远程节点的第一个消息为ConnEstablish消息,该消息包含:
- 对节点的TLS证书哈希值的签名
- 节点的PKI-ID
- Fabric成员服务提供(MSP)的证书
远程节点则进行如下三步操作: - 接收该消息
- 提取远程节点的TLS证书,并进行哈希
- 通过哈希验证ConnEstablish消息的签名,如果验证失败,节点拒绝连接
当Gossip组件初始化后,会同时给定一个管道验证策略来验证远程节点和所属组织的对应关系,用以确定将使用哪个管道来发送这个节点的消息。这个前提是节点有每个节点的PKI-ID 和节点所属组织的根证书。本地账本保存有最新的组织和管道之间的对应关系。
ordering服务发送的消息批次包含该管道的最新配置块序列号。这样,当节点收到另外一个节点的消息块,查看这个消息块的序列号和它自己账本中的最后一个序列号,确定是否需要转发这个消息块给另外的其他节点。如果接收到的这个消息块序列号比节点自己账本里的最后序列号更大,则不会转发这个消息给其他节点,只是延迟并保持在节点内存中,反之,当前账本的最后配置信息就是最新的,是可以安全的把这个消息块转发给其他的节点的。
4、锚节点
锚节点是定义在一个已经加入到管道的组织的节点。该节点主要用于节点的发现。
在一个管道中,锚节点可以被这个管道的其他任何节点发现和通信。因此,每一个加入到管道内的组织都至少有一个锚节点,一个组织的节点可以通过查找锚节点来发现这个管道内的其他组织的所有节点。
和Leader节点的区别
当ordering服务要发送一个区块到管道,这个块就必须先发送到一个组织的leader 节点,之后由leader节点把这个块通过gossip协议分发到这个组织的其他节点上。 也就是说,锚点是组织和组织之间的联通桥梁。而leader节点是peer节点和order服务之间的通信桥梁。更新锚点过程
根据当前的peer身份,获取到当前peer所在的组织
确保所设置的锚点要在当前的channel内
使用锚点信息,构造joinchannel的消息格式
根据配置信息,获取要更改组织下的所有锚点,这些锚点信息都放入joinchannel的消息中,用于gossip发送
对joinchannel消息中每个组织里的锚点:
遍历这个组织下的锚点,首先进行host和port合法性检测。如果当前节点就是锚点,就不做任何操作。之后,如果锚点不在当前组织内,需要判断当前节点是否可以对外连接,也就是说是否设置了外部访问端口
使用发现服务连接这个锚点,在连接的过程中,发送的消息是不需要节点签名的。只需要保证连接的时候,需要知道连接节点的PKIid(PKIid的获取是通过与远程节点进行握手操作获取到远程节点的签名信息和PKIid)
5、leader选举
leader节点是组织和orderer之间的通信桥梁,每个组织同一个时间只有一个leader节点。 leader节点的选举代码主要在election.go中,分为三个部分:对其他节点gossip传播过来的消息进行消息处理,确定当前可见的节点成员,leader选举算法
5.1 处理消息
这是当前节点接收并处理从其他节点发过来的关于leader选举相关消息。
1.1 首先通过adapter.Accept方法把gossip服务中获取到的proto.GossipMessage封装成msgImpl并放入msgChan中
1.2 之后把每一条消息交由handleMessages方法进行处理。在handleMessages方法中,如果不是stopChan的消息,就首先判断消息的发送者是否是一个活的peer,如果不是一个活的peer,就不处理这个消息。否则就对这个消息进行进一步处理。
选举消息有两种类型,一种是 proposal,一种是declaration的。proposal是一种大众推举和选择的选举过程,declaration是指定委派的方式,不需要其他节点的意见。如果是proposal消息,就把proposal的消息先存放在一个队列中,等在选举算法中使用;
如果收到第一条指定委派的方式的消息,会先把 leaderExists的标志位设置为1,同时使用睡眠模式和中断方式,利用锁机制,保证即使同时有多个指定委派方式消息,也只处理一个。
如果当前节点是leader节点,并且发送委派指定消息的PeerID比当前PeerID更小,那么就取消当前节点为leader节点的权限。同时根据当前节点的是否是leader的状态,相应的开启或是关闭该节点的deliver功能。
5.2 确定当前可见的节点
处理消息是开启了一个goroutine来实现的。在处理消息的同时,election.go还做了一件事情,那就是确认当前可见的节点成员。在方法waitForMembershipStabilization 中实现。
首先根据core.yaml里配置的startupGracePeriod 的时间,设置最终等待时间。这个时间表示确认当前可见的节点成员这个过程的最长时间,默认是15s。
获取当前可见的所有成员个数为viewSize。判断过程:
如果选举服务是活着的状态,就每隔1s(这个时间也是在core.yaml里membershipSampleInterval配置)重新获取一次节点个数newSize。
如果viewSize和newSize一致,说明这1秒内没有新增节点,就结束这个过程。因为一般发现一个节点只需要几十毫秒,1秒钟的时间足够发现所有的节点。
如果确认的时间超过了15s,也终止当前过程。如果已经确定出了leader节点,也同样终止过程
如果上述三个条件都不符合,把新获取到的节点个数newSize设置为viewSize,重复上面的判断过程
5.3 Leader选举算法
确认当前可见的节点成员后,启动一个goroutine进行选举算法。算法过程如下:
如果节点服务是活的状态,首先判断是否已经有leader节点。
如果没有leader节点,创建一个 已当前节点为leader节点,并且gossipMessage的Tag为GossipMessage_CHAN_AND_ORG的消息,然后通过gossip网络发送到远程其他节点。
等待其他所有的节点的propose消息。如果已经有leader节点或是该节点放弃称为leader节点,就结束选举流程。
拿到所有的propose消息后,遍历这些消息,如果这些消息中的发送者节点ID比当前的ID小,那么说明有比当前节点更适合做leader节点的节点,所以对于当前节点来说,选举过程结束,它不是一个leader节点。
如果所有的propose消息的节点ID都比当前节点大,那么当前节点为leader节点,记录当前节点为leader节点,同时通过StartDeliverForChannel方法开启当前节点的deliver消息功能。