Fork me on GitHub

区块链客户端应用程序

借助 Hyperledger Fabric Client SDK for Node.js,您可以轻松地使用 API 来与基于 Hyperledger Fabric v0.6 的区块链交互。本教程将介绍如何编写客户端应用程序中的一些最常见和必要的功能。教程中的所有代码示例都包含在可重用的样本客户端中,您可以下载并自定义它来满足您的需求。

本系列分三部分,本教程是最后一部分,您将学习如何开发一个 Node.js 客户端应用程序,用它来与基于 Hyperledger Fabric v0.6 的区块链网络通信。您将了解注册、登记和通过 TCert 执行访问控制,并获取设置区块链网络、基于 Cloudant 的键值存储,以及用于调用和查询区块链的 API 层的代码。按照本教程中的步骤,可以将第 1 部分中开发的链代码部署到 IBM Bluemix® 上的 Blockchain 服务上,并从客户端应用程序调用它。

前提条件

我们将继续介绍本教程系列前面两部分中介绍的住房贷款申请用例。

  • 本教程将引用第 1 部分中编写的链代码,所以在继续之前请复习该教程。
  • 您应该熟悉 Node.js 和 Bluebird promise。
  • 应该已经在机器上安装了 Node v4.5.0
  • 应该已经在机器上安装了 NPM v2.15.9
  • 应该已经在机器上安装了 Git v2.7.4
  • 您需要一个 Bluemix 帐户(获取一个免费试用账户)。
  • 您需要样本代码包(从本教程下载它)。

重要术语和概念

Hyperledger Fabric Client SDK 是 Hyperledger Fabric v0.6 的官方 SDK。它是用 TypeScript 编写的,拥有丰富的 API,可通过 Node.js 应用程序与基于 Hyperledger Fabric 的区块链网络进行交互。该 SDK 使用 gRPC 与区块链网络通信。

  • 。链是让程序运转起来的顶级对象。链对象用于配置对等节点、成员服务、安全性、键值存储和事件中心。链对象能访问可用的成员对象,而成员对象用于在区块链网络上执行事务。
  • 成员。成员对象表示能在区块链网络上执行事务的实体。成员对象用于向成员服务注册和登记。完成注册和登记后,成员对象将收到的证书存储在以前配置的键值存储中。然后可以使用成员对象与区块链网络上的对等节点进行通信,以便调用事务和查询账本。
  • 键值存储。键值存储用于存储成员元数据和登记证书。该 SDK 为键值存储提供了一种基于文件的默认实现,可用在客户端应用程序中。但是,建议您自行为键值存储编写更安全、更容易访问的实现。我将带领您在我们的客户端中实现一个基于 Cloudant NoSQL 数据库的键值存储。
  • 注册、登记和访问控制。要与基于 Hyperledger Fabric 的区块链网络交互,用户需要向证书颁发机构 (CA) 注册和登记。CA 生成事务证书,需要这些证书才能在区块链网络上执行事务,它们也可用于实现基于属性的访问控制。CA 提供了几种不同的证书服务:
    • 登记证书颁发机构 (ECA)。ECA 使新用户能向区块链网络注册,并在以后请求一个登记证书对。这个证书对包含两个证书,一个用于数据签名,另一个用于数据加密。
    • 事务证书颁发机构 (TCA)。用户在区块链网络中登记后,可以请求事务证书 (TCert)。要在区块链网络上部署链代码和调用链代码事务,需要使用 TCert。也可以在 TCert 中嵌入经过加密的用户定义属性。然后可以在链代码中访问这些属性来执行访问控制。一种确保隐私的最佳做法是为每个事务请求一个不同的 TCert。这是该 SDK 的默认行为。
    • TLS 证书颁发机构。TLS 证书保护客户端、对等节点和 CA 之间的通信渠道。TLS 证书可从 TLS 证书颁发机构 (TCA) 请求获得。
    • 属性证书颁发机构 (ACA)。ACA 将每个用户的属性和从属关系存储在一个数据库中。它证明某个特定用户是否拥有这些属性。对于经过认证的属性,ACA 会返回一个 ACert,其中包含加密的属性值。

    此图展示了用户、登记证书颁发机构 (ECA)、事务证书颁发机构 (TCA) 和属性证书颁发机构 (ACA) 之间的交互流。

    在上图中:

    1. 用户向 ECA 发出注册请求并传入一个登记 ID、角色、从属关系和属性键值对等信息。
    2. 如果传入的登记 ID 尚未注册,那么 ECA 会向用户回复一个一次性密码。
    3. 用户向 ECA 发出登记请求,并传入一个签名和加密密钥,以及在上一步中获得的一次性密码令牌。
    4. 完成验证后,ECA 会返回一个登记证书对。这个证书对包含两个证书,一个用于签名,另一个用于加密。然后,这个证书对连同其他用户元数据一起被存储在链对象中配置的键值存储中。
    5. ECA 向 ACA 发出一个获取属性请求,并传入第 4 步中生成的 ECert。
    6. ACA 将 ECert 连同其他细节一起存储在它的数据库中,这些细节包括从属关系、属性键/值对和它们的有效性等。
    7. 用户向 TCA 请求 TCert,TCert 可用于调用链代码。用户传入 ECert、要生成的 TCert 数量和一组属性,这些属性可以是第 1 步中传入的一组属性的子集。
    8. TCA 向 ACA 发出请求,以确保用户拥有这些属性。TCA 传入 ECert 和这组属性。
    9. ACA 在其数据库中查找这些属性。
    10. 如果未找到任何属性,则返回一个错误。如果找到所有或部分属性,则将一个包含已找到的属性的 ACert 返回给 TCA。
    11. TCA 创建一批 TCert 并将它们返回给用户。每个 TCert 都包含有效的属性。

