Hyperledger-Fabric · 2018年2月5日 4

Hyperledger-Fabric安装开发部署(五)NodeJs-SDK详解

简介

  • 开发我们的区块链对外的API接口,实现之前我们在Cli中进行的查询数据、插入数据、查询区块信息、查询交易信息、安装链码等等,将这些操作统一写成对外开放的RUSTful Web API。
  • 本篇内容介绍的相关代码可能需要了解很多Nodejs相关的知识,还有许多NPM中的插件框架;诸如express、log4j、express-jwt等等。

官方英文版教程:
http://hyperledger-fabric.readthedocs.io/en/latest/index.html

官方Nodejs-SDK教程:
https://fabric-sdk-node.github.io/


1. 详解balance-transfer中Nodejs-SDK相关内容

这里我们的侧重点完全在于区块链相关的代码部分,其他如JWT、express等内容不做深究。
先简单介绍一下涉及到的相关文件:

此次讲解的重点全部在app文件夹内,看命名也能看出来相关功能。主要讲解balance-transfer项目内的相关代码;从WebAPI接口逐渐深入,解释一些常用方法的使用,说明相关Nodejs-SDK的功能及使用方法等。
注:[1]意为标注额外说明

1.1 Register and enroll user-注册用户

源码中标记此部分的title为【Register and enroll user】,起初不是很理解Register和enroll在歪果仁眼里有什么样的区别;后来略有深入便知道了区别,先吧这两个当做不同的概念看待,接下来会讲述。

API:http://localhost:5010/users
在helper.js中有一个getRegisteredUsers方法:

var getRegisteredUsers = function(username, userOrg, isJson) {
    var member;
    var client = getClientForOrg(userOrg);
    var enrollmentSecret = null;
    //newDefaultKeyValueStore 获取KeyValueStore类的一个实例。默认情况下,它返回基于文件(FileKeyValueStore)的内置实现。
    //这可以通过配置设置key-value-store来覆盖,其值是替代实现的CommonJS模块的完整路径。
    //返回的是一个Promise类型的KeyValueStore实例。(如果没有接触过Nodejs的Promise,建议先补充相关知识,私以为这里的Promise使用的不是很好,误抄此种)
    return hfc.newDefaultKeyValueStore({
        path: getKeyStoreForOrg(getOrgName(userOrg))
    }).then((store) => {
        client.setStateStore(store);
        // clearing the user context before switching
        client._userContext = null;
        //getUserContext 按给定名称返回用户。这可以是一个同步调用或异步调用,取决于[checkPersistent](此方法的第二个参数),如果是true,方法是异步的,并返回一个Promise,否则它是同步的。
        //在config.json中有一个【"keyValueStore":"/tmp/fabric-client-kvs"】[1]。这个路径就对应了此方法读取用户的路径。
        //这一步相当于在判断用户是否存在,存在返回对应实体,否则null。
        return client.getUserContext(username, true).then((user) => {
            //isEnrolled检测当前用户是否注册过
            if (user && user.isEnrolled()) {
                logger.info('Successfully loaded member from persistence');
                return user;
            } else {
                let caClient = caClients[userOrg];
                //getAdminUser也在helper.js中和此方法雷同
                return getAdminUser(userOrg).then(function(adminUserObj) {
                    member = adminUserObj;
                    //register注册一个新用户并返回注册秘钥
                    //共有两个重载方法,具体可以查看其定义
                    return caClient.register({
                        enrollmentID: username,
                        affiliation: userOrg + '.department1'
                    }, member);
                }).then((secret) => {
                    enrollmentSecret = secret;
                    logger.debug(username + ' registered successfully');
                    //enroll是给registered的用户颁发X509 certificate,register和enroll在这里是完全不同的作用,一定要区分开
                    //在SDK中还有一个reenroll方法,作用不言而喻
                    //同时enroll也有两个重载方法
                    return caClient.enroll({
                        enrollmentID: username,
                        enrollmentSecret: secret
                    });
                }, (err) => {
                    logger.debug(username + ' failed to register');
                    return '' + err;
                    //return 'Failed to register '+username+'. Error: ' + err.stack ? err.stack : err;
                }).then((message) => {
                    //判断enroll的结果
                    if (message && typeof message === 'string' && message.includes(
                            'Error:')) {
                        logger.error(username + ' enrollment failed');
                        return message;
                    }
                    logger.debug(username + ' enrolled successfully');

                    member = new User(username);
                    member._enrollmentSecret = enrollmentSecret;
                    //设置此注册用户的实例对象
                    return member.setEnrollment(message.key, message.certificate, getMspID(userOrg));
                }).then(() => {
                    client.setUserContext(member);
                    return member;
                }, (err) => {
                    logger.error(util.format('%s enroll failed: %s', username, err.stack ? err.stack : err));
                    return '' + err;
                });;
            }
        });
    }).then((user) => {
        if (isJson && isJson === true) {
            var response = {
                success: true,
                secret: user._enrollmentSecret,
                message: username + ' enrolled Successfully',
            };
            return response;
        }
        return user;
    }, (err) => {
        logger.error(util.format('Failed to get registered user: %s, error: %s', username, err.stack ? err.stack : err));
        return '' + err;
    });
};

