在Fabric上编写第一个应用

一、关于FabCar

FabCar 例子演示了如何查询保存在账本上的 Car(我们业务对象例子),以及如何更新账本(向账本添加新的 Car)。 它包含以下两个组件:

  • 示例应用程序:调用区块链网络,调用智能合约中实现的交易。
  • 智能合约:实现了涉及与账本交互的交易。

我们将按照以下三个步骤进行:

  1. 搭建开发环境。 我们的应用程序需要和网络交互,所以我们需要一个智能合约和应用程序使用的基础网络。

  2. 浏览一个示例智能合约。 我们将查看示例智能合约 Fabcar 来学习他们的交易,还有应用程序是怎么使用他们来进行查询和更新账本的。

  3. 使用示例应用程序和智能合约交互。 我们的应用程序将使用 FabCar 智能合约来查询和更新账本上的汽车资产。我们将进入到应用程序的代码和他们创建的交易,包括查询一辆汽车,查询一批汽车和创建一辆新车。

二、准备工作

除了 Fabric 的标准 准备阶段 之外,本教程还利用了 Node.js 对应的 Hyperledger Fabric SDK。 有关最新的预备知识列表,请参阅 Node.js SDK README 。

如果您使用的是 Linux,您需要安装 Python v2.7,make,和 C/C++ 编译器工具链,如 GCC。可以执行如下命令安装其他工具:

sudo apt install build-essential

三、设置区块链网络

1、启动网络

cd fabric-samples/fabcar
./startFabric.sh go

此命令将部署两个 peer 节点和一个排序节点以部署 Fabric 测试网络。 我们将使用证书颁发机构 (Fabric-CA) 启动测试网络,而不是使用cryptogen工具。 我们将使用这些 CA 的其中一个来创建证书以及一些 key, 这些加密资料将在之后的步骤中被我们的应用程序使用。startFabric.sh 脚本还将部署和初始化在 mychannel 通道上的 FabCar 智能合约的 Go 版本,然后调用智能合约来把初始数据存储在帐本上。

2、示例应用

从 fabric-samples/fabcar 目录,进入到 javascript 文件夹。

cd javascript
npm install

这个指令将安装应用程序的主要依赖,这些依赖定义在 package.json 中。其中最重要的是 fabric-network 类;它使得应用程序可以使用身份、钱包和连接到通道的网关,以及提交交易和等待通知。

3、登记管理员用户

当我们创建网络的时候,一个管理员用户( admin)被证书授权服务器(CA)创建成了 注册员 。我们第一步要使用 enroll.js 程序为 admin 生成私钥、公钥和 x.509 证书。这个程序使用一个 证书签名请求 (CSR)——先在本地生成公钥和私钥,然后把公钥发送到 CA ,CA 会发布会一个让应用程序使用的证书。这三个证书会保存在钱包中,以便于我们以管理员的身份使用 CA 。

node enrollAdmin.js

这个命令将 CA 管理员的证书保存在 wallet 目录

4、注册和登记应用程序用户
既然我们的 admin 是用来与 CA 一起工作的。 我们也已经在钱包中有了管理员的凭据, 那么我们可以创建一个新的应用程序用户,它将被用于与区块链交互。 运行以下命令注册和记录一个名为 appUser 的新用户:

node registerUser.js

与 admin 注册类似,该程序使用 CSR 注册 appUser 并将其凭证与 admin 凭证一起存储在钱包中。 现在,我们有了两个独立用户的身份—— admin 和 appUser ——它们可以被我们的应用程序使用。

5、查询账本
区块链网络中的每个节点都拥有一个 账本 的副本,应用程序可以通过执行智能合约查询账本上最新的数据来实现来查询账本,并将查询结果返回给应用程序。

最常用的查询是查寻账本中询当前的值,也就是 世界状态 。世界状态是一个键值对的集合,应用程序可以根据一个键或者多个键来查询数据。

首先,我们来运行我们的 query.js 程序来返回账本上所有汽车的侦听。这个程序使用我们的第二个身份——user1——来操作账本。

node query.js

