目录

Fabric 智能合约开发

目录

[toc]

需安装的程序如下,详情可参阅:Setting up the development environment

  • Git client
  • Go【安装完成后要配置 GOPATH】
  • cURL
  • Docker version 17.06 or later
  • (macOS) Xcode Command Line Tools

下面,将下载测试网络并进行测试。

在终端中输入命令:

curl -sSL https://bit.ly/2ysbOFE | bash -s

上述命令在下载 Docker 镜像是需要的时间较长,可能在几个小时。

如果在运行上述 curl 命令时遇到错误,则可能是因为 curl 版本太旧而无法处理重定向或不支持的环境。

该命令运行的脚本做了什么?

  1. 克隆 hyperledger / fabric-samples 存储库
  2. 检出适当的版本标签
  3. 将 Hyperledger Fabric 平台特定的二进制文件和配置文件安装到Fabric-samples的/ bin和/ config目录中指定的版本
  4. 下载指定版本的 Hyperledger Fabric docker 映像

安装完之后可通过在本地计算机上运行该网络来了解 Fabric, 更有经验的开发人员可使用网络来测试其智能合约和应用程序,该网络只用作教育和测试的工具, 不应用作部署生产网络的模板。

fabric-samples 存储库的 test-network 目录中找到启动网络的脚本 network.sh

https://cdn.jsdelivr.net/gh/dfface/img0@master/0/0DBynL-64ARUT.png

启动 test network:

cd fabric-samples/test-network
./network.sh -h
./network.sh up
./network.sh down

./network.sh up 命令启动网络成功的效果应为:

Creating network "net_test" with the default driver
Creating volume "net_orderer.example.com" with default driver
Creating volume "net_peer0.org1.example.com" with default driver
Creating volume "net_peer0.org2.example.com" with default driver
Creating orderer.example.com    ... done
Creating peer0.org2.example.com ... done
Creating peer0.org1.example.com ... done
CONTAINER ID        IMAGE                               COMMAND             CREATED             STATUS                  PORTS                              NAMES
8d0c74b9d6af        hyperledger/fabric-orderer:latest   "orderer"           4 seconds ago       Up Less than a second   0.0.0.0:7050->7050/tcp             orderer.example.com
ea1cf82b5b99        hyperledger/fabric-peer:latest      "peer node start"   4 seconds ago       Up Less than a second   0.0.0.0:7051->7051/tcp             peer0.org1.example.com
cd8d9b23cb56        hyperledger/fabric-peer:latest      "peer node start"   4 seconds ago       Up 1 second             7051/tcp, 0.0.0.0:9051->9051/tcp   peer0.org2.example.com

一个网络所有的成员组织通常被称为联盟。该测试网络有两个联盟成员:Org1和Org2。该网络还包括一个 orderer 组织,它维护网络的 order 服务。

peer 是任何 Fabric 网络的基本组成部分,它存储区块链账本并在将交易提交到账本之前验证交易,运行包含业务逻辑的智能合约。网络中的任何 peer 都必须属于联盟的一个成员。在 test network 中每个组织各自操作一个 peer,分别是peer0.org1.example.compeer0.org2.example.com

每个 Fabric 网络还包括 order 服务。 当 peer 验证 transaction(事务) 或将 block of transactions(事务块) 添加到区块链中时,它不决定事务的顺序,也不决定是否将事务包含到新的块中。 order 服务允许 peer 专注于验证交易并将其提交到总账。 orderer 节点从客户端接收到认可的事务之后,它们就事务的顺序达成一致,然后将其添加到块中。然后将这些块分发给 peer 节点,这些节点将这些块添加到区块链中。orderer 节点还操作定义 Fabric 网络的 channel (通道),例如如何制作块以及节点可以使用哪个版本的Fabric,通道定义了哪些组织是联盟的成员。

test network 使用了一个节点的 Raft order 服务:orderer.example.com,此网络仅用了一个 order 节点,真实的网络将会有更多的,不同的 order 节点将使用 Raft 共识算法来就网络上的事务顺序达成一致。

现在的测试网络有两个 peer 节点和一个 orderer 节点,我们可以使用该脚本为 org1 和 org2 之间的事务创建一个通道,通道是特定网络成员之间的专用通信层,只能由受邀加入该通道的组织使用,并且对网络的其他成员不可见,每个通道都有一个单独的区块链账本。

