前言
本文主要目的是用于整理Hyperledger Fabric中关于chaincode 管理和操作的内容,作者以release-1.2为范本进行讲解。
主要参考链接: https://hyperledger-fabric.readthedocs.io/en/release-1.2/chaincode4noah.html
本文总计有两节:
第一节: chaincode Operator 介绍
第二节: 测试和验证
新入门的建议看一看有所了解,已经熟练也可以查缺补漏。
关键词
chaincode: 可以安装部署在fabric网络中的链上可执行程序;
CDS(ChaincodeDeploymentSpec
): 安装部署时向fabric节点提交的数据,其中包含 版本、名称、源码等等
SignedCDS
: 带有签名的CDS,其中比CDS多包含了 ownerlist 和 instantiation policy 数据
Instantiation Policy: 实例化策略,指定实例化chaincode时需要满足的条件
Endorsement Policy:背书策略,指定交易背书结果需要满足的条件。
lscc(Lifecycle system chaincode ):用于管理application chaincode的生命周期的系统级别chaincode
正文
chaincode 的生命周期
一个chaincode从编写到部署需要经历以下几个步骤:
-
package(optional)
install
,instantiate
,upgrade
(optional)
- stop(no implemented)
整个流程和我们安装发布一个web服务很相似,fabric chaincode现在支持 go、java、nodejs多种语言,入门的门槛要比ethereum、eos等公链低很多,而且利用fabirc提供的test库测试起来很方便,作者通常都是用go来开发chaincode,用protobuf作为数据传输协议。
chaincode的组成部分
ChaincodeDeploymentSpec:
包含版本和需要安装部署的chaincode
;
Instantiation Policy(Optional):
和设置背书策略所用的语法是一致
;
Owner Signatures
: 由“拥有”链码的实体签署的一组签名。
owner
Signatures
用于以下目的:
<1>
标明链码的所有权
;
<2>
允许验证包的内容
;
<3>
允许校验包的完整性
;
在一个
channel
中根据
chaincode
的实例化策略来验证
chaincode
实例化交易创建者的身份。光翻译文档太没有意思了啊,不直观,咱们直接上SingedCDS的数据结构看看,一目了然:
<1>最外层 SignedCDS的结构
// SignedChaincodeDeploymentSpec carries the CDS along with endorsements message SignedChaincodeDeploymentSpec { // This is the bytes of the ChaincodeDeploymentSpec bytes chaincode_deployment_spec = 1; // This is the instantiation policy which is identical in structure // to endorsement policy. This policy is checked by the VSCC at commit // time on the instantiation (all peers will get the same policy as it // will be part of the LSCC instantation record and will be part of the // hash as well) bytes instantiation_policy = 2; // The endorsements of the above deployment spec, the owner's signature over // chaincode_deployment_spec and Endorsement.endorser. repeated Endorsement owner_endorsements = 3; }
结构里面的三个成员和上面三个组成部分对应 .
<2> ChaincodeDeploymentSpec
// Specify the deployment of a chaincode. // TODO: Define `codePackage`. message ChaincodeDeploymentSpec { // Prevent removed tag re-use reserved 2; reserved "effective_date"; enum ExecutionEnvironment { DOCKER = 0; SYSTEM = 1; } ChaincodeSpec chaincode_spec = 1; bytes code_package = 3; ExecutionEnvironment exec_env= 4; }
ExecutionEnvironment 里面包含两种执行环境,分别对应了fabric中的两种chaincode类型: system chaincode(直接运行在节点上) 和 application chaincode(运行在docker 容器环境中);
code_package: chaincode 源代码的压缩包
exce_env: 传入给chaincode的环境变量
如果对fabric有过深入了解的读者应该明白,实际上所谓的chaincode部署安装后就是一个执行在docker 容器中的一段程序,这个程序像一个服务一样一直运行并且监听着从peer节点转发过来的外部请求,我们随便用docker inspect 命令去查看以下chaincode容器就能明白:
<3> ChaincodeSpec
// Carries the chaincode specification. This is the actual metadata required for // defining a chaincode. message ChaincodeSpec { enum Type { UNDEFINED = 0; GOLANG = 1; NODE = 2; CAR = 3; JAVA = 4; } Type type = 1; ChaincodeID chaincode_id = 2; ChaincodeInput input = 3; int32 timeout = 4; }
这个结构就包含了一些其他相关信息比如:chaincode的id,它是该chaincode在一个channel中的唯一识别符; Type:使用哪种语言编写的; timeout:执行chaincode请求的超时时间(这是为了避免chaincode内部死锁,导致peer节点无法返回执行结果给client); input ,传入给chaincode的参数,这时invoke时候执行用的.
作者就不在这里多讲整个chaincode的运行机制了,毕竟今天的重点不是这个,有空单独开一个文章给大家说说。
<4>Endorsement
// An endorsement is a signature of an endorser over a proposal response. By // producing an endorsement message, an endorser implicitly "approves" that // proposal response and the actions contained therein. When enough // endorsements have been collected, a transaction can be generated out of a // set of proposal responses. Note that this message only contains an identity // and a signature but no signed payload. This is intentional because // endorsements are supposed to be collected in a transaction, and they are all // expected to endorse a single proposal response/action (many endorsements // over a single proposal response) message Endorsement { // Identity of the endorser (e.g. its certificate) bytes endorser = 1; // Signature of the payload included in ProposalResponse concatenated with // the endorser's certificate; ie, sign(ProposalResponse.payload + endorser) bytes signature = 2; }
上面是一个主要用在proposal_response 标明应答者身份和保证数据完整性的数据结构,endorser实际上就是公钥证书,可以标明身份;signature是签名,这样你可以直接用endorser里的公钥去验证签名是否正确,看上面注释写的很清楚,这个签名是对(data + endorser)一起签的名,不光能校验数据的完整性,还能校验证书的完整性,一举两得。在给chaincodespec签名的时候也是一样,不单单是对cds签名:
// sign the concatenation of cds, instpolicy and the serialized endorser identity with this endorser's key signature, err := owner.Sign(append(cdsbytes, append(instpolicybytes, endorser...)...))
// each owner starts off the endorsements with one element. All such endorsed
// packages will be collected in a final package by CreateSignedCCDepSpecForInstall
// when endorsements will have all the entries
endorsements = make([]*peer.Endorsement, 1)
endorsements[0] = &peer.Endorsement{Signature: signature, Endorser: endorser}
创建一个Chaincode Package
有两种方式去打包一个
chaincode
:
第一种,一个
chaincode
对应多个
owner
第二种,一个
chaincode
只有一个
owner
,更简单的工作流程适用于部署仅具有发出安装事务的节点标识的签名的
SignedCDS
可以用peer
chaincode package
命令进行打包操作,大家可以自己动手体验一下,关于
package
的参数介绍如下:
Package the specified chaincode into a deployment spec. Usage: peer chaincode package [flags] Flags: -s, --cc-package create CC deployment spec for owner endorsements instead of raw CC deployment spec -c, --ctor string Constructor message for the chaincode in JSON format (default "{}") -i, --instantiate-policy string instantiation policy for the chaincode -l, --lang string Language the chaincode is written in (default "golang") -n, --name string Name of the chaincode -p, --path string Path to chaincode -S, --sign if creating CC deployment spec package for owner endorsements, also sign it with local MSP -v, --version string Version of the chaincode specified in install/instantiate/upgrade commands
这时关于package命令的help介绍:
读者们需要主要关注 -s -S -i 三个主要命令:
-s
: 指定了
-s
就会创建一个可以被签名的
sign CDS
来代替
raw CDS
,可以在其中附加实例化策略和
owner signatures
-S :
(
optional
)
如果指定了该参数则会调用本地
localMspId
身份去对
CDS
签名
;
如果不指定则不会签名,同时其他的
owner
也不能对它进行签名。
-i
:指定一个实例化
chaincode
的策略,它会指明那些身份可以实例化
chaincode
,如果没有指定策略,则使用缺省的策略:仅允许
peer
的
admin
身份来实例化
chaincode.
如果我们不使用-s 命令的话,那么-S 和 -i 即便是指定了也无法生效,peer-cli 会生成一个raw CDS ,就是上面那个结构二中的ChaincodeDeploymentSpec,它根本不包含其他两种设置。
示例命令如下:
#这是用 -i 指定了一个实例化策略,创建一个名为 ccpack.out的包
peer chaincode package -n mycc -p github.com/hyperledger/fabric/examples/chaincode/go/example02/cmd -v 0 -s -S -i "AND('OrgA.admin')" ccpack.out
读者们可以执行 `cat ccpack.out` 去看一下,除了上面一片乱码以外,下面还有一个字符串和一个证书以及签名,这说明peer-cli 默默的就帮我们用本地的LocalMSP(需要用户自己指定)进行了签名。然后把-S的符号去掉重复上面的动作你就会发现,签名没有了。
下面贴一段原文:
The optional -i option allows one to specify an instantiation policy for the chaincode. The instantiation policy has the same format as an endorsement policy
and specifies which identities can instantiate the chaincode. In the example above, only the admin of OrgA is allowed to instantiate the chaincode.
If no policy is provided, the default policy is used, which only allows the admin identity of the peer’s MSP to instantiate chaincode.
截止作者发文期间,这个文档的描述还是这样的,之所以单独贴出来是因为这个部分是一个坑,很大很大的坑。它里面所说的默认策略不是指当你不填加`insatiation policy`时,fabric定义的默认权限,而是指peer-cli在你不填加该属性时,自动为你添加的权限!它写的没问题,但是容易让人混淆,至于为什么耐心往下看就明白了。
Package 签名
在创建时签署的链代码包可以移交给其他所有者轮流进行检查和签名。
ChaincodeDeploymentSpec
可以选择由集体所有者签名,以创建
SignedChaincodeDeploymentSpec
(或
SignedCDS
)。
SignedCDS
包含
3
个元素:
CDS
包含源代码,链码的名称和版本
;
链码的实例化策略,语法和背书策略相同
;
通过
Endorserment数组组成
的
chainod
的
owner list;
在有多个owner的情况要轮流顺序签名:
#org1
peer chaincode signpackage ccpack.out signedccpack.out
#org2 peer chaincode signpackage signedccpack.out signedccpack2.out
还是像上面所说的操作一样,用 `cat signedccpack2.out` 命令查看一下,你就会看到底部多出来的签名和证书。
下面贴一段原文:
Note that this endorsement policy is determined out-of-band to provide proper MSP principals when the chaincode is instantiated on some channels.
If the instantiation policy is not specified, the default policy is any MSP administrator of the channel.
这里面也提到了default policy 和上面提到的不一样,作者当时也是经历了很多曲折才发现这个区别,最终为了确定到底default policy是什么,从源码中找出了以下注释:
the default instantiation policy allows any of the channel MSP admins to be able to instantiate
即:这句话很容易让人产生混淆,实际上来讲就是允许任意一个包含在channel内的组织的admin身份去实例化chaincode.
Install chaincode
在
fabric
中
install chaincode
,必须由
peer
对应的
admin Identity
来安装,这个是不需要设置的,也是无法更改的, 而且要向每一个需要安装的peer分别发送指令。
install chaincode
可以指定一个上面的安装包,也可以直接指定路径。不过需要注意的是,直接指定
chaincode
源码路径安装是没有自定义
instantiate policy
的,它将被赋予一个
default policy
(
在
Package
签名说的default policy
)。
接触过fabric环境搭建的读者因该很清楚,Install 命令是可以通过 -p 参数直接指定 chaincode 路径来安装的,也就是说现在有两种安装方式:第一种,直接安装; 第二种,打包安装。
示例命令:
#直接指定源码路径安装 peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/example02/cmd #安装chaicode package peer chaincode install ccpack.out
为什么fabric要搞的这么复杂?install chaincode必须要由peer 的admin 来安装,还要分别安装不支持广播:一方面是出于数据的隐私保护来考虑,这一点在fabirc中sidedb的特性上有体现,即便是同一个channel中的组织也未必所有的服务都是公共通用的;另一方面,即便是同一个组织的peer也不都需要安装 同一个chaincode,太浪费了。
另外还要提一点是 install 这个请求是不需要打包成交易发送给 orderer的,这是作者通过sdk得出的结论,client向peer发送完请求后就结束了,没有打包交易去广播。这又是为什么?很简单,如果打包成交易广播除去,每一个组织都能获取到chaincode了,还有什么隐私可言?
这里作者多说一句,Hyperledger fabric 确实功能丰富,配置灵活,但就是太灵活了,给使用造成不小的障碍。
Instantiation chaincode
在实例化的时候
,会根据所实例化的
chaincode
的
policy
来校验signed
proposal creator
的签名是不是与chaincode
instantiate policy
所吻合。如
install chaincode
中所阐述,如果是一个
default实例化
策略的
chaincode
,部署时任意一个
channel msp Admin.
在实例化的时候会指定该
chaincode
的背书策略,该背书策略指明了交易生效需要的背书条件。
示例命令:
#用 -P 指定了背书策略
peer chaincode instantiate -C mychannel -n mycc -v 1.0 -c '{"Args":["init","a","100","b","200"]}' -P "OR('Org1MSP.peer','Org2MSP.peer')"
这里需要给各位读者一个提醒:在使用 peer-cli去实例化智能合约的时候,如果是要在多个组织和peer上部署,需要在同一个区块时间内提交到orderer当中,否则后提交的会不成功,显示错误: xxx chaincode 已经存在。这个大家可以去试一试,一个channel内最起码要两个peer节点 A 和B, 无需指定实例化策略,直接用install -p 的方式安装,全都安装同一个chaincode,等PeerA 节点实例化交易被打包进区块后,再去提交向PeerB节点发送实例化交易。
为什么会这样?这是因为一个chaincode实例化交易被广播到本地后,及时是那些还没来得及实例化或是不安装的节点也会把已经实例化的结果写入到账本,所以再向PeerB发送实例化请求的时候发现本地的账本中记录该chaincode已经被实例化了。
但是作者上面说了,install是不需要广播的,为什么instantiation 需要广播?这时因为instantiation的时候需要调用chaincode中的init 函数,初始化一些参数(由 -c 传入)。第一,假如instantiation 请求不被打包成交易广播出去写入本地账本,那么初始化的参数是无法生效的(fabric 的两段校验机制);第二,fabric也好bitcoin也好实际上都和状态机有异曲同工之妙,都追求数据的最终一致性,不过bitcoin是异步一致(commit tx 后并不立即生效),而fabric和状态机(raft等)追求的是同步一致(在commit tx的同时,保证执行的tx是生效的),这个时候对不同节点之间的数据状态一致性是有很高要求的,如果没有上面的限制,有可能会导致不同节点之间数据的状态不一致。
peer-cli用上面的命令发送实例化请求,只会发送给实例化请求给一个peer 及 CORE_PEER_ADDRESS所指定的peer,在实际生成中肯定不能这样做。可参考fabric-sdk-go中的示例,由一个client 同时向多个peer节点发送实例化请求,然后在将peer返回的实例化结果打包成tx,广播出去,这样就不会造成冲突。
另外,再说一点,如上文所说chaincode实例化成功后会创建一个chaincode 容器,那么这个容器是什么时候创建的?是接收到实例化请求后,还是在实例化交易写入账本?答案是接收到实例化请求之后,也就是说可能tx提交不成功chaincode容器也可能会被创建,这点需要注意。
Upgrade Chaincode
在升级之前,必须在所需的
endorsor
上安装新版本的链代码。而且它与实例化一样
,只会在指定的
channel
内生效,其他
channel
不影响。
由于链接代码的多个版本可能同时处于活动状态,因此升级过程不会自动删除旧版本,暂时需要用户手动管理。所谓手动管理就是手动清理那些旧版本的容器和缓存在节点本地的chaincode包。
下面贴一段原文:
There’s one subtle difference with the instantiate transaction: the upgrade transaction is checked against the current chaincode instantiation policy, not the new policy (if specified).
This is to ensure that only existing members specified in the current instantiation policy may upgrade the chaincode.
执行更新操作的时候 Upgrade 需要去按照当前的chaincode的实例化策略去检测,而不是新chaincode指定的实例化策略。 这句话是错的,因为根据作者的测试结果显示,并非如此,而当作者看了源码后更加确信这句话是错的:
// executeUpgrade implements the "upgrade" Invoke transaction. [executeUpgrade](https://github.com/hyperledger/fabric/blob/9e9ebe651225104823d228a09e94432592252ca3/core/scc/lscc/lscc.go#L540)(...) {
//cdLedger.InstantiationPolicy 当前chaincode的实例化策略
err = lscc.support.CheckInstantiationPolicy(signedProp, chainName, cdLedger.InstantiationPolicy)
if err != nil {
return nil, err
} .... .... //retain chaincode specific data and fill channel specific ones cdfs.Escc = string(escc) cdfs.Vscc = string(vscc) cdfs.Policy = policy **// retrieve and evaluate new instantiation policy**
//cdfs.InstantiationPolicy 新chaincode 的实例化策略 cdfs.InstantiationPolicy, err = lscc.support.GetInstantiationPolicy(chainName, ccpackfs) if err != nil { return nil, err }
// CheckInstantiationPolicy checks whether the supplied signed proposal
// complies with the supplied instantiation policy
err = lscc.support.CheckInstantiationPolicy(signedProp, chainName, cdfs.InstantiationPolicy)
if err != nil {
return nil, err
}
…… ……
return cdfs, nil
}
这是lscc中管理更新的源码,我们可以看到它清清楚楚的在注释中写着“retrieve and evaluate new instantiation policy”, 也就是说它既要检测已经存在的实例化策略,还要检查新chaincode 指定的策略, 如果signedproposal 有一个策略不符合就不会成功。
示例命令:
#安装新版本的chaincode,安装规则不变
peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/chaincode/go/example02/cmd
#更新智能合约
peer chaincode upgrade -o orderer.example.com:7050 -n mycc -v 1.2 -c '{"Args":["init","a","100","b","200"]}' -P "AND('Org1MSP.peer','Org2MSP.peer')" -C mychannel
和instantiation Tx一样, upgrade Tx也是要广播的。
Stop
和
start
这个它暂时还没有实现,需要自己手动删除
peer
上的
cds
和 在
peer
创建的
docker
容器。
Invoke Transaction
发起一笔交易到
fabric
网络,执行
invoke
操作的时候需要
Endorser
节点的背书,在
Endorser
执行背书请求的时候会检测
client
提交的
signed Proposal
是否符合
channel
的策略(是否符合读写策略)
;
同时在
commit Tx
到账本时候,
committer
也会去检测
Tx
中的背书结果是否符合背书策略,来决定是否修改
state
,而这个背书策略是在实例化和更新的时候指定的。
示例命令
:
peer chaincode invoke -o orderer.example.com:7050 -C mychanel -n mycc -c '{"Args":["invoke","a","b","10"]}'
orderer是不会对Tx执行的结果进行检测的,它只会对提交的Tx是否符合channel 的write policy进行检测,只有peer在commit的时候会对Tx的进行检查(是否符合背书策略,正确与否),来决定Tx的结果是否生效。在fabric中即便你提交的交易中包含的背书不符合策略或者`双花交易`,它仍然会被写入到账本中,而fabric会特意提取出一个只包含有效交易的部分。
实验部分请见下一节。