输入结果应该类似下边:

Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
[{"Key":"CAR0","Record":{"color":"blue","docType":"car","make":"Toyota","model":"Prius","owner":"Tomoko"}},
{"Key":"CAR1","Record":{"color":"red","docType":"car","make":"Ford","model":"Mustang","owner":"Brad"}},
{"Key":"CAR2","Record":{"color":"green","docType":"car","make":"Hyundai","model":"Tucson","owner":"Jin Soo"}},
{"Key":"CAR3","Record":{"color":"yellow","docType":"car","make":"Volkswagen","model":"Passat","owner":"Max"}},
{"Key":"CAR4","Record":{"color":"black","docType":"car","make":"Tesla","model":"S","owner":"Adriana"}},
{"Key":"CAR5","Record":{"color":"purple","docType":"car","make":"Peugeot","model":"205","owner":"Michel"}},
{"Key":"CAR6","Record":{"color":"white","docType":"car","make":"Chery","model":"S22L","owner":"Aarav"}},
{"Key":"CAR7","Record":{"color":"violet","docType":"car","make":"Fiat","model":"Punto","owner":"Pari"}},
{"Key":"CAR8","Record":{"color":"indigo","docType":"car","make":"Tata","model":"Nano","owner":"Valeria"}},
{"Key":"CAR9","Record":{"color":"brown","docType":"car","make":"Holden","model":"Barina","owner":"Shotaro"}}]

我们仔细看看 query.js 程序如何使用 Fabric Node SDK 提供的 API 与我们的 Fabric 网络交互。