创建一个通道:

./network.sh createChannel -c mychannel # 名称为 mychannel 的通道

在创建了一个通道之后,就可以开始使用智能合约来与通道账本交互。

在Fabric中,智能合约以称为 chaincode(链码) 的软件包部署在网络上。 链码安装在组织的 peer 上,然后部署到通道,然后可以在该通道中用于交易并与区块链账本进行交互。 在将链码部署到通道之前,通道成员需要就链码定义达成共识。 当所需的组织数目达成一致时,可以将链码定义提交给通道,并准备使用链码。

启动链码:

./network.sh deployCC

初始化账本:

export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=$PWD/../config/
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n basic --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"InitLedger","Args":[]}'

查询账本:

peer chaincode query -C mychannel -n basic -c '{"Args":["GetAllAssets"]}'

转移资产:

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n basic --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"TransferAsset","Args":["asset6","Christopher"]}'

查询资产:

export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051

peer chaincode query -C mychannel -n basic -c '{"Args":["ReadAsset","asset6"]}'

智能合约的定义默认使用的 go 语言,智能合约如何编写将在下文介绍。

本节内容仅介绍如何定义和安装一个链码,更多内容请参阅:Fabric chaincode lifecycle

链码是定义单项或多项资产的软件,和能修改资产的交易指令;换句话说,它是业务逻辑。链码强制执行读取或更改键值对或其他状态数据库信息的规则。链码函数针对账本的当前状态数据库执行,并通过交易提案启动。链码执行会写入一组键值(写集),会被提交给网络并应用于所有节点的账本。

账本包含了与一组业务对象的当前和历史状态有关的事实,而智能合约定义了生成这些被添加到账本中的新事实的可执行逻辑。管理员通常使用链码将相关的智能合约组织起来进行部署,并且链码也可以用于 Fabric 的底层系统编程。

Hyperledger Fabric 用户经常交替使用智能合约链码。通常,智能合约定义的是控制状态中业务对象生命周期的交易逻辑,随后该交易逻辑被打包进链码,紧接着链码会被部署到区块链网络中。

链码是一个程序,用Go、Node.js 或者 Java 的指定接口的实现。链码运行在一个安全的 Docker 容器中,与背书节点进程相隔离。链码通过应用程序提交的事务来初始化和管理账本状态。

一些关键概念的中文解释可查阅:关键概念

Fabric链码生命周期要求组织同意定义链码的参数,例如名称、版本和链码背书策略。渠道成员通过以下四个步骤达成一致。并不是每个组织都需要完成每一步。

  1. 打包链码:可以由一个组织或每个组织完成此步骤。

  2. 在对等节点上安装链码:每个将使用链码来认可交易或查询帐本的组织都需要完成此步骤。

  3. 批准组织的链码定义:每个将使用链码的组织都需要完成此步骤。 链码定义需要得到足够多的组织的批准,才能满足该频道的LifecycleEndorsment策略(默认情况),然后才能在该频道上启动链码。

  4. 将链码定义提交到通道:一旦渠道上所需数量的组织批准了该链码定义,就需要由一个组织提交。 提交者首先从已经批准的组织的足够的对等节点那里收集背书,然后提交交易以提交链码的定义。

链码需要打包到一个tar文件中,然后才能安装到对等节点中。

可以使用 Fabric peer 命令、Node Fabric SDK 或第三方工具如 GNU tar(第三方工具打包需满足预定的格式,而 Fabric 提供的工具自动完成) 进行打包,打包时需要提供链码包标签以简明地描述该包。

https://cdn.jsdelivr.net/gh/dfface/img0@master/0/76vRjb-9RjECX.jpg

如图,链码分别由Org1和Org2打包。这两个组织都使用MYCC 1作为他们的包标签,以便使用名称和版本识别包。组织不需要使用相同的包标签。

需要在将执行交易和交易背书的每个对等节点上安装链码包。

无论使用CLI还是SDK,都需要使用管理员身份来完成此步骤。

对等节点将在链码安装后构建链码,如果链码有问题,将返回一个构建错误。

成功的安装命令将返回一个链码包标识符,它是包标签与包的散列值的组合。

保存该标识符用于下一步,也可使用 peer 命令获取安装的所有包的标识符。