开始实践

本教程中和可供下载的代码样本是使用以下软件和版本来开发和测试的:

1

在 Bluemix 上创建 IBM Blockchain 服务

要开发本教程中的样本客户端,您需要一个 Bluemix 帐户(获取一个 Bluemix 免费试用账户)。

  1. 拥有 Bluemix 帐户后,在 Bluemix 上创建 Blockchain 服务。一定要选择 Starter Developer plan (beta),这会为您设置一个基于 Hyperledger Fabric v0.6 的区块链网络。
  2. 创建服务后,导航到您的 Blockchain 服务主页,单击导航栏中的 Service credentials
  3. 单击 ACTIONS 下的 View credentials

    您现在可以看到您的服务的凭证。复制文本框中的全部内容,将它们存储在一个安全的位置。这些凭证包含客户端应用程序与区块链网络通信所需的信息。教程后面的章节将介绍如何将这些服务凭证放在样本客户端的配置文件中。

2

在 Bluemix 上创建 Cloudant NoSQL DB 服务

样本客户端使用了一个基于 Cloudant 的键值存储的自定义实现。Cloudant 服务是一个由 couch DB 提供助力的 NoSQL DB 服务。所以,与 Blockchain 服务一样,您将使用您的 Bluemix 帐户(获取 Bluemix 免费试用帐户)。

  1. 拥有 Bluemix 帐户后,让我们来创建 Cloudant 服务。选择 Lite (free) plan
  2. 创建服务后,导航到您的 Cloudant 服务主页,单击导航栏中的 Service credentials
  3. 单击 ACTIONS 下的 View credentials

您现在可以看到您的服务的凭证。复制文本框中的全部内容,将它们保存到一个安全位置供以后使用。

3

下载样本代码包

下载并解压样本代码包(从本教程下载它)。在终端中导航到解压的 code_package 目录的根路径,运行 npm install。此命令将下载运行样本客户端所需的所有模块。完成上述操作后,您会在 code_package 根路径中看到一个 node_modules 文件夹。

4

部署样本链代码

