NOTE: 如果你现在还不理解Fabric网络的基本架构,你可能需要先去了解一下怎么构建你的第一个网络那篇教程。
这一节中我们会有好多个示例程序来看看Fabric的应用是怎么工作的。这些apps(以及他们使用的智能合约)- 共同称为fabcar - 提供了一个Fabric功能的全面的展示
尤其是,我们会演示和证书认证(Certificate Authority)之间交互的流程,以及生成登记证书(enrollment certificates)的过程,在这之后会利用这些生成的标识(用户对象)来查询和更新账本。
我们会经过如下三个理论上的步骤:
-
建立起一个开发环境。我们的应用程序需要一个网络来运行,所以我们会下载一个,并且剥离出中间我们需要的那些用于注册/登记、查询和更新的模块:
-
学习我们app中要用到的示例智能合约的参数。我们的智能合约包含很多功能,这些功能允许我们和账本以不同的方式进行交互。我们会去研究这些智能合约,然和学习我们application要使用的那些功能。
-
开发一个可以查询和更新账本上的资产的应用程序。我们会去深入学习一下app的代码(这里的app都是用javascript写的),并且手动去控制变量来运行不同的查询和更新。
完成这个教程后,你就会对Fabric网络里面如何编程来通过智能合约和账本交互有个基本的认识。
1 构建你的开发环境
第一件事情,下载用于构建网络和应用程序的Fabric镜像,以及相关的组件。
接下来,下载fabric-samples工程,接着下载最新的稳定版本的Fabric镜像和可用的工具。
现在就可以看看我们fabric-samples目录下都有些什么
cd fabric-samples/fabcar && ls
你会看到这些:
enrollAdmin.js invoke.js package.json query.js registerUser.js startFabric.sh
在开始之前,我们还要做一些清理工作,删除所有的容器
docker rm -f $(docker ps -aq)
清除cached网络:
# Press 'y' when prompted by the command
docker network prune
最后的,如果你以及运行过本教程了,你还需要删除隐藏的chaincode镜像。
docker rmi dev-peer0.org1.example.com-fabcar-1.0-5c906e402ed29f20260ae42283216aa75549c571e2e380f3615826365d8269ba
1.1. 安装clients并且启动网络
NOTE: 后面的这些命令都要求你在fabcar这个子目录下进行。
运行下面的命令来安装应用程序的Fabric依赖,
- fabric-ca-client是用来允许我们的app和CA服务器通信并且获取识别材料;
- fabric-client允许我们来加载识别材料到节点和ordering服务上
npm install
使用脚本startFabric.sh来启动你的网络,这个命令会让我们的多个Fabric实体运转起来(spin up),并且为golang编写的chaincode启动一个智能合约容器:
./startFabric.sh
现在我们就有一个示例网络以及一些代码了,接下来就是看看这些是怎么组合到一起工作的。
2. Application怎么和网络交互?
为了更深入一步去看看我们的fabcar网络中的组件(还有如何部署),以及应用程序怎么和这些组件进行交互,请参看understand_fabcar_network.
开发者更加感兴趣的是要看看应用程序在做什么 - 以及研究代码的本身以了解应用程序是怎么构成的。现在最重要的事情是了解我们的应用程序是通过一个SDK来访问APIs,这些APIs允许我们查询和更新账本。
3. 登记管理(Admin)用户
NOTE: 接下来的两节涉及到和Certificate Authority之间的通信。你会发现运行后面这些程序的时候,输出CA的日志很有帮助
开启一个新的shell,使用如下命令来输出你的CA日志:
docker logs -f ca.example.com
然后回到原来的终端,
当我们启动这个网络的时候,一个管理用户 - admin - 就注册了。现在我们需要发送一个enroll的调用到CA服务,来获取这个用户的登记证书(enrollment certificate, eCert)。我们这里不会深入了解enrollment的细节,简单来说就是SDK以及我们的应用程序需要这个证书来为admin生成一个用户对象。之后我们才能使用这个管理对象(admin object)来注册和登记新的用户。
发送admin enroll调用给CA服务:
node enrollAdmin.js
这个程序会调用一个证书签字请求(certificate signing request,CSR),并且最终输出一个eCert证书和密钥材料到一个新创建的文件夹 - hfc-key-store - 这个文件夹在这个工程的根目录下。我们的apps会在这个位置寻找所需的材料用来创建或者加载不同用户的识别对象。
4. 注册和登记user1
我们现在就可以使用我们新生成的admin eCert证书来和CA服务通信,以注册和登记新的用户了。用户user1就是我们用来查询和更新账本的识别号。这里需要注意的一点,只有管理者标记(admin identity)才能发起新用户的注册和登记调用。为user1发送注册和登记调用,
node registerUser.js
类似于管理者的登记,这个函数调用一个CSR并且输出密钥和eCert到子目录hfc-key-store中。所以我们现在就有两个独立用户(admin和user1)的识别材料了。
是时候开始和账本进行交互了...
5. 查询账本
查询就是从账本读取数据。这个数据保存成了一系列的键值对,你可以查询单个key或者多个keys对于的值;或者,如果账本作为一个像JSON这样的富数据存储格式写入到DB中的,那么我们还可以进行更加复杂的查询操作。
下图演示的是查询操作工作的过程,
首先,我们运行一个query.js的程序来返回账本上所有汽车的列表。我们这里使用我们的第二个标识 - user1 - 作为这个应用程序的签名实体。我们程序里的这几行指定了user1作为签名者
fabric_client.getUserContext('user1', true);
回想一下我们之前放到目录hfc-key-store的登记材料,我们的应用程序需要获取这些识别材料。我们这个时候可以使用这些定义的用户对象来执行从账本读取的操作。queryAllCars函数会查询所有的汽车,已经预加载到app中了,我们这里可以简单的如下运行程序:
node query.js
会返回如下的内容,
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"}}]
这就是所有的10俩车,这个账本是一个基于键值对,在我们的实现中键值是CAR0到CAR9。
我们现在打开query.js文件,来仔细看看这个程序。
应用程序的初始化部分定义了一些变量,比如说通道名称,证书保存的位置,以及网络节点。在我们的示例app里,这些变量都以及打包进去了,但是在真是app中,这些变量需要通过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 chaincode,并且运行里面的queryAllCars函数,没有入参。
再看一眼我们的智能合约里面可用的函数,找到fabric-samples/chaincode/fabcar/go子目录下的fabcar.go文件并打开,
NOTE: 相同的函数也定义在Node.js版本的fabcar chaincode中。
我们可以看到有以下可用的调用: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之间的所有的汽车 - 查询总共会返回1000辆车(假设每个键值都标记了)。
下面这个图演示的是,一个应用程序怎么来调用chaincode上不同的函数的。每个函数必须基于chaincode shim接口上可用的API来进行编码,这些接口最终会映射到智能合约容器到相应的peer账本之间的接口。
我们来看一下queryAllCars这个函数,这个函数就和createCar这个函数类似,都允许我们来更新账本,并且最终添加一个新的区块到当前的链上。
首先,我们回到query.js程序,并修改指令来请求CAR4。我们把queryAllCars修改成queryCar并传递参数CAR4给这个函数
程序修改成这样:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR4']
};
保存函数,重新运行程序:
node query.js
你可以看到如下结果:
{"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
使用queryCar函数你可以查询任何一个键值来获取这些汽车的信息。
Great. At this point you should be comfortable with the basic query functions in the smart contract and the handful of
parameters in the query program. Time to update the ledger...
好的接下来我们来聊一聊怎么更新ledger
6. 更新账本
我们可以对账本进行各种各样的更新,但是我们从创建一个汽车开始。
接下来我们可以看到这个过程是怎样进行的。首先请求一个更新,背书,接着返回给应用程序,然后再发出去进行排序,最终写到每个节点的账本中。
Update Ledger
这里有一个单独的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
};
You’ll see that we can call one of two functions - createCar or changeCarOwner . First, let’s create a red Chevy
Volt and give it to an owner named Nick. We’re up to CAR9 on our ledger, so we’ll use CAR10 as the identifying key
here. Edit this code block to look like this:
我们看到这里我们可以调用两个函数 - createCar或者changeCarOwner。首先我们创建一个red Chevy Volt,并命名为Nick。然后使用键值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
};
保存然后运行
node invoke.js
终端会有一些log的打印,但是这里我们只关心这一条,
The transaction has been committed on peer localhost:7053
看到这条打印后,我们再回去运行查询程序,首先修改query.js程序
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR10']
};
先保存然后运行,
node query.js
返回结果是
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Nick"}
那么如果我们现在Nick要把他的这辆车转赠给Dave该怎么操作?
回到invoke.js程序,然后把函数createCar修改成changeCarOwner如下
var request = {
//targets: let default to the peer assigned to the client
chaincodeId: 'fabcar',
fcn: 'changeCarOwner',
args: ['CAR10', 'Dave'],
chainId: 'mychannel',
txId: tx_id
};
这里有两个入参,CAR10代表要交易的汽车,Dave代表新的主人。
执行一下更新,
node invoke.js
接着再查询一下结果
node query.js
返回结果如下:
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Dave"}
NOTE: 在真实场景中的应用程序,chaincode里面会有一些访问控制逻辑。比如说只有某些授权的用户可以创建一个新车,只有汽车的拥有着才能转移这个车给别人。
7. 总结
现在其实你应该对应用程序怎么和网络之间交互有了一些基本的理解了,我们可以看到智能合约,APIs,以及SDK分别扮演怎样的角色。
接下来我们会学习怎么去真正的写一个智能合约,以及这些更底层的应用程序函数是怎么被驱动的(尤其是涉及身份认知和成员服务的。)
网友评论