具体代码运行流程可以使用VScode调试功能逐步查阅;总结下来就是说如果需要注册一个用户,流程是:
1. 调用FabricCAClient.register()得到用户对应的secret;
2. 调用FabricCAClient.enroll(),传入对应用户与secret,得到用户对应的证书信息。

额外的方法还有:
FabricCAClient.reenroll()
重新给某用户颁发证书。

FabricCAClient.revoke()
撤销现有证书(注册证书或交易证书),或撤消发放给所有用户的证书。如果撤销特定的证书,则需要管理者密钥标识符和序列号。如果通过注册ID撤销,那么未来所有注册此ID的请求都将被拒绝。


[1]--'/tmp/fabric-client-kvs'创建用户的时候会在这里生成三个文件,如下图:

'qaz'是注册的用户名,另外两个文件就是一对公私钥。可以记事本打开qaz文件:

{
    "name": "qaz",
    "mspid": "Org1MSP",
    "roles": null,
    "affiliation": "",
    "enrollmentSecret": "qImZAqUEKCFj",
    "enrollment": {
        "signingIdentity": "31412966c73a4423ba7846c46188aaef35b18051cf8caa754f9686877de69c26",
        "identity": {
            "certificate": "-----BEGIN CERTIFICATE-----nMIIB7zCCAZWgAw..........................cWPzudxQfJg==n-----END CERTIFICATE-----n"
        }
    }
}

可以明显的看到'signingIdentity'所对应的value与其他两个文件名一致。
(此处公私钥观察下来不是同一时间生成的,具体有待之后深入考究)
这个文件夹内保存了相关证书信息。

1.2 调用ChainCode执行交易

这里可能是大部分人最关心的地方,怎么调用ChainCode。

api:http://localhost:5010/channels/mychannel/chaincodes/mycc
先找到app.js中如下的方法,这个api就是调用chaincode执行交易;

// Invoke transaction on chaincode on target peers
//这个带冒号的写法是express的一种路由的设置方法
//实际post地址类似http://localhost:5010/channels/mychannel/chaincodes/mycc
//用req.params.chaincodeName可以取得对应的值:mycc
app.post('/channels/:channelName/chaincodes/:chaincodeName', function(req, res) {
    logger.debug('==================== INVOKE ON CHAINCODE ==================');
    var peers = req.body.peers;
    var chaincodeName = req.params.chaincodeName;
...
...
});

先看结果再讲过程:

有一个点需要说明一下,"fcn":"invoke",这个fcn对应的值在某一个版本是move而不是invoke;args就是传递的参数,a向b转1(在chaincode中已经定义好了)。

{
    "peers":["peer1"],
    "fcn":"invoke",
    "args":["a","b","1"]
}

返回值是e16ed0b21094030d796ee210c0fd20922fdc099517ddf3c90f87b3a2a32bb0a8
这就是txid,在后边查询这比交易我们还会用到。


打开app/invoke-transaction.js,这里便是调用ChainCode执行交易的主方法:

