Hyperledger Fabric 1.0中,区块链网络中的各种节点(Peer节点、排序服务节点等)都提供了gRPC接口,只要有权限,应用程序就可以访问它们提供的功能。背书节点是 Peer节点的一种角色,管理和维护了链上的链码,可以通过背书节点开放的接口,执行智能合约的功能。有两种方式可以访问接口,命令行和各种语言的SDK 都可以。前面的章节已经介绍过命令的方式,第10章会详细介绍SDK的接口。加 入 会 员 微 信 dedao555
9.2.1 链码的生命周期
目前的版本中,链码提供了4个管理链码生命周期的命令,分别是链码的打包(package)、安装(install)、实例化(instantiate)、升级(upgrade)。在以后的版本中,还可能提供链码的停止(stop)和启动(start)命令,在不删除链码的情况下可以停止和启用链码。链码在安装和实例化以后,就处于激活的状态,应用程序或者命令行就可以调用和触发智能合约的功能了。链码安装以后随时都是可以升级的。
1.链码的打包
链码的内容主要包含以下3个部分。
·链码源码,但需通过ChaincodeDeploymentSpec或CDS定义。CDS依据代码及其他一些属性(名称、版本等)来定义链码。
·实例化策略,这是可选的,背书策略前面已经介绍过,可以参考前面相关的内容。
·链码的签名。
链码的签名实现了下面三个目标:
·表明是谁创建的链码;
·允许验证链码包里的内容;
·可以检测链码包是否被篡改。
链码的实例化策略会验证链码所有者的身份,进而验证其提交的链码源码、实例化策略是否有效。
(1)链码的创建
创建链码有两种方式。
·多个所有者:需要多个所有者对链码进行签名,先创建一个链码包SignedChaincode DeploymentSpec,然后发送给多个所有者进行签名。
·单一所有者:只有安装链码的节点对链码签名。
我们先来看复杂的情况。当然,如不需要了解多用户的情况,可以直接跳转到本节后面的“链码的安装”小节。
可通过下面的命令行创建带签名的链码包。
peer chaincode package -n mycc -p github.com/hyperledger/fabric/examples/
chaincode/go/chaincode_example02 -v 0 -s -S -i "AND('OrgA.admin')" ccpack.out
其中,-s选项生成一个有多个所有者签名的链码包,而不是简单地创建一个不带签名的 ChaincodeDeploymentSpec。如果指定了-s选项,当其他所有者要签名时,还需要指定-S选项。否则,创建的链码包 SignedChaincodeDeploymentSpec只会在ChaincodeDeploymentSpec基础上添加实例化策略,不会包含所有 者的签名。
-S选项可以使MSP(core.yaml中localMspid属性值定义的)对程序包进行签名。-S是可选的,如果创建了一个没有签名的包,那么其他的所有者不能通过对其使用signpackage命令签名。
可以通过-i选项为链码指定实例化策略。实例化策略与背书策略类似,它指明哪些身份可以对链码实例化。在上面的例子中,只允许OrgA管理员进行链码实例化。如果没有提供任何策略,系统将会采用默认策略,该策略只允许Peer节点MSP的管理员实例化链码。
(2)链码的签名
链码在创建时签名了才可以由其他的所有者校验签名和继续签名,签名的过程可以是线下操作(out-of-band)的。
链码SignedChaincodeDeploymentSpec是封装了ChaincodeDeploymentSpec的结构,主要是增加了实例化策略和所有者的签名,定义如下。
type SignedChaincodeDeploymentSpec struct {
// ChaincodeDeploymentSpec序列号后的字节数组
ChaincodeDeploymentSpec []byte
// 实例化策略,结构同背书策略
InstantiationPolicy []byte
// 所有者的签名,可以是多个
OwnerEndorsements []*Endorsement
}
从上面的结构可以看出,主要包含如下几个部分。
·ChaincodeDeploymentSpec包含链码的源码、名称和版本。
·实例化策略,在实例化的时候VSCC会验证。
·所有者的签名背书,包含了链码所有者的列表。
实例化策略定义了签名的MSPPrincipal和需要满足的规则,是需要线下确定的,链码实例化的时候会读取这些信息进行验证。如果不指定实例化策略,默认通道的管理员才能对链码实例化。实例化策略的定义如下。
type SignaturePolicyEnvelope struct {
// 策略的版本
Version int32 `json:"version,omitempty"`
// 策略的规则
Rule *SignaturePolicy `json:"rule,omitempty"`
// 策略的主体:可以是基于角色、基于组织和具体的身份
Identities []*common1.MSPPrincipal `json:"identities,omitempty"`
}
链码所有者可以对一个之前创建好的链码包进行签名,具体使用如下命令。
peer chaincode signpackage ccpack.out signedccpack.out
其中,命令中的ccpack.out和signedccpack.out分别指输入与输出包。signedccpack.out包含一个用本地MSP对包进行的附加签名。
2.链码的安装
链码安装的时候会把链码的源码打包到前面已经介绍过的结构Chaincode DeploymentSpec中,安装到需要运行链码的Peer节点上。链码安装是针对节点的,每次安装只对单个节点有效。需要给每个要运行链码的背书节 点都安装一遍,具体需要安装到哪些节点,可以根据背书策略来选择。
从安全角度考虑,链码应该只安装在需要执行的背书节点上,可以保护链码的逻辑被未授权的节点获取到。没有安装链码的节点是不能执行链码的智能合约的,但是可以验证链上的交易并且提交到本地账本中。
安装链码的命令如下。
peer chaincode install -n ChaincodeName -v version -p ChaincodePath
上述的-n选项标识的是链码的名字,-v选项标识的是链码的版本,-p选项标识的是链码源码的路径,其必须在用户设置的环境变量GOPATH目录下(比如$GOPATH/src/cc)。
链码安装的时候是需要管理员权限的,SignedProposal必须是Peer节点本地MSP设置的一个管理员签名的。
3.链码的实例化
链码的实例化会调用链码生命周期系统链码(LSCC)在通道上创建和初始化链码。链码在实例化之前是和通道无关的,实例化的时候才和通道绑定。链码可以和多个通道绑定,在通道上初始化后记录到通道的状态数据库中。同一个链码在不同通道上的数据是隔离的,不同通道之间不会有影响,这在前面的第7章已经介绍过。
链码实例化的时候会检查是否符合链码实例化的策略和通道的写入策略。实例化的策略验证过程是检查实例化交易的签名 是否符合策略的规则,验证通过才会写入到账本和状态数据中。通道的写入策略是在创建通道时指定的,这也是从安全角度考虑的,避免未授权的用户部署链码和调 用链码。前面我们提到过,默认的实例化策略是通道的任何一个管理员,所以链码的实例化交易提交者必须也是通道的管理员。
链码的实例化会指定链码的背书策略,用来确定通道上哪些节点执行的交易才能添加到账本中,这在前面的章节中已经介绍过。
命令行的链码实例化如下。
peer chaincode instantiate -n ChaincodeName -v version -c '{"Args":["john","0"]}'
-P "OR ('Org1.member','Org2.member')"
上面的-P选项指定的就是背书策略,例子中的策略只要是Org1或者Org2的任意一个成员的背书就可以了。
目前命令行的链码实例化只能指定AND和OR的背书策略,内部会转换成NOutOf策略,这在SDK的接口中是可以指定的。
链码实例化以后,就处于ready状态了,可以处理链码的调用和查询交易了,本章后面部分会详细地介绍背书节点侧和链码侧的有限状态机。
4.链码的升级
链码安装以后是随时可以升级的,链码的名称需要保持不变,必须要更新的是链码的版本,其他的部分,比如链码的所有者和实例化策略都是可选的。链码的版本是用字符串表示的,并没有指定比较版本大小的规则,是以更新的先后顺序为准的,具体的操作方法线下自行确定。
同样,升级之前还需要把链码安装到对应的背书节点上。链码的升级和实例化是类似的操作,也是绑定链码到通道上,执 行初始化的操作。链码升级只会影响到指定的通道,没有绑定链码新版本的通道还是继续旧的版本。同一个背书节点上,不同通道绑定的链码可能是不同的版本,所 以升级的时候不会主动删除旧的版本。
链码升级的时候同样会检查实例化策略,采用的是通道上链码最新版本的实例化策略,检查通过以后才会更新为链码新版本指定的实例化策略。
5.链码的停止和启动
链码的停止与启动功能还没实现,只能手动删除链码的容器和镜像,再删除背书节点本地保存的链码。链码容器和镜像的命名规则可以参考本章后面的容器管理部分。链码代码默认是在下面的目录结构中。
/var/hyperledger/production/chaincodes/<ccname>:<ccversion>
其中,/var/hyperledger/production目录是背书节点的环境变量CORE_PEER_FILESYSTEMPATH指定的。
9.2.2 应用程序和链码的交互流程
普通链码用来执行特定的智能合约功能,应用程序和链码的交互包含如下几个步骤。
第1步:应用程序或者命令行通过gRPC请求向背书节点发起链码的调用请求,背书节点再转发给链码执行,应用程序不能直接和链码通信。
第2步:背书节点会检查对应的链码是否启动,检查的方法是查看本地维护的映射表里是否有指定链上的链码名称和版本的记录。如果没有相关的记录,就会通过Docker的API发起创建或者启动容器的命令。
第3步:Docker服务根据API的命令启动容器,并建立和背书节点的gRPC连接。如果背书节点接收到请求以后检查链码容器已经启动,会直接跳过第2步和3步。
第4步:通过链码和背书节点建立好的gRPC连接,转发应用程序调用的请求,链码在执行过程中会和背书节点有多次的数据交互。
第5步:链码执行完以后,调用背书节点的ESCC对模拟执行的结果进行背书。
第6步:ESCC对模拟执行进行签名,返回背书的结果。
第7步:背书节点返回包含背书节点背书的结果给应用程序。
在整个的交易流程中,链码只参与业务逻辑模拟执行的过程,后续的交易排序和验证分别是排序服务和记账节点完成的。背书节点接收和处理请求后就返回应用程序了,不会转发请求给其他背书节点。应用程序可以自由选择背书节点发起请求,只要最终生成的交易能够满足背书策略就可以。
9.2.3 背书节点接收应用程序的请求处理
应用程序通过gRPC的接口发起请求,命令如下。
ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption)
(*ProposalResponse, error)
背书节点接收到请求以后,会做一些必要的检查,比如是否有权限提交交易、是否是重复交易等,真正的执行过程是在链码中完成的,ESCC最后对执行的结果进行签名背书。中间有任何异常都会终止后续的执行,返回结果给应用程序。链码调用时序图如图9-2所示。
图9-2 链码调用时序图
下面再来看几个重要流程的实现方式。
9.2.4 采用上下文实现交易的模拟执行
链码在业务逻辑的处理过程中会读取和操作账本数据,对这些数据的修改并没有直接影响到状态数据库,而是内部实现了一个交易模拟器,把过程数据记录到了模拟器中。交易模拟器的接口如下。
// TxSimulator可以通过最新连续快照来模拟交易
// Set* 方法用于支持KV-based数据模型。ExecuteUpdate方法用于支持富数据模型及相关查询
type TxSimulator interface {
QueryExecutor
// 用于对指定的namespace进行key和value的设定。namespace对应Chaincode的
//chaincodeId
SetState(namespace string, key string, value []byte) error
// 删除指定namespace和key
DeleteState(namespace string, key string) error
// 批量设置多个key的值
SetStateMultipleKeys(namespace string, kvs map[string][]byte) error
// 支持富数据模型 (参见上面QueryExecutor)
ExecuteUpdate(query string) error
// 封装了事务模拟的结果,包含丰富的详细信息:
// - 交易的提交状态的更新将引起状态的更新;
// - 在提交交易时,对交易的执行环境进行有效性验证。
// 对不同的账本以不同的形式展示上面提到的两点,实现对不同的数据模型的支持以及更好地展现相关信息
GetTxSimulationResults() ([]byte, error)
}
基本的原理是每次链码调用的时候生成一个交易模拟器TxSimulator,具体实现的 lockBasedTxSimulator内部封装了一个能够操作真实状态数据库的queryHelper。所有的数据读取操作会通过 queryHelper查询到真实的状态数据,并记录到rwsetBuilder里,数据的写入和删除操作只记录到rwsetBuilder里,并不会直 接提交到状态数据库中。当模拟执行完成以后,从rwsetBuilder中生成读写集,返回给应用程序。
交易模拟器本身是利用Golang的上下文(context)机制,把交易模拟器添加到上下文的txsimulatorkey中。下一节我们来看看如何利用上下文信息实现数据的分发。
9.2.5 链码消息的数据分发
普通链码和背书节点之间建立的是gRPC的长连接,背书节点如何区分同时发起的链码调用请求呢?
实际上,每个链码和背书节点建立的是不同的连接。相同链码的调用复用一个长连接,背书节点接收到链码的请求中有交 易号作为唯一标识,有限状态机根据交易号找到上下文信息,根据txsimulatorkey就能找到交易模拟器,对不同的交易做数据的隔离。
9.2.6 链码运行环境的管理
链码运行环境管理需要实现的接口如下。
type VM interface {
// 部署虚拟机
Deploy(ctxt context.Context, ccid ccintf.CCID, args []string, env []string, reader io.Reader) error
// 启动虚拟机
Start(ctxt context.Context, ccid ccintf.CCID, args []string, env []string, builder BuildSpecFactory, preLaunchFunc PrelaunchFunc) error
// 停止虚拟机
Stop(ctxt context.Context, ccid ccintf.CCID, timeout uint, dontkill bool,
dontremove bool) error
// 销毁虚拟机
Destroy(ctxt context.Context, ccid ccintf.CCID, force bool, noprune bool)
error
// 获取虚拟机名称
GetVMName(ccID ccintf.CCID) (string, error)
}
接口名称定义的是虚拟机,目前只支持Docker和系统进程空间的方式,未来可能支持更多的方式。
1.链码容器的管理
普通链码是运行在容器中的,线上环境中背书节点一般也是以容器的方式运行,背书节点启动的链码容器是和背书节点在 同一个宿主机上的。在容器中的程序如何来管理宿主机上的容器呢?实际上,背书节点是通过Docker守护进程提供的Docker Engine API创建和启动容器的,有两种方式可以访问Docker Engine API:
·Unix Socket文件,比如unix:///var/run/docker.sock;
·HTTP(S)连接,比如http://localhost:2375。
下面是背书节点配置文件core.yaml中的片段,在用docker-compose启动的时候需要确认是否设置了CORE_VM_ENDPOINT这个环境变量。
vm:
# Docker管理方式可以设置为如下几种:
# unix:///var/run/docker.sock
# http://localhost:2375
# https://localhost:2376
endpoint: unix:///var/run/docker.sock
Docker Engine API本身是RESTful API,可以用支持HTTP协议的客户端访问,如wget、curl、postman等,应用程序可以采用支持HTTP协议的库或者SDK。链码采用的是第三方的库:https://github.com/fsouza/go-dockerclient,调用的API如表9-1所示。
表9-1 调用Docker的API列表
特别说明一下,构建链码镜像的时候会写入背书节点TLS的根证书,链码节点根据这个根证书验证背书节点的gRPC连接 是否是安全的。在镜像构建的时候如果已经存在同名的镜像,就不会重复构建。只要组织和链码名称、版本都相同,则这种情况就很容易出现。尤其是本地搭建环境 的时候,如果重新生成了MSP的证书,没有删除已生成的镜像就部署链码,就可能会导致链码容器里的TLS根证书和背书节点的TLS根证书不匹配,出现连接 错误。链码在启动的时候会检查账本的链码信息是否和本地文件保存的链码文件信息一致,但并不检查链码容器镜像是否和账本的链码信息一致,其实是有隐藏的问 题的。
构建链码镜像的过程是跟链码语言相关的,主要的功能是在源码的基础上添加连接背书节点的SDK和其他的一些依赖, 再编译成可执行的二进制文件,编译环境是启动一个语言相关的临时容器,编译完成后拷贝生成的二进制文件到镜像文件里面。这个部分跟链码语言相关,比如 Golang会增加环境变量GOPATH和编译参数等,细节的内容就不展开了,感兴趣的读者可以查看源码的core/chaincode /platform目录。
构建完的链码镜像会写入一些构建时的标签信息,使用docker inspect可查看到如下标签,如表9-2所示。
链码容器在编译和启动过程中会继承背书节点的一些环境变量,如表9-3所示。
表9-2 获取Docker的标签信息列表
表9-3 链码容器继承背书节点的环境变量列表
启动链码的命令行参数--peer.address也是从背书节点的环境变量中获取的,这个命令行参数是链码和背书节点建立gRPC连接的服务端地址。有几个相关的环境变量:
·CORE_PEER_CHAINCODELISTENADDRESS;
·CORE_PEER_ADDRESS;
·CORE_CHAINCODE_PEERADDRESS。
这个几个环境变量的使用是有优先级的,如果设置了环境变量 CORE_PEER_CHAINCODELISTENADDRESS,则会以这个环境变量为准,否则以环境变量CORE_PEER_ADDRESS为准。 最后一个环境变量CORE_CHAINCODE_PEERADDRESS是备用的,只有前面两个设置有异常的情况下才会启用。如果都没有设置或者设置有问 题,就是默认值0.0.0.0:7051了。
2.系统链码的管理
系统链码运行在Peer节点的进程空间中,实现方式比容器的方式简单许多。比如部署的过程实际就是在内存中注册名 称和入口函数,标识系统链码在运行就可以了。启动的过程主要是建立好和背书节点的Golang通道,启动有限状态机对交互消息进行处理,后面的部分和容器 的过程是一样的。系统链码的调用过程如图9-6所示。
系统链码的调用过程跟普通链码不同的地方在于不需要启动容器,只需要检查是否启动,根据链码运行时环境找到对应的 Golang通道,通过通道发送调用链码的请求即可。系统链码调用的入口有两个,可以是节点进程内部发起调用,也可以是通过网络的方式调用。两种方式调用 的流程并不一样,进程内部发起的调用执行完成以后不会调用ESCC进行背书签名。只有部分系统链码可以外部调用,更详细的内容请看下面一个章节。
图9-6 系统链码运行示意图
来源:我是码农,转载请保留出处和链接!
本文链接:http://www.54manong.com/?id=1057