https://cdn.jsdelivr.net/gh/dfface/img0@master/0/XbegpC-ayzBqc.jpg

建议组织仅打包一次链码,然后在属于其组织的每个对等方上安装相同的软件包。 如果某个通道想要确保每个组织都运行相同的链码,则一个组织可以打包一个链码并将其发送到其他通道成员。

链码由链码定义控制。当一个通道的成员们核准一个链码定义的时候,该过程就像一个组织在其可接受的链码参数上的投票,批准后需要被管理员提交到订阅服务,此后将分布于所有对等节点中。链码定义包含了以下参数,需要得到组织间的一致同意。

Name、Version、Sequence、Endorsement Policy、Collection Configuration、ESCC/VSCC Plugins、Initialization

链码定义也包含包标识符。

https://cdn.jsdelivr.net/gh/dfface/img0@master/0/H5yBO4-B3rtpL.jpg

一旦有足够数量的通道成员批准了链码定义,组织就可以将定义提交给通道。

可使用 checkcommitreadinesspeer 命令来检查提交的链码定义是否成功(是否有足够的成员批准了)。

管理员的提交交易请求首先发送到了通道成员的对等节点,它们背书之后才发送到订阅服务,然后才提交链码定义给通道。

多少成员批准了才是足够的?由 Channel/Application/LifecycleEndorsement 策略控制,该策略默认需要大量组织背书,该策略与链码背书策略是分开的,即使链码背书策略仅需要少量组织同意,它还是默认要大量组织同意。当然,策略是可以更改的。

https://cdn.jsdelivr.net/gh/dfface/img0@master/0/ABjeft-AwggUr.jpg

组织可以批准链码定义而不需要安装链码包。如果一个组织不需要使用链码,他们可以批准一个没有包标识符的链码定义,以确保生命周期背书策略得到满足。

在将链码定义提交到通道之后,链码容器将在安装了链码的所有节点上启动,允许通道成员开始使用链码。链码容器启动可能需要几分钟时间。

https://cdn.jsdelivr.net/gh/dfface/img0@master/0/hr93D4-OI81An.jpg

根据前面了解到的《Fabric 链码生命周期》,本节介绍了如何将链码部署到通道,参阅:Deploying a smart contract to a channel

  • 打包链码
  • 安装链码
  • 核准链码定义
  • 提交链码定义到通道
cd fabric-samples/test-network
./network.sh down
./network.sh up createChannel

createChannel 命令创建一个名为 mychannel 的通道,带有两个通道成员Org1和Org2。该命令还将属于每个组织的对等点加入到通道中。

此步骤不是必需的,但对于链码故障诊断非常有用。要监视智能契约的日志,管理员可以使用 logspout 工具查看一组 Docker 容器的聚合输出。

Logspout工具将不断地将日志流传输到终端,因此需要使用一个新的终端窗口。打开一个新的终端并导航到test-network目录,已经写好了一个安装和配置的脚本了,可直接使用。

cd fabric-samples/test-network
cp ../commercial-paper/organization/digibank/configuration/cli/monitordocker.sh .
./monitordocker.sh net_test

不同语言( Go, JavaScript, 或 Typescript)编写的链码,打包过程是不一样的,以 Go 语言为例。

该示例使用Go模块来安装链码依赖项。 依赖关系列在 asset-transfer-basic/chaincode-go目录的 go.mod 文件中。该文件导入了 Fabric Contract API,可从 asset-transfer-basic/chaincode-go/chaincode/smartcontract.go 中查看定义的 SmartContract 类型。

cd fabric-samples/asset-transfer-basic/chaincode-go

SmartContract 类型用于为智能合约中定义的函数创建事务上下文,这些函数读取和写入数据到区块链账本。