var invokeChaincode = function(peerNames, channelName, chaincodeName, fcn, args, username, org) {
    logger.debug(util.format('n============ invoke transaction on organization %s ============n', org));
    var client = helper.getClientForOrg(org);
    var channel = helper.getChannelForOrg(org);
    var targets = (peerNames) ? helper.newPeers(peerNames, org) : undefined;
    var tx_id = null;
    //获取指定用户的model,虽然参数中没有用户,但是有token
    return helper.getRegisteredUsers(username, org).then((user) => {
        //返回一个新的TransactionID对象。
        //这里必须要取得正确的user,但是这里却没有将user赋给任何变量,因为在helper中已经帮我们做了。
        tx_id = client.newTransactionID();
        logger.debug(util.format('Sending transaction "%j"', tx_id));
        // send proposal to endorser
        //这里所用的链码必须要安装并且实例之后才可能使用
        var request = {
            chaincodeId: chaincodeName,
            fcn: fcn,
            args: args,
            chainId: channelName,
            txId: tx_id
        };
        //确定哪些peer会收到这个消息;当没有提供peer时,应该是channel下的所有都接受此消息。
        if (targets)
            request.targets = targets;
        //将交易发送到目标链码,并确定是否成功。
        //这里还有第二个参数timeout
        return channel.sendTransactionProposal(request);
    }, (err) => {
        logger.error('Failed to enroll user '' + username + ''. ' + err);
        throw new Error('Failed to enroll user '' + username + ''. ' + err);
    }).then((results) => {
        //results[0]中包含了来自所有peer中的执行结果
        //如果传入N个peer则proposalResponses.length==N
        var proposalResponses = results[0];
        //results[1]发送交易请求到orderer时会用到
        var proposal = results[1];
        var all_good = true;
        //循环判断每个peer的执行结果
        for (var i in proposalResponses) {
            let one_good = false;
            if (proposalResponses && proposalResponses[i].response &&
                proposalResponses[i].response.status === 200) {
                one_good = true;
                logger.info('transaction proposal was good');
            } else {
                logger.error('transaction proposal was bad');
            }
            all_good = all_good & one_good;
        }
        if (all_good) {
            logger.debug(util.format(
                'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s',
                proposalResponses[0].response.status, proposalResponses[0].response.message,
                proposalResponses[0].response.payload, proposalResponses[0].endorsement
                .signature));
            var request = {
                proposalResponses: proposalResponses,
                proposal: proposal
            };
            // set the transaction listener and set a timeout of 30sec
            // if the transaction did not get committed within the timeout period,
            // fail the test
            var transactionID = tx_id.getTransactionID();
            var eventPromises = [];

            if (!peerNames) {
                peerNames = channel.getPeers().map(function(peer) {
                    return peer.getName();
                });
            }
            //newEventHubs需要单独讲,后边会讲解[2]
            //简单说就是包含EventHub的数组
            var eventhubs = helper.newEventHubs(peerNames, org);
            for (let key in eventhubs) {
                let eh = eventhubs[key];
                eh.connect();

                let txPromise = new Promise((resolve, reject) => {
                    let handle = setTimeout(() => {
                        eh.disconnect();
                        reject();
                    }, 30000);
                    //注册一个事件,当传入txID的事务提交到一个块中后接收返回消息
                    //registerTxEvent有三个参数,这里只用了两个
                    //txid、onEvent、onError,后两个均是回调函数,成功失败分别调用
                    //由于连接建立是异步运行的,因此可能会在EventHub收到网络问题通知之前进行注册调用。最好的做法是做一个“onError”回调,当这个EventHub有问题的时候会被通知。
                    eh.registerTxEvent(transactionID, (tx, code) => {
                        clearTimeout(handle);
                        //注销当前的事务监听器。
                        eh.unregisterTxEvent(transactionID);
                        eh.disconnect();

                        if (code !== 'VALID') {
                            logger.error(
                                'The balance transfer transaction was invalid, code = ' + code);
                            reject();
                        } else {
                            logger.info(
                                'The balance transfer transaction has been committed on peer ' +
                                eh._ep._endpoint.addr);
                            resolve();
                        }
                    });
                });
                eventPromises.push(txPromise);
            };
            //将包含交易信息的数据发送给orderer进行下一步处理。这是事务生命周期的第二阶段。orderer会在这个channel下对交易进行排序,并将结果块交给peer进行链码的背书策略验证。
            //当验证成功后将标记这笔交易在块内部是有效的。在块中的所有事务都被验证并被标记为有效或无效后,该块将被提交到peer对应的channel下的账本。
            //可以看一下这个request的值,完全是从sendTransactionProposal()返回的,这个方法只能接收TransactionRequest类型的。
            var sendPromise = channel.sendTransaction(request);
            return Promise.all([sendPromise].concat(eventPromises)).then((results) => {
                logger.debug(' event promise all complete and testing complete');
                //到了这里就算结束了,根据这个返回结果判断我们的交易是否正确被写入。
                return results[0]; // the first returned value is from the 'sendPromise' which is from the 'sendTransaction()' call
            }).catch((err) => {
                logger.error(
                    'Failed to send transaction and get notifications within the timeout period.'
                );
                return 'Failed to send transaction and get notifications within the timeout period.';
            });
        } else {
            logger.error(
                'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'
            );
            return 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...';
        }
    }, (err) => {
        logger.error('Failed to send proposal due to error: ' + err.stack ? err.stack :
            err);
        return 'Failed to send proposal due to error: ' + err.stack ? err.stack :
            err;
    }).then((response) => {
        if (response.status === 'SUCCESS') {
            logger.info('Successfully sent transaction to the orderer.');
            return tx_id.getTransactionID();
        } else {
            logger.error('Failed to order the transaction. Error code: ' + response.status);
            return 'Failed to order the transaction. Error code: ' + response.status;
        }
    }, (err) => {
        logger.error('Failed to send transaction due to error: ' + err.stack ? err
            .stack : err);
        return 'Failed to send transaction due to error: ' + err.stack ? err.stack :
            err;
    });
};

