管理链上代码
最后更新于
链上代码(Chaincode),简称链码,一般指的是用户编写的用来实现智能合约的应用代码。
链码被部署在 Peer 节点上,运行在独立的沙盒(目前为 Docker 容器)中,并通过 gRPC 协议来与相应的 Peer 节点进行交互。链码被调用时,会按照预定逻辑根据当前账本状态来计算对账本的更新(读写集合)。
启动 Fabric 网络后,可以通过命令行或 SDK 进行链码调用。
注:用户链码有别于系统链码(System Chaincode)。系统链码指的是 Fabric Peer 中负责系统配置、查询、背书、验证等平台功能的代码逻辑,运行在 Peer 进程内,将在后续章节予以介绍。
用户可以通过命令行方式来管理链码。自 2.0 开始正式使用新的 peer lifecycle chaincode 命令来维护链码的生命周期,所支持的链码子命令包括 package(打包)、install(安装)、queryinstalled(查询已安装)、approveformyorg(批准提交)、commit(提交)、querycommitted(查询已提交)。原有的 peer chaincode 系列命令仍然支持。
各个命令的功能总结如下表所示:
命令 | 发往组件 | 功能 |
---|---|---|
package | 本地操作 | 打包本地链码为部署 Spec 包 |
install | Peer 节点 | 将链码信息打包并安装到 Peer |
list | Peer 节点 | 列出链码信息,包括某个 Peer 上安装过的链码或通道内实例化过的链码 |
instantiate | Peer 节点和排序服务 | 在通道中实例化链码 |
invoke | Peer 节点和排序服务 | 调用链码 |
upgrade | Peer 节点和排序服务 | 升级链码 |
query | Peer 节点 | 查询链码 |
可以通过 peer chaincode <subcommand> --help
来查看具体的命令使用说明。
这些操作管理了链码的整个生命周期,如下图所示。
首先,用户需要将链码安装到 Peer 节点,之后可以在 Peer 所属的某个通道内实例化链码容器。此时链码处于运行状态,应用可以通过 invoke 或 query 来调用链码。链码在一定时间(环境变量 CRC_MAX_IDLE_TIME 指定)内不被调用会处于空闲状态,自动被停止删除;如果被调用则重新实例化。此外,用户还可以升级链码到新的版本。
后面将以 Fabric 项目中自带的 Go 语言 example02 链码(路径在 examples/chaincode/go/chaincode_example02)为例进行相关命令讲解。
说明:这些命令的实现位于 github.com/hyperledger/fabric/internal/peer/lifecycle
。
链码操作支持如下全局命令选项,对应的功能如下表所示。
全局选项 | 类型 | 含义 |
---|---|---|
--cafile | string | 信任的排序服务的 TLS CA 的证书(PEM 编码格式)路径 |
--certfile | string | 与排序服务进行双向 TLS 认证时使用的本地证书文件路径 |
--clientauth | bool | 与排序服务通信时是否启用双向 TLS 认证 |
--connTimeout | duration | 客户端连接超时,默认为 3 秒 |
--keyfile | string | 与排序服务双向 TLS 认证时使用的本地私钥文件路径 |
-o, --orderer | string | Orderer 服务地址 |
--ordererTLSHostnameOverride | string | 验证 Orderer TLS 时候覆盖所校验的主机名 |
--tls | bool | 连接到 Orderer 服务时是否启用 TLS |
此外,不同子命令还可能支持一些子选项,如下表所示。
子命令选项 | 类型 | 含义 |
---|---|---|
--connectionProfile | string | 指定连接配置(Connection Profile)文件 |
-C, --chainID | string | 所面向的通道,默认为 "testchainid" |
-c, --ctor | string | 指定链码命令的参数信息,Json 格式,默认为 "{}" |
-E, --escc | string | 指定所用背书系统链码的名称,默认为 "escc" |
-l, --lang | string | 链码实现语言,默认为 "golang" |
-n, --name | string | 链码名称 |
--peerAddresses | string list | Peer 节点地址,可以指定多个 |
-o, --orderer | string | 排序服务地址 |
--tlsRootCertFiles | string list | 采用 TLS 时,信任的 Peer 的根证书列表,需要跟 Peer 节点地址列表给出的顺序一致 |
-p, --path | string | 所操作链码的本地路径,如果是 Go 语言为包路径(相对于 $GOPAH/src),如果是其它语言则为绝对路径 |
-P, --policy | string | 链码所关联的背书策略,例如 -P "OR ('Org1MSP.member','Org2MSP.member')" |
-t, --tid | string | ChaincodeInvocationSpec 中的 ID 生成算法和编码,目前支持默认的 sha256+base64 |
-v, --version | string | install/instantiate/upgrade 等命令中指定的版本信息 |
-V, --vscc | string | 指定所使用验证系统链码的名称,默认为 "vscc" |
注意,不同子命令具体支持不同的参数,总结如下表所示。
命令 | -C 通道 | -c cc 参数 | -E escc | -l 语言 | -n 名称 | -o Orderer | -p 路径 | -P policy | -v 版本 | -V vscc |
---|---|---|---|---|---|---|---|---|---|---|
install | 不支持 | 支持 | 不支持 | 支持 | 必需 | 不支持 | 必需 | 不支持 | 必需 | 不支持 |
instantiate | 必需 | 必需 | 支持 | 支持 | 必需 | 支持 | 不支持 | 支持 | 必需 | 支持 |
invoke | 支持 | 必需 | 不支持 | 支持 | 必需 | 支持 | 不支持 | 不支持 | 不支持 | 不支持 |
query | 支持 | 必需 | 不支持 | 支持 | 必需 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
upgrade | 必需 | 必需 | 支持 | 支持 | 必需 | 支持 | 不支持 | 支持 | 必需 | 支持 |
list | 支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
package | 不支持 | 支持 | 不支持 | 支持 | 必需 | 不支持 | 必需 | 不支持 | 必需 | 不支持 |
signpackage | 不支持 | 支持 | 不支持 | 支持 | 必需 | 不支持 | 必需 | 不支持 | 必需 | 不支持 |
其中,必需、支持和不支持三种情况的含义为:
必需:该参数必需被指定,包括通过命令行、环境变量、配置等。
支持:该参数可以被使用。某些时候如果不指定,可能采取默认值或自动获取。
不支持:该参数不应该使用。
通过将链码相关的数据进行封装,可以实现对其进行打包操作,生成 .tar.gz 文件,供安装使用。
打包命令支持三个特定参数:
-p, --path string:要安装的链码包的路径。
-l, --lang string:链码语言类型,默认为 golang。
--label string:链码包的标签。
例如,通过如下命令创建一个本地的链码打包文件 test_cc.tar.gz。
打包后的文件,也可以直接用于 install 操作,如
签名命令则对一个打包文件进行签名操作(添加当前 MSP 签名到签名列表中)。
其中,打包文件结构主要包括三部分的信息:
ChaincodeDeploymentSpec 结构;
实例化策略信息;
拥有者的签名列表。
实现的整体流程如下:
首先会调用 InitCmdFactory(isEndorserRequired, isOrdererRequired bool) (*ChaincodeCmdFactory, error)
方法初始化 Signer 等结构。对于打包命令来说纯属本地操作,不需要 Endorser 和 Orderer 的连接。
调用 getChaincodeSpec() 方法,解析命令行参数,根据所指定的数据生成 ChaincodeSpec 结构。
根据 ChaincodeSpec 结构,结合链码相关数据构造 ChaincodeDeploymentSpec 结构,并传入 getChaincodeInstallPackage 方法。
getChaincodeInstallPackage 方法基于传入的 ChaincodeDeploymentSpec 结构,添加实例化策略和签名信息等,生成一个 SignedChaincodeDeploymentSpec,并进一步作为 Data 生成一个 Envelope 结构,其中 ChannelHeader 指定为 CHAINCODE_PACKAGE。
将 Envelope 结构序列化,写到指定的本地文件。
其中,Envelope 结构如下图所示。
install 命令将链码的源码和环境等内容封装为一个链码安装打包文件(Chaincode Install Package,CIP),并传输到指定的 Peer 节点。
此过程只需要跟 Peer 节点打交道,无需修改账本状态,并不产生交易。只有安装过链码的节点才能进行链码实例化和进行背书处理。默认情况下,执行者需要为该 Peer 节点的管理员角色。
例如,采用如下命令可以部署 test_cc.1.0 的链码打包文件到指定的 Peer 节点。
Peer 节点收到请求,检查和解析后将 CIP 文件保存在 ${CORE_PEER_FILESYSTEMPATH}/chaincodes/
目录下(一般以 cc_name.version 格式命名,如 /var/hyperledger/production/chaincodes/testcc.1.0),供后续操作(如重新创建和启动链码容器)使用。
CIP 文件中主要包括如下数据。
ChaincodeDeploymentSpec
:链码的源码和一些关联环境,如名称和版本。
链码实例化策略,默认是任意通道上的 MSP 管理员身份均可。
拥有这个链码的实体的证书和签名。
安装时,添加本地 MSP 管理员的签名。
其中,ChaincodeDeploymentSpec(CDS)结构最为核心,主要包括 ChaincodeSpec(CS)数据结构(链码部署信息)和 CodePackage(链码源代码相关数据)。CDS 和 CS 结构同时也大量在其他链码命令(如实例化命令和升级命令)中使用。
链码安装的整体实现流程如下图所示。
主要步骤包括:
首先是构造带签名的提案结构(SignedProposal)。
调用 InitCmdFactory(isEndorserRequired, isOrdererRequired bool) (*ChaincodeCmdFactory, error)
方法,初始化 EndoserClient(跟 Peer 通信)、BroadcastClient(跟 Orderer 通信)、Signer(签名操作)等辅助结构体。所有链码子命令都会执行该过程,会根据需求具体初始化不同的结构。
然后根据命令行参数进行解析,判断是根据传入的打包文件来直接读取 ChaincodeDeploymentSpec(CDS)结构,还是根据传入参数从本地链码源代码文件来构造生成。
以本地重新构造情况为例,首先根据命令行中传入的路径、名称等信息,构造生成 ChaincodeSpec(CS)结构。
利用 ChaincodeSpec 结构,结合链码包数据生成一个 ChaincodeDeploymentSpec 结构(chainID 为空),调用本地的 install(msg proto.Message, cf *ChaincodeCmdFactory) error
方法。
install 方法基于传入的 ChaincodeDeploymentSpec 结构,构造一个对生命周期管理系统链码(LSCC)调用的 ChaincodeSpec 结构,其中,Type 为 ChaincodeSpec_GOLANG,ChaincodeId.Name 为“lscc”,Input 为 “install”+ChaincodeDeploymentSpec。进一步地,构造了一个 LSCC 的 ChaincodeInvocationSpec(CIS)结构,对 ChaincodeSpec 结构进行封装。
基于 LSCC 的 ChaincodeInvocationSpec 结构,添加头部结构,生成一个提案(Proposal)结构。其中,通道头部中类型为 ENDORSER_TRANSACTION,TxID 为对随机数+签名实体,进行 Hash。
对 Proposal 进行签名,转化为一个签名后的提案消息结构 SignedProposal。
将带签名的提案结构通过 EndorserClient 经由 gRPC 通道发送给 Peer 的 ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error)
接口。
Peer 模拟运行生命周期链码的调用交易进行处理,检查格式、签名和权限等,通过则保存到本地文件系统。
下图给出了链码安装过程中最为重要的 SignedProposal 数据结构,该结构对于大部分链码操作命令都是类似的,其中最重要的是 ChannelHeader 结构和 ChaincodeSpec 结构中参数的差异。
instantiate 命令通过构造生命周期管理系统链码(Lifecycle System Chaincode,LSCC)的交易,将安装过的链码在指定通道上进行实例化部署调用,在节点上创建容器启动,并执行初始化操作。实例化链码需要同时跟 Peer 和 Orderer 打交道。
执行 instantiate 命令的用户身份必须满足实例化的策略(默认为通道内 MSP 管理员角色),并且在所指定的通道上拥有写(Write)权限。在 instantiate 命令中可以通过 -P
参数指定链码调用的背书策略(Endorsement Policy),在 Commit 阶段会进行策略检查。
例如,如下命令会启动 test_cc.1.0 链码,会将参数 '{"Args":["init","a","100","b","200"]}'
传入链码中的 Init()
方法执行。命令会生成一笔交易,因此需指定排序节点地址。
其中,collection.json 为私密账本(sideDB)特性中使用(Fabric v1.1.0 开始支持),可以实现在同一通道内实现私密数据局部共享。如果不指定该参数则默认不启用该特性,意味着通道内所有成员都可以看到链码调用结果。
collection.json 的一个示例如下所示。
其中定义了 public 和 private 两个集合,分别包括两个组织和单个组织。当在链码逻辑中指定某个键值属于特定集合时,只有集合内成员能看到明文的读写集合,非集合成员即便在同一通道内也无法获取隐私数据。对应 policy 只支持 OR 语法,指定哪些组织可以看到隐私数据集合。
链码实例化实现的整体流程如下图所示。
主要步骤包括:
首先,类似链码安装命令,需要创建一个 SignedProposal 消息。注意 instantiate 和 upgrade 支持 policy、escc、vscc 等参数。LSCC 的 ChaincodeSpec 结构中,Input 中包括类型(“deploy”)、通道 ID、ChaincodeDeploymentSpec 结构、背书策略、escc 和 vscc 等。
调用 EndorserClient,发送 gRPC 消息,将签名后的 Proposal 发给指定的 Peer 节点(Endorser),调用 ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error)
方法,进行背书处理。节点会模拟运行 LSCC 的调用交易,启动链码容器。实例化成功后会返回 ProposalResponse 消息(其中包括背书签名)。
根据 Peer 返回的 ProposalResponse 消息,创建一个 SignedTX(Envelop 结构的交易,带有签名)。
使用 BroadcastClient 将交易消息通过 gRPC 通道发给 Orderer,Orderer 会进行全网排序,并广播给 Peer 进行确认提交。
其中,SignedProposal 结构如下图所示。
交易 Envelope 结构如下图所示。
Peer 返回的 ProposalResponse 消息定义如下。
注:目前命令行下的 instantiate 命令还不支持指定实例化策略,Peer 会采用默认的实例化策略(组织管理员身份)。
通过 invoke 命令可以调用运行中的链码的方法。-c
参数指定的函数名和参数会被传入到链码的 Invoke() 方法进行处理。调用链码操作需要同时跟 Peer 和 Orderer 打交道。
例如,对部署成功的链码执行调用操作,由 a
向 b
转账 10 元。
在 peer0 容器中执行如下操作,注意验证最终结果状态正常 response:<status:200 message:"OK" >
。
这一命令会调用最新版本的 test_cc 链码,将参数 '{"Args":["invoke","a","b","10"]}'
传入链码中的 Invoke()
方法执行。命令会生成一笔交易,需指定排序者地址。
需要注意,invoke 命令不支持指定链码版本,默认调用最新版本的链码。
实现上,基本过程如下图所示。
实现上,基本过程如下:
首先,也是要创建一个 SignedProposal 消息。根据传入的各种参数,生成 ChaincodeSpec 结构(其中,Input 为传入的调用参数)。然后,根据 ChaincodeSpec、chainID、签名实体等,生成 ChaincodeInvocationSpec 结构。进而封装生成 Proposal 结构(通道头部中类型为 ENDORSER_TRANSACTION),并进行签名。
调用 EndorserClient,发送 gRPC 消息,将签名后的 Proposal 发给指定的 Peer 节点(Endorser),调用 ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error)
方法,进行背书处理。节点会模拟运行链码调用交易,成功后会返回 ProposalResponse 消息(带有背书签名)。
根据 Peer 返回的 ProposalResponse 消息,创建一个 SignedTX(Envelop 结构的交易,带有签名)。
调用 BroadcastClient 将交易消息通过 gRPC 通道发给 Orderer 进行全网排序并广播给 Peer 进行确认提交。
在此过程中,发给 Peer 节点的交易提案数据结构如下图所示。
发给排序服务的交易数据结构如下图所示。
注意 invoke 是异步操作,invoke 成功只能保证交易已经进入 Orderer 进行排序,但无法保证最终写到账本中(例如交易未通过 Committer 验证而被拒绝)。需要通过 eventHub 或查询方式来进行确认交易是否最终写入到账本上。
查询链码可以通过 query 子命令。
该子命令实际上是 invoke 操作与 Peer 打交道的部分,即将签名后的 Proposal 发给指定的 Peer 节点的 ProcessProposal() gRPC 接口。最终将 -c
指定的命令参数发送给了链码中的 Invoke()
方法执行。
与 invoke 操作的区别在于,query 操作用来查询 Peer 上账本状态(需要链码支持查询逻辑),不生成交易,也不需要与 Orderer 打交道。
例如,执行如下命令会向 Peer 查询状态 a
的值,并返回查询结果。
在实例化链码容器后,可以在 peer0 容器中执行如下命令,注意输出无错误信息,最后的结果为初始值 Query Result: 100
。
类似的,查询 b
的余额,注意最终返回结果为初始值 Query Result: 200
。
在执行完 a 向 b 转账 10 的交易后,再次查询 a
和 b
的余额,发现发生变化。
a
的新余额为 90。
b
的新余额为 210。
query 的主要实现过程如下所示。
根据传入的各种参数,最终构造签名提案,通过 endorserClient 发送给指定的 Peer。
成功的话,获取到 ProposalResponse,打印出 proposalResp.Response.Payload 内容。
注意 invoke 和 query 的区别,query 子命令会返回从 Peer 的查询结果,但不创建 SignedTx 发送到 Orderer。
当需要修复链码漏洞或进行功能拓展时,可以对链码进行升级,部署新版本的链码。Fabric 支持在保留现有状态的前提下对链码进行升级。
假设某通道上正在运行中的链码为 test_cc,版本为 1.0,可以通过如下步骤进行升级操作。
首先,安装新版本的链码,打包到 Peer 节点。
运行以下 upgrade 命令升级指定通道上的链码,需要指定相同的链码名称 test_cc。
这一命令会在通道 test_cc 上实例化新版本链码 test_cc.1.1 并启动一个新容器。运行在其他通道上的旧版本的链码将不受影响。升级操作跟实例化操作十分类似,唯一区别在于不改变实例化的策略。这就保证了只有拥有实例化权限的用户才能进行升级操作。
升级过程会将给定的参数(如例子中的 '{"Args":["re-init","c","60"]}'
)传入新链码的 Init()
方法中执行。只要 Init()
方法中对应的逻辑不改写状态,则升级前后链码的所有状态值可以保持不变。因此,如果链码将来要考虑在保留状态情况下升级,需要在编写 Init()
方法时妥善处理升级时的逻辑。
链码实例化实现的整体流程如下图所示,十分类似实例化过程。
首先,需要创建一个封装了 LSCC 调用交易的 SignedProposal 消息。注意 instantiate 和 upgrade 支持指定 policy、escc、vscc 等参数。LSCC 的 ChaincodeSpec 结构中,Input 中包括类型(“upgrade”)、通道 ID、ChaincodeDeploymentSpec 结构、背书策略、escc 和 vscc 等。
调用 EndorserClient,发送 gRPC 消息,将签名后的 Proposal 发给指定的 Peer 节点(Endorser),调用 ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error)
方法,进行背书处理。节点会模拟运行 LSCC 的调用交易,启动链码容器。实例化成功后会返回 ProposalResponse 消息(其中包括背书签名)。
根据 Peer 返回的 ProposalResponse 消息,创建一个 SignedTX(Envelop 结构的交易,带有签名)。
使用 BroadcastClient 将交易消息通过 gRPC 通道发给 Orderer,Orderer 会进行全网排序,并广播给 Peer 进行确认提交。
list 子命令支持查询某个节点上已经安装的链码信息(--installed)或某个通道内已经实例化的链码信息(--instantiated)。请求发送到 Peer 节点,调用 LSCC 系统链码提供的接口进行查询。
例如,如下命令查询默认 Peer 上安装的链码情况,返回结果显示已安装链码 exp02 的 v1.0 版本。
例如,如下命令查询通道 ${APP_CHANNEL} 内实例化的链码情况,返回结果显示已实例化链码 exp02 的 v1.0 版本。
链码列出安装和实例化过程的实现流程如下图所示。
首先,根据输入参数创建一个 LSCC 的链码调用规范。LSCC 的 ChaincodeSpec 结构中,Input 参数根据查询类型设置为 getinstalledchaincodes 或 getchaincodes。
根据 LSCC 的链码调用规范封装为一个 Proposal 结构,头部类型为 ENDORSER_TRANSACTION,通道 ID 设置为空(查询安装信息)或给定的通道(查询实例化信息)。
调用 EndorserClient,发送 gRPC 消息,将签名后的 Proposal 发给指定的 Peer 节点(Endorser),调用 ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error)
方法,进行背书处理。节点会运行 LSCC 的相应逻辑,成功后会返回 ProposalResponse 消息。
客户端解析 Peer 返回的 ProposalResponse 消息,如果成功则打印查询结果。