// SmartContract provides functions for managing an Asset
type SmartContract struct {
    contractapi.Contract
}
// Asset describes basic details of what makes up a simple asset
type Asset struct {
	ID             string `json:"ID"`
	Color          string `json:"color"`
	Size           int    `json:"size"`
	Owner          string `json:"owner"`
	AppraisedValue int    `json:"appraisedValue"`
}
// InitLedger adds a base set of assets to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
	assets := []Asset{
		{ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
		{ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
		{ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
		{ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
		{ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
		{ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
	}

	for _, asset := range assets {
		assetJSON, err := json.Marshal(asset)
		if err != nil {
			return err
		}

		err = ctx.GetStub().PutState(asset.ID, assetJSON)
		if err != nil {
			return fmt.Errorf("failed to put to world state. %v", err)
		}
	}

	return nil
}
// CreateAsset issues a new asset to the world state with given details.
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
    exists, err := s.AssetExists(ctx, id)
    if err != nil {
        return err
    }
    if exists {
        return fmt.Errorf("the asset %s already exists", id)
    }

    asset := Asset{
        ID:             id,
        Color:          color,
        Size:           size,
        Owner:          owner,
        AppraisedValue: appraisedValue,
    }
    assetJSON, err := json.Marshal(asset)
    if err != nil {
        return err
    }

    return ctx.GetStub().PutState(id, assetJSON)
}

可通过访问API文档智能合约处理来了解有关Go contract API的更多信息。

要安装智能合约依赖关系,从 asset-transfer-basic/chaincode-go 目录运行以下命令。如果命令成功,go包将安装在一个 vendor 文件夹中。

GO111MODULE=on go mod vendor 

现在已经有了依赖项,可以创建链码包了。返回到 test-network 文件夹中的工作目录,这样就可以将链码与其他网络工件打包在一起了。

cd ../../test-network
export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=$PWD/../config/
peer lifecycle chaincode package basic.tar.gz --path ../asset-transfer-basic/chaincode-go/ --lang golang --label basic_1.0

这个命令将在当前目录中创建一个名为 basic.tar.gz 的包。--lang标志用于指定链码语言,--path标志提供智能合约代码的位置。该路径必须是完全限定的路径或相对于当前工作目录的路径。--label标志用于指定一个链码标签,该标签将在安装后标识链码。建议标签包括链码名称和版本。

现在我们已经创建了链码包,可以在测试网络的对等节点上安装链码了。

链码需要安装在每一个将认可交易的对等节点上。

先在Org1对等节点上安装链码。设置以下环境变量以作为Org1 admin用户操作对等CLI。核心对等点地址将被设置为指向Org1对等节点,peer0.org1.example.com。

export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
peer lifecycle chaincode install basic.tar.gz

export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051

peer lifecycle chaincode install basic.tar.gz

如果命令成功,对等节点将生成并返回包标识符。这个包ID将在下一步中用于批准链码。

peer lifecycle chaincode queryinstalled
Installed chaincodes on peer:
Package ID: basic_1.0:69de748301770f6ef64b42aa6bb6cb291df20aa39542c3ef94008615704007f3, Label: basic_1.0
export CC_PACKAGE_ID=basic_1.0:69de748301770f6ef64b42aa6bb6cb291df20aa39542c3ef94008615704007f3

peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_ADDRESS=localhost:7051

peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

在足够多的组织批准了链码定义之后,一个组织可以将链码定义提交给通道。

peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name basic --version 1.0 --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --output json

    {
            "Approvals": {
                    "Org1MSP": true,
                    "Org2MSP": true
            }
    }
  
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
peer lifecycle chaincode querycommitted --channelID mychannel --name basic --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
Committed chaincode definition for chaincode 'basic' on channel 'mychannel':
Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc, Approvals: [Org1MSP: true, Org2MSP: true]

这里以命令行调用为例,也可编写 Node.js 、Go、Java程序。

peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n basic --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"initLedger","Args":[]}'
peer chaincode query -C mychannel -n basic -c '{"Args":["getAllAssets"]}'
docker stop logspout
docker rm logspout
./network.sh down

前面介绍了链码的安装,网络的测试,本节将介绍链码如何使用 Go 语言开发,目前 Fabric 对 Go 支持更为友好。本节将展示一个简单的链码示例应用程序,并介绍链码 Shim API 中每种方法的用途。本节参阅:Chaincode for Developers v 2.2

本教程概述了Fabric Chaincode Shim API提供的底层API。还可使用Fabric Contract API提供的高级API,可使用 Java 、JavaScript 方式编写,请查看智能合约处理

该应用程序是一个基本的链码示例(使用 Go 语言),该示例将一步步描述如何编写链码,用于在账本上创建资产(键-值对)。

mkdir sacc && cd sacc
go mod init sacc
touch sacc.go

为链码的必要依赖项添加Go导入语句。

package main

import (
    "fmt"

    "github.com/hyperledger/fabric-chaincode-go/shim"
    "github.com/hyperledger/fabric-protos-go/peer"
)

// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}
// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data, so be careful to avoid a scenario where you
// inadvertently clobber your ledger's data!
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
  // Get the args from the transaction proposal
  args := stub.GetStringArgs()
  if len(args) != 2 {
    return shim.Error("Incorrect arguments. Expecting a key and a value")
  }

  // Set up any variables or assets here by calling stub.PutState()

  // We store the key and the value on the ledger
  err := stub.PutState(args[0], []byte(args[1]))
  if err != nil {
    return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
  }
  return shim.Success(nil)
}

Invoke 函数的参数将是要调用的 chaincode 应用程序函数的名称。 此应用程序将仅具有两个功能:setget,用于设置资产的值或检索其当前状态。 首先调用ChaincodeStubInterface.GetFunctionAndParameters 来提取函数名称和该链码函数的参数。

然后验证函数名称是 set 还是 get ,然后调用那些链码函数,并通过 shim.Successshim.Error 函数返回适当的响应,这些响应会将响应序列化为 gRPC protobuf 消息。

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
    // Extract the function and args from the transaction proposal
    fn, args := stub.GetFunctionAndParameters()

    var result string
    var err error
    if fn == "set" {
            result, err = set(stub, args)
    } else {
            result, err = get(stub, args)
    }
    if err != nil {
            return shim.Error(err.Error())
    }

    // Return the result as success payload
    return shim.Success([]byte(result))
}

为访问账本状态,将利用 ChaincodeStubInterface.PutStateChaincodeStubInterface.GetState 函数。

// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 2 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
    }

    err := stub.PutState(args[0], []byte(args[1]))
    if err != nil {
            return "", fmt.Errorf("Failed to set asset: %s", args[0])
    }
    return args[1], nil
}

// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 1 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key")
    }

    value, err := stub.GetState(args[0])
    if err != nil {
            return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
    }
    if value == nil {
            return "", fmt.Errorf("Asset not found: %s", args[0])
    }
    return string(value), nil
}

更多Go语言的API,请参阅package shim for go v2.2

常用的接口:

// State 操作
GetState(key string) ([]byte, error)
PutState(key string, value []byte) error
DelState(key string) error
RangeQueryState(startKey, endKey string) (StateRangeQueryIteratorInterface, error)
// Chaincode相互调⽤
InvokeChaincode(chaincodeName string, args [][]byte) ([]byte, error)QueryChaincode(chaincodeName string, args [][]byte) ([]byte, error)

最后,我们需要添加 main 函数,它将调用 shim 。函数开始。这里是整个链码程序的源代码。

package main

import (
    "fmt"

    "github.com/hyperledger/fabric-chaincode-go/shim"
    "github.com/hyperledger/fabric-protos-go/peer"
)

// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}

// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
    // Get the args from the transaction proposal
    args := stub.GetStringArgs()
    if len(args) != 2 {
            return shim.Error("Incorrect arguments. Expecting a key and a value")
    }

    // Set up any variables or assets here by calling stub.PutState()

    // We store the key and the value on the ledger
    err := stub.PutState(args[0], []byte(args[1]))
    if err != nil {
            return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
    }
    return shim.Success(nil)
}

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
    // Extract the function and args from the transaction proposal
    fn, args := stub.GetFunctionAndParameters()

    var result string
    var err error
    if fn == "set" {
            result, err = set(stub, args)
    } else { // assume 'get' even if fn is nil
            result, err = get(stub, args)
    }
    if err != nil {
            return shim.Error(err.Error())
    }

    // Return the result as success payload
    return shim.Success([]byte(result))
}

// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 2 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
    }

    err := stub.PutState(args[0], []byte(args[1]))
    if err != nil {
            return "", fmt.Errorf("Failed to set asset: %s", args[0])
    }
    return args[1], nil
}

// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    if len(args) != 1 {
            return "", fmt.Errorf("Incorrect arguments. Expecting a key")
    }

    value, err := stub.GetState(args[0])
    if err != nil {
            return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
    }
    if value == nil {
            return "", fmt.Errorf("Asset not found: %s", args[0])
    }
    return string(value), nil
}

// main function starts up the chaincode in the container during instantiate
func main() {
    if err := shim.Start(new(SimpleAsset)); err != nil {
            fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
    }
}

