fabric-go-sdk #
1、概述 #
Fabric的Peer节点和Orderer节点都提供了基于GRPC协议(Google开发的远程过程调用RPC)的接口,通过这些接口可以和Peer节点与Orderer节点进行命令/数据交互,为了简化开发,官方提供了多语言版本的SDK。
fabric-go-sdk官方网址为https://github.com/hyperledger/fabric-sdk-go
pkg目录是fabric-go-sdk的主要实现,internel目录和third_party目录包含了fabric-go-sdk依赖的一些代码。
pkg/fabsdk:Fabric SDK 的主包。此包支持基于配置创建上下文。这些上下文由下面列出的客户端包使用。参考:https ://godoc.org/github.com/hyperledger/fabric-sdk-go/pkg/fabsdk
pkg/client/channel:提供通道事务能力。参考:https ://godoc.org/github.com/hyperledger/fabric-sdk-go/pkg/client/channel
pkg/client/event:提供通道事件能力。参考:https ://godoc.org/github.com/hyperledger/fabric-sdk-go/pkg/client/event
pkg/client/ledger:启用对通道底层账本的查询。参考:https ://godoc.org/github.com/hyperledger/fabric-sdk-go/pkg/client/ledger
pkg/client/resmgmt:提供安装链码等资源管理能力。参考:https ://godoc.org/github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt
pkg/client/msp:启用身份管理功能。参考:https ://godoc.org/github.com/hyperledger/fabric-sdk-go/pkg/client/msp
基本工作流程
1) 使用配置实例化一个 fabsdk 实例。 注意:fabsdk 维护缓存,因此您应该最小化 fabsdk 本身的实例。 2) 使用您的 fabsdk 实例创建基于用户和组织的上下文。 注意:通道上下文还需要通道 ID。 3) 使用它的 New func 创建一个客户端实例,传递上下文。 注意:您为所需的每个上下文创建一个新的客户端实例。 4)使用每个客户提供的功能来创建您的解决方案! 5) 调用 fabsdk.Close() 释放资源和缓存。
2、准备网络环境 #
准备证书文件 #
具体参照solo节点测试
-
在
$GOPATH/src
目录下创建一个名为sdktest
的文件夹做为项目根目录,在此目录下创建名为fixtures
的文件夹存放我们网络相关配置文件。-
编辑
crypto-config.yaml
的文件(这里为一个组织两个节点) -
cryptogen generate --config=crypto-config.yaml 生成证书
-
-
在
fixtures
路径下创建一个名为configtx.yaml
的文件-
编辑configtx.yaml文件
-
生成创世块文件
-
生成通道文件
-
锚节点更新(两个组织都要更新)
-
完成后:channel-artifacts文件夹 channel.tx Org1MSPanchors.tx Org1MSPanchors.tx genesis.block
-
-
配置docker-compose文件
version: '2' volumes: orderer.example.com: peer0.org1.example.com: peer1.org1.example.com: networks: test: services: orderer.example.com: container_name: orderer.example.com image: hyperledger/fabric-orderer:2.3 environment: - FABRIC_LOGGING_SPEC=DEBUG - ORDERER_GENERAL_LISTENADDRESS=0.0.0.0 - ORDERER_GENERAL_LISTENPORT=7050 - ORDERER_GENERAL_GENESISMETHOD=file - ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block - ORDERER_GENERAL_LOCALMSPID=OrdererMSP - ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp - ORDERER_GENERAL_TLS_ENABLED=true - ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key - ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt - ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt] working_dir: /opt/gopath/src/github.com/hyperledger/fabric command: orderer volumes: - ./channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block - ./crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/msp:/var/hyperledger/orderer/msp - ./crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/:/var/hyperledger/orderer/tls - orderer.example.com:/var/hyperledger/production/orderer ports: - 7050:7050 networks: - test peer0.org1.example.com: container_name: peer0.org1.example.com image: hyperledger/fabric-peer:2.3 environment: - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=fixtures_test - FABRIC_LOGGING_SPEC=DEBUG - CORE_PEER_TLS_ENABLED=true - CORE_PEER_PROFILE_ENABLED=true - CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt - CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key - CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt - CORE_PEER_ID=peer0.org1.example.com - CORE_PEER_ADDRESS=peer0.org1.example.com:7051 - CORE_PEER_LISTENADDRESS=0.0.0.0:7051 - CORE_PEER_CHAINCODEADDRESS=peer0.org1.example.com:7052 - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052 - CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org1.example.com:7051 - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.example.com:7051 - CORE_PEER_LOCALMSPID=Org1MSP - CORE_LEDGER_STATE_STATEDATABASE=CouchDB - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb0:5984 - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=123456 volumes: - /var/run/:/host/var/run/ - ./crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp:/etc/hyperledger/fabric/msp - ./crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls:/etc/hyperledger/fabric/tls - peer0.org1.example.com:/var/hyperledger/production working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer command: peer node start ports: - 7051:7051 depends_on: - orderer.example.com networks: - test peer1.org1.example.com: container_name: peer1.org1.example.com image: hyperledger/fabric-peer:2.3 environment: - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=fixtures_test - FABRIC_LOGGING_SPEC=DEBUG - CORE_PEER_TLS_ENABLED=true - CORE_PEER_PROFILE_ENABLED=true - CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt - CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key - CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt - CORE_PEER_ID=peer1.org1.example.com - CORE_PEER_ADDRESS=peer1.org1.example.com:9051 - CORE_PEER_LISTENADDRESS=0.0.0.0:9051 - CORE_PEER_CHAINCODEADDRESS=peer1.org1.example.com:9052 - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:9052 - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org1.example.com:9051 - CORE_PEER_GOSSIP_BOOTSTRAP=peer1.org1.example.com:9051 - CORE_PEER_LOCALMSPID=Org1MSP - CORE_LEDGER_STATE_STATEDATABASE=CouchDB - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb1:5984 - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=123456 volumes: - /var/run/:/host/var/run/ - ./crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/msp:/etc/hyperledger/fabric/msp - ./crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls:/etc/hyperledger/fabric/tls - peer1.org1.example.com:/var/hyperledger/production working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer command: peer node start ports: - 9051:9051 depends_on: - orderer.example.com networks: - test ca.org1.example.com: image: hyperledger/fabric-ca:1.4.9 container_name: ca.org1.example.com environment: - FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server - FABRIC_CA_SERVER_CA_NAME=ca.org1.example.com - FABRIC_CA_SERVER_CA_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org1.example.com-cert.pem - FABRIC_CA_SERVER_CA_KEYFILE=/etc/hyperledger/fabric-ca-server-config/priv_sk - FABRIC_CA_SERVER_TLS_ENABLED=true - FABRIC_CA_SERVER_TLS_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org1.example.com-cert.pem - FABRIC_CA_SERVER_TLS_KEYFILE=/etc/hyperledger/fabric-ca-server-config/priv_sk ports: - 7054:7054 command: sh -c 'fabric-ca-server start -b admin:adminpw -d' volumes: - ./crypto-config/peerOrganizations/org1.example.com/ca/:/etc/hyperledger/fabric-ca-server-config networks: - test couchdb0: container_name: couchdb0 image: hyperledger/fabric-couchdb:latest environment: - COUCHDB_USER=admin - COUCHDB_PASSWORD=123456 ports: - "5984:5984" networks: - test couchdb1: container_name: couchdb1 image: hyperledger/fabric-couchdb:latest environment: - COUCHDB_USER=admin - COUCHDB_PASSWORD=123456 ports: - "7984:5984" networks: - test
3、配置文件config.yaml #
具体介绍:config-yaml文件详解
改好的配置文件如下:
version: 1.0.0
client:
organization: org1
logging:
level: info
cryptoconfig:
path: /Users/tianzhiwei/go/src/sdktest/fixtures/crypto-config
credentialStore:
path: "/tmp/state-store"
cryptoStore:
path: /tmp/msp
BCCSP:
security:
enabled: true
default:
provider: "SW"
hashAlgorithm: "SHA2"
softVerify: true
level: 256
tlsCerts:
systemCertPool: true
client:
key:
path: /Users/tianzhiwei/go/src/sdktest/fixtures/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/tls/client.key
cert:
path: /Users/tianzhiwei/go/src/sdktest/fixtures/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/tls/client.crt
channels:
mychannel:
peers:
peer0.org1.example.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.org1.example.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
policies:
queryChannelConfig:
minResponses: 1
maxTargets: 1
retryOpts:
attempts: 5
initialBackoff: 500ms
maxBackoff: 5s
backoffFactor: 2.0
organizations:
org1:
mspid: Org1MSP
cryptoPath: peerOrganizations/org1.example.com/users/{username}@org1.example.com/msp
peers:
- peer0.org1.example.com
- peer1.org1.example.com
ordererorg:
mspID: OrdererMSP
cryptoPath: ordererOrganizations/example.com/users/{username}@example.com/msp
orderers:
orderer.example.com:
url: orderer.example.com:7050
grpcOptions:
ssl-target-name-override: orderer.example.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /Users/tianzhiwei/go/src/sdktest/fixtures/crypto-config/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem
peers:
peer0.org1.example.com:
url: peer0.org1.example.com:7051
grpcOptions:
ssl-target-name-override: peer0.org1.example.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /Users/tianzhiwei/go/src/sdktest/fixtures/crypto-config/peerOrganizations/org1.example.com/tlsca/tlsca.org1.example.com-cert.pem
peer1.org1.example.com:
url: peer1.org1.example.com:9051
grpcOptions:
ssl-target-name-override: peer1.org1.example.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /Users/tianzhiwei/go/src/sdktest/fixtures/crypto-config/peerOrganizations/org1.example.com/tlsca/tlsca.org1.example.com-cert.pem
entityMatchers:
peer:
- pattern: (\w+).org1.example.com:(\d+)
urlSubstitutionExp: ${1}.org1.example.com:${2}
sslTargetOverrideUrlSubstitutionExp: ${1}.org1.example.com
mappedHost: peer0.org1.example.com
4、sdk #
1. 定义所需结构体 #
在项目路径下新建名为sdkInit
的文件夹,此文件夹用于存放实现sdk的代码。在sdkInit
路径下新建sdkInfo.go
并编辑如下:
-
mspclient "github.com/hyperledger/fabric-sdk-go/pkg/client/msp"
msp
包允许在Fabric网络上创建和更新用户。msp客户端支持以下操作:注册、重注册、登记和获取身份签名。基本工作流为:- 准备客户端上下文
- 创建msp客户端
- 登记用户
- 注册用户
-
"github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt"
resmgmt
包用于在Fabric网络中创建和更新资源。它允许管理员创建和/或更新通道,并允许对等节点加入通道。管理员还可以在对等节点上执行链码相关操作,如安装、实例化、升级链码等。基本工作流为:- 准备客户端上下文
- 创建资源管理器客户端
- 创建新通道
- 节点加入通道
- 查询对等节点的通道,安装/实例化的链代码等。
-
contextAPI "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context"
提供所需上下文接口
package sdkInit
import (
mspclient "github.com/hyperledger/fabric-sdk-go/pkg/client/msp"
"github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt"
contextAPI "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context"
)
type OrgInfo struct {
OrgAdminUser string // 管理员用户名,如"Admin"
OrgName string // 组织名,如"Org1"
OrgMspId string // 组织MSPid,如"Org1MSP"
OrgUser string // 用户名,如"User1"
orgMspClient *mspclient.Client // MSP客户端
OrgAdminClientContext *contextAPI.ClientProvider // 客户端上下文信息
OrgResMgmt *resmgmt.Client // 资源管理客户端
OrgPeerNum int // 组织节点个数
OrgAnchorFile string // 锚节点配置文件路径
}
type SdkEnvInfo struct {
// 通道信息
ChannelID string // 通道名称,如"simplecc"
ChannelConfig string // 通道配置文件路径
// 组织信息
Orgs []*OrgInfo
// 排序服务节点信息
OrdererAdminUser string // orederer管理员用户名,如"Admin"
OrdererOrgName string // orderer组织名,如"OrdererOrg"
OrdererEndpoint string // orderer端点,如"orderer.example.com"
OrdererClientContext *contextAPI.ClientProvider // orderer客户端上下文
// 链码信息
ChaincodeID string // 链码名称
ChaincodePath string // 链码路径
ChaincodeVersion string // 链码版本
}
2、初始化 #
在sdkInit
路径下新建sdkSetting.go
并编辑如下:
"github.com/hyperledger/fabric-sdk-go/pkg/core/config"
:获取所需配置文件"github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"
:fabsdk
包允许客户端使用Hyperledger Fabric网络。
package sdkInit
import (
"fmt"
mb "github.com/hyperledger/fabric-protos-go/msp"
pb "github.com/hyperledger/fabric-protos-go/peer"
"github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
mspclient "github.com/hyperledger/fabric-sdk-go/pkg/client/msp"
"github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt"
"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry"
"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/status"
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/msp"
"github.com/hyperledger/fabric-sdk-go/pkg/core/config"
lcpackager "github.com/hyperledger/fabric-sdk-go/pkg/fab/ccpackager/lifecycle"
"github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"
"github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/common/policydsl"
"strings"
)
func Setup(configFile string, info *SdkEnvInfo) (*fabsdk.FabricSDK, error) {
var err error
sdk, err := fabsdk.New(config.FromFile(configFile)) // 使用fabsdk包的new方法根据config.yaml文件提供的网络信息初始化sdk
if err != nil {
return nil, err
}
// 为组织获得Client句柄和Context信息
for _, org := range info.Orgs {
// 初始化组织msp客户端
org.orgMspClient, err = mspclient.New(sdk.Context(), mspclient.WithOrg(org.OrgName))
if err != nil {
return nil, err
}
// 创建所有所需上下文信息
orgContext := sdk.Context(fabsdk.WithUser(org.OrgAdminUser), fabsdk.WithOrg(org.OrgName))
org.OrgAdminClientContext = &orgContext
// 新建客户端资源管理器实例
resMgmtClient, err := resmgmt.New(orgContext)
if err != nil {
return nil, fmt.Errorf("根据指定的资源管理客户端Context创建通道管理客户端失败: %v", err)
}
org.OrgResMgmt = resMgmtClient
}
// 为Orderer获得Context信息
ordererClientContext := sdk.Context(fabsdk.WithUser(info.OrdererAdminUser), fabsdk.WithOrg(info.OrdererOrgName))
info.OrdererClientContext = &ordererClientContext
return sdk, nil
}
3、调用创建通道函数及加入通道 #
func CreateAndJoinChannel(info *SdkEnvInfo) error {
fmt.Println(">> 开始创建通道......")
if len(info.Orgs) == 0 {
return fmt.Errorf("通道组织不能为空,请提供组织信息")
}
// 获得所有组织的签名信息
signIds := []msp.SigningIdentity{}
for _, org := range info.Orgs {
// Get signing identity that is used to sign create channel request
orgSignId, err := org.orgMspClient.GetSigningIdentity(org.OrgAdminUser)
if err != nil {
return fmt.Errorf("GetSigningIdentity error: %v", err)
}
signIds = append(signIds, orgSignId)
}
// 创建通道,createChannel方法在下面定义
if err := createChannel(signIds, info); err != nil {
return fmt.Errorf("Create channel error: %v", err)
}
fmt.Println(">> 创建通道成功")
fmt.Println(">> 加入通道......")
for _, org := range info.Orgs {
// 加入通道
if err := org.OrgResMgmt.JoinChannel(info.ChannelID, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithOrdererEndpoint("orderer.example.com")); err != nil {
return fmt.Errorf("%s peers failed to JoinChannel: %v", org.OrgName, err)
}
}
fmt.Println(">> 加入通道成功")
return nil
}
4、创建通道 #
func createChannel(signIDs []msp.SigningIdentity, info *SdkEnvInfo) error {
// Channel management client 负责管理通道,如创建更新通道
chMgmtClient, err := resmgmt.New(*info.OrdererClientContext)
if err != nil {
return fmt.Errorf("Channel management client create error: %v", err)
}
// 根据channel.tx创建通道
req := resmgmt.SaveChannelRequest{ChannelID: info.ChannelID,
ChannelConfigPath: info.ChannelConfig,
SigningIdentities: signIDs}
if _, err := chMgmtClient.SaveChannel(req, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithOrdererEndpoint("orderer.example.com")); err != nil {
return fmt.Errorf("error should be nil for SaveChannel of orgchannel: %v", err)
}
fmt.Println(">>>> 使用每个org的管理员身份更新锚节点配置...")
//根据锚节点文件更新锚节点,与上面创建通道流程相同
for i, org := range info.Orgs {
req = resmgmt.SaveChannelRequest{ChannelID: info.ChannelID,
ChannelConfigPath: org.OrgAnchorFile,
SigningIdentities: []msp.SigningIdentity{signIDs[i]}}
if _, err = org.OrgResMgmt.SaveChannel(req, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithOrdererEndpoint("orderer.example.com")); err != nil {
return fmt.Errorf("SaveChannel for anchor org %s error: %v", org.OrgName, err)
}
}
fmt.Println(">>>> 使用每个org的管理员身份更新锚节点配置完成")
return nil
}
5、链码生命周期 #
链码运行在一个隔离于背书peer节点进程的安全的Docker容器中。链码通过应用提交的交易来初始化以及管理账本状态。从hyperledger fabric v2.0版本开始启用了新的链码生命周期,Fabric 链码生命周期是一个过程,它允许多个组织在使用一个链码之前就如何操作达成一致。Fabric链码生命周期需要组织同意定义一个链码的参数,比如说名称、版本以及链码背书策略。通道成员通过以下四步达成共识。不是通道上的每一个组织都需要完成每一步。
-
打包链码:这一步可以被一个或者每一个组织完成。
-
安装链码在你的 peer 节点上:每一个用链码的组织需要完成这一步。
-
为你的组织批准链码定义:使用链码的每一个组织需要完成这一步。链码能够在通道上运行之前,链码定义需要被足够多的组织批准来满足通道的生命周期背书(LifecycleEndorsement)策略(默认为大多数组织)。
-
提交链码定义到链上:一旦通道上所需数量的组织已经同意,提交交易需要被提交。提交者首先从已同意组织中的足够的peer节点中收集背书,然后通过提交交易来提交链码声明。
1. 添加DiscoverLocalPeers
方法
#
DiscoverLocalPeers
方法可以自动查找的所有节点
在sdkInit
路径下新建一个名为intergration.go
的文件。编辑如下:
package sdkInit
import (
"fmt"
"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry"
"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/status"
contextAPI "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context"
fabAPI "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
contextImpl "github.com/hyperledger/fabric-sdk-go/pkg/context"
)
// 查找本地节点
func DiscoverLocalPeers(ctxProvider contextAPI.ClientProvider, expectedPeers int) ([]fabAPI.Peer, error) {
ctx, err := contextImpl.NewLocal(ctxProvider)
if err != nil {
return nil, fmt.Errorf("error creating local context: %v", err)
}
discoveredPeers, err := retry.NewInvoker(retry.New(retry.TestRetryOpts)).Invoke(
func() (interface{}, error) {
peers, serviceErr := ctx.LocalDiscoveryService().GetPeers()
if serviceErr != nil {
return nil, fmt.Errorf("getting peers for MSP [%s] error: %v", ctx.Identifier().MSPID, serviceErr)
}
if len(peers) < expectedPeers {
return nil, status.New(status.TestStatus, status.GenericTransient.ToInt32(), fmt.Sprintf("Expecting %d peers but got %d", expectedPeers, len(peers)), nil)
}
return peers, nil
},
)
if err != nil {
return nil, err
}
return discoveredPeers.([]fabAPI.Peer), nil
}
2. 链码自动化生命周期 #
继续在sdkSetting.go
文件中编辑如下:
- 导入所需包
import (
[......]
mb "github.com/hyperledger/fabric-protos-go/msp"
pb "github.com/hyperledger/fabric-protos-go/peer"
"github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/status"
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
lcpackager "github.com/hyperledger/fabric-sdk-go/pkg/fab/ccpackager/lifecycle"
"github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/common/policydsl"
"strings"
)
- 打包链码
在被安装到peer
节点之前,链码需要被打包进一个tar
文件。当你创建一个链码包的时候,你需要提交一个用来创建简明易读的包描述的链码包标签。
使用fabric-go-sdk
将会自动以这个格式来创建文件。
- 链码需要被打包进一个以
.tar.gz
文件扩展名结尾的tar
文件。 tar
文件需要包含两个文件(没有目录):metadata.json
和另一个包含了链码文件的 tar 文件code.tar.gz
。metadata.json
包含了指定链码语言、代码路径、以及包标签的 JSON 文件。
func packageCC(ccName, ccVersion, ccpath string) (string, []byte, error) {
label := ccName + "_" + ccVersion // 链码的标签
desc := &lcpackager.Descriptor{ // 使用lcpackager包中的Descriptor结构体添加描述信息
Path: ccpath, //链码路径
Type: pb.ChaincodeSpec_GOLANG, //链码的语言
Label: label, // 链码的标签
}
ccPkg, err := lcpackager.NewCCPackage(desc) // 使用lcpackager包中NewCCPackage方法对链码进行打包
if err != nil {
return "", nil, fmt.Errorf("Package chaincode source error: %v", err)
}
return desc.Label, ccPkg, nil
}
- 安装链码
你需要在每个要执行和背书交易的peer节点上安装链码包。使用SDK时,你需要以 Peer Administrator
(peer所在组织的管理员) 的身份来完成这步。链码安装后,你的 peer 节点会构建链码,并且如果你的链码有问题,会返回一个构建错误。**建议每个组织只打包链码一次,然后安装相同的包在属于他们组织的每一个peer节点上。**如果某个通道希望确保每个组织都运行同样的链码,某一个组织可以打包链码并通过带外数据(不通过链上)把它发送给其他通道成员.
通过指令成功安装链码后会返回链码包标识符,它是包标签和包哈希值的结合。这个包标识符用来关联安装在你的peer节点上的链码包已被批准的链码。为下一步的操作保存这个标识符。你也可以查询安装在peer节点上的包来查看包标识符。
func installCC(label string, ccPkg []byte, orgs []*OrgInfo) error {
installCCReq := resmgmt.LifecycleInstallCCRequest{
Label: label,
Package: ccPkg,
}
// 使用lcpackager中的ComputePackageID方法查询并返回链码的packageID
packageID := lcpackager.ComputePackageID(installCCReq.Label, installCCReq.Package)
for _, org := range orgs {
orgPeers, err := DiscoverLocalPeers(*org.OrgAdminClientContext, org.OrgPeerNum)
if err != nil {
fmt.Errorf("DiscoverLocalPeers error: %v", err)
}
// 检查是否安装链码,如果未安装则继续执行
if flag, _ := checkInstalled(packageID, orgPeers[0], org.OrgResMgmt); flag == false {
// 使用resmgmt中的LifecycleInstallCC方法安装链码,其中WithRetry方法为安装不成功时重试安装,DefaultResMgmtOpts为默认的重试安装规则
if _, err := org.OrgResMgmt.LifecycleInstallCC(installCCReq, resmgmt.WithTargets(orgPeers...), resmgmt.WithRetry(retry.DefaultResMgmtOpts)); err != nil {
return fmt.Errorf("LifecycleInstallCC error: %v", err)
}
}
}
return nil
}
//检查是否安装过链码
func checkInstalled(packageID string, peer fab.Peer, client *resmgmt.Client) (bool, error) {
flag := false
resp1, err := client.LifecycleQueryInstalledCC(resmgmt.WithTargets(peer))
if err != nil {
return flag, fmt.Errorf("LifecycleQueryInstalledCC error: %v", err)
}
for _, t := range resp1 {
if t.PackageID == packageID {
flag = true
}
}
return flag, nil
}
- 获取已安装链码包
func getInstalledCCPackage(packageID string, org *OrgInfo) error {
// use org1
orgPeers, err := DiscoverLocalPeers(*org.OrgAdminClientContext, 1)
if err != nil {
return fmt.Errorf("DiscoverLocalPeers error: %v", err)
}
// 使用resmgmt中的LifecycleGetInstalledCCPackage方法,对于给定的packageID检索已安装的链码包
if _, err := org.OrgResMgmt.LifecycleGetInstalledCCPackage(packageID, resmgmt.WithTargets([]fab.Peer{orgPeers[0]}...)); err != nil {
return fmt.Errorf("LifecycleGetInstalledCCPackage error: %v", err)
}
return nil
}
- 查询安装
func queryInstalled(packageID string, org *OrgInfo) error {
orgPeers, err := DiscoverLocalPeers(*org.OrgAdminClientContext, 1)
if err != nil {
return fmt.Errorf("DiscoverLocalPeers error: %v", err)
}
// 使用resmgmt中的LifecycleQueryInstalledCC方法,返回在指定节点上安装的链码packageID
resp1, err := org.OrgResMgmt.LifecycleQueryInstalledCC(resmgmt.WithTargets([]fab.Peer{orgPeers[0]}...))
if err != nil {
return fmt.Errorf("LifecycleQueryInstalledCC error: %v", err)
}
packageID1 := ""
for _, t := range resp1 {
if t.PackageID == packageID {
packageID1 = t.PackageID
}
}
// 查询的packageID与给定的packageID不一致则报错
if !strings.EqualFold(packageID, packageID1) {
return fmt.Errorf("check package id error")
}
return nil
}
-
各组织批准链码
通过 链码定义来管理链码。当通道成员批准一个链码定义,这个批准便作为一个组织在接受链码参数方面的投票。这些同意的组织定义允许通道成员在链码可以在通道上使用之前达成一致意见(同意链码运行在此通道上)。链码定义包含了以下需要持续在组织之间保持一致的参数:
- 名称:应用调用链码时使用的名称。
- 版本:一个版本号或者和给定链码包关联的值。如果你升级链码二进制文件(译者注:打包后的链码文件),你也需要改变你的链码版本。
- 序列号:链码被定义的次数。这个值是一个整数,并且被用来追踪链码的更新次数。例如当你第一次安装并且同意一个链码定义,这个序列号会是1。当你下一次更新链码,序列号会是2。
- 背书策略:哪些组织需要执行并且验证交易输出。背书策略可以表达为传递给 CLI 工具的字符串或者它能参考通道配置中的一个策略。默认情况下,背书策略设置为
Channel/Application/Endorsement
,默认通道中大多数组织为一笔交易背书。 - 集合配置(私有数据集合配置):和你链码相关的私有数据集合定义文件的路径。了解更多关于私有数据集合的信息。
ESCC/VSCC
插件:这个链码使用的定制的背书或者验证插件名称。- 初始化: 如果你使用 Fabric Chaincode Shim API 提供的低级别的 API,你的链码需要包含用来初始化链码的
Init
方法。链码接口需要这个方法,但不必要被你的应用调用。 当你批准一个链码定义时,你可以指定是否Init
方法必须在调用(调用非 init 方法)之前被执行。如果你指定需要Init
,Fabric 会确保Init
方法在链码中的其他方法之前被调用,并且只会被调用一次。 请求执行Init
方法允许你实现链码初始化时运行的逻辑,例如设置一些初始状态。每次你的链码版本更新,你都需要调用Init
来初始化链码,假定链码定义增加了版本号意味着Init
是需要的。
func approveCC(packageID string, ccName, ccVersion string, sequence int64, channelID string, orgs []*OrgInfo, ordererEndpoint string) error {
mspIDs := []string{}
// 获取各个组织的mspID
for _, org := range orgs {
mspIDs = append(mspIDs, org.OrgMspId)
}
// 签名策略,由所有给出的mspid签名
ccPolicy := policydsl.SignedByNOutOfGivenRole(int32(len(mspIDs)), mb.MSPRole_MEMBER, mspIDs)
// approve所需参数
approveCCReq := resmgmt.LifecycleApproveCCRequest{
Name: ccName, // 链码名
Version: ccVersion, // 版本
PackageID: packageID, // 链码包id
Sequence: sequence, // 序列号
EndorsementPlugin: "escc", // 系统内置链码escc
ValidationPlugin: "vscc", // 系统内置链码vscc
SignaturePolicy: ccPolicy, // 组织签名策略
InitRequired: true, // 是否初始化
}
for _, org := range orgs{
orgPeers, err := DiscoverLocalPeers(*org.OrgAdminClientContext, org.OrgPeerNum)
fmt.Printf(">>> chaincode approved by %s peers:\n", org.OrgName)
for _, p := range orgPeers {
fmt.Printf(" %s\n", p.URL())
}
if err!=nil{
return fmt.Errorf("DiscoverLocalPeers error: %v", err)
}
// 使用resmgmt中的LifecycleApproveCC方法为组织批准链码
if _, err := org.OrgResMgmt.LifecycleApproveCC(channelID, approveCCReq, resmgmt.WithTargets(orgPeers...), resmgmt.WithOrdererEndpoint(ordererEndpoint), resmgmt.WithRetry(retry.DefaultResMgmtOpts));err != nil {
fmt.Errorf("LifecycleApproveCC error: %v", err)
}
}
return nil
}
- 查询已批准的链码
func queryApprovedCC(ccName string, sequence int64, channelID string, orgs []*OrgInfo) error {
// queryApproved所需参数
queryApprovedCCReq := resmgmt.LifecycleQueryApprovedCCRequest{
Name: ccName, // 链码名称
Sequence: sequence,// 序列号
}
for _, org := range orgs{
orgPeers, err := DiscoverLocalPeers(*org.OrgAdminClientContext, org.OrgPeerNum)
if err!=nil{
return fmt.Errorf("DiscoverLocalPeers error: %v", err)
}
// Query approve cc
for _, p := range orgPeers {
resp, err := retry.NewInvoker(retry.New(retry.TestRetryOpts)).Invoke(
func() (interface{}, error) {
// LifecycleQueryApprovedCC返回有关已批准的链码定义的信息
resp1, err := org.OrgResMgmt.LifecycleQueryApprovedCC(channelID, queryApprovedCCReq, resmgmt.WithTargets(p))
if err != nil {
return nil, status.New(status.TestStatus, status.GenericTransient.ToInt32(), fmt.Sprintf("LifecycleQueryApprovedCC returned error: %v", err), nil)
}
return resp1, err
},
)
if err != nil {
return fmt.Errorf("Org %s Peer %s NewInvoker error: %v", org.OrgName, p.URL(), err)
}
if resp==nil{
return fmt.Errorf("Org %s Peer %s Got nil invoker", org.OrgName, p.URL())
}
}
}
return nil
}
- 检查智能合约是否就绪
func checkCCCommitReadiness(packageID string, ccName, ccVersion string, sequence int64, channelID string, orgs []*OrgInfo) error {
mspIds := []string{}
for _, org := range orgs {
mspIds = append(mspIds, org.OrgMspId)
}
// 签名策略,由所有给出的mspid签名
ccPolicy := policydsl.SignedByNOutOfGivenRole(int32(len(mspIds)), mb.MSPRole_MEMBER, mspIds)
// 所需所有参数,同上
req := resmgmt.LifecycleCheckCCCommitReadinessRequest{
Name: ccName,
Version: ccVersion,
//PackageID: packageID,
EndorsementPlugin: "escc",
ValidationPlugin: "vscc",
SignaturePolicy: ccPolicy,
Sequence: sequence,
InitRequired: true,
}
for _, org := range orgs{
orgPeers, err := DiscoverLocalPeers(*org.OrgAdminClientContext, org.OrgPeerNum)
if err!=nil{
fmt.Errorf("DiscoverLocalPeers error: %v", err)
}
for _, p := range orgPeers {
resp, err := retry.NewInvoker(retry.New(retry.TestRetryOpts)).Invoke(
func() (interface{}, error) {
// 使用resmgmt中的LifecycleCheckCCCommitReadiness方法检查链代码的“提交准备”,返回组织批准。
resp1, err := org.OrgResMgmt.LifecycleCheckCCCommitReadiness(channelID, req, resmgmt.WithTargets(p))
fmt.Printf("LifecycleCheckCCCommitReadiness cc = %v, = %v\n", ccName, resp1)
if err != nil {
return nil, status.New(status.TestStatus, status.GenericTransient.ToInt32(), fmt.Sprintf("LifecycleCheckCCCommitReadiness returned error: %v", err), nil)
}
flag := true
for _, r := range resp1.Approvals {
flag = flag && r
}
if !flag {
return nil, status.New(status.TestStatus, status.GenericTransient.ToInt32(), fmt.Sprintf("LifecycleCheckCCCommitReadiness returned : %v", resp1), nil)
}
return resp1, err
},
)
if err != nil {
return fmt.Errorf("NewInvoker error: %v", err)
}
if resp==nil{
return fmt.Errorf("Got nill invoker response")
}
}
}
return nil
}
- 提交智能合约定义
一旦足够多的通道成员同意一个链码定义,某个组织能够提交定义到通道。你可以用上述 checkcommitreadiness
方法在将链码定义提交到通道之前,基于哪个通道成员已经批准了该定义,来检查提交链码定义是否应该成功。(根据通道成员同意的状况,来判断提交是否可能成功)。提交交易请求首先发送给通道成员的 peer节点,peer节点会查询链码定义被他们组织同意的状况,并且为定义背书如果所在组织已经同意了。交易然后提交给排序服务,排序服务会把链码定义提交给通道。提交定义交易需要以 Organization Administrator 身份来提交。
链码在被成功提交到通道之前,需要被同意的组织的数量是通过 Channel/Application/LifecycleEndorsement
策略来管理的。默认情况下,这个策略需要通道中大多数的组织来给交易背书。生命周期背书策略不同于链码背书策略。例如,尽管一个链码背书策略只需要一个或两个组织的签名,根据默认策略大多数的通道成员仍然需要批准链码定义。当提交一个通道定义,你需要面向足够多的 peer 组织,以确保你的生命周期背书策略被满足。
你也可以设置 Channel/Application/LifecycleEndorsement
策略为一个签名策略并且明确指明通道上可以批准链码定义的组织集合。这允许你创建一个其中大多数组织作为链码管理者并且治理通道业务逻辑的通道。如果你的通道有大量的Idemix(身份混合,实现零知识证明)组织,你也可以用一个签名策略(策略只需要一个签名),因为这些组织不能批准链码定义或者为链码背书并且可能阻碍通道达成大多数成员同意的结果。
一个组织在不安装链码包的条件下能够批准链码定义。如果一个组织不需要使用链码,他们可以在没有包身份的情况下批准一个链码定义来确保生命周期背书策略被满足。
在链码定义已经提交到通道上后,链码容器会在所有的链码安装到的 peer 节点上启动,来允许通道成员开始使用链码。可能会花费几分钟的时间来启动链码容器。你可以用链码定义来要求调用 Init
方法初始化链码。如果 Init
方法调用是需要的,链码的第一个调用必须是调用 Init
方法。Init
方法的调用服从于链码的背书策略。
func commitCC(ccName, ccVersion string, sequence int64, channelID string, orgs []*OrgInfo, ordererEndpoint string) error{
mspIDs := []string{}
for _, org := range orgs {
mspIDs = append(mspIDs, org.OrgMspId)
}
ccPolicy := policydsl.SignedByNOutOfGivenRole(int32(len(mspIDs)), mb.MSPRole_MEMBER, mspIDs)
// commit所需参数信息,内容同上
req := resmgmt.LifecycleCommitCCRequest{
Name: ccName,
Version: ccVersion,
Sequence: sequence,
EndorsementPlugin: "escc",
ValidationPlugin: "vscc",
SignaturePolicy: ccPolicy,
InitRequired: true,
}
// LifecycleCommitCC将链代码提交给给定的通道
_, err := orgs[0].OrgResMgmt.LifecycleCommitCC(channelID, req, resmgmt.WithOrdererEndpoint(ordererEndpoint), resmgmt.WithRetry(retry.DefaultResMgmtOpts))
if err != nil {
return fmt.Errorf("LifecycleCommitCC error: %v", err)
}
return nil
}
- 查询已提交的智能合约定义
func queryCommittedCC( ccName string, channelID string, sequence int64, orgs []*OrgInfo) error {
req := resmgmt.LifecycleQueryCommittedCCRequest{
Name: ccName,
}
for _, org := range orgs {
orgPeers, err := DiscoverLocalPeers(*org.OrgAdminClientContext, org.OrgPeerNum)
if err!=nil{
return fmt.Errorf("DiscoverLocalPeers error: %v", err)
}
for _, p := range orgPeers {
resp, err := retry.NewInvoker(retry.New(retry.TestRetryOpts)).Invoke(
func() (interface{}, error) {
// LifecycleQueryCommittedCC查询给定通道上提交的链码
resp1, err := org.OrgResMgmt.LifecycleQueryCommittedCC(channelID, req, resmgmt.WithTargets(p))
if err != nil {
return nil, status.New(status.TestStatus, status.GenericTransient.ToInt32(), fmt.Sprintf("LifecycleQueryCommittedCC returned error: %v", err), nil)
}
flag := false
for _, r := range resp1 {
if r.Name == ccName && r.Sequence == sequence {
flag = true
break
}
}
if !flag {
return nil, status.New(status.TestStatus, status.GenericTransient.ToInt32(), fmt.Sprintf("LifecycleQueryCommittedCC returned : %v", resp1), nil)
}
return resp1, err
},
)
if err != nil {
return fmt.Errorf("NewInvoker error: %v", err)
}
if resp==nil{
return fmt.Errorf("Got nil invoker response")
}
}
}
return nil
}
- 智能合约初始化
func initCC(ccName string, upgrade bool, channelID string, org *OrgInfo, sdk *fabsdk.FabricSDK) error {
// 准备通道客户端上下文
clientChannelContext := sdk.ChannelContext(channelID, fabsdk.WithUser(org.OrgUser), fabsdk.WithOrg(org.OrgName))
// 通道客户端用于查询执行交易
client, err := channel.New(clientChannelContext)
if err != nil {
return fmt.Errorf("Failed to create new channel client: %s", err)
}
// 调用链码初始化
_, err = client.Execute(channel.Request{ChaincodeID: ccName, Fcn: "init", Args: nil, IsInit: true},
channel.WithRetry(retry.DefaultChannelOpts))
if err != nil {
return fmt.Errorf("Failed to init: %s", err)
}
return nil
}
- 智能合约完整生命周期(即整合调用上述方法)
func CreateCCLifecycle(info *SdkEnvInfo, sequence int64, upgrade bool, sdk *fabsdk.FabricSDK) error {
if len(info.Orgs) == 0 {
return fmt.Errorf("the number of organization should not be zero.")
}
// 打包链码
fmt.Println(">> 开始打包链码......")
label, ccPkg, err := packageCC(info.ChaincodeID, info.ChaincodeVersion, info.ChaincodePath)
if err != nil {
return fmt.Errorf("pakcagecc error: %v", err)
}
packageID := lcpackager.ComputePackageID(label, ccPkg)
fmt.Println(">> 打包链码成功")
// 安装链码
fmt.Println(">> 开始安装链码......")
if err := installCC(label, ccPkg, info.Orgs); err != nil {
return fmt.Errorf("installCC error: %v", err)
}
// 检索已安装链码包
if err := getInstalledCCPackage(packageID, info.Orgs[0]); err != nil {
return fmt.Errorf("getInstalledCCPackage error: %v", err)
}
// 查询已安装链码
if err := queryInstalled(packageID, info.Orgs[0]); err != nil {
return fmt.Errorf("queryInstalled error: %v", err)
}
fmt.Println(">> 安装链码成功")
// 批准链码
fmt.Println(">> 组织认可智能合约定义......")
if err := approveCC(packageID, info.ChaincodeID, info.ChaincodeVersion, sequence, info.ChannelID, info.Orgs, info.OrdererEndpoint); err != nil {
return fmt.Errorf("approveCC error: %v", err)
}
// 查询批准
if err:=queryApprovedCC(info.ChaincodeID, sequence, info.ChannelID, info.Orgs);err!=nil{
return fmt.Errorf("queryApprovedCC error: %v", err)
}
fmt.Println(">> 组织认可智能合约定义完成")
// 检查智能合约是否就绪
fmt.Println(">> 检查智能合约是否就绪......")
if err:=checkCCCommitReadiness(packageID, info.ChaincodeID, info.ChaincodeVersion, sequence, info.ChannelID, info.Orgs); err!=nil{
return fmt.Errorf("checkCCCommitReadiness error: %v", err)
}
fmt.Println(">> 智能合约已经就绪")
// Commit
fmt.Println(">> 提交智能合约定义......")
if err:=commitCC(info.ChaincodeID, info.ChaincodeVersion, sequence, info.ChannelID, info.Orgs, info.OrdererEndpoint);err!=nil{
return fmt.Errorf("commitCC error: %v", err)
}
// 查询Commit结果
if err:=queryCommittedCC(info.ChaincodeID, info.ChannelID, sequence, info.Orgs); err!=nil{
return fmt.Errorf("queryCommittedCC error: %v", err)
}
fmt.Println(">> 智能合约定义提交完成")
// 初始化
fmt.Println(">> 调用智能合约初始化方法......")
if err:=initCC(info.ChaincodeID, upgrade, info.ChannelID, info.Orgs[0], sdk); err!=nil{
return fmt.Errorf("initCC error: %v", err)
}
fmt.Println(">> 完成智能合约初始化")
return nil
}
5、启动项目 #
main方法 #
在项目的根目录下新建一个名为main.go
的文件,为项目的主函数。在这里我们实现将组织通道等相关信息实例化,以及调用前面的函数实现创建通道加入通道将链码实例化。
import (
"fmt"
"sdktest/sdkInit"
"os"
)
const (
cc_name = "simplecc"
cc_version = "1.0.0"
)
func main() {
// init orgs information
orgs := []*sdkInit.OrgInfo{
{
OrgAdminUser: "Admin",
OrgName: "Org1",
OrgMspId: "Org1MSP",
OrgUser: "User1",
OrgPeerNum: 1,
OrgAnchorFile: os.Getenv("GOPATH") + "/src/sdktest/fixtures/channel-artifacts/Org1MSPanchors.tx",
},
}
// init sdk env info
info := sdkInit.SdkEnvInfo{
ChannelID: "mychannel",
ChannelConfig: os.Getenv("GOPATH") + "/src/sdktest/fixtures/channel-artifacts/channel.tx",
Orgs: orgs,
OrdererAdminUser: "Admin",
OrdererOrgName: "OrdererOrg",
OrdererEndpoint: "orderer.example.com",
ChaincodeID: cc_name,
ChaincodePath: os.Getenv("GOPATH") + "/src/sdktest/chaincode/",
ChaincodeVersion: cc_version,
}
// sdk setup
sdk, err := sdkInit.Setup("config.yaml", &info)
if err != nil {
fmt.Println(">> SDK setup error:", err)
os.Exit(-1)
}
// create channel and join
if err := sdkInit.CreateAndJoinChannel(&info); err != nil {
fmt.Println(">> Create channel and join error:", err)
os.Exit(-1)
}
// create chaincode lifecycle
if err := sdkInit.CreateCCLifecycle(&info, 1, false, sdk); err != nil {
fmt.Println(">> create chaincode lifecycle error: %v", err)
os.Exit(-1)
}
// invoke chaincode set status
fmt.Println(">> 通过链码外部服务设置链码状态......")
}
2. 添加链码文件 #
在项目根目录下新建一个名为chaincode
的文件夹….
详情请见智能合约
3. 启动项目 #
在命令行中进入chaincode路径,并使用以下命令为链码添加依赖包。
go mod init
go mod vendor
在命令行中进入项目根目录,并使用以下命令为项目添加依赖包。
go mod init
go mod tidy
在命令行中进入fixtures路径,并使用以下命令启动网络。
docker-compose up -d
在命令行中进入项目根目录,并使用以下命令build整个项目。
go build
接着使用以下命令运行项目。
./sdktest
运行成功后输出:
>> 开始创建通道......
>>>> 使用每个org的管理员身份更新锚节点配置...
>>>> 使用每个org的管理员身份更新锚节点配置完成
>> 创建通道成功
>> 加入通道......
>> 加入通道成功
>> 开始打包链码......
>> 打包链码成功
>> 开始安装链码......
>> 安装链码成功
>> 组织认可智能合约定义......
>>> chaincode approved by Org1 peers:
peer0.org1.example.com:7051
peer1.org1.example.com:9051
>> 组织认可智能合约定义完成
>> 检查智能合约是否就绪......
LifecycleCheckCCCommitReadiness cc = simplecc, = {map[Org1MSP:true]}
LifecycleCheckCCCommitReadiness cc = simplecc, = {map[Org1MSP:true]}
>> 智能合约已经就绪
>> 提交智能合约定义......
>> 智能合约定义提交完成
>> 调用智能合约初始化方法......
>> 完成智能合约初始化
>> 通过链码外部服务设置链码状态......