运行客户端之前,您需要将 code_package/chaincode/ 下的 chaincode_sample.go 部署到早先创建的 IBM Blockchain 服务中。

  1. 导航到 IBM Blockchain 服务仪表板,单击 APIs
  2. 在 APIs 部分,单击 Network’s Enroll IDs。这部分列出了已向区块链网络注册的用户,这些用户只需要登记即可部署和调用链代码。

    选择 user_type1_0 用户。记下此用户的 ID 和密码。

    备注:不要登记 admin 和 WebAppAdmin 用户。
    它们是有权注册其他用户的特殊用户。我们的 Node.js 客户端将负责登记和利用这些用户。

  3. 导航到 /registrar 选项卡,在有效负载中填入登记 ID 和登记密码:
    1
    2
    3
    4
    {
      "enrollId": "user_type1_0",
      "enrollSecret": "5c9db3deb6"
    }

    单击 Try It Out 按钮,以便对区块链网络中选定的对等节点执行 REST 调用。您将获得一个响应,表明该用户已成功登录。现在可以使用 user_type1_0 部署链代码了。

  4. 导航到 /chaincode 选项卡。
    单击 DeploySpec 部分旁边的文本区域。将文本区域的内容替换为以下内容:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
        "jsonrpc": "2.0",
        "method": "deploy",
        "params": {
            "type": 1,
            "chaincodeID": {
                "path": "https://github.com/varojha1/Hyperledger-Fabric-v6-tutorial/chaincode"
            },
            "ctorMsg": {
                "function": "init",
                "args": [
                     
                ]
            },
            "secureContext": "user_type1_0"
        },
        "id": 0
    }

    chaincodeID 是 GitHub 存储库中的 chaincode_sample.go 文件的路径。对于本教程,我已创建了存储库,并上传了客户端代码和链代码供您使用。

  5. 单击 Try It Out 按钮执行部署请求。您会获得类似这样的响应:
    1
    2
    3
    4
    5
    6
    7
    8
    {
        "jsonrpc": "2.0",
        "result": {
            "status": "OK",
            "message": "36ebb862f73a0e18701338fe2b8de99e31202ff8649c8b9909fa0c0fada22b127a997e5f8bf074f35a36a60e765a2919a8a405ee29f"
        },
        "id": 1
    }

    复制 message 键的内容并保存。它表示刚部署的链代码的链代码 ID
  6. 在编辑器中打开 code_package/config/runtime.json 文件:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    {
        "env": "local",
        "processname": "BlockchainSampleClient",
        "request": {
            "size": "16mb"
        },
     
        "chaincode": {
            "id": "",
            "name": "cp_chaincode"
        },
     
        "databases": {
            "devWorksAppMaster": "devworks_app_master"
     
        },
     
        "VCAP_SERVICES": {
            "cloudantNoSQLDB": [{
                "name": "Cloudant NoSQL DB-vern",
                "label": "cloudantNoSQLDB",
                "plan": "Lite",
                "credentials": {
                     
                }
            }],
            "ibm-blockchain-5-prod": [{
                "name": "Blockchain-v6-devWorks",
                "label": "ibm-blockchain-5-prod",
                "plan": "ibm-blockchain-plan-5-prod",
                "credentials": {
                     
                }
            }]
        },
     
        "log4js": {
            "replaceConsole": true,
            "appenders": [{
                "type": "console"
            }, {

    在 chaincode 对象下,将 id 的值替换为您在第 4-5 步中保存的链代码 ID

    将 ibm-blockchain-5-prod 对象下的 credentials 对象的内容替换为您在第 1-3 步中从 Bluemix 上的 Blockchain 服务获取的服务凭证。这些服务凭证有一个证书链接,可下载用于客户端与 Blockchain 服务之间的 TLS 通信的证书:
    "cert": https://blockchain-certs.mybluemix.net/us.blockchain.ibm.com.cert

    尽管我已将该证书包含在 code_package 中,但建议您将该证书替换为上述链接中找到的证书。将该证书放在 code_package 的根文件夹中。

    类似地,如果您想使用基于 Cloudant 的键值存储实现来运行客户端,可将 cloudantNoSQLDB 对象下的 credentials 对象的内容替换为您在第 2-3 步中从 Bluemix 上的 Cloudant 服务获取的服务凭证

代码包结构

我们快速看看 code_package 的结构,然后再继续介绍第 7 步。

  • blockchain_sample_client.js 包含客户端代码,该代码将注册和登记用户,并创建和从区块链获取贷款申请。
  • chaincode 包含 chaincode_sample.go 文件,该文件将被部署到区块链供以后调用。
  • config 文件夹包含 runtime.json 文件,您之前已更新了该文件。它包含客户端应用程序的所有配置信息。
  • src 文件夹包含 blockchain_sample_client 依赖的所有源代码。
  • src/blockchain/blockchain_network.js 文件包含所有设置和引导代码,用于设置和配置客户端来与区块链网络通信。
  • src/blockchain/blockchain_sdk.js 文件包含的 Hyperledger Fabric Client 代码用于与区块链网络交互,以便执行注册、登记、调用和查询。
  • src/database/datastore.js 包含与 Bluemix 上的 Cloudant 服务通信所需的代码。
  • src/database/model/kv_store_model.js 是一个基于 Cloudant 的键值存储实现, Hyperledger Fabric Client 客户端将使用它管理 ECert 和用户元数据。
  1. 在终端中导航到 code_package/ 并运行:
    node blockchain_sample_client.js

    这是输出的一个片段:

    样本客户端能成功地在区块链上创建一个抵押贷款申请,随后从区块链账本获得它。

区块链样本客户端

现在详细分析客户端代码,以了解它的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function runClient(){
     
    logHelper.initialize(config.processname, config.env, config.log4js);
    logger = logHelper.getLogger('blockchain_sample_client.js');
 
    var user = 'vojha25';
    var affiliation = 'Bank_Home_Loan_Admin';
 
    var args = process.argv.slice(2);
    if(args.length >=1){
        var input = JSON.parse(args);
        if(validate.isValidString(input['user'])){
            user = input['user'];
        }
        if(validate.isValidString(input['affiliation'])){
            affiliation = input['affiliation'];
        }
    }
 
    setup()
    .then(function(resp){
         return bcSdk.recursiveRegister({username: user, affiliation: affiliation})
    })
    .then(function(resp){
        return bcSdk.recursiveLogin({username: user, password: resp['body']['password'] })
    })
    .then(function(resp){
        var id = Math.floor(Math.random() * (100000 - 1)) + 1;
        var maStr = '{"propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":162000,"otherExpenditure":0,"monthlyRent":41500,"monthlyLoanPayment":40000},"status":"Submitted","requestedAmount":4000000,"fairMarketValue":5800000,"approvedAmount":4000000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}';
        var ma = JSON.parse(maStr);
        ma['id'] = 'la'+id;
        return bcSdk.createMortgageApplication({user: user, mortgageApplication: ma})
 
    })
    .then(function(resp){
        var ma = resp.body;
        return bcSdk.getMortgageApplication({user: user, id: ma['id']})
    })
    .then(function(resp){
        logHelper.logMessage(logger,"runClient","Fetched mortgage application",resp.body);
    })
    .catch(function(err){
        logHelper.logError(logger,"runClient","Error Occurred",err);
    })
}

上面的清单中显示的 blockchain_sample_client.js 将执行以下操作:

  1. 读取在调用客户端时传入的命令行参数。客户端需要两个值:用户名和从属关系。如果在客户端调用期间未提供任何值,客户端将使用默认值。
  2. 然后客户端会执行区块链网络的设置。在设置期间,会初始化链对象,并为其配置客户端通信的所有对等节点和成员服务。设置期间还会初始化 Cloudant 数据存储。
  3. 然后客户端会继续注册默认用户,或者注册作为命令行参数传入的用户。
  4. 随后是登记步骤,这一步使用注册期间获取的一次性密码来获取 ECert 对。这个 ECert 对将存储在基于 Cloudant 的键值存储中。
  5. 然后客户端会创建一个样本抵押贷款申请 JSON 对象。它随后会调用 blockchain_sdk.js 文件中的 createMortgageApplication API。这个 createMortgageApplication 方法调用区块链中的合适的链代码方法,并传入贷款申请 JSON 内容。
  6. 成功创建抵押贷款申请后,客户端会调用 getMortgageApplication 方法,该方法进而会调用合适的链代码方法来查询区块链,以获取上一步中创建的抵押贷款申请。

网络设置功能

设置

我们现在来详细分析 setup 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function setup(){
    return new Promise(function(resolve, reject){
        try{
            logHelper.logMethodEntry(logger,"setup");
 
            //Fetch IBM Bluemix Cloudant and Blockchain service instance configuration
            var cloudantConfig = config.VCAP_SERVICES[constants.VCAP_SERVICES_CLOUDANT][0];
            var blockchainConfig = config.VCAP_SERVICES[constants.VCAP_SERVICES_BLOCKCHAIN][0];
             
            //Setup datastore
            var result = datastore.initSync(cloudantConfig);
            if(result.statusCode != constants.SUCCESS){
                logHelper.logError(logger,'Could not initialize datastore', result);
                return reject({statusCode: 500, body: ''});
            }
 
            //Setup Cloudant based KeyValueStore
            var cloudantSetupDone = false;
            getCloudantKeyValStore(datastore, config.databases[constants.APP_MASTER_DB])
            .then(function(resp){
                cloudantKvStore = resp.body;
                cloudantSetupDone = true;
                return blockchainNetwork.setupBlockchain({blockchainConfig: blockchainConfig, ccName: constants['BLOCKCHAIN_CHAINCODE_NAME'] , kvStore: cloudantKvStore })
            })
            .then(function(resp){
                return resolve({statusCode: 200, body: ''});
            })
            .catch(function(err){
                if(cloudantSetupDone != true){
                    logHelper.logError(logger,'Could not initialize CloudantKeyValueStore', err);
                }
                else{
                    logHelper.logError(logger,'Blockchain setup failed. exiting...',err);
                }
                return reject(err);
            });
             
             
        }
        catch(err){
            logHelper.logError(logger,'Could not complete setup', err);
            throw reject({statusCode: 500, body: err});
        }
    })
     
}

在上面的清单中:

  • 第 7 和第 8 行会从 runtime.json 文件中获取 Cloudant 和 Blockchain 服务凭证,我们之前已更新过这些信息。config 模块会解析 runtime.json 文件,并将其内容作为一个 JSON 对象提供。
  • 第 11 到第 15 行会初始化 Cloudant 数据存储。datastore.js 文件用于获取一个数据库实例的句柄,该句柄可用于在我们的 Cloudant 数据库上执行 CRUD 操作。
  • 第 18 到第 24 行会使用之前获得的数据库实例,初始化基于 Cloudant 的自定义键值存储实现。Hyperledger Fabric Client SDK 链对象将使用这个键值存储来管理 ECert 和其他用户元数据。随后是区块链网络设置。

区块链网络设置

现在让我们来看看 setupBlockchain 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function setupBlockchain(params){
    return new Promise(function(resolve,reject){
        try{
            logHelper.logEntryAndInput(logger, 'setupBlockchain', params);
 
            if(!validate.isValidJson(params)){
                logHelper.logError(logger, 'setupBlockchain', 'Invalid params');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not setup blockchain. Invalid params' });
            }
            var goPath = __dirname+'/../../chaincode/';
            process.env.GOPATH = goPath;
             
             
            chainInit({ccName: params.ccName, kvStorePath: params.kvStorePath, kvStore: params.kvStore})
            .then(function(resp){
                return loadNetworkConfiguration(params.blockchainConfig);
            })
            .then(function(resp){
                return configureNetwork();
            })
            .then(function(resp){
                logHelper.logMessage(logger, 'setupBlockchain', 'blockchain setup complete');
                isSetupComplete = true;
                return resolve({statusCode: constants.SUCCESS, body: 'blockchain setup complete'});
            })
            .catch(function(err){
                logHelper.logError(logger, 'setupBlockchain', 'Could not setup blockchain', err);
                return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not setup blockchain', err});
            });
        }
        catch(err){
            logHelper.logError(logger, 'setupBlockchain', 'Could not setup blockchain', err);
            return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not setup blockchain', err});
        }
    });
}

此设置包含 3 个函数。第 14 行 以 chainInit 开头,随后是 loadNetworkConfiguration,最后是 configureNetwork

这是初始化这个链的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
var kvStore = params.kvStore;
            if(!validate.isValid(kvStore)){
                logHelper.logError(logger, 'chainInit', 'Invalid kvStore');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not initialize Chain. Invalid kvStore' })
                 
            }
 
            chain = hfc.newChain(ccName);
            // Configure the KeyValStore which is used to store sensitive keys
            // as so it is important to secure this storage.
            chain.setKeyValStore(kvStore);
            chain.setECDSAModeForGRPC(true);
  • 第 1 到第 6 行负责输入验证。
  • 第 8 行中,使用 Hyperledger Fabric Client SDK 模块 hfc 创建了一个新的链对象。
  • 第 11 行中,为这个链对象配置了键值存储实现,以便用它来管理用户的 ECert。在本例中,该实现是 Cloudant 键值存储实现。Hyperledger Fabric Client SDK 还提供了一个默认的基于文件的键值存储实现可供使用。(但是,要将区块链客户端应用程序部署到云上,不建议使用基于文件的实现,因为重新部署客户端代码会导致基于文件的键值存储丢失。在这种情况下,所有以前注册的用户都将失去作用,因为它们没有必要的证书来执行验证。)
  • 第 12 行中,用于 TLS 通信的密码组被设置为 ECDSA。

链对象的配置属性

  • IsSecurityEnabled:如果为链对象配置了有效的成员服务端点,则需要考虑启用安全性。启用安全性意味着,将使用 TCert 生成一个基于 ECDSA 的数字签名,所有链代码调用事务中都会发送该签名。各个对等节点都将使用这个 TCert 来验证调用方。
  • IsPreFetchMode:在预取模式中,链对象在初始化时将向成员服务请求一批 TCert,而不是等待一次事务调用。这么做是为了提高性能。
  • TCertBatchSize:获取 TCert 批量请求中包含的要返回的 TCert 数量。默认值为 200。

加载区块链网络配置

让我们来看看 loadNetworkConfiguration 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function loadNetworkConfiguration(blockchainConfig){
    return new Promise(function(resolve, reject){
        try {
            logHelper.logEntryAndInput(logger, 'loadNetworkConfiguration');
             
            if(!validate.isValidJson(blockchainConfig)){
                logHelper.logError(logger, 'loadNetworkConfiguration', 'Invalid blockchainConfig');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not loadNetworkConfiguration. Invalid blockchainConfig' });
            }
             
            var bcService = blockchainConfig;
             
            var peers = bcService.credentials.peers;
            for (var i in peers) {
                peerURLs.push(constants['BLOCKCHAIN_NW_PROTOCOL'] + peers[i].discovery_host + ":" + peers[i].discovery_port);
                peerHosts.push("" + peers[i].discovery_host);
            }
            var ca = bcService.credentials.ca;
            for (var i in ca) {
                caURL = constants['BLOCKCHAIN_NW_PROTOCOL'] + ca[i].url;
            }
 
            //users are only found if security is on
            if (bcService.credentials.users) users = bcService.credentials.users;
            for (var z in users) {
                if (users[z].username == constants['BLOCKCHAIN_REGISTRAR_ID']) {
                    registrarPassword = users[z].secret;
                }
            }
 
            logHelper.logMessage(logger, 'loadNetworkConfiguration', 'Successfully loaded network configuration');
            return resolve({statusCode: constants.SUCCESS, body: 'Successfully loaded network configuration'});
 
        }
        catch (err) {
            logHelper.logError(logger, 'loadNetworkConfiguration', 'Could not load Network Configuration', err);
            return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not load Network Configuration'});
        }
    });
     
}
  • 第 13 到第 17 行将会解析 Blockchain 服务配置,以获取对等节点列表,并将这些对等节点的发现 URL 添加到 peerHosts 集合中。
  • 第 18 到第 21 行会获取 CA(成员服务)的 URL。constants['BLOCKCHAIN_NW_PROTOCOL'] 的值为 grpcs。Bluemix 上的 IBM Blockchain 服务仅支持安全通信。
  • 第 23 到第 29 行从 WebAppAdmin 的 Blockchain 服务凭证中包含的预注册用户列表中获取密码。之前已经提到过,WebAppAdmin 用户拥有注册员权利。这意味着可使用 WebAppAdmin 用户向区块链注册其他新用户。此密码以后会用于登记 WebAppAdmin。

区块链网络配置

让我们来看看 configureNetwork 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function configureNetwork() {
    return new Promise(function(resolve, reject){
 
        try{
            logHelper.logEntryAndInput(logger, 'configureNetwork');
            var pem;
            fs.readFile(constants['BLOCKCHAIN_NW_CERT_PATH'], function(err, data){
                if(validate.isValid(err)){
                    logHelper.logError(logger,'configureNetwork', 'Could not read cert: '+constants['BLOCKCHAIN_NW_CERT_PATH']);
                    return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not read cert: '+constants['BLOCKCHAIN_NW_CERT_PATH'] });
                }
                else{
                    pem = data;
                    chain.setMemberServicesUrl(caURL, { pem: pem });
                    for (var i in peerURLs) {
                        chain.addPeer(peerURLs[i], { pem: pem });
                    }
 
                    recursiveLogin({username: constants['BLOCKCHAIN_REGISTRAR_ID'], password: registrarPassword, chain: chain })
                    .then(function(resp){
                        logHelper.logMessage(logger,'configureNetwork', 'Successfully enrolled registrar: '+constants['BLOCKCHAIN_REGISTRAR_ID']);
                        var registrarMember = resp.body;
                        chain.setRegistrar(registrarMember);
                        return resolve({statusCode: constants.SUCCESS, body: 'Network configuration complete'});
                    })
                    .catch(function(err){
                        return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not enroll registrar: '+constants['BLOCKCHAIN_REGISTRAR_ID'] });
                    })
 
                }
 
            });
        
        }
        catch(err){
            logHelper.logError(logger, 'configureNetwork', 'Could not configure network', err);
            return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not configure network', err});
        }
    });
     
}
  • 第 7 行会读取我们从 Blockchain 服务凭证中提供的证书路径下载的 pem 文件。已为 Bluemix Blockchain 网络中的所有通信启用了 TLS。这也包括客户端应用程序与网络实体(比如成员服务和对等节点)之间的通信。
  • 第 14 到第 17 行将之前获取的成员服务和对等节点 URL 添加到链对象和 pem 文件中。链对象在与成员服务和对等节点通信时会使用 pem 文件。
  • 第 19 到第 28 行会在区块链网络中登记 WebAppAdmin。之前已经解释过,登记后将生成 ECert 对,链对象会将该证书对存储在 Cloudant 键值存储中。如果通过 Cloudant 服务查看您的 Cloudant 数据库中的文档,现在将会看到一个针对 member.WebAppAdmin 的条目,如下面示例所示:
    {"name":"WebAppAdmin","enrollment":{"key":"f4b19c7195d2da0ea
    02a47fa8e2aabdc0b4ba610720a696e895b400fb81cb9be","cert":"d2
    d2d2d454e44204543445341205055424c4943204b45592d2d2d2d2d
    0a","queryStateKey":{"type":"Buffer","data":[91,181,140,162,159,21
    8,158,144,230,192,52,99,100,155,235,23,72,242,97,158,82,29,54,22
    2]}}}
  • 第 23 行将链的注册员设置为 WebAppAdmin 成员对象。链将使用这个注册员向区块链注册新成员/用户。

区块链设置到此就完成了。现在看看其他重要方法:

使用功能

注册新用户

让我们来看看 doRegister 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var roles = params.roles;
            if(!validate.isValidArray(roles)){
                roles = ['client'];
            }
 
            var enrollsecret
            var chain = bcNetwork.getChain();
            var reg = chain.getRegistrar();
            var chainAsync = Promise.promisifyAll(chain);
 
            chainAsync.getMemberAsync(username)
            .then(function(member){
                var memberAsync = Promise.promisifyAll(member);
                 
                    var registrationRequest = {
                        enrollmentID: username,
                        attributes: [
                            {name: 'role', value: affiliation},
                            {name: 'username', value: username}
                        ],
                        affiliation: 'group1',
                        registrar: reg,
                        roles: roles
                         
                    };
                     
                return memberAsync.registerAsync(registrationRequest);
            })
            .then(function(enrollsec){
                logHelper.logMessage(logger, 'registerUser', 'Successfully registered user on blockchain: '+username);
                enrollsecret = enrollsec;
                return resolve({statusCode: constants.SUCCESS, body: {password: enrollsecret}});
                 
            })

要向基于 Hyperledger Fabric 的区块链注册新用户,可在注册请求中发送以下字段:

  1. enrollmentID(字符串):此成员/用户的登记 ID。
  2. roles(字符串数组):表示与此成员有关联的角色。有效的角色包括 client(默认)、peervalidator 和 auditor
  3. affiliation(字符串):此成员的从属关系。从属关系可在 manifest.yaml 文件中找到。对于 Bluemix 上的 Blockchain 服务,父分组是 group1
  4. attributes(属性数组):授予此成员的属性名称和值。这些属性由用户定义,存储在 ACA 中。所有这些属性或部分属性都将嵌入在此成员请求的 TCert 中。链代码可以使用相同的属性,通过解析调用方 TCert 来实现访问控制。
  5. registrar:用户可以具有注册员的角色,该角色使用户能注册其他成员。需要在请求中包含拥有注册员权利的成员对象。这个注册员成员将用于注册所请求的成员。
  • 第 7 和第 8 行将从 blockchain_network.js 文件获取链对象,并从该链对象获取注册员对象。
  • 第 11 行将从链中获取成员对象。在基于 Hyperledger Fabric 与区块链网络进行任何交互时,我们都必须拥有一个成员对象的句柄,代表这个成员对象来执行所有链代码部署和调用。getMember 方法从 Cloudant 键值存储中获取成员 ECert 和其他元数据信息。
  • 第 15 到第 23 行将创建一个注册请求。请注意 username 和 role 的属性键/值对。本教程系列的第 1 部分介绍了链代码如何利用这些属性来实现访问控制。
  • 第 27 行将会调用成员对象上的注册方法,后续代码会获取从 ECA 返回的一次性密码令牌,以便在登记流程中使用它。

登记用户

这是 doLogin 方法的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var chain = bcNetwork.getChain();
            var chainAsync = Promise.promisifyAll(chain);
 
            chainAsync.getMemberAsync(username)
            .then(function(member){
                var memberAsync = Promise.promisifyAll(member);
                return memberAsync.enrollAsync(password);
            })
            .then(function(crypto){
                logHelper.logMessage(logger, 'doLogin', 'Successfully logged in user on blockchain: '+username);
                return resolve({statusCode: constants.SUCCESS, body: crypto});
            })
            .catch(function(err){
                logHelper.logError(logger, 'doLogin', 'Could not login user on blockchain: '+username, err);
                return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not login user' });
            });
  • 和之前一样,第 1 到 6 行将获取链对象并从链中获取成员对象。
  • 第 7 行将调用成员对象上的登记方法,并传入从注册响应中获得的密码。
  • 第 9 行成功地从 ECA 获取 ECert 对。

创建抵押贷款申请

这是 createMortgageApplication 方法的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function createMortgageApplication(params) {
    return new Promise(function(resolve, reject){
        var mortgageApplication;
        try{
            logHelper.logEntryAndInput(logger, 'createMortgageApplication', params);
 
            if(!validate.isValidJson(params)){
                logHelper.logError(logger, 'createMortgageApplication', 'Invalid params');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not create mortgage application. Invalid params' })
            }
 
            var user = params.user;
            if(!validate.isValidString(user)){
                logHelper.logError(logger, 'createMortgageApplication', 'Invalid user');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not create mortgage application. Invalid user' })
            }
 
            mortgageApplication = params.mortgageApplication;
            if(!validate.isValidJson(mortgageApplication)){
                logHelper.logError(logger, 'createMortgageApplication', 'Invalid mortgageApplication');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not create mortgage application. Invalid mortgageApplication' })
            }
 
            
            var id = mortgageApplication['id'];
            var payload = JSON.stringify(mortgageApplication);
 
            var reqSpec = getRequestSpec({functionName: 'CreateLoanApplication', args: [id, payload]});
            recursiveInvoke({requestSpec: reqSpec, user: user})
            .then(function(resp){
                logHelper.logMessage(logger, 'createMortgageApplication', 'Successfully created mortgageApplication', resp.body);
                return resolve({statusCode: constants.SUCCESS, body: mortgageApplication});
            })
            .catch(function(err){  
                logHelper.logError(logger, 'createMortgageApplication', 'Could not create mortgageApplication', err);
                return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not create mortgageApplication' });
 
            });
 
        }
        catch(err){
            logHelper.logError(logger, 'createMortgageApplication', 'Could not create mortgage application on blockchain ledger: ', err);
            return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not create mortgage application' });
        }
    });
}
  • 第 7 到第 22 行执行输入验证。该方法需要两个参数:User(将用于调用链代码事务)和要持久保存在区块链上的抵押贷款申请 JSON 内容。
  • 第 28 行将创建调用链代码需要使用的请求规范。
  • 第 29 行将调用 recursiveInvoke 函数,该函数根据所提供的请求规范来实际调用链代码。

用于链代码调用的请求规范

这是 getRequestSpec 方法的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function getRequestSpec(params){
 
        if(!validate.isValidJson(params)){
            logHelper.logError(logger, 'getRequestSpec', 'Invalid params');
            throw new Error("Invalid params");
        }
 
        var chaincodeID = config['chaincode']['id']
        if(!validate.isValidString(chaincodeID)){
            logHelper.logError(logger, 'getRequestSpec', 'Invalid chaincodeID');
            throw new Error("Invalid chaincodeID");
        }
 
        var functionName = params.functionName;
        if(!validate.isValidString(functionName)){
            logHelper.logError(logger, 'getRequestSpec', 'Invalid function name');
            throw new Error("Invalid function name");
        }
 
        var args = []
         
        if(validate.isValidArray(params.args)){
            args = params.args;
        }
 
        var attributes = ['username', 'role']
         
        if(validate.isValidArray(params.attributes)){
            attributes = params.attributes;
        }
 
        var spec = {
            chaincodeID: chaincodeID,
            fcn: functionName,
            args: args,
            attrs: attributes
        }
 
        return spec;
}
  • 第 8 行将从配置 (runtime.json) 中获取部署的链代码的链代码 ID。这是我们想要调用的链代码。
  • 第 14 行将获取要在链代码中调用的函数的名称。在本例中,它是 CreateLoanApplication 方法。
  • 第 26 行将输入属性键。这些键已硬编码到角色和用户名中,但在理想情况下,应将它们作为参数传递给 getRequestSpec方法。

    备注:如果未在请求规范中传入属性键,这些属性不会在 TCert 获取请求中发送给 TCA,因此得到的 TCert 将没有任何嵌入式属性。当链代码尝试解析此 TCert 来获取特定属性(例如角色和用户名)实现访问控制时,该操作会失败。第 32 到 37 行包含实际的请求规范模式。

调用部署在区块链上的链代码

这是 doInvoke 方法的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var chain = bcNetwork.getChain();
            var chainAsync = Promise.promisifyAll(chain);
             
 
            chainAsync.getMemberAsync(user)
            .then(function(member){
                 
                var tx = member.invoke(requestSpec);
                tx.on('submitted', function(data) {
                    logHelper.logMessage(logger, 'doInvoke', 'Transaction for invoke submitted ',requestSpec);
                    return resolve({statusCode: constants.SUCCESS, body: data});
                     
                });
 
                tx.on('complete', function(data) {
                    //logHelper.logMessage(logger, 'doInvoke', 'Transaction for invoke complete ',data);
                     
                });
 
                tx.on('error', function (err) {
                    logHelper.logError(logger, 'doInvoke', 'Could not perform invoke ',err);
                    return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not perform invoke ' });
                    
                });
            })
  • 第 1 到第 7 行将重复获取链对象并获取成员对象的过程。
  • 第 8 行将在成员对象上调用 invoke 方法,以便根据传入的请求规范调用链代码。这次调用会返回一个事务对象。

备注 1:您需要在事务对象上注册 submittedcomplete 和 error 事件,才能获知事务的状态

  • Submitted:事务已成功提交到区块链网络并被该网络执行。此时会从区块链网络返回一个事务 UUID。
  • Error:事务无法提交到区块链网络,或者无法被区块链网络接受。
  • Complete:事务已完成。这不代表成功完成。(complete 事件会连同 submitted 和 error 事件一起被调用。)

备注 2:调用请求仅返回链代码调用的事务 UUID。它不会返回从链代码函数本身返回的任何数据。

备注 3:即使实际的链代码函数抛出了错误且没有成功完成,也不会调用 error 事件。此时会调用 complete 事件。

我采用的方法是等待 submitted 事件。这至少表明链代码函数将被调用。要确保成功执行链代码,您可以:

  • 订阅一个将在成功执行链代码函数后生成的事件。
  • 查询区块链。

从区块链获取抵押贷款申请

这是 getMortgageApplication 方法的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function getMortgageApplication(params) {
    return new Promise(function(resolve, reject){
        
        try{
            logHelper.logEntryAndInput(logger, 'getMortgageApplication', params);
 
            if(!validate.isValidJson(params)){
                logHelper.logError(logger, 'getMortgageApplication', 'Invalid params');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not fetch mortgage application. Invalid params' })
            }
 
            var user = params.user;
            if(!validate.isValidString(user)){
                logHelper.logError(logger, 'getMortgageApplication', 'Invalid user');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not fetch mortgage application. Invalid user' })
            }
 
            var id = params.id;
            if(!validate.isValidString(id)){
                logHelper.logError(logger, 'getMortgageApplication', 'Invalid id');
                return reject({statusCode: constants.INVALID_INPUT, body: 'Could not fetch mortgage application. Invalid id' })
            }
 
            var reqSpec = getRequestSpec({functionName: 'GetLoanApplication', args: [id]});
            recursiveQuery({requestSpec: reqSpec, user: user})
            .then(function(resp){
                logHelper.logMessage(logger, 'GetMortgageApplication', 'Successfully fetched mortgage application', resp.body);
                return resolve({statusCode: constants.SUCCESS, body: resp.body});
            })
            .catch(function(err){  
                logHelper.logError(logger, 'GetMortgageApplication', 'Could not fetch mortgage application', err);
                return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not fetch mortgage applications' });
 
            });
 
        }
        catch(err){
            logHelper.logError(logger, 'getMortgageApplication', 'Could not fetch property ad ', err);
            return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not fetch mortgage application ' });
        }
    });
}

getMortgageApplication 方法在结构上类似于 createMortgageApplication 方法。它接受需要代表其调用链代码的用户,以及要获取的抵押贷款申请的 ID。

随后是 getRequestSpec 方法,该方法像之前一样创建请求规范并调用 recursiveQuery 方法来查询区块链。

使用链代码查询区块链

这是 doQuery 方法的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var chain = bcNetwork.getChain();
            var chainAsync = Promise.promisifyAll(chain);
 
            chainAsync.getMemberAsync(user)
            .then(function(member){
                 
                var tx = member.query(requestSpec);
                tx.on('submitted', function() {
                    logHelper.logMessage(logger, 'doQuery','Transaction for query submitted');
                });
 
                tx.on('complete', function(data) {
                    try{
                        logHelper.logMessage(logger, 'doQuery', 'Transaction for query complete ',requestSpec);
                        var buffer = new Buffer(data.result);
                        var jsonResp = JSON.parse(buffer.toString());
                        return resolve({statusCode: constants.SUCCESS, body: jsonResp});
                    }
                    catch(err){
                        logHelper.logError(logger,'doQuery','Could not parse query response',err);
                        return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not parse query response ' });
                    }
                });
 
                tx.on('error', function (err) {
                    logHelper.logError(logger, 'doQuery', 'Could not perform query ',err);
                    return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not perform query ' });
                    
                });
            })
  • 第 7 行将对成员对象调用 query 方法,并传入合适的请求规范。

在区块链上成功执行查询事务后,将调用 complete 事件。从区块链读取的数据需要读入到一个缓冲区中。因为我们的链代码实现将数据存储为 JSON 字符串,所以需要将返回的内容解析为 JSON。

订阅事件

基于 Hyperledger Fabric 的区块链网络拥有一个内置事件中心,我们的客户端应用程序将连接到该事件中心并监听事件。

这是连接到事件中心并订阅事件的代码段。此代码段包含在 blockchain_network.js 文件中,但由于下面的已知问题部分提到的第二个问题,它已被注释掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
chain.eventHubConnect(peerEventHosts[0],{pem:pem});
setupEvents();
 
/**
 * Sample method to showcase how to subscribe and consume events emitted from blockchain
 */
function setupEvents(){
    try{
    var eh = chain.getEventHub();
    var cid = config['chaincode']['id'];
    var regid = eh.registerChaincodeEvent(cid, "^eventSender$", function(event) {
        console.log(event);
        var buffer = new Buffer(event.payload);
        console.log(buffer.toString());
    });
    console.log("EVENT SETUP DONE");
}
catch(err){
    console.log(err);
    console.log("Could not setup events");
}
}
 
process.on('exit', function (){
    console.log('exit called');
    chain.eventHubDisconnect();
});
  • 第 1 行将连接指定对等节点的事件中心。runtime.json 中的 Blockchain 服务的服务凭证拥有每个对等节点的 event_host和 event_port 值。这些值用于与事件中心建立连接。
  • 第 2 行将调用 setupEvents() 方法,该方法可用于订阅和处理来自区块链的事件。
  • 第 11 行将展示如何注册链代码事件,并定义该事件发布时的处理函数。我们部署的链代码会在成功创建贷款申请时发出一个事件。我们在这里注册了同一个事件。

如果您运行的客户端启用了事件(取消注释 blockchain_network.js 中的事件代码),客户端将接收链代码中定义的createLoanApplication 事件并打印到控制台。

Hyperledger Fabric Client SDK 的已知问题

  • 在成员对象上调用 invoke、query、deploy 时,出现随机/间歇性的安全握手失败错误。

    为通信启用 TLS 时会出现这种错误。发生此故障的一个主要原因是,操作系统中没有足够的文件描述符。该错误在 Mac 机器(EL Capitan 版)上最常见,在 Windows 7 机器上较少发生,而将应用程序/客户端部署在 Bluemix 上并运行时,几乎可以忽略该错误。

    为了在开发和测试期间解决此问题,我将对区块链网络的所有 Hyperledger Fabric Client SDK 调用都包装在递归函数中。可通过修改 blockchain_sdk.js 文件中的 retryInterval 和 retryLimit 变量,更改重试间隔和重试次数。

  • 事件中心未连接或在连接后崩溃。

    此错误已被归因于所依赖的一些第三方库(比如 grpc node 模块)中未解决的 bug。

结束语

通过本文中的步骤和代码,您可以开发您自己的 Node.js 客户端来与基于 Hyperledger Fabric v0.6 的区块链网络通信,并将第 1 部分中开发的链代码部署到 Bluemix 上的 IBM Blockchain 服务,以便可从您的客户端应用程序调用它。下载与本教程相关的代码并开始实践吧。

这个基于 Hyperledger Fabric v0.6 的教程系列已介绍完毕。我正针对 Hyperledger Fabric v1.0 更新本系列,敬请期待!

posted @ 2018-09-08 18:39  stardsd  阅读(1478)  评论(0编辑  收藏  举报