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
版本太旧而无法处理重定向或不支持的环境。
该命令运行的脚本做了什么?
- 克隆 hyperledger / fabric-samples 存储库
- 检出适当的版本标签
- 将 Hyperledger Fabric 平台特定的二进制文件和配置文件安装到Fabric-samples的/ bin和/ config目录中指定的版本
- 下载指定版本的 Hyperledger Fabric docker 映像
安装完之后可通过在本地计算机上运行该网络来了解 Fabric, 更有经验的开发人员可使用网络来测试其智能合约和应用程序,该网络只用作教育和测试的工具, 不应用作部署生产网络的模板。
在 fabric-samples
存储库的 test-network
目录中找到启动网络的脚本 network.sh
。
启动 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.com
和 peer0.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链码生命周期要求组织同意定义链码的参数,例如名称、版本和链码背书策略。渠道成员通过以下四个步骤达成一致。并不是每个组织都需要完成每一步。
-
打包链码:可以由一个组织或每个组织完成此步骤。
-
在对等节点上安装链码:每个将使用链码来认可交易或查询帐本的组织都需要完成此步骤。
-
批准组织的链码定义:每个将使用链码的组织都需要完成此步骤。 链码定义需要得到足够多的组织的批准,才能满足该频道的LifecycleEndorsment策略(默认情况),然后才能在该频道上启动链码。
-
将链码定义提交到通道:一旦渠道上所需数量的组织批准了该链码定义,就需要由一个组织提交。 提交者首先从已经批准的组织的足够的对等节点那里收集背书,然后提交交易以提交链码的定义。
链码需要打包到一个tar文件中,然后才能安装到对等节点中。
可以使用 Fabric peer 命令、Node Fabric SDK 或第三方工具如 GNU tar(第三方工具打包需满足预定的格式,而 Fabric 提供的工具自动完成) 进行打包,打包时需要提供链码包标签以简明地描述该包。
如图,链码分别由Org1和Org2打包。这两个组织都使用MYCC 1作为他们的包标签,以便使用名称和版本识别包。组织不需要使用相同的包标签。
需要在将执行交易和交易背书的每个对等节点上安装链码包。
无论使用CLI还是SDK,都需要使用管理员身份来完成此步骤。
对等节点将在链码安装后构建链码,如果链码有问题,将返回一个构建错误。
成功的安装命令将返回一个链码包标识符,它是包标签与包的散列值的组合。
保存该标识符用于下一步,也可使用 peer 命令获取安装的所有包的标识符。
建议组织仅打包一次链码,然后在属于其组织的每个对等方上安装相同的软件包。 如果某个通道想要确保每个组织都运行相同的链码,则一个组织可以打包一个链码并将其发送到其他通道成员。
链码由链码定义控制。当一个通道的成员们核准一个链码定义的时候,该过程就像一个组织在其可接受的链码参数上的投票,批准后需要被管理员提交到订阅服务,此后将分布于所有对等节点中。链码定义包含了以下参数,需要得到组织间的一致同意。
Name、Version、Sequence、Endorsement Policy、Collection Configuration、ESCC/VSCC Plugins、Initialization
链码定义也包含包标识符。
一旦有足够数量的通道成员批准了链码定义,组织就可以将定义提交给通道。
可使用 checkcommitreadiness
的 peer
命令来检查提交的链码定义是否成功(是否有足够的成员批准了)。
管理员的提交交易请求首先发送到了通道成员的对等节点,它们背书之后才发送到订阅服务,然后才提交链码定义给通道。
多少成员批准了才是足够的?由 Channel/Application/LifecycleEndorsement
策略控制,该策略默认需要大量组织背书,该策略与链码背书策略是分开的,即使链码背书策略仅需要少量组织同意,它还是默认要大量组织同意。当然,策略是可以更改的。
组织可以批准链码定义而不需要安装链码包。如果一个组织不需要使用链码,他们可以批准一个没有包标识符的链码定义,以确保生命周期背书策略得到满足。
在将链码定义提交到通道之后,链码容器将在安装了链码的所有节点上启动,允许通道成员开始使用链码。链码容器启动可能需要几分钟时间。
根据前面了解到的《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 应用程序函数的名称。 此应用程序将仅具有两个功能:set
和 get
,用于设置资产的值或检索其当前状态。 首先调用ChaincodeStubInterface.GetFunctionAndParameters
来提取函数名称和该链码函数的参数。
然后验证函数名称是 set
还是 get
,然后调用那些链码函数,并通过 shim.Success
或 shim.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.PutState
和 ChaincodeStubInterface.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 package
和 peer 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。