通过调用 GetCreator() 函数,Chaincode可以将客户端(提交者)证书用于访问控制决策。此外,Go shim提供了从提交者证书中提取客户端身份的扩展api,这些身份可用于访问控制决策,无论这是基于客户端身份本身,还是基于org身份,还是基于客户端身份属性。

例如,表示为键/值的资产可以将客户端的身份包括为值的一部分(例如,作为表示该资产所有者的JSON属性),并且只有该客户端可以被授权对键/值进行更新。 客户端身份库扩展API可以在链码中使用,以检索此提交者信息以做出此类访问控制决策。

有关更多细节,请参阅客户端标识(CID)库文档

若要将客户端标识shim扩展作为依赖项添加到链码,请参见用Go编写的链码的外部依赖项管理

您的Go链码依赖于不属于标准库的Go包(如链码shim)。当安装到对等节点时,这些包的源代码必须包含在您的链码包中。如果您已经将您的链码结构化为一个模块,最简单的方法是在打包链码之前使用go mod vendor提供依赖关系。

go mod tidy
go mod vendor

这样就将链码的外部依赖项放置到本地 vendor 目录中。

一旦依赖关系在链码目录中被找到,peer chaincode packagepeer chaincode install操作就会将与依赖关系相关的代码包含到链码包中。

一般使用 Go 语言编写链码,打包安装部署到节点后,可使用 Java、Go、JavaScript 等语言编写相关业务代码执行智能合约。

开发链码主要用到的语言是 Go,因为 fabric 对它的支持目前最好。而应用层使用 其它语言也可以,nodejs是比较通用和快速的选择。

部署和调用链码之前需要先创建通道、加入通道、更新锚节点,然后才能进行有关链码的操作,比如安装链码、实例化链码、invoke和query等。

可查看 fabric-samples 下的 fabcar 文件夹中使用 Go 编写的链码(位置在 fabric-samples/chaincode/fabcar/go/fabcar.go 文件)如下:

package main

import (
	"encoding/json"
	"fmt"
	"strconv"

	"github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SmartContract provides functions for managing a car
type SmartContract struct {
	contractapi.Contract
}

// Car describes basic details of what makes up a car
type Car struct {
	Make   string `json:"make"`
	Model  string `json:"model"`
	Colour string `json:"colour"`
	Owner  string `json:"owner"`
}

// QueryResult structure used for handling result of query
type QueryResult struct {
	Key    string `json:"Key"`
	Record *Car
}

// InitLedger adds a base set of cars to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
	cars := []Car{
		Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
		Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
		Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
		Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
		Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
		Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
		Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
		Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
		Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
		Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
	}

	for i, car := range cars {
		carAsBytes, _ := json.Marshal(car)
		err := ctx.GetStub().PutState("CAR"+strconv.Itoa(i), carAsBytes)

		if err != nil {
			return fmt.Errorf("Failed to put to world state. %s", err.Error())
		}
	}

	return nil
}

// 增
// CreateCar adds a new car to the world state with given details
func (s *SmartContract) CreateCar(ctx contractapi.TransactionContextInterface, carNumber string, make string, model string, colour string, owner string) error {
	car := Car{
		Make:   make,
		Model:  model,
		Colour: colour,
		Owner:  owner,
	}

	carAsBytes, _ := json.Marshal(car)

	return ctx.GetStub().PutState(carNumber, carAsBytes)
}

// 查
// QueryCar returns the car stored in the world state with given id
func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) {
	carAsBytes, err := ctx.GetStub().GetState(carNumber)

	if err != nil {
		return nil, fmt.Errorf("Failed to read from world state. %s", err.Error())
	}

	if carAsBytes == nil {
		return nil, fmt.Errorf("%s does not exist", carNumber)
	}

	car := new(Car)
	_ = json.Unmarshal(carAsBytes, car)

	return car, nil
}

// 查
// QueryAllCars returns all cars found in world state
func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) {
	startKey := ""
	endKey := ""

	resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey)

	if err != nil {
		return nil, err
	}
	defer resultsIterator.Close()

	results := []QueryResult{}

	for resultsIterator.HasNext() {
		queryResponse, err := resultsIterator.Next()

		if err != nil {
			return nil, err
		}

		car := new(Car)
		_ = json.Unmarshal(queryResponse.Value, car)

		queryResult := QueryResult{Key: queryResponse.Key, Record: car}
		results = append(results, queryResult)
	}

	return results, nil
}