/*
 * Copyright IBM Corp. All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

'use strict';

const { Gateway, Wallets } = require('fabric-network');
const path = require('path');
const fs = require('fs');


async function main() {
    try {
        // load the network configuration
        const ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json');
        const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

        // Create a new file system based wallet for managing identities.
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = await Wallets.newFileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user.
        const identity = await wallet.get('appUser');
        if (!identity) {
            console.log('An identity for the user "appUser" 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: 'appUser', discovery: { enabled: true, asLocalhost: true } });

        // 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()}`);

        // Disconnect from the gateway.
        await gateway.disconnect();
        
    } catch (error) {
        console.error(`Failed to evaluate transaction: ${error}`);
        process.exit(1);
    }
}

main();

应用程序首先从 fabric-network 模块 引入两个 key 类:Wallets 和 Gateway 到 scope 中。 这些类将用于定位钱包中的 appUser 身份, 并使用它连接到网络:

const { Gateway, Wallets } = require('fabric-network');

首先,程序使用 Wallet 类从我们的文件系统获取应用程序用户。

const identity = await wallet.get('appUser');

一旦程序有了身份标识,它便会使用 Gateway 类连接到我们的网络。

const gateway = new Gateway();
await gateway.connect(ccpPath, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: true } });

ccpPath 描述了连接配置文件的路径, 我们的应用程序将使用该配置文件连接到我们的网络。 连接配置文件从 fabric-samples/test-network 目录中被加载进来, 并解析为 JSON 文件:

const ccpPath = path.resolve(__dirname, '..', '..', 'test-network','organizations','peerOrganizations','org1.example.com', 'connection-org1.json');

一个网络可以被差分成很多通道,代码中下一个很重要的一行是将应用程序连接到网络中特定的通道 mychannel 上:

const network = await gateway.getNetwork('mychannel');

在这个通道中,我们可以通过 FabCar 智能合约来和账本进行交互:

const contract = network.getContract('fabcar');

在 fabcar 中有许多不同的 交易 ,我们的应用程序先使用 queryAllCars 交易来查询账本世界状态的值:

const result = await contract.evaluateTransaction('queryAllCars');

evaluateTransaction 方法代表了一种区块链网络中和智能合约最简单的交互。它只是根据配置文件中的定义连接一个节点,然后向节点发送请求,请求内容将在节点中执行。智能合约查询节点账本上的所有汽车,然后把结果返回给应用程序。这次交互没有导致账本的更新。

四、FabCar 智能合约

在fabric-samples的仓库中,有一个智能合约实例——FabCar
FabCar 智能合约示例有以下几种语言版本:

  • Golang
  • Java
  • JavaScript
  • Typescript

1、打开一个新终端,并导航到 fabric-samples 仓库里 JavaScript 版本的 FabCar 智能合约:

cd fabric-samples/chaincode/fabcar/javascript/lib

2、在文本编辑器中打开 fabcar.js 文件:

/*
 * Copyright IBM Corp. All Rights Reserved.
 *
 * 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 = '';
        const endKey = '';
        const allResults = [];
        for await (const {key, value} of ctx.stub.getStateByRange(startKey, endKey)) {
            const strValue = Buffer.from(value).toString('utf8');
            let record;
            try {
                record = JSON.parse(strValue);
            } catch (err) {
                console.log(err);
                record = strValue;
            }
            allResults.push({ Key: key, Record: record });
        }
        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;

3、进一步看一下合约中的 queryAllCars ,看一下它是怎么和账本交互的。
async queryAllCars(ctx) {

  const startKey = '';
  const endKey = '';

  const iterator = await ctx.stub.getStateByRange(startKey, endKey);

段代码展示了如何使用 getStateByRange 在一个 key 范围内从账本中检索所有的汽车。 给出的空 startKey 和 endKey 将被解释为从起始到结束的所有 key。 另一个例子是,如果使用 startKey = ‘CAR0’, endKey = ‘CAR999’ , 那么 getStateByRange 将以字典顺序检索在 CAR0 和 CAR999 之间 key 的汽车。 其余代码遍历查询结果,并将结果封装为 JSON,以供示例应用程序使用。

4、应用程序调用智能合约的流程:

在这里插入图片描述
每一个交易都使用一组 API 比如 getStateByRange 来和账本进行交互

5、回到主目录下fabcar的query.js中

/*
 * Copyright IBM Corp. All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

'use strict';

const { Gateway, Wallets } = require('fabric-network');
const path = require('path');
const fs = require('fs');


async function main() {
    try {
        // load the network configuration
        const ccpPath = path.resolve(__dirname, '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com', 'connection-org1.json');
        const ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8'));

        // Create a new file system based wallet for managing identities.
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = await Wallets.newFileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user.
        const identity = await wallet.get('appUser');
        if (!identity) {
            console.log('An identity for the user "appUser" 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: 'appUser', discovery: { enabled: true, asLocalhost: true } });

        // 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()}`);

        // Disconnect from the gateway.
        await gateway.disconnect();
        
    } catch (error) {
        console.error(`Failed to evaluate transaction: ${error}`);
        process.exit(1);
    }
}

main();

可以看到有一段代码为:

const result = await contract.evaluateTransaction('queryCar', 'CAR4');

就是查询合约中CAR4的汽车

6、返回到 fabcar/javascript 目录。现在,再次运行 query 程序:

node query.js

你应该会看到如下:

Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"color":"black","docType":"car","make":"Tesla","model":"S","owner":"Adriana"}

五、更新账本

从一个应用程序的角度来说,更新一个账本很简单。应用程序向区块链网络提交一个交易,当交易被验证和提交后,应用程序会收到一个交易成功的提醒。但是在底层,区块链网络中各组件中不同的 共识 程序协同工作,来保证账本的每一个更新提案都是合法的,而且有一个大家一致认可的顺序。

在这里插入图片描述
上图中,我们可以看到完成这项工作的主要组件。同时,多个节点中每一个节点都拥有一份账本的副本,并可选的拥有一份智能合约的副本,网络中也有一个排序服务。排序服务保证网络中交易的一致性;它也将连接到网络中不同的应用程序的交易以定义好的顺序生成区块。

我们对账本的的第一个更新是创建一辆新车。我们有一个单独的程序叫做 invoke.js ,用来更新账本。和查询一样,使用一个编辑器打开程序定位到我们构建和提交交易到网络的代码段:

await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');

看一下应用程序如何调用智能合约的交易 createCar 来创建一量车主为 Tom 的黑色 Honda Accord 汽车。我们使用 CAR12 作为这里的键,这也说明了我们不必使用连续的键。

保存并运行程序:

node invoke.js

如果执行成功,你将看到类似输出:

Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been submitted

submitTransaction 比 evaluateTransaction 更加复杂。除了跟一个单独的 peer 进行互动外,SDK 会将 submitTransaction 提案发送给在区块链网络中的每个需要的组织的 peer。其中的每个 peer 将会使用这个提案来执行被请求的智能合约,以此来产生一个建议的回复,它会为这个回复签名并将其返回给 SDK。SDK 搜集所有签过名的交易反馈到一个单独的交易中,这个交易会被发送给排序节点。排序节点从每个应用程序那里搜集并将交易排序,然后打包进一个交易的区块中。接下来它会将这些区块分发给网络中的每个 peer,在那里每笔交易会被验证并提交。最后,SDK 会被通知,这允许它能够将控制返回给应用程序。
将:

const result = await contract.evaluateTransaction('queryCar', 'CAR4');

改为:

const result = await contract.evaluateTransaction('queryCar', 'CAR12');

再次保存,然后查询:

node query.js

你创建了一辆汽车并验证了它记录在账本上!

现在我们已经完成了,我们假设 Tom 很大方,想把他的 Honda Accord 送给一个叫 Dave 的人。为了完成这个,返回到 invoke.js 然后利用输入的参数,将智能合约的交易从 createCar 改为 changeCarOwner :

await contract.submitTransaction('changeCarOwner', 'CAR12', 'Dave');

第一个参数 CAR12 表示将要易主的车。第二个参数 Dave 表示车的新主人。

再次保存并执行程序:

node invoke.js

现在我们来再次查询账本,以确定 Dave 和 CAR12 键已经关联起来了:

node query.js

将返回如下结果:

Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"color":"Black","docType":"car","make":"Honda","model":"Accord","owner":"Dave"}

六、清除数据

当你完成FabCar示例的尝试后,您就可以使用 networkDown.sh 脚本关闭测试网络。

./networkDown.sh

参考:编写你的第一个应用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值