总结一下其中的大概流程及其重点:
1. helper.getRegisteredUsers()//验证并获取一个用户
2. Client.newTransactionID()//生成一个txid
3. Channel.sendTransactionProposal()//传入对应的chennel、chaincode、方法名参数等信息,并将得到的结果验证处理。
4. EventHub.registerTxEvent()//注册一个事件,等待事务提交到块后返回
5. Channel.sendTransaction()//将包含交易信息的数据发送给orderer进行进一步处理


[2]--关于'helper.newEventHubs()'这里简单介绍一下,在helper.js中有如下代码:

var newPeers = function(names, org) {
    return newRemotes(names, true, org);
};
//这里就是我们刚才使用方法
var newEventHubs = function(names, org) {
    return newRemotes(names, false, org);
};
//两种调用的区别是一个返回peer数据,一个是EventHub数组
//在Fabric中一个事务处理需要比较长的时间,用EventHub做一个事件监听的功能
//network-config.json中events属性就是对应的地址
function newRemotes(names, forPeers, userOrg) {
    let client = getClientForOrg(userOrg);
    let targets = [];
    // find the peer that match the names
    for (let idx in names) {
        let peerName = names[idx];
        if (ORGS[userOrg].peers[peerName]) {
            // found a peer matching the name
            let data = fs.readFileSync(path.join(__dirname, ORGS[userOrg].peers[peerName]['tls_cacerts']));
            let grpcOpts = {
                pem: Buffer.from(data).toString(),
                'ssl-target-name-override': ORGS[userOrg].peers[peerName]['server-hostname']
            };
            if (forPeers) {
                targets.push(client.newPeer(ORGS[userOrg].peers[peerName].requests, grpcOpts));
            } else {
                let eh = client.newEventHub();
                eh.setPeerAddr(ORGS[userOrg].peers[peerName].events, grpcOpts);
                targets.push(eh);
            }
        }
    }
    if (targets.length === 0) {
        logger.error(util.format('Failed to find peers matching the names %s', names));
    }
    return targets;
}

1.3 调用ChainCode查询数据

这里依然是大部分人关心的地方,数据怎么查。

