environment:
fabric v1.4.2
1.概述
在Fabric中交易的處理過(guò)程欢搜,客戶端將提案首先發(fā)送到背書(shū)節(jié)點(diǎn),背書(shū)節(jié)點(diǎn)檢提案的合法性谴轮。如果合法的話炒瘟,背書(shū)節(jié)點(diǎn)將通過(guò)交易所屬的鏈碼臨時(shí)執(zhí)行一個(gè)交易,并執(zhí)行背書(shū)節(jié)點(diǎn)在本地持有的狀態(tài)副本第步。
Chaincode應(yīng)該僅僅被安裝于chaincode所有者的背書(shū)節(jié)點(diǎn)上疮装,鏈碼運(yùn)行在節(jié)點(diǎn)上的沙盒(Docker容器)中,并通過(guò)gRPC協(xié)議與相應(yīng)的Peer節(jié)點(diǎn)進(jìn)行交互粘都,以使該chaincode邏輯對(duì)整個(gè)網(wǎng)絡(luò)的其他成員保密廓推。
請(qǐng)務(wù)必在一條channel上每一個(gè)要運(yùn)行你chaincode的背書(shū)節(jié)點(diǎn)上安裝你的chaincode
其他沒(méi)有chaincode的成員將無(wú)權(quán)成為chaincode影響下的交易的認(rèn)證節(jié)點(diǎn)(endorser)。也就是說(shuō)翩隧,他們不能執(zhí)行chaincode樊展。不過(guò),他們?nèi)钥梢则?yàn)證交易并提交到賬本上。
ChainCode要在區(qū)塊鏈網(wǎng)絡(luò)中運(yùn)行专缠,需要經(jīng)過(guò)鏈碼安裝和鏈碼實(shí)例化兩個(gè)步驟雷酪。
鏈碼的安裝涉及到3個(gè)服務(wù),分別是client藤肢,peer背書(shū)節(jié)點(diǎn)和LSCC容器
主要流程:
- 客戶端構(gòu)造提案信息并發(fā)送給背書(shū)節(jié)點(diǎn)
- 背書(shū)節(jié)點(diǎn)檢提案的合法性
- 背書(shū)節(jié)點(diǎn)調(diào)用lscc容器
- lscc容器進(jìn)行鏈碼安裝
- 提案背書(shū)返回
以下是在客戶端執(zhí)行"peer chaincode install ..."
的業(yè)務(wù)流程圖:
2. 客戶端構(gòu)造提案信息并發(fā)送給背書(shū)節(jié)點(diǎn)
客戶端執(zhí)行鏈碼安裝命令:
#-n 指定mycc是由用戶定義的鏈碼名字太闺,-v 指定1.0是鏈碼的版本糯景,-p 是指定鏈碼的路徑
peer chaincode install -p chaincodedev/chaincode/sacc -n mycc -v 1.0
客戶端的整個(gè)流程切入點(diǎn)為fabric/peer/main.go
的main
函數(shù)
...
mainCmd.AddCommand(chaincode.Cmd(nil)) // chaincode命令入口
...
然后繼續(xù)找到peer/chaincode/chaincode.go
// Cmd returns the cobra command for Chaincode
func Cmd(cf *ChaincodeCmdFactory) *cobra.Command {
addFlags(chaincodeCmd)
chaincodeCmd.AddCommand(installCmd(cf)) // 執(zhí)行鏈碼的安裝
chaincodeCmd.AddCommand(instantiateCmd(cf)) // 鏈碼的實(shí)例化
chaincodeCmd.AddCommand(invokeCmd(cf)) // 鏈碼的調(diào)用嘁圈,具體調(diào)用什么方法要看鏈碼是怎么寫(xiě)的
chaincodeCmd.AddCommand(packageCmd(cf, nil)) // 鏈碼的打包
chaincodeCmd.AddCommand(queryCmd(cf)) // 對(duì)鏈碼數(shù)據(jù)進(jìn)行查詢,這個(gè)只是向指定的Peer節(jié)點(diǎn)請(qǐng)求查詢數(shù)據(jù)蟀淮,不會(huì)生成交易最后打包區(qū)塊的
chaincodeCmd.AddCommand(signpackageCmd(cf)) // 對(duì)已打包的鏈碼進(jìn)行簽名操作
chaincodeCmd.AddCommand(upgradeCmd(cf)) // 更新鏈碼最住,之前提到過(guò) -v是指定鏈碼的版本,如果需要對(duì)鏈碼進(jìn)行更新的話怠惶,使用這條命令涨缚,比較常用
chaincodeCmd.AddCommand(listCmd(cf)) // 如果已指定通道的話,則查詢已實(shí)例化的鏈碼策治,否則查詢當(dāng)前Peer節(jié)點(diǎn)已安裝的鏈碼
return chaincodeCmd
}
繼續(xù)找到peer/chaincode/install.go
的 installCmd
函數(shù)脓魏,可以看出chaincodeInstall
為主要的入口函數(shù)
// 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 {
var ccpackfile string
if len(args) > 0 {
ccpackfile = args[0]
}
// 入口函數(shù)
return chaincodeInstall(cmd, ccpackfile, cf)
},
}
flagList := []string{ // 在安裝鏈碼的命令中指定的相關(guān)參數(shù)
"lang",
"ctor",
"path",
"name",
"version",
"peerAddresses",
"tlsRootCertFiles",
"connectionProfile",
}
attachFlags(chaincodeInstallCmd, flagList)
return chaincodeInstallCmd
}
// 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 {
// 如果ChaincodeCmdFactory為空,則初始化一個(gè)
cf, err = InitCmdFactory(cmd.Name(), true, false)
if err != nil {
return err
}
}
var ccpackmsg proto.Message
// 這個(gè)地方有兩種情況通惫,鏈碼可能是根據(jù)傳入?yún)?shù)從本地鏈碼源代碼文件讀取茂翔,也有可能是由其他節(jié)點(diǎn)簽名打包完成發(fā)送過(guò)來(lái)的
if ccpackfile == "" {
// 這里是從本地鏈碼源代碼文件讀取,一般從這里進(jìn)去
if chaincodePath == common.UndefinedParamValue || chaincodeVersion == common.UndefinedParamValue || chaincodeName == common.UndefinedParamValue {
return fmt.Errorf("Must supply value for %s name, path and version parameters.", chainFuncName)
}
//generate a raw ChaincodeDeploymentSpec
// 生成ChaincodeDeploymentSpce
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
//by multiple owners with the "signpackage" sub-command)
// 首先從ccpackfile中獲取數(shù)據(jù),主要就是從文件中讀取已定義的ChaincodeDeploymentSpec
var cds *pb.ChaincodeDeploymentSpec
ccpackmsg, cds, err = getPackageFromFile(ccpackfile)
if err != nil {
return err
}
//get the chaincode details from cds
// 由于ccpackfile中已經(jīng)定義完成了以上的數(shù)據(jù)結(jié)構(gòu)履腋,所以這里就直接獲取了
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
}
2.1 構(gòu)造ChaincodeCmdFactory結(jié)構(gòu)體
我們進(jìn)去看看InitCmdFactory
做了什么珊燎,位置在peer/chaincode/common.go
// InitCmdFactory init the ChaincodeCmdFactory with default clients
func InitCmdFactory(cmdName string, isEndorserRequired, isOrdererRequired bool) (*ChaincodeCmdFactory, error) {
var err error
var endorserClients []pb.EndorserClient
var deliverClients []api.PeerDeliverClient
if isEndorserRequired {
if err = validatePeerConnectionParameters(cmdName); err != nil {
return nil, errors.WithMessage(err, "error validating peer connection parameters")
}
for i, address := range peerAddresses {
var tlsRootCertFile string
if tlsRootCertFiles != nil {
tlsRootCertFile = tlsRootCertFiles[i]
}
endorserClient, err := common.GetEndorserClientFnc(address, tlsRootCertFile)
if err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("error getting endorser client for %s", cmdName))
}
endorserClients = append(endorserClients, endorserClient)
deliverClient, err := common.GetPeerDeliverClientFnc(address, tlsRootCertFile)
if err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("error getting deliver client for %s", cmdName))
}
deliverClients = append(deliverClients, deliverClient)
}
if len(endorserClients) == 0 {
return nil, errors.New("no endorser clients retrieved - this might indicate a bug")
}
}
certificate, err := common.GetCertificateFnc()
if err != nil {
return nil, errors.WithMessage(err, "error getting client cerificate")
}
signer, err := common.GetDefaultSignerFnc()
if err != nil {
return nil, errors.WithMessage(err, "error getting default signer")
}
var broadcastClient common.BroadcastClient
if isOrdererRequired {
if len(common.OrderingEndpoint) == 0 {
if len(endorserClients) == 0 {
return nil, errors.New("orderer is required, but no ordering endpoint or endorser client supplied")
}
endorserClient := endorserClients[0]
orderingEndpoints, err := common.GetOrdererEndpointOfChainFnc(channelID, signer, endorserClient)
if err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("error getting channel (%s) orderer endpoint", channelID))
}
if len(orderingEndpoints) == 0 {
return nil, errors.Errorf("no orderer endpoints retrieved for channel %s", channelID)
}
logger.Infof("Retrieved channel (%s) orderer endpoint: %s", channelID, orderingEndpoints[0])
// override viper env
viper.Set("orderer.address", orderingEndpoints[0])
}
broadcastClient, err = common.GetBroadcastClientFnc()
if err != nil {
return nil, errors.WithMessage(err, "error getting broadcast client")
}
}
return &ChaincodeCmdFactory{
EndorserClients: endorserClients,
DeliverClients: deliverClients,
Signer: signer,
BroadcastClient: broadcastClient,
Certificate: certificate,
}, nil
}
返回了ChaincodeCmdFactory
的結(jié)構(gòu)體挎峦,定義為:
// ChaincodeCmdFactory holds the clients used by ChaincodeCmd
type ChaincodeCmdFactory struct {
EndorserClients []pb.EndorserClient // 用于向背書(shū)節(jié)點(diǎn)發(fā)送消息
DeliverClients []api.PeerDeliverClient // 用于與Order節(jié)點(diǎn)通信
Certificate tls.Certificate // TLS證書(shū)相關(guān)
Signer msp.SigningIdentity // 用于消息的簽名
BroadcastClient common.BroadcastClient // 用于廣播消息
}
2.2 構(gòu)造鏈碼部署標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu)ChaincodeDeploymentSpec
找到定義genChaincodeDeploymentSpec
//genChaincodeDeploymentSpec creates ChaincodeDeploymentSpec as the package to install
func genChaincodeDeploymentSpec(cmd *cobra.Command, chaincodeName, chaincodeVersion string) (*pb.ChaincodeDeploymentSpec, error) {
// 首先根據(jù)鏈碼名稱與鏈碼版本查找當(dāng)前鏈碼是否已經(jīng)安裝過(guò)炼杖,如果安裝過(guò)則返回鏈碼已存在的錯(cuò)誤
if existed, _ := ccprovider.ChaincodePackageExists(chaincodeName, chaincodeVersion); existed {
return nil, fmt.Errorf("chaincode %s:%s already exists", chaincodeName, chaincodeVersion)
}
// 獲取鏈碼標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu)
spec, err := getChaincodeSpec(cmd)
if err != nil {
return nil, err
}
// 獲取鏈碼部署標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu)
cds, err := getChaincodeDeploymentSpec(spec, true)
if err != nil {
return nil, fmt.Errorf("error getting chaincode code %s: %s", chaincodeName, err)
}
return cds, nil
}
先看getChaincodeSpec
,位于peer/chaincode/common.go
// getChaincodeSpec get chaincode spec from the cli cmd pramameters
func getChaincodeSpec(cmd *cobra.Command) (*pb.ChaincodeSpec, error) {
spec := &pb.ChaincodeSpec{}
// 檢查由用戶輸入的命令中的參數(shù)信息,比如格式桥言,是否有沒(méi)有定義過(guò)的參數(shù)等等
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
// 定義一個(gè)鏈碼輸入?yún)?shù)結(jié)構(gòu)
input := &pb.ChaincodeInput{}
if err := json.Unmarshal([]byte(chaincodeCtorJSON), &input); err != nil {
return spec, errors.Wrap(err, "chaincode argument error")
}
chaincodeLang = strings.ToUpper(chaincodeLang)
// 最后封裝為ChaincodeSpec結(jié)構(gòu)體返回
spec = &pb.ChaincodeSpec{
Type: pb.ChaincodeSpec_Type(pb.ChaincodeSpec_Type_value[chaincodeLang]),
ChaincodeId: &pb.ChaincodeID{Path: chaincodePath, Name: chaincodeName, Version: chaincodeVersion},
Input: input,
}
return spec, nil
}
封裝返回ChaincodeSpec
結(jié)構(gòu)體
// Carries the chaincode specification. This is the actual metadata required for
// defining a chaincode.
type ChaincodeSpec struct {
Type ChaincodeSpec_Type // 鏈碼的編寫(xiě)語(yǔ)言延旧,GOLANG谋国、JAVA
ChaincodeId *ChaincodeID // ChaincodeId,鏈碼路徑迁沫、鏈碼名稱烹卒、鏈碼版本
Input *ChaincodeInput // 鏈碼的具體執(zhí)行參數(shù)信息
Timeout int32
}
剛才生成的ChaincodeSpec
作為getChaincodeDeploymentSpec
函數(shù)的輸入?yún)?shù),返回ChaincodeDeploymentSpec
結(jié)構(gòu)體
// getChaincodeDeploymentSpec get chaincode deployment spec given the chaincode spec
func getChaincodeDeploymentSpec(spec *pb.ChaincodeSpec, crtPkg bool) (*pb.ChaincodeDeploymentSpec, error) {
var codePackageBytes []byte
// 首先判斷是否當(dāng)前Fabric網(wǎng)絡(luò)處于開(kāi)發(fā)模式弯洗,如果不是的話進(jìn)入這里
if chaincode.IsDevMode() == false && crtPkg {
var err error
// 然后對(duì)之前創(chuàng)建的鏈碼標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu)進(jìn)行驗(yàn)證旅急,驗(yàn)證是否為空,鏈碼類型路徑等信息
if err = checkSpec(spec); err != nil {
return nil, err
}
// #獲取鏈碼信息的有效載荷
codePackageBytes, err = container.GetChaincodePackageBytes(platformRegistry, spec)
if err != nil {
err = errors.WithMessage(err, "error getting chaincode package bytes")
return nil, err
}
}
// 最后封裝為ChaincodeDeploymentSpec牡整,這里如果Fabric網(wǎng)絡(luò)處于開(kāi)發(fā)模式下藐吮,codePackageBytes為空
chaincodeDeploymentSpec := &pb.ChaincodeDeploymentSpec{ChaincodeSpec: spec, CodePackage: codePackageBytes}
return chaincodeDeploymentSpec, nil
}
// Specify the deployment of a chaincode.
// TODO: Define `codePackage`.
type ChaincodeDeploymentSpec struct {
ChaincodeSpec *ChaincodeSpec // ChaincodeSpec消息
CodePackage []byte // 鏈碼文件打包
ExecEnv ChaincodeDeploymentSpec_ExecutionEnvironment // 鏈碼執(zhí)行環(huán)境,DOCKER或SYSTEM
}
2.3 創(chuàng)建提案結(jié)構(gòu)、簽名和發(fā)送提案
//install the depspec to "peer.address"
func install(msg proto.Message, cf *ChaincodeCmdFactory) error {
// 首先獲取一個(gè)用于發(fā)起提案與簽名的creator
creator, err := cf.Signer.Serialize()
if err != nil {
return fmt.Errorf("Error serializing identity for %s: %s", cf.Signer.GetIdentifier(), err)
}
// 從ChaincodeDeploymentSpec中創(chuàng)建一個(gè)用于安裝鏈碼的Proposal
prop, _, err := utils.CreateInstallProposalFromCDS(msg, creator)
if err != nil {
return fmt.Errorf("Error creating proposal %s: %s", chainFuncName, err)
}
var signedProp *pb.SignedProposal
// 對(duì)創(chuàng)建的Proposal進(jìn)行簽名
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節(jié)點(diǎn)谣辞,而不是所有Peer節(jié)點(diǎn)迫摔,依舊是調(diào)用了主要的方法ProcessProposal
proposalResponse, err := cf.EndorserClients[0].ProcessProposal(context.Background(), signedProp)
// 到這里,Peer節(jié)點(diǎn)對(duì)提案處理完成之后泥从,整個(gè)鏈碼安裝的過(guò)程就結(jié)束了
if err != nil {
return fmt.Errorf("Error endorsing %s: %s", chainFuncName, err)
}
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
}
2.3.1 創(chuàng)建提案結(jié)構(gòu)
CreateInstallProposalFromCDS
位于protos/utils/proutils.go
// 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")
}
調(diào)用createProposalFromCDS
// createProposalFromCDS returns a deploy or upgrade proposal given a
// serialized identity and a ChaincodeDeploymentSpec
// 傳入的參數(shù)說(shuō)明一下:chainID為空句占,msg,creator由之前的方法傳入躯嫉,propType為install纱烘,args為空
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.
var ccinp *peer.ChaincodeInput
var b []byte
var err error
if msg != nil {
b, err = proto.Marshal(msg)
if err != nil {
return nil, "", err
}
}
switch propType {
// 這里就判斷propTypre類型,如果是deploy,或者是upgrade需要鏈碼已經(jīng)實(shí)例化完成
case "deploy":
fallthrough
// 如果是deploy不跳出代碼塊祈餐,繼續(xù)執(zhí)行upgrade中的代碼
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...)
// 與安裝鏈碼相同擂啥,都需要定義一個(gè)ChaincodeInput結(jié)構(gòu)體,該結(jié)構(gòu)體保存鏈碼的基本信息
ccinp = &peer.ChaincodeInput{Args: Args}
case "install":
ccinp = &peer.ChaincodeInput{Args: [][]byte{[]byte(propType), b}}
}
// wrap the deployment in an invocation spec to lscc...
// 安裝鏈碼需要使用到生命周期系統(tǒng)鏈碼帆阳,所以這里定義了一個(gè)lsccSpce哺壶,注意這里的ChaincodeInvocationSpec在下面使用到
lsccSpec := &peer.ChaincodeInvocationSpec{
ChaincodeSpec: &peer.ChaincodeSpec{
Type: peer.ChaincodeSpec_GOLANG,
ChaincodeId: &peer.ChaincodeID{Name: "lscc"},
Input: ccinp,
},
}
// ...and get the proposal for it
// 根據(jù)ChaincodeInvocationSpec創(chuàng)建Proposal
return CreateProposalFromCIS(common.HeaderType_ENDORSER_TRANSACTION, chainID, lsccSpec, creator)
}
從結(jié)構(gòu)體ChaincodeInvocationSpec
可以看到用戶鏈碼安裝需要調(diào)用到系統(tǒng)鏈碼lscc
通過(guò)CreateProposalFromCIS=>CreateChaincodeProposal=>CreateChaincodeProposalWithTransient
// 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) {
// generate a random nonce
// 生成一個(gè)隨機(jī)數(shù)
nonce, err := crypto.GetRandomNonce()
if err != nil {
return nil, "", err
}
// compute txid
// 計(jì)算出一個(gè)TxID,具體是根據(jù)HASH算法生成的
txid, err := ComputeTxID(nonce, creator)
if err != nil {
return nil, "", err
}
// 用了這個(gè)方法,將之前生成的數(shù)據(jù)傳入進(jìn)去
return CreateChaincodeProposalWithTxIDNonceAndTransient(txid, typ, chainID, cis, nonce, creator, transientMap)
}
再看CreateChaincodeProposalWithTxIDNonceAndTransient
函數(shù)
/ 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) {
// 首先是構(gòu)造一個(gè)ChaincodeHeaderExtension結(jié)構(gòu)體
ccHdrExt := &peer.ChaincodeHeaderExtension{ChaincodeId: cis.ChaincodeSpec.ChaincodeId}
// 將該結(jié)構(gòu)體序列化
ccHdrExtBytes, err := proto.Marshal(ccHdrExt)
if err != nil {
return nil, "", errors.Wrap(err, "error marshaling ChaincodeHeaderExtension")
}
// 將ChaincodeInvocationSpec結(jié)構(gòu)體序列化
cisBytes, err := proto.Marshal(cis)
if err != nil {
return nil, "", errors.Wrap(err, "error marshaling ChaincodeInvocationSpec")
}
// ChaincodeProposalPayload結(jié)構(gòu)體
ccPropPayload := &peer.ChaincodeProposalPayload{Input: cisBytes, TransientMap: transientMap}
// 序列化
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
// 創(chuàng)建一個(gè)時(shí)間戳
timestamp := util.CreateUtcTimestamp()
// 構(gòu)造Header結(jié)構(gòu)體蜒谤,包含兩部分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,
},
),
}
// 序列化
hdrBytes, err := proto.Marshal(hdr)
if err != nil {
return nil, "", err
}
// 最后構(gòu)造成一個(gè)Proposal
prop := &peer.Proposal{
Header: hdrBytes,
Payload: ccPropPayloadBytes,
}
return prop, txid, nil
}
最后返回Proposal
結(jié)構(gòu)體山宾,定義見(jiàn)protos\peer\proposal.pb.go
type Proposal struct {
// The header of the proposal. It is the bytes of the Header
Header []byte `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
// The payload of the proposal as defined by the type in the proposal
// header.
Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"`
// Optional extensions to the proposal. Its content depends on the Header's
// type field. For the type CHAINCODE, it might be the bytes of a
// ChaincodeAction message.
Extension []byte `protobuf:"bytes,3,opt,name=extension,proto3" json:"extension,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
到這里install
調(diào)用的CreateInstallProposalFromCDS
完畢,返回Proposal
結(jié)構(gòu)體
關(guān)系有點(diǎn)復(fù)雜鳍徽,給出一個(gè)類圖能看得清晰點(diǎn)
2.3.2 簽名
回到install
资锰,看GetSignedProposal
對(duì)剛創(chuàng)建的提案結(jié)構(gòu)進(jìn)行簽名
函數(shù)位于protos/utils/txutils.go
// GetSignedProposal returns a signed proposal given a Proposal message and a
// signing identity
func GetSignedProposal(prop *peer.Proposal, signer msp.SigningIdentity) (*peer.SignedProposal, error) {
// check for nil argument
if prop == nil || signer == nil {
return nil, errors.New("nil arguments")
}
// 獲取提案信息的字節(jié)數(shù)組
propBytes, err := GetBytesProposal(prop)
if err != nil {
return nil, err
}
// 對(duì)字節(jié)數(shù)組進(jìn)行簽名
signature, err := signer.Sign(propBytes)
if err != nil {
return nil, err
}
// 返回SignedProposal結(jié)構(gòu)體
return &peer.SignedProposal{ProposalBytes: propBytes, Signature: signature}, nil
}
返回SignedProposal
結(jié)構(gòu)體,定義位于protos/peer/proposal.pb.go
type SignedProposal struct {
// The bytes of Proposal
ProposalBytes []byte `protobuf:"bytes,1,opt,name=proposal_bytes,json=proposalBytes,proto3" json:"proposal_bytes,omitempty"`
// Signaure over proposalBytes; this signature is to be verified against
// the creator identity contained in the header of the Proposal message
// marshaled as proposalBytes
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
2.3.3 發(fā)送提案
提案簽名完后install
調(diào)用ProcessProposal
發(fā)送提案到peer節(jié)點(diǎn)進(jìn)行處理,參數(shù)帶了SignedProposal
結(jié)構(gòu)體
接下來(lái)client端就等到peer的proposalResponse
3. 背書(shū)節(jié)點(diǎn)檢提案的合法性
當(dāng)client調(diào)用了ProposalResponse
消息就發(fā)送到peer背書(shū)節(jié)點(diǎn),也就是走peer節(jié)點(diǎn)背書(shū)提案流程.
要看安裝鏈碼前做了什么旬盯,直接看peer節(jié)點(diǎn)背書(shū)提案流程就好台妆。
4. 背書(shū)節(jié)點(diǎn)調(diào)用lscc容器
我們從core/endorser/endorser.go
的callChaincode=>Execute
函數(shù)開(kāi)始講
// call specified chaincode (system or user)
func (e *Endorser) callChaincode(txParams *ccprovider.TransactionParams, version string, input *pb.ChaincodeInput, cid *pb.ChaincodeID) (*pb.Response, *pb.ChaincodeEvent, error) {
... ...
// is this a system chaincode
// 執(zhí)行鏈碼,如果是用戶鏈碼具體怎么執(zhí)行的要看用戶寫(xiě)的鏈碼邏輯胖翰,執(zhí)行完畢后返回響應(yīng)信息與鏈碼事件
res, ccevent, err = e.s.Execute(txParams, txParams.ChannelID, cid.Name, version, txParams.TxID, txParams.SignedProp, txParams.Proposal, input)
if err != nil {
return nil, nil, err
}
... ...
在core/chaincode/chaincode_support.go
找到Execute
// Execute invokes chaincode and returns the original response.
func (cs *ChaincodeSupport) Execute(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (*pb.Response, *pb.ChaincodeEvent, error) {
// 主要是啟動(dòng)鏈碼容器接剩,調(diào)用鏈碼
resp, err := cs.Invoke(txParams, cccid, input)
// 對(duì)鏈碼執(zhí)行結(jié)果進(jìn)行處理
return processChaincodeExecutionResult(txParams.TxID, cccid.Name, resp, err)
}
主要看Invoke
:
// Invoke will invoke chaincode and return the message containing the response.
// The chaincode will be launched if it is not already running.
func (cs *ChaincodeSupport) Invoke(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (*pb.ChaincodeMessage, error) {
// 啟動(dòng)鏈碼容器
h, err := cs.Launch(txParams.ChannelID, cccid.Name, cccid.Version, txParams.TXSimulator)
if err != nil {
return nil, err
}
// TODO add Init exactly once semantics here once new lifecycle
// is available. Enforced if the target channel is using the new lifecycle
//
// First, the function name of the chaincode to invoke should be checked. If it is
// "init", then consider this invocation to be of type pb.ChaincodeMessage_INIT,
// otherwise consider it to be of type pb.ChaincodeMessage_TRANSACTION,
//
// Secondly, A check should be made whether the chaincode has been
// inited, then, if true, only allow cctyp pb.ChaincodeMessage_TRANSACTION,
// otherwise, only allow cctype pb.ChaincodeMessage_INIT,
cctype := pb.ChaincodeMessage_TRANSACTION
// 給鏈碼發(fā)送消息
return cs.execute(cctype, txParams, cccid, input, h)
}
// Launch starts executing chaincode if it is not already running. This method
// blocks until the peer side handler gets into ready state or encounters a fatal
// error. If the chaincode is already running, it simply returns.
func (cs *ChaincodeSupport) Launch(chainID, chaincodeName, chaincodeVersion string, qe ledger.QueryExecutor) (*Handler, error) {
cname := chaincodeName + ":" + chaincodeVersion
// 如果是系統(tǒng)鏈碼,在peer啟動(dòng)的時(shí)候已經(jīng)初始化了萨咳,所以如果是鏈碼安裝在下面語(yǔ)句直接返回了
if h := cs.HandlerRegistry.Handler(cname); h != nil {
return h, nil
}
// 此處到得容器相關(guān)的信息懊缺,包括生產(chǎn)容器的具體類型是系統(tǒng)鏈碼容器還是用戶鏈碼容器
// 在后面會(huì)說(shuō)明,系統(tǒng)鏈碼啟動(dòng)的容器是:inprocVM---inproContainer,用戶鏈碼啟動(dòng)的容器是DockerVM---DockerContainer
ccci, err := cs.Lifecycle.ChaincodeContainerInfo(chaincodeName, qe)
if err != nil {
// TODO: There has to be a better way to do this...
if cs.UserRunsCC {
chaincodeLogger.Error(
"You are attempting to perform an action other than Deploy on Chaincode that is not ready and you are in developer mode. Did you forget to Deploy your chaincode?",
)
}
return nil, errors.Wrapf(err, "[channel %s] failed to get chaincode container info for %s", chainID, cname)
}
// 啟動(dòng)Runtime中的Launch
if err := cs.Launcher.Launch(ccci); err != nil {
return nil, errors.Wrapf(err, "[channel %s] could not launch chaincode %s", chainID, cname)
}
h := cs.HandlerRegistry.Handler(cname)
if h == nil {
return nil, errors.Wrapf(err, "[channel %s] claimed to start chaincode container for %s but could not find handler", chainID, cname)
}
return h, nil
}
根據(jù)之前的信息培他,我們調(diào)用的是lscc
來(lái)安裝鏈碼鹃两,所以在peer啟動(dòng)的時(shí)候已經(jīng)初始化lscc
鏈碼容器了,所以回直接返回handler
對(duì)象舀凛,后面的語(yǔ)句就不說(shuō)了俊扳,在啟動(dòng)鏈碼容器的章節(jié)再詳細(xì)研究。
接著我們看execute
函數(shù)猛遍,調(diào)用createCCMessage
創(chuàng)建一個(gè)ChaincodeMessage結(jié)構(gòu)體消息
.Execute
負(fù)責(zé)把消息發(fā)送出去
// execute executes a transaction and waits for it to complete until a timeout value.
func (cs *ChaincodeSupport) execute(cctyp pb.ChaincodeMessage_Type, txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput, h *Handler) (*pb.ChaincodeMessage, error) {
input.Decorations = txParams.ProposalDecorations
// 創(chuàng)建一個(gè)ChaincodeMessage結(jié)構(gòu)體消息
ccMsg, err := createCCMessage(cctyp, txParams.ChannelID, txParams.TxID, input)
if err != nil {
return nil, errors.WithMessage(err, "failed to create chaincode message")
}
ccresp, err := h.Execute(txParams, cccid, ccMsg, cs.ExecuteTimeout)
if err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("error sending"))
}
return ccresp, nil
}
在core/chaincode/handler.go
找到Execute
func (h *Handler) Execute(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, msg *pb.ChaincodeMessage, timeout time.Duration) (*pb.ChaincodeMessage, error) {
chaincodeLogger.Debugf("Entry")
defer chaincodeLogger.Debugf("Exit")
txParams.CollectionStore = h.getCollectionStore(msg.ChannelId)
txParams.IsInitTransaction = (msg.Type == pb.ChaincodeMessage_INIT)
txctx, err := h.TXContexts.Create(txParams)
if err != nil {
return nil, err
}
defer h.TXContexts.Delete(msg.ChannelId, msg.Txid)
if err := h.setChaincodeProposal(txParams.SignedProp, txParams.Proposal, msg); err != nil {
return nil, err
}
// 異步發(fā)送grpc消息
h.serialSendAsync(msg)
var ccresp *pb.ChaincodeMessage
select {
case ccresp = <-txctx.ResponseNotifier:
// response is sent to user or calling chaincode. ChaincodeMessage_ERROR
// are typically treated as error
case <-time.After(timeout):
err = errors.New("timeout expired while executing transaction")
ccName := cccid.Name + ":" + cccid.Version
h.Metrics.ExecuteTimeouts.With(
"chaincode", ccName,
).Add(1)
}
return ccresp, err
}
這里關(guān)鍵是h.serialSendAsync(msg)
語(yǔ)句馋记,功能是把包裝好的信息以grpc協(xié)議發(fā)送出去号坡,直接就等返回結(jié)果了。
至此Execute
調(diào)用的Invoke
就在等返回結(jié)果梯醒,結(jié)果返回就調(diào)用processChaincodeExecutionResult
對(duì)鏈碼結(jié)果進(jìn)行處理
5. lscc容器進(jìn)行鏈碼安裝
5.1 容器信息接收以及處理
peer發(fā)送的信息哪去了呢宽堆?
我們定位到code/chaincode/shim/chaincode.go
,我們看到兩個(gè)入口函數(shù)Start
和StartInProc
,Start
為用戶鏈碼的入口函數(shù)茸习,而StartInProc
是系統(tǒng)鏈碼的入口函數(shù)畜隶,他們同時(shí)都調(diào)用了chatWithPeer
,因?yàn)槲覀冋{(diào)用的是lscc,就看StartInProc
// StartInProc is an entry point for system chaincodes bootstrap. It is not an
// API for chaincodes.
func StartInProc(env []string, args []string, cc Chaincode, recv <-chan *pb.ChaincodeMessage, send chan<- *pb.ChaincodeMessage) error {
chaincodeLogger.Debugf("in proc %v", args)
var chaincodename string
for _, v := range env {
if strings.Index(v, "CORE_CHAINCODE_ID_NAME=") == 0 {
p := strings.SplitAfter(v, "CORE_CHAINCODE_ID_NAME=")
chaincodename = p[1]
break
}
}
if chaincodename == "" {
return errors.New("error chaincode id not provided")
}
stream := newInProcStream(recv, send)
chaincodeLogger.Debugf("starting chat with peer using name=%s", chaincodename)
err := chatWithPeer(chaincodename, stream, cc)
return err
}
chatWithPeer就是開(kāi)啟grpc的接收模式在等到節(jié)點(diǎn)發(fā)來(lái)信息号胚,接收到信息后就調(diào)用handleMessage
處理信息籽慢。
func chatWithPeer(chaincodename string, stream PeerChaincodeStream, cc Chaincode) error {
// Create the shim handler responsible for all control logic
handler := newChaincodeHandler(stream, cc)
defer stream.CloseSend()
// Send the ChaincodeID during register.
chaincodeID := &pb.ChaincodeID{Name: chaincodename}
payload, err := proto.Marshal(chaincodeID)
if err != nil {
return errors.Wrap(err, "error marshalling chaincodeID during chaincode registration")
}
// Register on the stream
chaincodeLogger.Debugf("Registering.. sending %s", pb.ChaincodeMessage_REGISTER)
if err = handler.serialSend(&pb.ChaincodeMessage{Type: pb.ChaincodeMessage_REGISTER, Payload: payload}); err != nil {
return errors.WithMessage(err, "error sending chaincode REGISTER")
}
// holds return values from gRPC Recv below
type recvMsg struct {
msg *pb.ChaincodeMessage
err error
}
msgAvail := make(chan *recvMsg, 1)
errc := make(chan error)
receiveMessage := func() {
in, err := stream.Recv()
msgAvail <- &recvMsg{in, err}
}
// 開(kāi)啟線程等信息
go receiveMessage()
for {
select {
case rmsg := <-msgAvail:
switch {
case rmsg.err == io.EOF:
err = errors.Wrapf(rmsg.err, "received EOF, ending chaincode stream")
chaincodeLogger.Debugf("%+v", err)
return err
case rmsg.err != nil:
err := errors.Wrap(rmsg.err, "receive failed")
chaincodeLogger.Errorf("Received error from server, ending chaincode stream: %+v", err)
return err
case rmsg.msg == nil:
err := errors.New("received nil message, ending chaincode stream")
chaincodeLogger.Debugf("%+v", err)
return err
default:
chaincodeLogger.Debugf("[%s]Received message %s from peer", shorttxid(rmsg.msg.Txid), rmsg.msg.Type)
// 處理接收到的信息
err := handler.handleMessage(rmsg.msg, errc)
if err != nil {
err = errors.WithMessage(err, "error handling message")
return err
}
go receiveMessage()
}
case sendErr := <-errc:
if sendErr != nil {
err := errors.Wrap(sendErr, "error sending")
return err
}
}
}
}
因?yàn)槲覀冃畔㈩愋蜑?code>ChaincodeMessage_TRANSACTION,所以我們?cè)?code>core/chaincode/shim/handler.go順著handleMessage=>handleReady
扎到handleTransaction
// handleTransaction Handles request to execute a transaction.
func (handler *Handler) handleTransaction(msg *pb.ChaincodeMessage, errc chan error) {
// The defer followed by triggering a go routine dance is needed to ensure that the previous state transition
// is completed before the next one is triggered. The previous state transition is deemed complete only when
// the beforeInit function is exited. Interesting bug fix!!
go func() {
//better not be nil
var nextStateMsg *pb.ChaincodeMessage
defer func() {
handler.triggerNextState(nextStateMsg, errc)
}()
errFunc := func(err error, ce *pb.ChaincodeEvent, errStr string, args ...interface{}) *pb.ChaincodeMessage {
if err != nil {
payload := []byte(err.Error())
chaincodeLogger.Errorf(errStr, args...)
return &pb.ChaincodeMessage{Type: pb.ChaincodeMessage_ERROR, Payload: payload, Txid: msg.Txid, ChaincodeEvent: ce, ChannelId: msg.ChannelId}
}
return nil
}
// Get the function and args from Payload
input := &pb.ChaincodeInput{}
unmarshalErr := proto.Unmarshal(msg.Payload, input)
if nextStateMsg = errFunc(unmarshalErr, nil, "[%s] Incorrect payload format. Sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_ERROR.String()); nextStateMsg != nil {
return
}
// Call chaincode's Run
// Create the ChaincodeStub which the chaincode can use to callback
stub := new(ChaincodeStub)
err := stub.init(handler, msg.ChannelId, msg.Txid, input, msg.Proposal)
if nextStateMsg = errFunc(err, stub.chaincodeEvent, "[%s] Transaction execution failed. Sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_ERROR.String()); nextStateMsg != nil {
return
}
res := handler.cc.Invoke(stub)
// Endorser will handle error contained in Response.
resBytes, err := proto.Marshal(&res)
if nextStateMsg = errFunc(err, stub.chaincodeEvent, "[%s] Transaction execution failed. Sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_ERROR.String()); nextStateMsg != nil {
return
}
// Send COMPLETED message to chaincode support and change state
chaincodeLogger.Debugf("[%s] Transaction completed. Sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_COMPLETED)
nextStateMsg = &pb.ChaincodeMessage{Type: pb.ChaincodeMessage_COMPLETED, Payload: resBytes, Txid: msg.Txid, ChaincodeEvent: stub.chaincodeEvent, ChannelId: stub.ChannelId}
}()
}
其中關(guān)鍵語(yǔ)句res := handler.cc.Invoke(stub)
,這語(yǔ)句是調(diào)用相應(yīng)鏈碼的Invoke
函數(shù),所以我們找到core/scc/lscc/lscc.go
下的Invoke
函數(shù)
5.2 鏈碼安裝
進(jìn)去core/scc/lscc/lscc.go
的Invoke
函數(shù)可以看到涕刚,這里有"INSTALL", "DEPLOY", "UPGRADE"
等操作嗡综,我們只看INSTALL
部分乙帮。
關(guān)鍵調(diào)用函數(shù)是executeInstall
// Invoke implements lifecycle functions "deploy", "start", "stop", "upgrade".
// Deploy's arguments - {[]byte("deploy"), []byte(<chainname>), <unmarshalled pb.ChaincodeDeploymentSpec>}
//
// Invoke also implements some query-like functions
// Get chaincode arguments - {[]byte("getid"), []byte(<chainname>), []byte(<chaincodename>)}
func (lscc *LifeCycleSysCC) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
args := stub.GetArgs()
if len(args) < 1 {
return shim.Error(InvalidArgsLenErr(len(args)).Error())
}
function := string(args[0])
// Handle ACL:
// 1. get the signed proposal
// 獲取簽名提案
sp, err := stub.GetSignedProposal()
if err != nil {
return shim.Error(fmt.Sprintf("Failed retrieving signed proposal on executing %s with error %s", function, err))
}
switch function {
// 鏈碼安裝
case INSTALL:
if len(args) < 2 {
return shim.Error(InvalidArgsLenErr(len(args)).Error())
}
// 2. check local MSP Admins policy
// 檢查 local MSP Admins 策略
if err = lscc.PolicyChecker.CheckPolicyNoChannel(mgmt.Admins, sp); err != nil {
return shim.Error(fmt.Sprintf("access denied for [%s]: %s", function, err))
}
depSpec := args[1]
err := lscc.executeInstall(stub, depSpec)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success([]byte("OK"))
... ...
接著看executeInstall
// executeInstall implements the "install" Invoke transaction
func (lscc *LifeCycleSysCC) executeInstall(stub shim.ChaincodeStubInterface, ccbytes []byte) error {
// 還原鏈碼結(jié)構(gòu)CDSPackage
ccpack, err := ccprovider.GetCCPackage(ccbytes)
if err != nil {
return err
}
// 獲取ChaincodeDeploymentSpec結(jié)構(gòu)數(shù)據(jù)
cds := ccpack.GetDepSpec()
if cds == nil {
return fmt.Errorf("nil deployment spec from from the CC package")
}
// 鏈碼名字是否合法
if err = lscc.isValidChaincodeName(cds.ChaincodeSpec.ChaincodeId.Name); err != nil {
return err
}
// 鏈碼版本是否合法
if err = lscc.isValidChaincodeVersion(cds.ChaincodeSpec.ChaincodeId.Name, cds.ChaincodeSpec.ChaincodeId.Version); err != nil {
return err
}
// 系統(tǒng)鏈碼不給安裝
if lscc.SCCProvider.IsSysCC(cds.ChaincodeSpec.ChaincodeId.Name) {
return errors.Errorf("cannot install: %s is the name of a system chaincode", cds.ChaincodeSpec.ChaincodeId.Name)
}
// Get any statedb artifacts from the chaincode package, e.g. couchdb index definitions
// 解壓狀態(tài)db數(shù)據(jù)
statedbArtifactsTar, err := ccprovider.ExtractStatedbArtifactsFromCCPackage(ccpack, lscc.PlatformRegistry)
if err != nil {
return err
}
if err = isValidStatedbArtifactsTar(statedbArtifactsTar); err != nil {
return InvalidStatedbArtifactsErr(err.Error())
}
chaincodeDefinition := &cceventmgmt.ChaincodeDefinition{
Name: ccpack.GetChaincodeData().Name,
Version: ccpack.GetChaincodeData().Version,
Hash: ccpack.GetId()} // Note - The chaincode 'id' is the hash of chaincode's (CodeHash || MetaDataHash), aka fingerprint
// HandleChaincodeInstall will apply any statedb artifacts (e.g. couchdb indexes) to
// any channel's statedb where the chaincode is already instantiated
// Note - this step is done prior to PutChaincodeToLocalStorage() since this step is idempotent and harmless until endorsements start,
// that is, if there are errors deploying the indexes the chaincode install can safely be re-attempted later.
// 處理安裝杜漠,含有db數(shù)據(jù)
err = cceventmgmt.GetMgr().HandleChaincodeInstall(chaincodeDefinition, statedbArtifactsTar)
defer func() {
cceventmgmt.GetMgr().ChaincodeInstallDone(err == nil)
}()
if err != nil {
return err
}
// Finally, if everything is good above, install the chaincode to local peer file system so that endorsements can start
// 最后把文件寫(xiě)到指定文件路徑
if err = lscc.Support.PutChaincodeToLocalStorage(ccpack); err != nil {
return err
}
logger.Infof("Installed Chaincode [%s] Version [%s] to peer", ccpack.GetChaincodeData().Name, ccpack.GetChaincodeData().Version)
return nil
}
HandleChaincodeInstall
為處理statedb,而PutChaincodeToLocalStorage
是把鏈碼文件安裝到本地文件目錄
鏈碼安裝到peer的默認(rèn)路徑/var/hyperledger/production/chaincodes
到此鏈碼的安裝完畢
6 提案背書(shū)返回
lscc鏈碼安裝完畢后察净,返回信息給peer節(jié)點(diǎn)驾茴,peer節(jié)點(diǎn)就給提案背書(shū)返回給client服務(wù)端,至此鏈碼安裝完畢氢卡。
參考:
5-ChainCode生命周期锈至、分類及安裝、實(shí)例化命令解析
fabric源碼解讀【peer chaincode】:安裝鏈碼
Fabric1.4源碼解析:客戶端安裝鏈碼