006-基于hyperledger fabric1.4( 官方文档)编写第一个应用【外部nodejs调用】
一、概述
官方原文地址 Writing Your First Application
如果对fabric网络的基本运行机制不熟悉的话,请看这里。
注意:本教程是对fabric应用以及如何使用智能合约的简单介绍,对fabric应用及智能合约的详细介绍请看应用开发部分和商业票据教程。
本教程将介绍一些示例程序以助于理解fabric应用是如何工作的。这些应用和所使用的智能合约被称为FabCar。它们是理解Hyperledger Fabric blockchain的很好的起点。你将会学习如何编写一个应用和智能合约来查询或更新账本,以及如何使用CA生成区块链应用程序交互所需要的X.509证书。
我们会使用SDK(详细介绍在这里)来调用智能合约,该合约使用智能合约SDK查询和更新账本(详细介绍在这里)。
二、开发fabric应用包括三个主要步骤
1. 设置开发环境:应用程序需要一个网络来进行交互,因此我们将得到一个智能合约和应用程序所需要的基本网络。
2. 学习智能合约示例——FabCar:此合约是用JavaScript编写的。我们会查看该合约,理解其中的交易,以及它是如何被应用程序用来查看和更新账本的。
3. 使用FabCar开发一个示例程序:该程序会使用FabCar智能合约来查询和更新账本中的汽车资产(car assets)。我们将深入了解应用程序代码及其创建的交易,包括查询汽车、查询一系列汽车以及创建一辆新车。
三、示例下载学习
3.1、代码下载
cd $GOPATH/src/github.com/hyperledger/ git clone https://github.com/hyperledger/fabric-samples.git cd fabric-samples/fabcar
此代码放到与fabric并行目录下,没特殊要求
3.2、在fabcar下会有如下文件
javascript javascript-low-level startFabric.sh typescript
3.3、关闭曾经创建过的网络
注意:此部分需要你进入first-network子目录
如果你已经完成了 Building Your First Network ,说明你已经下载了fabric-samples并且启动了网络。在开始本教程之前,必须先关闭此网络:
./byfn.sh down
如果你之前运行过本教程,请使用以下命令删除所有容器(不管与fabric相关与否,都会删除):
docker rm -f $(docker ps -aq)
docker rmi -f $(docker images | grep fabcar | awk '{print $3}')
注意如果这里自己有其他的应用使用,推荐一个一个删除
如果你的环境和相关依赖没有配置好,请根据 Prerequisites 以及 Install Samples, Binaries and Docker Images 进行配置。
3.4、启动网络
注意:此部分需要你进入fabcar子目录
在fabcar目录下运行./startFabric.sh javascript
实际测试增加了javascript会启动失败,
如遇到:
ERROR: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
到上面这个问题,可以参考这里解决
再次运行,得到下面的结果:dsafdsfsfsdfsdfdsfdsfsd
这表明已经成功启动了示例网络,并且fabcar智能合约也被成功的安装和实例化了。接下来进行应用程序的安装。
3.5、安装应用程序
注意:此部分需要你进入fabcar/javascript子目录
运行下面的命令安装应用程序所需的fabric依赖(在此之前要保证已经安装了npm):
npm install
3.6、注册admin用户
注意:以下两个部分涉及与证书颁发机构的通信。当运行程序时,通过打开新的终端shell并运行docker logs -f ca.example.com,您可能会发现流式传输CA日志非常有用。
当我们创建网络时,一个名为admin的管理用户被创建为证书颁发机构(CA)的注册器。我们的第一步是使用enroll.js程序为管理员生成私钥、公钥和X.509证书。此过程使用证书签名请求(CSR)(私钥和公钥首先在本地生成,然后将公钥发送到CA,CA返回编码的证书供应用程序使用。然后,这三个凭证存储在钱包中,允许我们充当CA的管理员。)
接下来注册admin用户(此命令将会把CA管理员的凭证存储在wallet目录中):
node enrollAdmin.js
3.7、注册user用户
上一步已经注册了管理员用户,钱包中有了管理员的凭证,现在可以注册一个新用户(user1)用于查询和更新分类账:
node registerUser.js
现在我们就有了两个独立用户admin和user1的凭证了,后面的应用程序会用到这些凭证。
3.8、查询账本
区块链网络中的每一个节点都有一个账本的副本,应用程序可以通过调用智能合约查询账本,该智能合约查询账本的最新值(世界状态)并将其返回给应用程序。世界状态是一组键值对,应用程序可以查询单个键或多个键。
现在首先运行query.js程序获取账本上所有车辆的信息,此程序使用第二个身份(user1)来获取账本:
node query.js
查看query.js
/* * SPDX-License-Identifier: Apache-2.0 */ 'use strict'; const { FileSystemWallet, Gateway } = require('fabric-network'); const fs = require('fs'); const path = require('path'); const ccpPath = path.resolve(__dirname, '..', '..', 'basic-network', 'connection.json'); const ccpJSON = fs.readFileSync(ccpPath, 'utf8'); const ccp = JSON.parse(ccpJSON); async function main() { try { // Create a new file system based wallet for managing identities. const walletPath = path.join(process.cwd(), 'wallet'); const wallet = new FileSystemWallet(walletPath); console.log(`Wallet path: ${walletPath}`); // Check to see if we've already enrolled the user. const userExists = await wallet.exists('user1'); if (!userExists) { console.log('An identity for the user "user1" does not exist in the wallet'); console.log('Run the registerUser.js application before retrying'); return; } // Create a new gateway for connecting to our peer node. const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'user1', discovery: { enabled: false } }); // Get the network (channel) our contract is deployed to. const network = await gateway.getNetwork('mychannel'); // Get the contract from the network. const contract = network.getContract('fabcar'); // Evaluate the specified transaction. // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4') // queryAllCars transaction - requires no arguments, ex: ('queryAllCars') const result = await contract.evaluateTransaction('queryAllCars'); console.log(`Transaction has been evaluated, result is: ${result.toString()}`); } catch (error) { console.error(`Failed to evaluate transaction: ${error}`); process.exit(1); } } main();
程序开始部分首先引用了fabric-network模块的FileSystemWallet
和Gateway两个关键类。这两个类用来定位user1的钱包身份,以及连接网络:
const { FileSystemWallet, Gateway } = require('fabric-network');
应用程序使用一个网关来连接网络:
const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'user1', discovery: { enabled: false } });
接下来尝试修改query.js,使其只查询CAR4:
//const result = await contract.evaluateTransaction('queryAllCars'); const result = await contract.evaluateTransaction('queryCar', 'CAR4');
具体链码可以查看:fabric-samples/chaincode查找,核心代码
/* * SPDX-License-Identifier: Apache-2.0 */ 'use strict'; const { Contract } = require('fabric-contract-api'); class FabCar extends Contract { async initLedger(ctx) { console.info('============= START : Initialize Ledger ==========='); const cars = [ { color: 'blue', make: 'Toyota', model: 'Prius', owner: 'Tomoko', }, { color: 'red', make: 'Ford', model: 'Mustang', owner: 'Brad', }, { color: 'green', make: 'Hyundai', model: 'Tucson', owner: 'Jin Soo', }, { color: 'yellow', make: 'Volkswagen', model: 'Passat', owner: 'Max', }, { color: 'black', make: 'Tesla', model: 'S', owner: 'Adriana', }, { color: 'purple', make: 'Peugeot', model: '205', owner: 'Michel', }, { color: 'white', make: 'Chery', model: 'S22L', owner: 'Aarav', }, { color: 'violet', make: 'Fiat', model: 'Punto', owner: 'Pari', }, { color: 'indigo', make: 'Tata', model: 'Nano', owner: 'Valeria', }, { color: 'brown', make: 'Holden', model: 'Barina', owner: 'Shotaro', }, ]; for (let i = 0; i < cars.length; i++) { cars[i].docType = 'car'; await ctx.stub.putState('CAR' + i, Buffer.from(JSON.stringify(cars[i]))); console.info('Added <--> ', cars[i]); } console.info('============= END : Initialize Ledger ==========='); } async queryCar(ctx, carNumber) { const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state if (!carAsBytes || carAsBytes.length === 0) { throw new Error(`${carNumber} does not exist`); } console.log(carAsBytes.toString()); return carAsBytes.toString(); } async createCar(ctx, carNumber, make, model, color, owner) { console.info('============= START : Create Car ==========='); const car = { color, docType: 'car', make, model, owner, }; await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); console.info('============= END : Create Car ==========='); } async queryAllCars(ctx) { const startKey = 'CAR0'; const endKey = 'CAR999'; const iterator = await ctx.stub.getStateByRange(startKey, endKey); const allResults = []; while (true) { const res = await iterator.next(); if (res.value && res.value.value.toString()) { console.log(res.value.value.toString('utf8')); const Key = res.value.key; let Record; try { Record = JSON.parse(res.value.value.toString('utf8')); } catch (err) { console.log(err); Record = res.value.value.toString('utf8'); } allResults.push({ Key, Record }); } if (res.done) { console.log('end of data'); await iterator.close(); console.info(allResults); return JSON.stringify(allResults); } } } async changeCarOwner(ctx, carNumber, newOwner) { console.info('============= START : changeCarOwner ==========='); const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state if (!carAsBytes || carAsBytes.length === 0) { throw new Error(`${carNumber} does not exist`); } const car = JSON.parse(carAsBytes.toString()); car.owner = newOwner; await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); console.info('============= END : changeCarOwner ==========='); } } module.exports = FabCar;
再次运行query.js:
$ node query-car4.js
Wallet path: /Users/lihongxu6/work/mygo/src/github.com/hyperledger/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is: {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
现在,大概已经对查询交易有了大概的了解,接下来看看如何更新账本。
3.9、更新账本
3.9.1、创建
我们可以做很多方面的更新操作,但先从创建一辆新车开始吧。
从应用程序的角度来看,更新账本是很简单的。应用先提交一个交易到区块链网络,然后当这个交易被证明有效后,应用会收到一个交易成功的通知。这个过程涉及到共识机制:
上图显示了更新账本所涉及到的主要组件。区块链网络包含多个peer,每个peer都维护一份账本副本,并且选择性的维护一个智能合约副本,除此之外,网络还包括一个排序服务。排序服务能够创建一个包含连接到此网络的不同应用所提交的经过排序的交易的区块。
我们使用invoke.js程序来实现创建一辆新车的更新操作:以下是在invoke中的一段代码:
await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');
运行
$ node invoke.js Wallet path: /Users/lihongxu6/work/mygo/src/github.com/hyperledger/fabric-samples/fabcar/javascript/wallet 2019-04-03T10:26:48.661Z - info: [TransactionEventHandler]: _strategySuccess: strategy success for transaction "5a9563b1397339de4de191aa3c6e52371033811b9aa3d3435bc429b2afabe54f" Transaction has been submitted
注意一下,invoke.js程序与区块链交互使用的是submitTransaction API而不是evaluateTransaction。
submitTransaction比evaluateTransaction复杂的多。SDK会将submitTransaction提案发送给每一个需要的组织中的peer,而不是只和单个peer交互。所有这些接收提案的peer将会执行提案要求执行的智能合约生成交易响应,并对交易响应进行签名后发回给SDK。SDK将收集到的所有经过签名的交易响应合并到一个新的交易中,然后将其发送给orderer。orderer将来自各个应用的交易进行收集和排序后,将这些交易放到区块中。然后将这个新区块(每个交易都是经过证明合法的)分发给网络中的所有peer节点。最后,通知SDK将控制权交回给应用程序。
上一段很重要,涉及到很多工作,而这些工作都是由submitTransaction完成的。应用程序、智能合约、peers、ordering service协同工作来保证账本的一致性的过程叫做共识过程,共识机制的详细介绍看这里。
通过已下查看即可【注意链码调用方法】
node query.js
查看结果中会有新加入的记录
3.9.2、修改
现在,假设Tom非常慷慨,想把Honda Accord车送给Dave,为了实现这个过程,需要修改invoke.js程序,
// await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom'); await contract.submitTransaction('changeCarOwner', 'CAR12', 'Dave');
再次执行invoke.js程序(由于网络原因,可能会由于连接超时而报错,多运行几次即可):
$ node invoke.js Wallet path: /Users/lihongxu6/work/mygo/src/github.com/hyperledger/fabric-samples/fabcar/javascript/wallet 2019-04-03T10:35:21.125Z - info: [TransactionEventHandler]: _strategySuccess: strategy success for transaction "d64761e0880c70b4578b53e467e05cfa8696cfb2791bce5d3062bc1196894359" Transaction has been submitted
再次执行query.js查看owner值是否为Dave:
可以看到,所有者确实变为了Dave。
至此,就成功完成了fabric1.4官方文档中的writing your first application部分。这一部分比较简单,更深入的请看下面:
3.10、更高级的应用【Additional resources】
As we said in the introduction, we have a whole section on Developing Applications that includes in-depth information on smart contracts, process and data design, a tutorial using a more in-depth Commercial Paper tutorial and a large amount of other material relating to the development of applications.
四、旧版本说明
以下是1.1版本的示例,chaincode在根目录下
3、开启网络配置[注意docker版本17.06,在1.12.6版本没有-e命令]
./startFabric.sh
4、查询一个账本
安装node
yum install gcc-c++
npm install
查询[注意配置query.js中的ip地址]
node query.js
展示如下数据
Create a client and set the wallet location Set wallet path, and associate user PeerAdmin with application Check user is enrolled, and set a query URL in the network Make query Assigning transaction_id: f2f45cb045d6290d199e1b2d4eb3b60b1e9cafeff8d09e2b7683dd8578492be7 returned from query Query result count = 1 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"}}]
5、分析query.js
1》初始化参数,包含了用户ID,信道,链码,网络连接入口
var options = { wallet_path: path.join(__dirname, './creds'), user_id: 'PeerAdmin', channel_id: 'mychannel', chaincode_id: 'fabcar', network_url: 'grpc://localhost:7051', };
2》查询代码
var transaction_id = client.newTransactionID(); // queryCar - requires 1 argument, ex: args: ['CAR4'], // queryAllCars - requires no arguments , ex: args: [''], const request = { chaincodeId: options.chaincode_id, txId: transaction_id, fcn: 'queryAllCars', args: [''] }; return channel.queryByChaincode(request);
这里设置了链码ID,交易ID,以及调用的链码的方法fcn,方法参数args等
3》链码:/chaincode/fabcar/目录下fabcar.go
此文件匹配上文的链码ID,包含了如下方法:initLedger
, queryCar
,queryAllCars
, createCar
and changeCarOwner
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response { startKey := "CAR0" endKey := "CAR999" resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
此处就是查询范围内的数据。
查看所有方法
6、测试
cp query.js query1.js
vim query1.js
修改内部访问链码方法
const request = { chaincodeId: options.chaincode_id, txId: transaction_id, fcn: 'queryCar', args: ['CAR4'] };
执行:node query1.js
Create a client and set the wallet location Set wallet path, and associate user PeerAdmin with application Check user is enrolled, and set a query URL in the network Make query Assigning transaction_id: ca88dc3b60f4df009a709f2f5ee5ad3b54f43d03a7e0b931042e2797f70c795d returned from query Query result count = 1 Response is {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
7、更新账本数据
使用fabcar目录下的invoke.js
修改,中的 fcn,以及args等参数
var request = { targets: targets, chaincodeId: options.chaincode_id, fcn: '', args: [''], chainId: options.channel_id, txId: tx_id
如下
var request = { targets: targets, chaincodeId: options.chaincode_id, fcn: 'createCar', args: ['CAR10', 'Chevy', 'Volt', 'Red', 'Nick'], chainId: options.channel_id, txId: tx_id
执行命令
node invoke.js
成功后会有
The transaction has been committed on peer localhost:7053
执行
cp query1.js query2.js vim query2.js
将query2.js中查询条件参数,变为CAR10即可
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Nick"}
ok,可以继续调试其他方法。