推荐参考两个博客:
- fabric源码解析——序 : 国内同道的源码分析系列,模块化
- Hyperledger Fabric源码深度解析 : 对MSP,BCCSP的介绍到位
- To know the internals of certain permissioned blockchain platform : IBM员工,参与HF项目的概念总结文章系列
选择peer chaincode instantiate作为开篇,是因为笔者学习官网过程中没有完全理解install与instantiate的区别,所以选择深入源码学习来加深理解
官网Building Your First Network例子中,命令为
1 | peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc -v 1.0 -c '{"Args":["init","a", "100", "b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')" |
此前,需要先设置环境变量
1 | CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp |
客户端调用peer chaincode instatiate
命令部署chaincode,代码在‘peer/chaincode/instantiate.go’这个文件下,命令关联的方法是chaincodeDeploy
(使用了流行的第三方命令行cobra库,具体可自行学习)。
1 | func chaincodeDeploy(cmd *cobra.Command, args []string, cf *ChaincodeCmdFactory) error { |
这个方法主要分为3个步骤(源码只列出关键步骤,省略判断逻辑及错误处理等,下同)。
- 入参
cf *ChaincodeCmdFactory
为nil,未初始化。(可回溯到peer启动peer/main.go#main
时看到mainCmd.AddCommand(chaincode.Cmd(nil))
)。初始化chaincode命令工厂,包含客户端及辅助类。这个方法在所有的chaincode命令前都先执行,入参有所不同。下面关注下初始化的过程1
2
3
4
5
6
7
8// ChaincodeCmdFactory holds the clients used by ChaincodeCmd
type ChaincodeCmdFactory struct {
EndorserClients []pb.EndorserClient // 与endorser交互的客户端
DeliverClients []api.PeerDeliverClient //
Certificate tls.Certificate //
Signer msp.SigningIdentity
BroadcastClient common.BroadcastClient // 与orderer交互的客户端
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// peer/chaincode/common.go
func InitCmdFactory(cmdName string, isEndorserRequired, isOrdererRequired bool) (*ChaincodeCmdFactory, error) {
if isEndorserRequired {
if err = validatePeerConnectionParameters(cmdName); err != nil {}
for i, address := range peerAddresses {
endorserClient, err := common.GetEndorserClientFnc(address, tlsRootCertFile)
endorserClients = append(endorserClients, endorserClient)
deliverClient, err := common.GetPeerDeliverClientFnc(address, tlsRootCertFile)
deliverClients = append(deliverClients, deliverClient)
}
}
certificate, err := common.GetCertificateFnc()
signer, err := common.GetDefaultSignerFnc()
var broadcastClient common.BroadcastClient
if isOrdererRequired {
endorserClient := endorserClients[0]
orderingEndpoints, err := common.GetOrdererEndpointOfChainFnc(channelID, signer, endorserClient)
// override viper env
viper.Set("orderer.address", orderingEndpoints[0])
broadcastClient, err = common.GetBroadcastClientFnc()
}
return &ChaincodeCmdFactory{
EndorserClients: endorserClients,
DeliverClients: deliverClients,
Signer: signer,
BroadcastClient: broadcastClient,
Certificate: certificate,
}, nil
} - 入参为
cmdName(命令名) string(命令名), isEndorserRequired(是否需要初始化endorser client), isOrdererRequired(是否需要初始化orderer broadcast client) bool
。这个是根据命令传参,例如instantiate部署chaincode既需要endorser背书,也需要orderer排序打包并且扩散全网,而install安装则只需要背书。 - instantiate需要背书,
isEndorserRequired=true
。validatePeerConnectionParameters
校验命令参数。例如,校验peerAddresses个数及相应的tlsRootCertFiles匹配。注意,这里有// currently only support multiple peer addresses for invoke
注释,对非invoke方法只能传入一个peerAddress,如install,instantiate。这也说明,这些操作只能作用于单个节点,而且下面也可以看到只需要单个节点的背书。 GetEndorserClientFnc
和GetPeerDeliverClientFnc
方法非常类似。都是构造以下这个client(主要是grpc client, hyperledger fabric使用grpc作为rpc方案),数据结构完全一致,只是后续的用途有所不同。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// GetEndorserClient returns a new endorser client. If the both the address and
// tlsRootCertFile are not provided, the target values for the client are taken
// from the configuration settings for "peer.address" and
// "peer.tls.rootcert.file"
func GetEndorserClient(address, tlsRootCertFile string) (pb.EndorserClient, error) {
if address != "" {
// 从命令行传入peer address, --peerAddresses, --tlsRootCertFiles
peerClient, err = NewPeerClientForAddress(address, tlsRootCertFile)
} else {
// 从环境变量读取peer address
peerClient, err = NewPeerClientFromEnv()
}
return peerClient.Endorser()
}
&PeerClient{
commonClient: commonClient{
GRPCClient: gClient, // grpc client. 工程使用grpc作为节点间的远程调用方案
address: address, // “peer.address”
sn: override // "peer.tls.serverhostoverride"在上面的例子中,命令里并没有提供
peerAddress
和tlsRootCertFile
这两个参数,因此从环境变量里读取。在源码中,使用了流行的第三方库viper来进行配置加载和管理。也就是我们要先设置的变量CORE_PEER_ADDRESS
和CORE_PEER_TLS_ROOTCERT_FILE
。生成客户端X509证书
GetCertificateFnc
。客户端通信时是否需要校验X509证书是由环境变量peer.tls.enabled
和peer.tls.clientAuthRequired
这两个参数决定的。在该tutorial例子中并没有设置。如果需要校验,则另外需要提供两个参数peer.tls.clientKey.file
和peer.tls.clientCert.file
,分别对应节点的public/private key文件。需要说明的是,在前面构造的endorseClient和peerDeliverClient过程中,也已经读取这两个参数和文件,生成X509 key pair,可以进行tls通信。这里的方法也是构建peerClient,获取该key pair。(TODO:以后专题分析加密)GetDefaultSignerFnc
调用本地MSPmspmgmt.GetLocalMSP().GetDefaultSigningIdentity()
获取signer,后续用于发送的proposal进行签名,构造signedProposal。(TODO:以后专题分析msp成员管理服务)isOrdererRequired
这个参数初始化时候传入,根据后续操作是否需要与orderer交互乃至全网同步而是否选择初始化。chaincode instantiate需要全网同步,因此需要初始化。命令行-o
参数指定orderer地址,如果没有指定,则通过GetOrdererEndpointOfChainFnc
获取orderer地址。这个是通过调用cscc(configuration system chaincode,配置系统链码)的GetConfigBlock方法(参数为channelId)实现的。流程与一般的chaincode调用相似。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22func GetOrdererEndpointOfChain(chainID string, signer msp.SigningIdentity, endorserClient pb.EndorserClient) ([]string, error) {
// query cscc for chain config block
invocation := &pb.ChaincodeInvocationSpec{
ChaincodeSpec: &pb.ChaincodeSpec{
Type: pb.ChaincodeSpec_Type(pb.ChaincodeSpec_Type_value["GOLANG"]),
ChaincodeId: &pb.ChaincodeID{Name: "cscc"},
Input: &pb.ChaincodeInput{Args: [][]byte{[]byte(cscc.GetConfigBlock), []byte(chainID)}},
},
}
creator, err := signer.Serialize()
prop, _, err := putils.CreateProposalFromCIS(pcommon.HeaderType_CONFIG, "", invocation, creator)
signedProp, err := putils.GetSignedProposal(prop, signer)
proposalResp, err := endorserClient.ProcessProposal(context.Background(), signedProp)
// parse config block
block, err := putils.GetBlockFromBlockBytes(proposalResp.Response.Payload)
envelopeConfig, err := putils.ExtractEnvelope(block, 0)
bundle, err := channelconfig.NewBundleFromEnvelope(envelopeConfig)
return bundle.ChannelConfig().OrdererAddresses(), nil
}
- 构造
ChaincodeInvocationSpec
cis描述该chaincode请求,包含请求的语言(GOLAND),chaincodeId(“cscc”),具体请求Input(cscc.GetConfigBlock)等。这个请求体格式通用于所有的系统及普通chaincode。然后,构造proplsal,并通过上面MSP获取的signer进行签名,在被调用方需要依次判断该节点是否有权限调用chaincode。 - 接着,通过上面构建等endorserClient进行grpc调用
grpc.Invoke(ctx, "/protos.Endorser/ProcessProposal", in, out, c.cc, opts...)
,简单介绍下参数(ctx context.Context,in *SignedProposal, opts …grpc.CallOption, out ProposalResponse, c.cc endorser.grpc.ClientConn)。此处grpc调用的方法是/protos.Endorser/ProcessProposal
,endorser端校验,调用cscc,得到结果ProposalResponse返回。这个后面详述。 - 最后,从rpc调用的结果bytes依次反序列化成block,获取并反序列化block中第一个envelope,把envelope的payload反序列化成configtx,并且获取相应的信息channelConfig,最后从channelConfig中获取到OrdererAddresses。(这里的结构处理是根据cscc的GetConfigBlock调用结果)选择第一个orderer的地址重写为系统变量
orderer.address
。(TODO:这里是否会把所有的请求都集中到第一个orderer有待研究cscc的方法)
GetBroadcastClientFnc
与上面的GetEndorserClient
主要流程非常相似,主要区别有两点。第一,GetEndorserClient
构建的client,使用的是peer的设置,而GetBroadcastClientFnc
获取的是orderer的设置,从环境变量里获取构造参数,configFromEnv("orderer")
,这里orderer.address
这个参数正是上面的方法从chaincode config block里获取并设置为环境变量的。第二,调用了orderClient.Broadcast()
,这里grpc.NewClientStream(ctx, &_AtomicBroadcast_serviceDesc.Streams[0], c.cc, "/orderer.AtomicBroadcast/Broadcast", opts...)
直接指明了grpc的远程调用方法为"/orderer.AtomicBroadcast/Broadcast"
.
至此,完成了chaincode调用的初始化构造过程。后续的chaincode调用就可以直接使用此过程中构建的client及signer等执行不同的操作。