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)。我们将深入了解应用程序代码及其创建的交易,包括查询汽车、查询一系列汽车以及创建一辆新车。
设置区块链网络
注意:此部分需要你进入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 进行配置。
启动网络
注意:此部分需要你进入fabcar子目录
在fabcar目录下运行./startFabric.sh javascript
[root@slave2 fabcar]# ./startFabric.sh javascript
# don't rewrite paths for Windows Git Bash users
export MSYS_NO_PATHCONV=1
docker-compose -f docker-compose.yml down
Removing network net_basic
WARNING: Network net_basic not found.
docker-compose -f docker-compose.yml up -d ca.example.com orderer.example.com peer0.org1.example.com couchdb
Creating network "net_basic" with the default driver
Pulling couchdb (hyperledger/fabric-couchdb:)...
ERROR: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
遇到上面这个问题,可以参考这里解决
再次运行,得到下面的结果:
[root@slave2 fabcar]# ./startFabric.sh javascript
# don't rewrite paths for Windows Git Bash users
export MSYS_NO_PATHCONV=1
docker-compose -f docker-compose.yml down
Removing network net_basic
docker-compose -f docker-compose.yml up -d ca.example.com orderer.example.com peer0.org1.example.com couchdb
Creating network "net_basic" with the default driver
Pulling couchdb (hyperledger/fabric-couchdb:)...
latest: Pulling from hyperledger/fabric-couchdb
3b37166ec614: Already exists
504facff238f: Already exists
ebbcacd28e10: Already exists
c7fb3351ecad: Already exists
2e3debadcbf7: Already exists
fc435e46e32e: Already exists
a4922bafdce8: Already exists
14675a1189ca: Already exists
33f930d7053e: Already exists
7aa21e006739: Already exists
806ba27e29bb: Already exists
36861d712d08: Pull complete
d90f5e3e2060: Pull complete
24997f7eee08: Pull complete
3178b1827d0f: Pull complete
7483b6e6320e: Pull complete
3ca8322e6d72: Pull complete
e3f87c16fd0e: Pull complete
fa9f1a1e037c: Pull complete
08b197ff3973: Pull complete
ed0c2b2f2e3e: Pull complete
Pulling ca.example.com (hyperledger/fabric-ca:)...
latest: Pulling from hyperledger/fabric-ca
3b37166ec614: Already exists
504facff238f: Already exists
ebbcacd28e10: Already exists
c7fb3351ecad: Already exists
2e3debadcbf7: Already exists
fc435e46e32e: Already exists
a4922bafdce8: Already exists
c8ec0cae397c: Pull complete
3153e2e7116e: Pull complete
d84abf263d15: Pull complete
45ff112943d3: Pull complete
c4678d50bc7a: Pull complete
Creating couchdb ... done
Creating ca.example.com ... done
Creating orderer.example.com ... done
Creating peer0.org1.example.com ... done
# wait for Hyperledger Fabric to start
# incase of errors when running later commands, issue export FABRIC_START_TIMEOUT=<larger number>
export FABRIC_START_TIMEOUT=10
#echo ${FABRIC_START_TIMEOUT}
sleep ${FABRIC_START_TIMEOUT}
# Create the channel
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp/users/Admin@org1.example.com/msp" peer0.org1.example.com peer channel create -o orderer.example.com:7050 -c mychannel -f /etc/hyperledger/configtx/channel.tx
2019-02-18 19:24:51.026 UTC [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2019-02-18 19:24:51.176 UTC [cli.common] readBlock -> INFO 002 Received block: 0
# Join peer0.org1.example.com to the channel.
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp/users/Admin@org1.example.com/msp" peer0.org1.example.com peer channel join -b mychannel.block
2019-02-18 19:24:52.163 UTC [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2019-02-18 19:24:52.694 UTC [channelCmd] executeJoin -> INFO 002 Successfully submitted proposal to join channel
Creating cli ... done
2019-02-18 19:24:56.794 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
2019-02-18 19:24:56.794 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
2019-02-18 19:24:56.886 UTC [chaincodeCmd] install -> INFO 003 Installed remotely response:<status:200 payload:"OK" >
2019-02-18 19:24:57.357 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
2019-02-18 19:24:57.357 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
2019-02-18 19:26:44.388 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
Total setup execution time : 345 secs ...
Next, use the FabCar applications to interact with the deployed FabCar contract.
The FabCar applications are available in multiple programming languages.
Follow the instructions for the programming language of your choice:
JavaScript:
Start by changing into the "javascript" directory:
cd javascript
Next, install all required packages:
npm install
Then run the following applications to enroll the admin user, and register a new user
called user1 which will be used by the other applications to interact with the deployed
FabCar contract:
node enrollAdmin
node registerUser
You can run the invoke application as follows. By default, the invoke application will
create a new car, but you can update the application to submit other transactions:
node invoke
You can run the query application as follows. By default, the query application will
return all cars, but you can update the application to evaluate other transactions:
node query
TypeScript:
Start by changing into the "typescript" directory:
cd typescript
Next, install all required packages:
npm install
Next, compile the TypeScript code into JavaScript:
npm run build
Then run the following applications to enroll the admin user, and register a new user
called user1 which will be used by the other applications to interact with the deployed
FabCar contract:
node dist/enrollAdmin
node dist/registerUser
You can run the invoke application as follows. By default, the invoke application will
create a new car, but you can update the application to submit other transactions:
node dist/invoke
You can run the query application as follows. By default, the query application will
return all cars, but you can update the application to evaluate other transactions:
node dist/query
这表明已经成功启动了示例网络,并且fabcar智能合约也被成功的安装和实例化了。接下来进行应用程序的安装。
安装应用程序
注意:此部分需要你进入fabcar/javascript子目录
运行下面的命令安装应用程序所需的fabric依赖(在此之前要保证已经安装了npm):
npm install
执行这个命令时可能会出现下面两个问题:
问题一:
gyp WARN EACCES attempting to reinstall using temporary dev dir "/root/gopath/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/node_modules/pkcs11js/.node-gyp"
gyp WARN EACCES user "root" does not have permission to access the dev dir "/root/gopath/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/node_modules/pkcs11js/.node-gyp/8.9.4"
2019年2月21日补充:这个错误应该是命令没有权限,正确的应该用npm install --unsafe-perm,在运行商业票据教程的时候遇到的了同样的问题,用这个命令解决了,因此在这里补充一下。
问题二:
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN fabcar@1.0.0 No repository field.
(具体的解决办法看这里)
2019年2月21修改:
上面的解决办法实际上是错误的。实际原因是/fabric-samples/fabcar/javascript目录下的package.json文件中定义的faccar版本是1.0.0,这也就是上面问题二显示fabcar@1.0.0 No repository field的原因。因此,直接修改package.json文件的fabcar版本为1.4.0即可。(这也就顺便解决了下一步注册admin用户是出现的grpc_node.node缺失的问题)
上述过程安装的是package.json中定义的应用程序主要依赖项。其中最重要的是Fabric Network类。它使应用程序能够使用证书、钱包和网关连接到通道、提交交易和等待通知。本教程还使用Fabric CA客户端类向用户注册各自的证书颁发机构,生成有效的证书,然后由Fabric网络类方法使用。安装完成后就可以运行应用程序了。
注册admin用户
注意:以下两个部分涉及与证书颁发机构的通信。当运行程序时,通过打开新的终端shell并运行docker logs -f ca.example.com,您可能会发现流式传输CA日志非常有用。
当我们创建网络时,一个名为admin的管理用户被创建为证书颁发机构(CA)的注册器。我们的第一步是使用enroll.js程序为管理员生成私钥、公钥和X.509证书。此过程使用证书签名请求(CSR)(私钥和公钥首先在本地生成,然后将公钥发送到CA,CA返回编码的证书供应用程序使用。然后,这三个凭证存储在钱包中,允许我们充当CA的管理员。)
接下来注册admin用户(此命令将会把CA管理员的凭证存储在wallet目录中):
[root@slave2 javascript]# node enrollAdmin.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
Successfully enrolled admin user "admin" and imported it into the wallet
(这一步我遇到了两个问题,一个是node版本不对,应该8.9.x版本;另一个是grpc_node.node文件缺失,看这里解决)
实际原因是npm install时使用的package.json文件中的fabcar版本不对应(具体看前面)
注册user1用户
上一步已经注册了管理员用户,钱包中有了管理员的凭证,现在可以注册一个新用户(user1)用于查询和更新分类账:
[root@slave2 javascript]# node registerUser.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
Successfully registered and enrolled admin user "user1" and imported it into the wallet
现在我们就有了两个独立用户admin和user1的凭证了,后面的应用程序会用到这些凭证。
查询账本
区块链网络中的每一个节点都有一个账本的副本,应用程序可以通过调用智能合约查询账本,该智能合约查询账本的最新值(世界状态)并将其返回给应用程序。世界状态是一组键值对,应用程序可以查询单个键或多个键。
现在首先运行query.js程序获取账本上所有车辆的信息,此程序使用第二个身份(user1)来获取账本:
[root@slave2 javascript]# node query.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result 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"}}]
接下来看一下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 } });
(注意:此时的fabric1.4官方文档内容可能还有一部分没更新,比如上面这段代码,文档中跟实际下载的fabric不同)
接下来尝试修改query.js,使其只查询CAR4:
//const result = await contract.evaluateTransaction('queryAllCars');
const result = await contract.evaluateTransaction('queryCar', 'CAR4');
再次运行query.js:
[root@slave2 javascript]# node query.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is: {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
现在,大概已经对查询交易有了大概的了解,接下来看看如何更新账本。
更新账本
我们可以做很多方面的更新操作,但先从创建一辆新车开始吧。
从应用程序的角度来看,更新账本是很简单的。应用先提交一个交易到区块链网络,然后当这个交易被证明有效后,应用会收到一个交易成功的通知。这个过程涉及到共识机制:
上图显示了更新账本所涉及到的主要组件。区块链网络包含多个peer,每个peer都维护一份账本副本,并且选择性的维护一个智能合约副本,除此之外,网络还包括一个排序服务。排序服务能够创建一个包含连接到此网络的不同应用所提交的经过排序的交易的区块。
我们使用invoke.js程序来实现创建一辆新车的更新操作:
[root@slave2 javascript]# node invoke.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
2019-02-19T10:35:27.807Z - info: [TransactionEventHandler]: _strategySuccess: strategy success for transaction "1d9036a444b1385060fc4dcc402bd30480cb66a48ab4952d107e49aeb3f6172b"
Transaction has been submitted
注意一下,invoke.js程序与区块链交互使用的是submitTransaction API而不是evaluateTransaction。
submitTransaction is much more sophisticated than evaluateTransaction. Rather than interacting with a single peer, the SDK will send the submitTransaction proposal to every required organization’s peer in the blockchain network. Each of these peers will execute the requested smart contract using this proposal, to generate a transaction response which it signs and returns to the SDK. The SDK collects all the signed transaction responses into a single transaction, which it then sends to the orderer. The orderer collects and sequences transactions from every application into a block of transactions. It then distributes these blocks to every peer in the network, where every transaction is validated and committed. Finally, the SDK is notified, allowing it to return control to the application.
submitTransaction比evaluateTransaction复杂的多。SDK会将submitTransaction提案发送给每一个需要的组织中的peer,而不是只和单个peer交互。所有这些接收提案的peer将会执行提案要求执行的智能合约生成交易响应,并对交易响应进行签名后发回给SDK。SDK将收集到的所有经过签名的交易响应合并到一个新的交易中,然后将其发送给orderer。orderer将来自各个应用的交易进行收集和排序后,将这些交易放到区块中。然后将这个新区块(每个交易都是经过证明合法的)分发给网络中的所有peer节点。最后,通知SDK将控制权交回给应用程序。
上一段很重要,涉及到很多工作,而这些工作都是由submitTransaction完成的。应用程序、智能合约、peers、ordering service协同工作来保证账本的一致性的过程叫做共识过程,共识机制的详细介绍看这里。
为了查看这个交易(创建新车)是否被写入了账本中,需要修改query.js程序:
//const result = await contract.evaluateTransaction('queryAllCars');
//const result = await contract.evaluateTransaction('queryCar', 'CAR4');
const result = await contract.evaluateTransaction('queryCar', 'CAR12');
再次执行query.js程序:
[root@slave2 javascript]# node query.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is: {"colour":"Black","make":"Honda","model":"Accord","owner":"Tom"}
出现生面的结果就说明已经成功创建一个一辆新车并记录到了账本中。
现在,假设Tom非常慷慨,想把Honda Accord车送给Dave,为了实现这个过程,需要修改invoke.js程序,
//await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');
await contract.submitTransaction('changeCarOwner', 'CAR12', 'Dave');
再次执行invoke.js程序(由于网络原因,可能会由于连接超时而报错,多运行几次即可):
[root@slave2 javascript]# node invoke.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
2019-02-19T11:08:34.641Z - error: [Remote.js]: Error: Failed to connect before the deadline URL:grpc://localhost:7051
2019-02-19T11:08:34.643Z - error: [Network]: _initializeInternalChannel: Unable to initialize channel. Attempted to contact 1 Peers. Last error was Error: Failed to connect before the deadline URL:grpc://localhost:7051
Failed to submit transaction: Error: Unable to initialize channel. Attempted to contact 1 Peers. Last error was Error: Failed to connect before the deadline URL:grpc://localhost:7051
[root@slave2 javascript]# node invoke.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
2019-02-19T11:09:16.338Z - info: [TransactionEventHandler]: _strategySuccess: strategy success for transaction "159c7c9a20d903f04dca550c22e8748f07164d5323ff2c25662f583641c77465"
再次执行query.js查看owner值是否为Dave:
[root@slave2 javascript]# node query.js
Wallet path: /root/go/src/github.com/hyperledger/fabric/scripts/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is: {"colour":"Black","make":"Honda","model":"Accord","owner":"Dave"}
可以看到,所有者确实变为了Dave。
至此,就成功完成了fabric1.4官方文档中的writing your first application部分。这一部分比较简单,更深入的请看下面:
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。