// 改
// ChangeCarOwner updates the owner field of car with given id in world state
func (s *SmartContract) ChangeCarOwner(ctx contractapi.TransactionContextInterface, carNumber string, newOwner string) error {
	car, err := s.QueryCar(ctx, carNumber)

	if err != nil {
		return err
	}

	car.Owner = newOwner

	carAsBytes, _ := json.Marshal(car)

	return ctx.GetStub().PutState(carNumber, carAsBytes)
}

func main() {

	chaincode, err := contractapi.NewChaincode(new(SmartContract))

	if err != nil {
		fmt.Printf("Error create fabcar chaincode: %s", err.Error())
		return
	}

	if err := chaincode.Start(); err != nil {
		fmt.Printf("Error starting fabcar chaincode: %s", err.Error())
	}
}

其应用层 Java 客户端(位置在 fabric-samples/fabcar/java )例子如下,可使用 IDE 打开(使用Gradle自动下载依赖):

public class ClientApp {

	static {
		System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true");
	}

	public static void main(String[] args) throws Exception {
		// Load a file system based wallet for managing identities.
		Path walletPath = Paths.get("wallet");
		Wallet wallet = Wallets.newFileSystemWallet(walletPath);
		// load a CCP
		Path networkConfigPath = Paths.get("..", "..", "test-network", "organizations", "peerOrganizations", "org1.example.com", "connection-org1.yaml");

		Gateway.Builder builder = Gateway.createBuilder();
		builder.identity(wallet, "appUser").networkConfig(networkConfigPath).discovery(true);

		// create a gateway connection
		try (Gateway gateway = builder.connect()) {

			// get the network and contract
			Network network = gateway.getNetwork("mychannel");
			Contract contract = network.getContract("fabcar");

			byte[] result;

			result = contract.evaluateTransaction("queryAllCars");
			System.out.println(new String(result));

			contract.submitTransaction("createCar", "CAR10", "VW", "Polo", "Grey", "Mary");

			result = contract.evaluateTransaction("queryCar", "CAR10");
			System.out.println(new String(result));

			contract.submitTransaction("changeCarOwner", "CAR10", "Archie");

			result = contract.evaluateTransaction("queryCar", "CAR10");
			System.out.println(new String(result));
		}
	}

}

在fabric-samples 下的 fabcar 项目中,使用startFabric.sh up启动网络后(在启动网络的过程中,脚本执行过程中也能验证上面《部署一个智能合约到某通道》的相关结果),可运行测试程序,得出的打印结果如下:

Successfully enrolled user "admin" and imported it into the wallet
Successfully enrolled user "appUser" and imported it into the wallet
[{"Key":"CAR0","Record":{"make":"Toyota","model":"Prius","colour":"blue","owner":"Tomoko"}},{"Key":"CAR1","Record":{"make":"Ford","model":"Mustang","colour":"red","owner":"Brad"}},{"Key":"CAR2","Record":{"make":"Hyundai","model":"Tucson","colour":"green","owner":"Jin Soo"}},{"Key":"CAR3","Record":{"make":"Volkswagen","model":"Passat","colour":"yellow","owner":"Max"}},{"Key":"CAR4","Record":{"make":"Tesla","model":"S","colour":"black","owner":"Adriana"}},{"Key":"CAR5","Record":{"make":"Peugeot","model":"205","colour":"purple","owner":"Michel"}},{"Key":"CAR6","Record":{"make":"Chery","model":"S22L","colour":"white","owner":"Aarav"}},{"Key":"CAR7","Record":{"make":"Fiat","model":"Punto","colour":"violet","owner":"Pari"}},{"Key":"CAR8","Record":{"make":"Tata","model":"Nano","colour":"indigo","owner":"Valeria"}},{"Key":"CAR9","Record":{"make":"Holden","model":"Barina","colour":"brown","owner":"Shotaro"}}]
{"make":"VW","model":"Polo","colour":"Grey","owner":"Mary"}
{"make":"VW","model":"Polo","colour":"Grey","owner":"Archie"}

此结果与使用 peer 命令行方式、node.js 客户端方式得到的结果是一致的,这些应用层的API可参阅:Chaincode API