api:http://localhost:5010/channels/mychannel/chaincodes/mycc?peer=peer1&fcn=query&args=%5B%22b%22%5D
此API地址看起来和刚才的一样,只不过请求方式是get,歪果仁经常使用post、get、delete、put;国内大多统一使用post;

注意URL中最后的args=%5B%22b%22%5D,URLDecode后是args=["b"];其他参数与上一个接口的雷同。
下图是执行结果:


查询的方法与上边执行交易的方法比较相似,我本地的这个版本的源码在此处有一个小BUG,无伤大雅,可以查看标注[3]。
打开app/query.js,这里便是调用ChainCode执行查询的主方法:

var queryChaincode = function(peer, channelName, chaincodeName, args, fcn, username, org) {
    var channel = helper.getChannelForOrg(org);
    var client = helper.getClientForOrg(org);
    var target = buildTarget(peer, org);
    //验证user和之前一样的功能
    return helper.getRegisteredUsers(username, org).then((user) => {
        tx_id = client.newTransactionID();
        //组织发送请求的一些数据
        var request = {
            chaincodeId: chaincodeName,
            txId: tx_id,
            fcn: fcn,
            args: args
        };
        //这里有一个BUG,可能由于SDK版本更新,源码忘记修改了[3],这里不影响执行,具体后边说
        //在Fabric V1.0 中所有的请求都会发送到链码的Invoke方法
        //这里和上个方法一样会根据所选peer不同在多个peer上执行对应的chaincode
        //返回所有chaincode执行结果的集合
        return channel.queryByChaincode(request, target);
    }, (err) => {
        logger.info('Failed to get submitter \''+username+'\'');
        return 'Failed to get submitter \''+username+'\'. Error: ' + err.stack ? err.stack :
            err;
    }).then((response_payloads) => {
        //我们在上边的截图中看到的返回值是"b now has 214 after the move"
        //看下边的这段代码会发现chaincode其实只返回了"214"这个值,其他都是这里拼接的
        //这里应该根据不同的业务查询一个或多个节点(我本地peer2的没有安装链码,所以我在这里查询到的response_payloads[0]==214,response_payloads[1]是一段错误信息)
        if (response_payloads) {
            for (let i = 0; i < response_payloads.length; i++) {
                logger.info(args[0]+' now has ' + response_payloads[i].toString('utf8') +
                    ' after the move');
                return args[0]+' now has ' + response_payloads[i].toString('utf8') +
                    ' after the move';
            }
        } else {
            logger.error('response_payloads is null');
            return 'response_payloads is null';
        }
    }, (err) => {
        logger.error('Failed to send query due to error: ' + err.stack ? err.stack :
            err);
        return 'Failed to send query due to error: ' + err.stack ? err.stack : err;
    }).catch((err) => {
        logger.error('Failed to end to end test with error:' + err.stack ? err.stack :
            err);
        return 'Failed to end to end test with error:' + err.stack ? err.stack :
            err;
    });
};

总结一下,简单说这里只有一个步骤:
1. Channel.queryByChaincode()//这里相对就很简单了,只有这一个重点

有了前边的基础,这里看起来就非常简单了


[3]--关于query.js中queryChaincode方法的一个bug,找到如下代码:

        tx_id = client.newTransactionID();
        var request = {
            chaincodeId: chaincodeName,
            txId: tx_id,
            fcn: fcn,
            args: args
        };
        return channel.queryByChaincode(request, target);

以下是修改后的代码:

        tx_id = client.newTransactionID();
        var request = {
            chaincodeId: chaincodeName,
            txId: tx_id,
            fcn: fcn,
            args: args,
            targets : [target]
        };
        return channel.queryByChaincode(request);

可以明显的看到channel.queryByChaincode()其实只有一个参数,这一点与之前执行交易方法的代码相同,targets是一个数组所以在这里使用[target]赋值,道理和之前也一样,可以同时在多个节点上操作,并返回其对应的操作结果。

1.4 使用txid查询

这一步在整个区块链中也是非常重要的一个环节,查询验证每一次的交易。
在1.2 中我们执行了一笔交易得到一个返回值
e16ed0b21094030d796ee210c0fd20922fdc099517ddf3c90f87b3a2a32bb0a8

