前言
官方英文:Writing Your First Application
仍然感谢中文翻译!不过官方的教程有更新。我已经按官方英文进行了补充和修正,但是不保存翻译的准确性。如果希望看完整及原汁原味的文档,推荐看官方英文文档。
本文基于Mac OS
以及Hyperledger Fabric 1.1.0
。
Writing Your First Application - 编写第一个应用
注意:如果你对Fabric的原理及架构还不熟悉,你可能需要先学习Introduction 和 Building Your First Network。
这篇文章中,我们会学习Fabric app是如何工作的。这些app(以及它们使用的智能合约),我们把它称为fabcar
,提供了许多Fabric函数。尤其是Certificate Authority对进程的影响,以及生成证书登记。然后我们会使用这些生成的身份(用户对象)来查询和更新账本。
通过以下三个步骤:
- 设置一个开发环境。我们的应用需要一个网络环境,所以我们下载一些组件,以便进行注册/登记,查询和更新。
学习应用程序中所用到的智能合约例子的参数。智能合约包含的各种功能让我们可以用多种方式和账本进行交互。
开发能够查询以及更新资产的应用程序。我们会进入到app的代码内部(我们的app使用Javascript编写),手动修改变量来进行不同方式的查询和更新。
完成本教程之后,你应该会基本了解一个带有智能合约的应用程序如何编码,以及如何与Fabric网络中的账本(或者节点)进行交互的。
设置开发环境
首先配置开发环境,下载fabric-samples
代码以及镜像文件。这些内容请参考Hyperledger Fabric 1.1.0环境搭建。
现在进入到文件夹,然后看看里面都有些什么:
yuyangdeMacBook-Pro:~ yuyang$ cd /Users/yuyang/fabric-sample/fabric-samples/fabcar
yuyangdeMacBook-Pro:fabcar yuyang$ ls
enrollAdmin.js package.json registerUser.js
invoke.js query.js startFabric.sh
在开始之前,我们先做些清理工作。执行以下命令以关闭旧的或者启动着的容器:
yuyangdeMacBook-Pro:fabcar yuyang$ docker rm -f $(docker ps -aq)
清除网络中的缓存:
# Press 'y' when prompted by the command
yuyangdeMacBook-Pro:fabcar yuyang$ docker network prune
WARNING! This will remove all networks not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Networks:
net_byfn
最后如果你准备好运行,你可能需要删除潜在的fabcar
智能合约镜像。如果是第一次运行,你将不需要这些在你系统中的链码镜像。
yuyangdeMacBook-Pro:fabcar yuyang$ docker rmi dev-peer0.org1.example.com-fabcar-1.0-5c906e402ed29f20260ae42283216aa75549c571e2e380f3615826365d8269ba
安装客户端&启动网络
注意:接下来的命令都是运行在
fabcar
文件夹中的。
执行以下命令为应用安装Fabric依赖。fabric-ca-client
允许我们的app连接CA server并且检索身份材料。fabric-client
允许我们获取身份材料,并且与节点和排序服务对话。
yuyangdeMacBook-Pro:fabcar yuyang$ npm install
使用startFabric.sh
脚本启动网络。这个命令会初始化各种Fabric实体,还会启动一个使用Golang编写的智能合约容器。
./startFabric.sh
你也可以使用Node.js编写的链码,将刚才的指令修改为以下命令启动:
./startFabric.sh node
好了,现在我们有了简单的网络以及一些代码,现在看看他们是怎么一起工作的。
应用程序如何与网络进行交互(How Applications Interact with the Network)
如果你想深入了解fabcar
网络中的组件(以及怎样部署的),更新详细的了解应用和组件是如何交互的,可以阅读Understanding the Fabcar Network。
登记管理员用户
接下来的两部分内容都与CA的通讯有关,你可以通过查看CA的日志来获得更多信息。
新开一个命令行窗口,输入以下指令查看CA日志:
yuyangdeMacBook-Pro:fabcar yuyang$ docker logs -f ca.example.com
现在回到fabcar
上来... ...
当我们启动网络时,我们通过Certificate Authority注册了一个管理员用户admin
。现在我们需要向CA服务器发送一个登记请求,然后为其取回一个登记证书。这里我们不深入登记的细节,只要知道这个证书是构成管理员用户的必要条件。我们随后会使用这个管理员来注册和登记新的用户。现在向CA服务器发送管理员登记请求:
yuyangdeMacBook-Pro:fabcar yuyang$ node enrollAdmin.js
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully enrolled admin user "admin"
Assigned the admin user to the fabric client ::{"name":"admin","mspid":"Org1MSP","roles":null,"affiliation":"","enrollmentSecret":"","enrollment":{"signingIdentity":"c5e8730cb530c1e2db68c233cb8684023032451560db835a2404c2db6d9ff757","identity":{"certificate":"-----BEGIN CERTIFICATE-----\nMIICAjCCAaigAwIBAgIUCbtDEKsU4rC1FqRcX2F/Ilwv6WAwCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTgwMzIxMDQzNTAwWhcNMTkwMzIxMDQ0\nMDAwWjAhMQ8wDQYDVQQLEwZjbGllbnQxDjAMBgNVBAMTBWFkbWluMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEEc/eyDHDU5aPW5x1+5FojpsFWc4kgKDjflx30uZl\nXRZXmonEgYuVQYyVMHSaDrRjiTc1aewion01/4CSwCHodKNsMGowDgYDVR0PAQH/\nBAQDAgeAMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFP/+uwBM8hgooR+DggGmMmKR\nXiijMCsGA1UdIwQkMCKAIEI5qg3NdtruuLoM2nAYUdFFBNMarRst3dusalc2Xkl8\nMAoGCCqGSM49BAMCA0gAMEUCIQDwpO9f41gNPnvG5CitVUIe/Df5/2elTL2uMKcq\n/jUi4gIgCRtU78R7rJIjSo1UPo61I5O60zgQRkUJfl6o0xHEVKg=\n-----END CERTIFICATE-----\n"}}}
这行代码会调用一个证书签名请求(CSR),最后会在项目根目录生成一个新的文件夹hfc-key-store
,里面包含了证书和密钥材料。当我们的app需要创建和读取不同身份用户时,需要定位到此文件夹。
User1的注册和登记(Register and Enroll user1)
使用刚刚生成的管理员证书,我们再一次联通CA服务器来注册和登记一个新用户。user1
是我们用来查询和更新账本的用户。这里着重说明的是,admin
发起了新用户的注册和登记工作(就好像admin
扮演了登记员的角色)。现在为admin
发起登记和注册请求:
yuyangdeMacBook-Pro:fabcar yuyang$ node registerUser.js
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded admin from persistence
Successfully registered user1 - secret:ErdiJNAOBHAN
Successfully enrolled member user "user1"
User1 was successfully registered and enrolled and is ready to intreact with the fabric network
和管理员登记一样,上面的指令会调用CSR然后将证书和密钥放入hfc-key-store
文件夹中。现在我们有了两个用户的身份材料。是时候与账本交互了。
查询账本(Querying the Ledger)
查询是指如何从账本中读取数据。您可以查询单个或者多个键的值,如果账本是以类似于JSON这样的数据存储格式写入的,则可以执行更复杂的搜索(如查找包含某些关键字的所有资产)。
下图是一个查询流程的示意图:
首先,运行query.js
程序,返回账本上所有汽车列表。我们使用user1
作为签名实体。我们的程序中已经指定了user1
为签名实体:
fabric_client.getUserContext('user1', true);
user1
的登记材料已经放在了hfc-key-store
文件夹中,我们只需要简单的告诉程序去获取它就行了。在定义了用户对象后,我们继续读取账本的流程。queryAllCars
这个方法已经被提前定义在了app中,它可以查询所有的cars。执行以下指令:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
返回应如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is [{"Key":"CAR0", "Record":{"colour":"blue","make":"Toyota","model":"Prius","owner":"Tomoko"}},{"Key":"CAR1", "Record":{"colour":"red","make":"Ford","model":"Mustang","owner":"Brad"}},{"Key":"CAR2", "Record":{"colour":"green","make":"Hyundai","model":"Tucson","owner":"Jin Soo"}},{"Key":"CAR3", "Record":{"colour":"yellow","make":"Volkswagen","model":"Passat","owner":"Max"}},{"Key":"CAR4", "Record":{"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}},{"Key":"CAR5", "Record":{"colour":"purple","make":"Peugeot","model":"205","owner":"Michel"}},{"Key":"CAR6", "Record":{"colour":"white","make":"Chery","model":"S22L","owner":"Aarav"}},{"Key":"CAR7", "Record":{"colour":"violet","make":"Fiat","model":"Punto","owner":"Pari"}},{"Key":"CAR8", "Record":{"colour":"indigo","make":"Tata","model":"Nano","owner":"Valeria"}},{"Key":"CAR9", "Record":{"colour":"brown","make":"Holden","model":"Barina","owner":"Shotaro"}}]
这里有10辆车,一辆属于Adriana的黑色Tesla Model S、一辆属于Brad的红色Ford Mustang、一辆属于Pari的紫罗兰色Fiat Punto等等。账本是基于Key/Value 的,在这里,关键字是从CAR0
到CAR9
。这一点特别重要。
现在让我们来看看代码内容。使用编辑器(例如atom或visual studio)打开query.js
程序。
应用程序的初始部分定义了变量,如链码,通道名称和网络端点。在我们的app中,这些变量已经定义好了,但是在真实的开发中,这些变量应该又开发者指定。
var channel = fabric_client.newChannel('mychannel');
var peer = fabric_client.newPeer('grpc://localhost:7051');
channel.addPeer(peer);
var member_user = null;
var store_path = path.join(__dirname, 'hfc-key-store');
console.log('Store path:'+store_path);
var tx_id = null;
这是构建查询的代码块:
// queryCar chaincode function - requires 1 argument, ex: args: ['CAR4'],
// queryAllCars chaincode function - requires no arguments , ex: args: [''],
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryAllCars',
args: ['']
};
当程序运行时,它会调用节点上的fabcar
链码,执行queryAllCars
函数,不传任何参数。
要查看链码提供的其他函数,转至到fabric-samples
子目录chaincode/fabcar/go
,并在编辑器中打开fabcar.go
。
里面也有使用Node.js版本的链码。
你会看到,我们可以调用下面的函数- initLedger
、queryCar
、queryAllCars
、createCar
和changeCarOwner
。
让我们仔细看看queryAllCars
函数是如何与账本进行交互的。
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {
startKey := "CAR0"
endKey := "CAR999"
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
这里定义了queryAllCars
的范围。在CAR0
和CAR999
的每辆车。因此,我们理论上可以创建1,000辆汽车,queryAllCars函数将会显示出每一辆汽车的信息。
下面的图演示了app如何调用链码上的不同方法。每一个方法都必须在chaincode shim interface
中可见的API中编码。这可以使智能合约容器与节点账本有序连接。
我们可以看到我们用过的queryAllCars
函数,还有一个叫做createCar
,这个函数可以让我们更新账本,并最终在链上增加一个新区块。
现在我们返回query.js
程序并编辑请求构造函数以查询特定的车辆。为达此目的,我们将函数queryAllCars
更改为queryCar
并将特定的“Key” 传递给args参数。在这里,我们使用CAR4
。 所以我们编辑后的query.js
程序现在应该包含以下内容:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR4']
};
保存程序并返回fabcar
目录。现在再次运行程序:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
您应该看到以下内容:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
这样,我们就从查询所有车变成了只查询一辆车:Adriana的黑色Tesla Model S。使用queryCar
函数,我们可以查询任意关键字(例如CAR0
),并获得与该车相对应的制造厂商、型号、颜色和所有者。
很好,现在您应该比较熟悉该链码的基本查询功能以及带参数的查询功能了。现在是时候更新账本了…
更新账本(Updating the Ledger)
现在我们已经完成了一些账本查询,并且增加了一些代码,我们准备好更新账本了。有许多种更新账本的方法,不过我们先从创造一辆车开始。
下面的示意图演示了这个流程。
账本更新是从生成交易提案的应用程序开始的。就像查询一样,我们将会构造一个请求,用来识别要进行交易的通道ID、函数以及智能合约。该程序然后调用channel.SendTransactionProposalAPI
将交易建议发送给peer(s)进行认证。
网络(即endorsing peer
)返回一个提案答复,应用程序以此来创建和签署交易请求。该请求通过调用channel.sendTransaction API
发送到排序服务器。排序服务器将把交易打包进区块,然后将区块“发送”到通道上的所有peers进行认证。(在我们的例子中,我们只有一个endorsing peer
。)
最后,应用程序使用eh.setPeerAddr API
连接到peer的事务监听端口,并调用eh.registerTxEvent
注册与特定交易ID相关联的事务。该API使得应用程序获得事务的结果(即成功提交或不成功)。把它当作一个通知机制。
我们初始调用的目标是简单地创建一个新的汽车。我们有一个独立的用于这些交易的JavaScript程序 - invoke.js
。就像查询一样,使用编辑器打开程序并转到构建调用的代码块:
// createCar chaincode function - requires 5 args, ex: args: ['CAR12', 'Honda', 'Accord', 'Black', 'Tom'],
// changeCarOwner chaincode function - requires 2 args , ex: args: ['CAR10', 'Barry'],
// must send the proposal to endorsing peers
var request = {
//targets: let default to the peer assigned to the client
chaincodeId: 'fabcar',
fcn: '',
args: [''],
chainId: 'mychannel',
txId: tx_id
};
我们可以调用函数createCar
或者changeCarOwner
。首先我们创建一个红色的Chevy Volt,并把它归属于Nick。在账本中我们的Key值已经用到了CAR9
,所以这里我们将使用CAR10
。更新代码块如下:
var request = {
//targets: let default to the peer assigned to the client
chaincodeId: 'fabcar',
fcn: 'createCar',
args: ['CAR10', 'Chevy', 'Volt', 'Red', 'Nick'],
chainId: 'mychannel',
txId: tx_id
};
保存并运行程序:
yuyangdeMacBook-Pro:fabcar yuyang$ node invoke.js
输入如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Assigning transaction_id: ea460cb160e0d5c15737b35aade81bbaaa7b29ccbd3b34f43929809290c213fa
Transaction proposal was good
Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK"
The transaction has been committed on peer localhost:7053
Send transaction promise and event listener promise have completed
Successfully sent transaction to the orderer.
Successfully committed the change to the ledger by the peer
我们关心的是这个:
The transaction has been committed on peer localhost:7053
可以看到交易已经被确认。现在回到query.js
,然后修改参数CAR4
为CAR10
。
修改前:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR4']
};
修改后:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR10']
};
保存后,然后查询:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
结果如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Nick"}
恭喜!你已经创造了一辆车!
最后,我们来调用最后一个函数changeCarOwner
。Nick很慷慨,他想把他的Chevy Volt送给Dave。所以,我们简单编辑invoke.js
如下:
var request = {
//targets: let default to the peer assigned to the client
chaincodeId: 'fabcar',
fcn: 'changeCarOwner',
args: ['CAR10', 'Dave'],
chainId: 'mychannel',
txId: tx_id
};
第一个参数定义了哪辆车被变更主人。第二个参数定义了新主人姓名。
保存并执行:
yuyangdeMacBook-Pro:fabcar yuyang$ node invoke.js
结果如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Assigning transaction_id: c5f23e3da0812761b47755c3487ad9cf8291a2b756061473ce0b85aa0fce1411
Transaction proposal was good
Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK"
The transaction has been committed on peer localhost:7053
Send transaction promise and event listener promise have completed
Successfully sent transaction to the orderer.
Successfully committed the change to the ledger by the peer
现在我们去查询账本,看看CAR10
是不是已经在Dave名下:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
结果如下,Dave拥有了CAR10
:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Dave"}
真实情况下,链码需要权限控制。例如只有某些具有权限的人才能创造新车,也应该只有车主才能转让汽车所有权。