用了 Fabric 也有一个多月了,写过使用过其中的链码,但是还没有搞清楚链码从安装到实例化,再到链码调用的整个具体流程是怎样的。接下来会写几篇文章,从源码角度来分析一下链码,本篇文章就先来分析一下链码的安装过程。
本文源码剖析都是在 Fabric1.4 版本中解析
如何找到链码相关的源码入口
在源码文件结构中,peer 目录是 peer 有关的命令的相关解析,它下面每一个目录都对应着一个子命令,例如 chaincode 目录对应的就是 peer chaincode
相关的命令,当然下面还会有一些子命令,总之整体的项目结构是这样的,可以根据相关文件去找相关命令的入口。
链码的安装相关源码在 peer/chaincode/install.go
中
链码安装源码分析
Fabric 命令相关的库大部分使用的是
cobra
这个命令库,它是一个功能很强大的命令库,并且经常与viper
配置库相结合使用。这个库具体的使用这里就不细说了,我们只挑挑重点的说
在 33 行 installCmd
函数中,有一个 RunE
字段,指定了 install
命令最终去执行的函数,它最终会去执行chaincodeInstall
函数。
// installCmd returns the cobra command for Chaincode Deploy
func installCmd(cf *ChaincodeCmdFactory) *cobra.Command {
chaincodeInstallCmd = &cobra.Command{
Use: "install",
Short: fmt.Sprint(installDesc),
Long: fmt.Sprint(installDesc),
ValidArgs: []string{"1"},
RunE: func(cmd *cobra.Command, args []string) error {
// 定义链码文件,为命令的第一个参数
// 官方举例的一条安装命令如下:
// peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/chaincode_example02/go/
// 这种情况下args为0,-p后面跟的是指定的链码文件path
var ccpackfile string
if len(args) > 0 {
ccpackfile = args[0]
}
return chaincodeInstall(cmd, ccpackfile, cf)
},
}
// 执行可以在install命令中指定的相关flag
flagList := []string{
"lang",
"ctor",
"path",
"name",
"version",
"peerAddresses",
"tlsRootCertFiles",
"connectionProfile",
}
attachFlags(chaincodeInstallCmd, flagList)
return chaincodeInstallCmd
}
接下来就来看下chaincodeInstall
的相关实现
// chaincodeInstall installs the chaincode. If remoteinstall, does it via a lscc call
func chaincodeInstall(cmd *cobra.Command, ccpackfile string, cf *ChaincodeCmdFactory) error {
// Parsing of the command line is done so silence cmd usage
cmd.SilenceUsage = true
var err error
if cf == nil {
// 这里传进来的参数cf是nil,到这里就会初始化一个ChaincodeCmdFactory结构体,类似于一个链码命令工厂
cf, err = InitCmdFactory(cmd.Name(), true, false)
// ChaincodeCmdFactory holds the clients used by ChaincodeCmd
//type ChaincodeCmdFactory struct {
// EndorserClients []pb.EndorserClient // 背书客户端,用于背书
// DeliverClients []api.PeerDeliverClient // Deliver客户端,用于向peer的DeliverServer发送消息
// Certificate tls.Certificate // tls证书相关信息
// Signer msp.SigningIdentity // 用于消息的签名
// BroadcastClient common.BroadcastClient // 广播客户端,用于向orderer节点发送消息
// }
if err != nil {
return err
}
}
var ccpackmsg proto.Message
// 这里ccpackfile可能为空也可能不为空,分为两种情况
// ccpackfile为空,则链码可能是根据传入参数从本地链码源代码文件读取
// ccpackfile不为空,根据注释,是根据package子命令或者signpackage打包的一个链码package,具体可以看两者的实现
// 我们只分析ccpackfile为空的情况
if ccpackfile == "" {
// 从本地源代码出读取,-p参数指定chaincodePath,-v参数指定chaincodeVersion,-n参数指定chaincodeName,这也就是一条标准的chaincodeinstall命令必须要包含的参数,少一个都不行
if chaincodePath == common.UndefinedParamValue || chaincodeVersion == common.UndefinedParamValue || chaincodeName == common.UndefinedParamValue {
return fmt.Errorf("Must supply value for %s name, path and version parameters.", chainFuncName)
}
// 生成一个ChaincodeDeploymentSpec对象,下面看看这个函数的实现
ccpackmsg, err = genChaincodeDeploymentSpec(cmd, chaincodeName, chaincodeVersion)
if err != nil {
return err
}
} else {
//read in a package generated by the "package" sub-command (and perhaps signed
// ...........
}
err = install(ccpackmsg, cf)
return err
}
genChaincodeDeploymentSpec()
方法在第99行:
// ChaincodePackageExists returns whether the chaincode package exists in the file system
func ChaincodePackageExists(ccname string, ccversion string) (bool, error) {
// chaincodeInstallPath是一个全局变量,在peer节点启动时创建chaincode server时设置了,是peer容器内部的一个链码路径
// 默认路径是 /var/hyperledger/production/chaincodes
path := filepath.Join(chaincodeInstallPath, ccname+"."+ccversion)
_, err := os.Stat(path)
if err == nil {
// chaincodepackage already exists
return true, nil
}
return false, err
}
//genChaincodeDeploymentSpec creates ChaincodeDeploymentSpec as the package to install
func genChaincodeDeploymentSpec(cmd *cobra.Command, chaincodeName, chaincodeVersion string) (*pb.ChaincodeDeploymentSpec, error) {
// 根据链码名称与链码版本号,查找chaincode package是否已经被安装了
// 该函数实现在上面
if existed, _ := ccprovider.ChaincodePackageExists(chaincodeName, chaincodeVersion); existed {
return nil, fmt.Errorf("chaincode %s:%s already exists", chaincodeName, chaincodeVersion)
}
// 走到这都是链码还未安装
// 获取一个 ChaincodeSpec 数据结构,声明链码的标准信息
spec, err := getChaincodeSpec(cmd)
if err != nil {
return nil, err
}
// 获取一个 ChaincodeDeploymentSpec 数据结构,指定链码的部署信息
cds, err := getChaincodeDeploymentSpec(spec, true)
if err != nil {
return nil, fmt.Errorf("error getting chaincode code %s: %s", chaincodeName, err)
}
// 最终返回得到的ChaincodeDeploymentSpec数据结构
return cds, nil
}
上面函数主要有两个关键的函数,getChaincodeSpec()
和getChaincodeDeploymentSpec()
,先来看看getChaincodeSpec()
函数,代码实现在 peer/chaincode/common.go
69行
// Carries the chaincode specification. This is the actual metadata required for
// defining a chaincode.
type ChaincodeSpec struct {
Type ChaincodeSpec_Type
ChaincodeId *ChaincodeID
Input *ChaincodeInput
Timeout int32
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
// Carries the chaincode function and its arguments.
// UnmarshalJSON in transaction.go converts the string-based REST/JSON input to
// the []byte-based current ChaincodeInput structure.
type ChaincodeInput struct {
Args [][]byte
Decorations map[string][]byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
// ChaincodeID contains the path as specified by the deploy transaction
// that created it as well as the hashCode that is generated by the
// system for the path. From the user level (ie, CLI, REST API and so on)
// deploy transaction is expected to provide the path and other requests
// are expected to provide the hashCode. The other value will be ignored.
// Internally, the structure could contain both values. For instance, the
// hashCode will be set when first generated using the path
type ChaincodeID struct {
Path string
Name string
Version string
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
// getChaincodeSpec get chaincode spec from the cli cmd pramameters
func getChaincodeSpec(cmd *cobra.Command) (*pb.ChaincodeSpec, error) {
// 定义一个链码标准数据结构
spec := &pb.ChaincodeSpec{}
// 检查用户输入的命令中的参数信息,如果出错则返回命令使用的错误
if err := checkChaincodeCmdParams(cmd); err != nil {
// unset usage silence because it's a command line usage error
cmd.SilenceUsage = false
return spec, err
}
// Build the spec
// 定义一个链码输入参数结构,该结构体用于保存链码中定义的功能以及参数等信息,install命令一般不指定这个命令,在执行invoke命令时指定的-c参数就是指定了chaincodeCtorJSON,一般会这么指定
// -c '{"Args":["a","100"]}'
input := &pb.ChaincodeInput{}
if err := json.Unmarshal([]byte(chaincodeCtorJSON), &input); err != nil {
return spec, errors.Wrap(err, "chaincode argument error")
}
// chaincodeLang默认是golang,将其转换为答谢字母
chaincodeLang = strings.ToUpper(chaincodeLang)
// 最后封装成一个完整的 ChaincodeSpec 对象返回
spec = &pb.ChaincodeSpec{
Type: pb.ChaincodeSpec_Type(pb.ChaincodeSpec_Type_value[chaincodeLang]), // 该字段最终是一个GOLANG类型对应的ChaincodeSpec_Type_value类型,是一个int32类型,GOLANG对应的是1
ChaincodeId: &pb.ChaincodeID{Path: chaincodePath, Name: chaincodeName, Version: chaincodeVersion}, // 该字段是一个ChaincodeID类型的数据,包括chaincodePath,chaincodeName以及chaincodeVersion
Input: input, // 刚刚设置的 Input
}
return spec, nil
}
调用getChaincodeSpec()
获得了一个链码标准数据结构以后,就可以用它作为参数传给getChaincodeDeploymentSpec(spec, true)
获取一个 ChaincodeDeploymentSpec 数据结构,该方法在peer/chaincode/common.go
的50行:
type ChaincodeDeploymentSpec struct {
ChaincodeSpec *ChaincodeSpec
CodePackage []byte
ExecEnv ChaincodeDeploymentSpec_ExecutionEnvironment // 是一个int32类型
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
// getChaincodeDeploymentSpec get chaincode deployment spec given the chaincode spec
func getChaincodeDeploymentSpec(spec *pb.ChaincodeSpec, crtPkg bool) (*pb.ChaincodeDeploymentSpec, error) {
var codePackageBytes []byte
// 判断当前是否为开发者模式,不是的话进入这里
if chaincode.IsDevMode() == false && crtPkg {
var err error
// 检查传进来的ChaincodeSpec对象,检查是否为空,链码类型等信息
if err = checkSpec(spec); err != nil {
return nil, err
}
// 获取链码的payload,也就是获取链码源代码,以及一些环境变量等等信息,最终返回的是一个[]byte信息
codePackageBytes, err = container.GetChaincodePackageBytes(platformRegistry, spec)
if err != nil {
err = errors.WithMessage(err, "error getting chaincode package bytes")
return nil, err
}
}
// 最终返回一个ChaincodeDeploymentSpec对象,这里如果Fabric出于开发者模式,那么codePackageBytes就是空
chaincodeDeploymentSpec := &pb.ChaincodeDeploymentSpec{ChaincodeSpec: spec, CodePackage: codePackageBytes}
return chaincodeDeploymentSpec, nil
}
返回到最初的 chaincodeInstall()
函数,如果是本地安装的话,接下来一步就是要安装链码了,这里提一下如果 ccpackfile 不会空的那个分支,不同的点是该分支是从打包好的链码文件中获取链码相关信息,最终得到的也是一个ChaincodeDeploymentSpec 对象
func chaincodeInstall(cmd *cobra.Command, ccpackfile string, cf *ChaincodeCmdFactory) error {
// ...........
var ccpackmsg proto.Message
if ccpackfile == "" {
// ...........
// 刚刚看到这里
ccpackmsg, err = genChaincodeDeploymentSpec(cmd, chaincodeName, chaincodeVersion)
if err != nil {
return err
}
} else {
var cds *pb.ChaincodeDeploymentSpec
// 从文件中读取已定义的ChaincodeDeploymentSpec
ccpackmsg, cds, err = getPackageFromFile(ccpackfile)
if err != nil {
return err
}
// 下面其实都是一些链码信息的检查之类的
//get the chaincode details from cds
cName := cds.ChaincodeSpec.ChaincodeId.Name
cVersion := cds.ChaincodeSpec.ChaincodeId.Version
//if user provided chaincodeName, use it for validation
if chaincodeName != "" && chaincodeName != cName {
return fmt.Errorf("chaincode name %s does not match name %s in package", chaincodeName, cName)
}
//if user provided chaincodeVersion, use it for validation
if chaincodeVersion != "" && chaincodeVersion != cVersion {
return fmt.Errorf("chaincode version %s does not match version %s in packages", chaincodeVersion, cVersion)
}
}
err = install(ccpackmsg, cf)
return err
}
看下 install()函数
,在peer/chaincode/install.go
63 行:
//install the depspec to "peer.address"
func install(msg proto.Message, cf *ChaincodeCmdFactory) error {
// 从 ChaincodeCmdFactory 中获取一个用于发起提案与签名的creator
creator, err := cf.Signer.Serialize()
if err != nil {
return fmt.Errorf("Error serializing identity for %s: %s", cf.Signer.GetIdentifier(), err)
}
// 从ChaincodeDeploymentSpec中创建一个用于安装链码的提案
prop, _, err := utils.CreateInstallProposalFromCDS(msg, creator)
if err != nil {
return fmt.Errorf("Error creating proposal %s: %s", chainFuncName, err)
}
// ...........
}
来看下CreateInstallProposalFromCDS()
这个函数,在protos/utils/proputils.go
的498行与536行:
// CreateInstallProposalFromCDS returns a install proposal given a serialized
// identity and a ChaincodeDeploymentSpec
func CreateInstallProposalFromCDS(ccpack proto.Message, creator []byte) (*peer.Proposal, string, error) {
return createProposalFromCDS("", ccpack, creator, "install")
}
// createProposalFromCDS returns a deploy or upgrade proposal given a
// serialized identity and a ChaincodeDeploymentSpec
func createProposalFromCDS(chainID string, msg proto.Message, creator []byte, propType string, args ...[]byte) (*peer.Proposal, string, error) {
// in the new mode, cds will be nil, "deploy" and "upgrade" are instantiates.
// chainID为空,msg,creator由上层传入,propType为install,args为空
var ccinp *peer.ChaincodeInput
var b []byte
var err error
if msg != nil {
// 将msg转化为[]byte
b, err = proto.Marshal(msg)
if err != nil {
return nil, "", err
}
}
// 根据 propType 选择不同的case,实例化走depoly,升级走upgrade
switch propType {
case "deploy":
fallthrough
case "upgrade":
cds, ok := msg.(*peer.ChaincodeDeploymentSpec)
if !ok || cds == nil {
return nil, "", errors.New("invalid message for creating lifecycle chaincode proposal")
}
Args := [][]byte{[]byte(propType), []byte(chainID), b}
Args = append(Args, args...)
ccinp = &peer.ChaincodeInput{Args: Args}
case "install":
// 走这个case
ccinp = &peer.ChaincodeInput{Args: [][]byte{[]byte(propType), b}}
}
// wrap the deployment in an invocation spec to lscc...
// 安装链码是用系统链码lscc调用的,所以创建了一个链码调用标准数据结构,其中ChaincodeSpec字段,ChaincodeID的name字段复制的是lscc,它的参数就是刚刚case中赋值的ccinp,由此可以推断它最终执行的是lscc的install函数
lsccSpec := &peer.ChaincodeInvocationSpec{
ChaincodeSpec: &peer.ChaincodeSpec{
Type: peer.ChaincodeSpec_GOLANG,
ChaincodeId: &peer.ChaincodeID{Name: "lscc"},
Input: ccinp,
},
}
// ...and get the proposal for it
// 根据ChaincodeInvocationSpec创建Proposal
return CreateProposalFromCIS(common.HeaderType_ENDORSER_TRANSACTION, chainID, lsccSpec, creator)
}
下面来看看CreateProposalFromCIS()
函数,在protos/utils/proputils.go
464行
这里备注一下一些简称,方便源代码的阅读与理解。
CDS(Chaincode Deployment Spec),链码部署标准数据结构
CIS(Chaincode Invocation Spec),链码调用标准数据结构
// CreateProposalFromCIS returns a proposal given a serialized identity and a
// ChaincodeInvocationSpec
func CreateProposalFromCIS(typ common.HeaderType, chainID string, cis *peer.ChaincodeInvocationSpec, creator []byte) (*peer.Proposal, string, error) {
return CreateChaincodeProposal(typ, chainID, cis, creator)
}
// CreateChaincodeProposal creates a proposal from given input.
// It returns the proposal and the transaction id associated to the proposal
func CreateChaincodeProposal(typ common.HeaderType, chainID string, cis *peer.ChaincodeInvocationSpec, creator []byte) (*peer.Proposal, string, error) {
return CreateChaincodeProposalWithTransient(typ, chainID, cis, creator, nil)
}
// CreateChaincodeProposalWithTransient creates a proposal from given input
// It returns the proposal and the transaction id associated to the proposal
func CreateChaincodeProposalWithTransient(typ common.HeaderType, chainID string, cis *peer.ChaincodeInvocationSpec, creator []byte, transientMap map[string][]byte) (*peer.Proposal, string, error) {
// 生成一个nonce,并根据nonce和creator计算出一个txid
// generate a random nonce
nonce, err := crypto.GetRandomNonce()
if err != nil {
return nil, "", err
}
// compute txid
txid, err := ComputeTxID(nonce, creator)
if err != nil {
return nil, "", err
}
return CreateChaincodeProposalWithTxIDNonceAndTransient(txid, typ, chainID, cis, nonce, creator, transientMap)
}
// CreateChaincodeProposalWithTxIDNonceAndTransient creates a proposal from
// given input
func CreateChaincodeProposalWithTxIDNonceAndTransient(txid string, typ common.HeaderType, chainID string, cis *peer.ChaincodeInvocationSpec, nonce, creator []byte, transientMap map[string][]byte) (*peer.Proposal, string, error) {
// txid是上个函数计算出来的,typ是最初指定的交易Header类型HeaderType_ENDORSER_TRANSACTION
// chainID为空,cis是传进来的ChaincodeInvocationSpec对象,nonce是上一个函数传进来的
// creator是最初的方法传进来,transientMap为nil
// 构造一个ChaincodeHeaderExtension对象
ccHdrExt := &peer.ChaincodeHeaderExtension{ChaincodeId: cis.ChaincodeSpec.ChaincodeId}
// 序列化ccHdrExt
ccHdrExtBytes, err := proto.Marshal(ccHdrExt)
if err != nil {
return nil, "", errors.Wrap(err, "error marshaling ChaincodeHeaderExtension")
}
// 序列化cis
cisBytes, err := proto.Marshal(cis)
if err != nil {
return nil, "", errors.Wrap(err, "error marshaling ChaincodeInvocationSpec")
}
// 构造一个ChaincodeProposalPayload对象,链码提案payload,包含input,transientMap
ccPropPayload := &peer.ChaincodeProposalPayload{Input: cisBytes, TransientMap: transientMap}
// 序列化 ccPropPayload
ccPropPayloadBytes, err := proto.Marshal(ccPropPayload)
if err != nil {
return nil, "", errors.Wrap(err, "error marshaling ChaincodeProposalPayload")
}
// TODO: epoch is now set to zero. This must be changed once we
// get a more appropriate mechanism to handle it in.
var epoch uint64
timestamp := util.CreateUtcTimestamp()
// 构造Header结构体,包含ChannelHeader和SignatureHeader
hdr := &common.Header{
ChannelHeader: MarshalOrPanic(
&common.ChannelHeader{
Type: int32(typ),
TxId: txid,
Timestamp: timestamp,
ChannelId: chainID,
Extension: ccHdrExtBytes,
Epoch: epoch,
},
),
SignatureHeader: MarshalOrPanic(
&common.SignatureHeader{
Nonce: nonce,
Creator: creator,
},
),
}
// 序列化header
hdrBytes, err := proto.Marshal(hdr)
if err != nil {
return nil, "", err
}
// 构造Proposal提案结构体并返回
prop := &peer.Proposal{
Header: hdrBytes,
Payload: ccPropPayloadBytes,
}
return prop, txid, nil
}
返回到 install()
函数中:
// 刚刚看到这里
prop, _, err := utils.CreateInstallProposalFromCDS(msg, creator)
if err != nil {
return fmt.Errorf("Error creating proposal %s: %s", chainFuncName, err)
}
var signedProp *pb.SignedProposal
// 对创建的提案进行签名,具体细节不展开了
signedProp, err = utils.GetSignedProposal(prop, cf.Signer)
if err != nil {
return fmt.Errorf("Error creating signed proposal %s: %s", chainFuncName, err)
}
// install is currently only supported for one peer
// 安装链码只能一个一个安装,只在制定的peer节点安装,调用背书客户端的ProcessProposal方法,发送交易提案
proposalResponse, err := cf.EndorserClients[0].ProcessProposal(context.Background(), signedProp)
if err != nil {
return fmt.Errorf("Error endorsing %s: %s", chainFuncName, err)
}
// 接下来都是一些处理提案响应的操作,判断提案status等等
if proposalResponse != nil {
if proposalResponse.Response.Status != int32(pcommon.Status_SUCCESS) {
return errors.Errorf("Bad response: %d - %s", proposalResponse.Response.Status, proposalResponse.Response.Message)
}
logger.Infof("Installed remotely %v", proposalResponse)
} else {
return errors.New("Error during install: received nil proposal response")
}
return nil
}
这里提一下EndorserClient.ProcessProposal()
这个方法,这是一个很常用的方法,用于向背书节点发送交易提案,即 EndoserClient
向 EndoserServer
发送 GRPC 请求,由EndoserServer
处理请求。EndoserServer
在 peer 节点启动时启动,具体细节可以看protos/peer/peer.pb.go
和protos/peer/peer.proto
两个文件,我这里仅贴出部分代码,关于 peer 节点是如何处理交易提案的,这个之后单独会写一篇文章来介绍。
protos/peer/peer.pb.go
type EndorserClient interface {
ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error)
}
type endorserClient struct {
cc *grpc.ClientConn
}
func NewEndorserClient(cc *grpc.ClientConn) EndorserClient {
return &endorserClient{cc}
}
// 最终执行的是这个ProcessProposal方法,调用GRPC客户端的invoke函数
func (c *endorserClient) ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error) {
out := new(ProposalResponse)
err := c.cc.Invoke(ctx, "/protos.Endorser/ProcessProposal", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
protos/peer/peer.proto
syntax = "proto3";
option java_package = "org.hyperledger.fabric.protos.peer";
option go_package = "github.com/hyperledger/fabric/protos/peer";
package protos;
import "peer/proposal.proto";
import "peer/proposal_response.proto";
message PeerID {
string name = 1;
}
message PeerEndpoint {
PeerID id = 1;
string address = 2;
}
// 服务端包含ProcessProposal这个方法
service Endorser {
rpc ProcessProposal(SignedProposal) returns (ProposalResponse) {}
}
链码安装源码总结
下面总结一下链码安装
- 用户执行安装链码的命令,以此命令为例:
peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/chaincode_example02/go/
- 初始化一个链码命令工厂,包含背书客户端,分发客户端,tls证书,签名者,广播客户端等成员信息
- 判断 ccpackfile 是否为空
- 如果为空,则从本地链码源代码文件读取
- 判断链码是否已经存在,如果存在直接返回错误信息,如果不存在接着往下走
- 验证用户给入的命令参数
- 构造
ChaincodeInput
对象,即命令中的-c选项,install命令一般没有 - 根据
ChaincodeInput
对象构造ChaincodeSpec
对象,即链码标准数据结构 - 根据生成的
ChaincodeSpec
对象 spec 构造ChaincodeDeploymentSpec
对象cds
,主要有两个字段,一个字段是 spec,一个字段是从链码源代码中获取 payload 信息,可以理解为读取链码源代码,将其转化为 []byte 类型存储 - 最终返回生成的
ChaincodeDeploymentSpec
对象
- 如果不为空,则从给定的package中直接读取已定义的
ChaincodeDeploymentSpec
信息
- 如果为空,则从本地链码源代码文件读取
- 执行链码的安装,获取签名者 creator
- 执行
CreateInstallProposalFromCDS()
函数从 cds(即生成的ChaincodeDeploymentSpec
对象)中创建提案Proposal
- 对
Proposal
进行签名,得到一个SignedProposal
对象signedProp
- 将
signedProp
通过EndorserClient.ProcessProposal
方法发往指定的背书节点,即需要安装链码的节点,由背书节点的EndorserServer
进行处理
顺带说一句,链码安装具体最终执行的是 lscc 生命周期链码的 install 函数,关于 lscc 的解析,之后也会专门写一篇文章