来试一试能否查询到结果:
API:http://localhost:5010/channels/mychannel/transactions/e16ed0b21094030d796ee210c0fd20922fdc099517ddf3c90f87b3a2a32bb0a8?peer=peer1

返回了一个非常复杂的json,这个json建议查看官方教程学习,这里我们继续分析代码。
打开query.js,找到如下方法:

var getTransactionByID = function(peer, trxnID, username, org) {
    var target = buildTarget(peer, org);
    var channel = helper.getChannelForOrg(org);
    //这个方法已经很熟悉了
    return helper.getRegisteredUsers(username, org).then((member) => {
        //通过txid查询指定peer上的账本信息
        return channel.queryTransaction(trxnID, target);
    }, (err) => {
        logger.info('Failed to get submitter "' + username + '"');
        return 'Failed to get submitter "' + username + '". Error: ' + err.stack ?
            err.stack : err;
    }).then((response_payloads) => {
        if (response_payloads) {
            logger.debug(response_payloads);
            return response_payloads;
        } else {
            logger.error('response_payloads is null');
            return 'response_payloads is null';
        }
    }, (err) => {
        logger.error('Failed to send query due to error: ' + err.stack ? err.stack :
            err);
        return 'Failed to send query due to error: ' + err.stack ? err.stack : err;
    }).catch((err) => {
        logger.error('Failed to query with error:' + err.stack ? err.stack : err);
        return 'Failed to query with error:' + err.stack ? err.stack : err;
    });
};

1.5 Chaincode安装更新

在真实的项目中我们可能要频繁的更新升级智能合约,以便于符合我们新的业务。

Fabric的智能合约是有版本管理的,可以用同样的chaincode名称(如mycc),但是拥有不同的版本号,区分调用,不同的节点也可以使用不同版本的chaincode,一个节点也可以加入数个chaincode。

新的chaincode必须被安装、实例后才可以使用,并且实例化的时候会调用对应chaincode中Init()方法。

chaincode还有一个操作是更新,但是更新在此项目中没有对应代码,也是比较简单的,使用"Channel.sendUpgradeProposal()"类似于实例化的过程,前提也是链码已经安装。

有了以上的基础相信自己写一个更新chaincode并不难。

更新chaincode的流程可以参照【1.2 调用ChainCode执行交易】来做,必须要调用sendTransaction()方法。
和1.2流程相同的有4种,如SDK原文所示:

sendTransaction(request)
Send the proposal responses that contain the endorsements of a transaction proposal to the orderer for further processing. This is the 2nd phase of the transaction lifecycle in the fabric. The orderer will globally order the transactions in the context of this channel and deliver the resulting blocks to the committing peers for validation against the chaincode's endorsement policy. When the committering peers successfully validate the transactions, it will mark the transaction as valid inside the block. After all transactions in a block have been validated, and marked either as valid or invalid (with a reason code), the block will be appended (committed) to the channel's ledger on the peer.

The caller of this method must use the proposal responses returned from the endorser along with the original proposal that was sent to the endorser. Both of these objects are contained in the ProposalResponseObject returned by calls to any of the following methods:
installChaincode()
sendInstantiateProposal()
sendUpgradeProposal()
sendTransactionProposal()

下图的我在另一个项目中写个一个查询chaincode的API,可以看到peer1对应了好几个chaincode,并且有不同版本:

1.6 其他SDK

在balance-transfer项目中,还有很多API接口;诸如创建channel、peer加入channel、查询块信息等等,这些内容相对比较简单一些,由于时间等原因这里暂时不再继续对此项目分析讲解;
将写作的重点转意至Fabric其他方面(如概念的汇总、运行的流程、基本原理等),SDK的深入留给大家慢慢自己拓展。


关于SDK能说的东西其实还是非常多的,如果有哪部分的流程不熟悉,可以留言,会后续补充对应的SDK内容。

.
.
.
.
.
.
.
.
【本文章出自NM1024.com,转载请注明作者出处。】






>>转载请注明原文链接地址:Hyperledger-Fabric安装开发部署(五)NodeJs-SDK详解