From 1f22ed9bbefa94c5d6324692e145a18c7c9f8791 Mon Sep 17 00:00:00 2001 From: Bret Harrison Date: Fri, 10 Feb 2017 13:00:26 -0500 Subject: [PATCH] NodeSDK - add Queries Add queryBlock(number), queryBlockByHash(hash), queryInfo(), and queryTransaction(id) to 'Chain'. Update headless and add new test script for network testing. Requires that end-to-end be run first to provide block and transactions to be queried. Check primary peer to be sure on the chain's peer list Change-Id: I57a8dbe62840bfb8d1b856a5a23e96b5bd1b8267 Signed-off-by: Bret Harrison --- fabric-client/lib/Chain.js | 266 ++++++++++++++++++++++++++++++++++-- fabric-client/lib/Peer.js | 2 +- test/unit/end-to-end.js | 2 +- test/unit/headless-tests.js | 107 +++++++++++++++ test/unit/query.js | 262 +++++++++++++++++++++++++++++++++++ 5 files changed, 626 insertions(+), 13 deletions(-) create mode 100644 test/unit/query.js diff --git a/fabric-client/lib/Chain.js b/fabric-client/lib/Chain.js index 49a5043144..37a75025ae 100644 --- a/fabric-client/lib/Chain.js +++ b/fabric-client/lib/Chain.js @@ -99,6 +99,8 @@ var Chain = class { this.cryptoPrimitives = utils.getCryptoSuite(); this._peers = []; + this._primary_peer = null; // if not set, will use the first peer on the list + this._orderers = []; this._clientContext = clientContext; @@ -227,6 +229,45 @@ var Chain = class { return this._peers; } + /** + * Set the primary peer + * The peer to use for doing queries. + * Peer must be a peer on this chain's peer list. + * Default: When no primary peer has been set the first peer + * on the list will be used. + * @param {Peer} peer An instance of the Peer class. + * @throws Error when peer is not on the existing peer list + */ + setPrimaryPeer(peer) { + if(peer) { + for (let i = 0; i < this._peers.length; i++) { + if (this._peers[i] === peer) { + this._primary_peer = this._peers[i]; + return; + } + } + } + throw new Error('The primary peer must be on this chain\'s peer list'); + } + + /** + * Get the primary peer + * The peer to use for doing queries. + * Default: When no primary peer has been set the first peer + * on the list will be used. + * @returns {Peer} peer An instance of the Peer class. + */ + getPrimaryPeer() { + logger.debug('getPrimaryPeer :: start'); + var result = this._primary_peer; + if(!result) { + result = this._peers[0]; + logger.info(' Primary peer was not set, using %s',result); + } + // return what we found + return result; + } + /** * Add orderer endpoint to a chain object, this is a local-only operation. * A chain instance may choose to use a single orderer node, which will broadcast @@ -814,28 +855,219 @@ var Chain = class { /** * Queries for various useful information on the state of the Chain * (height, known peers). + * This query will be made to the primary peer. * @returns {object} With height, currently the only useful info. */ queryInfo() { - //to do + logger.debug('queryInfo - start'); + var request = { + targets: [this.getPrimaryPeer()], + chaincodeId : 'qscc', + chainId: '', + txId: utils.buildTransactionID(), + nonce: utils.getNonce(), + fcn : 'GetChainInfo', + args: [ this._name] + }; + return this.sendTransactionProposal(request) + .then( + function(results) { + var responses = results[0]; + logger.debug('queryInfo - got responses=' + responses.length); + if(responses && Array.isArray(responses)) { + //will only be one response as we are only querying the primary peer + if(responses.length > 1) { + return Promise.reject(new Error('Too many results returned')); + } + let response = responses[0]; + if(response instanceof Error ) { + return Promise.reject(response); + } + if(response.response) { + logger.debug('queryInfo - response status %d:', response.response.status); + var chain_info = _ledgerProto.BlockchainInfo.decode(response.response.payload); + return Promise.resolve(chain_info); + } + // no idea what we have, lets fail it and send it back + return Promise.reject(response); + } + return Promise.reject(new Error('Payload results are missing from the query chain info')); + } + ).catch( + function(err) { + logger.error('Failed Query chain info. Error: %s', err.stack ? err.stack : err); + return Promise.reject(err); + } + ); + } + + /** + * Queries the ledger for Block by block hash. + * This query will be made to the primary peer. + * @param {[byte]} block hash of the Block. + * @returns {object} Object containing the block. + */ + queryBlockByHash(blockHash) { + logger.debug('queryBlockByHash - start'); + if(!blockHash) { + return Promise.reject( new Error('Blockhash bytes are required')); + } + var request = { + targets: [this.getPrimaryPeer()], + chaincodeId : 'qscc', + chainId: '', + txId: utils.buildTransactionID(), + nonce: utils.getNonce(), + fcn : 'GetBlockByHash', + args: [ this._name], + argbytes : blockHash + }; + return this.sendTransactionProposal(request) + .then( + function(results) { + var responses = results[0]; + logger.debug('queryBlockByHash - got response'); + if(responses && Array.isArray(responses)) { + //will only be one response as we are only querying the primary peer + if(responses.length > 1) { + return Promise.reject(new Error('Too many results returned')); + } + let response = responses[0]; + if(response instanceof Error ) { + return Promise.reject(response); + } + if(response.response) { + logger.debug('queryBlockByHash - response status %d:', response.response.status); + var block = _commonProto.Block.decode(response.response.payload); + logger.debug('queryBlockByHash - looking at block number:'+block.Header.Number); + return Promise.resolve(block); + } + // no idea what we have, lets fail it and send it back + return Promise.reject(response); + } + return Promise.reject(new Error('Payload results are missing from the query')); + } + ).catch( + function(err) { + logger.error('Failed Query block. Error: %s', err.stack ? err.stack : err); + return Promise.reject(err); + } + ); } /** * Queries the ledger for Block by block number. + * This query will be made to the primary peer. * @param {number} blockNumber The number which is the ID of the Block. * @returns {object} Object containing the block. */ queryBlock(blockNumber) { - //to do + logger.debug('queryBlock - start blockNumber %s',blockNumber); + var block_number = null; + if(Number.isInteger(blockNumber) && blockNumber >= 0) { + block_number = blockNumber.toString(); + } else { + return Promise.reject( new Error('Block number must be a postive integer')); + } + var request = { + targets: [this.getPrimaryPeer()], + chaincodeId : 'qscc', + chainId: '', + txId: utils.buildTransactionID(), + nonce: utils.getNonce(), + fcn : 'GetBlockByNumber', + args: [ this._name, block_number] + }; + return this.sendTransactionProposal(request) + .then( + function(results) { + var responses = results[0]; + logger.debug('queryBlock - got response'); + if(responses && Array.isArray(responses)) { + //will only be one response as we are only querying the primary peer + if(responses.length > 1) { + return Promise.reject(new Error('Too many results returned')); + } + let response = responses[0]; + if(response instanceof Error ) { + return Promise.reject(response); + } + if(response.response) { + logger.debug('queryBlock - response status %d:', response.response.status); + var block = _commonProto.Block.decode(response.response.payload); + logger.debug('queryBlock - looking at block number:'+block.Header.Number); + return Promise.resolve(block); + } + // no idea what we have, lets fail it and send it back + return Promise.reject(response); + } + return Promise.reject(new Error('Payload results are missing from the query')); + } + ).catch( + function(err) { + logger.error('Failed Query block. Error: %s', err.stack ? err.stack : err); + return Promise.reject(err); + } + ); } /** * Queries the ledger for Transaction by number. + * This query will be made to the primary peer. * @param {number} transactionID * @returns {object} Transaction information containing the transaction. */ queryTransaction(transactionID) { - //to do + logger.debug('queryTransaction - start transactionID %s',transactionID); + var transaction_id = null; + if(transactionID) { + transaction_id = transactionID.toString(); + } else { + return Promise.reject( new Error('Transaction id is required')); + } + var request = { + targets: [this.getPrimaryPeer()], + chaincodeId : 'qscc', + chainId: '', + txId: utils.buildTransactionID(), + nonce: utils.getNonce(), + fcn : 'GetTransactionByID', + args: [ this._name, transaction_id] + }; + return this.sendTransactionProposal(request) + .then( + function(results) { + var responses = results[0]; + logger.debug('queryTransaction - got response'); + if(responses && Array.isArray(responses)) { + //will only be one response as we are only querying the primary peer + if(responses.length > 1) { + return Promise.reject(new Error('Too many results returned')); + } + let response = responses[0]; + if(response instanceof Error ) { + return Promise.reject(response); + } + if(response.response) { + logger.debug('queryTransaction - response status %d:', response.response.status); + //logger.debug('queryTransaction - responses[i] -- %j:', responses[i]); + var envelope = _commonProto.Envelope.decode(response.response.payload); + //logger.debug('queryTransaction - envelope :: %j:', envelope); + var payload = _commonProto.Payload.decode(envelope.payload); + logger.debug('queryTransaction - transaction ID :: %s:', payload.header.chainHeader.txID); + return Promise.resolve(envelope); + } + // no idea what we have, lets fail it and send it back + return Promise.reject(response); + } + return Promise.reject(new Error('Payload results are missing from the query')); + } + ).catch( + function(err) { + logger.error('Failed Transaction Query. Error: %s', err.stack ? err.stack : err); + return Promise.reject(err); + } + ); } /** @@ -956,13 +1188,13 @@ var Chain = class { * @returns {Promise} A Promise for a `ProposalResponse` */ sendTransactionProposal(request) { - logger.debug('Chain.sendTransactionProposal - start'); + logger.debug('sendTransactionProposal - start'); var errorMsg = null; // Verify that a Peer has been added if (this.getPeers().length < 1) { errorMsg = 'Missing peer objects in Transaction proposal chain'; - logger.error('Chain.sendDeploymentProposal error '+ errorMsg); + logger.error('sendDeploymentProposal error '+ errorMsg); return Promise.reject(new Error(errorMsg)); } @@ -974,20 +1206,27 @@ var Chain = class { } if(errorMsg) { - logger.error('Chain.sendTransactionProposal error '+ errorMsg); + logger.error('sendTransactionProposal error '+ errorMsg); return Promise.reject(new Error(errorMsg)); } var args = []; // leaving this for now... but this call is always an invoke and we are not telling caller to include 'fcn' any longer args.push(Buffer.from(request.fcn ? request.fcn : 'invoke', 'utf8')); - logger.debug('Chain.sendTransactionProposal - adding function arg:%s', request.fcn ? request.fcn : 'invoke'); + logger.debug('sendTransactionProposal - adding function arg:%s', request.fcn ? request.fcn : 'invoke'); for (let i=0; i { logger.info('Executing step2'); - tx_id = utils.buildTransactionID({length:12}); + tx_id = '5678'; //utils.buildTransactionID({length:12}); nonce = utils.getNonce(); // send proposal to endorser var request = { diff --git a/test/unit/headless-tests.js b/test/unit/headless-tests.js index 9d3180faa9..bb87957624 100644 --- a/test/unit/headless-tests.js +++ b/test/unit/headless-tests.js @@ -793,6 +793,113 @@ test('\n\n ** Chain - method tests **\n\n', function (t) { t.end(); }); +test('\n\n ** Chain query tests', function(t) { + var peer = new Peer('grpc://localhost:7051'); + _chain.addPeer(peer); + var test_peer = new Peer('grpc://localhost:7051'); + t.throws( + function () { + _chain.setPrimaryPeer(test_peer); + }, + /^Error: The primary peer must be on this chain\'s peer list/, + 'Not able to set a primary peer even if has the same addresss' + ); + t.doesNotThrow( + function () { + _chain.setPrimaryPeer(peer); + }, + null, + 'Able to set a primary peer as long as same peer' + ); + test_peer = new Peer('grpc://localhost:7099'); + t.throws( + function () { + _chain.setPrimaryPeer(test_peer); + }, + /^Error: The primary peer must be on this chain\'s peer list/, + 'Not Able to set a primary peer when not on the list' + ); + t.throws( + function () { + _chain.setPrimaryPeer(); + }, + /^Error: The primary peer must be on this chain\'s peer list/, + 'Not Able to set a primary peer to a null peer' + ); + + _chain.queryBlockByHash() + .then( + function(results) { + t.fail('Error: Blockhash bytes are required'); + t.end(); + }, + function(err) { + var errMessage = 'Error: Blockhash bytes are required'; + if(err.toString() == errMessage) t.pass(errMessage); + else t.fail(errMessage); + return _chain.queryTransaction(); + } + ).then( + function(results) { + t.fail('Error: Transaction id is required'); + t.end(); + }, + function(err) { + t.pass(err); + return _chain.queryBlock('a'); + } + ).then( + function(results) { + t.fail('Error: block id must be integer'); + t.end(); + }, + function(err) { + var errMessage = 'Error: Block number must be a postive integer'; + if(err.toString() == errMessage) t.pass(errMessage); + else t.fail(errMessage); + return _chain.queryBlock(); + } + ).then( + function(results) { + t.fail('Error: block id is required'); + t.end(); + }, + function(err) { + var errMessage = 'Error: Block number must be a postive integer'; + if(err.toString() == errMessage) t.pass(errMessage); + else t.fail(errMessage); + return _chain.queryBlock(-1); + } + ).then( + function(results) { + t.fail('Error: block id must be postive integer'); + t.end(); + }, + function(err) { + var errMessage = 'Error: Block number must be a postive integer'; + if(err.toString() == errMessage) t.pass(errMessage); + else t.fail(errMessage); + return _chain.queryBlock(10.5); + } + ).then( + function(results) { + t.fail('Error: block id must be integer'); + t.end(); + }, + function(err) { + var errMessage = 'Error: Block number must be a postive integer'; + if(err.toString() == errMessage) t.pass(errMessage); + else t.fail(errMessage); + t.end(); + } + ).catch( + function(err) { + t.fail('should not have gotten the catch ' + err); + t.end(); + } + ); +}); + // User tests ///////// test('\n\n ** User - constructor set get tests **\n\n', function (t) { _client = new Client(); diff --git a/test/unit/query.js b/test/unit/query.js new file mode 100644 index 0000000000..2486e9aadb --- /dev/null +++ b/test/unit/query.js @@ -0,0 +1,262 @@ +/** + * Copyright 2016 IBM All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This is an end-to-end test that focuses on exercising all parts of the fabric APIs +// in a happy-path scenario +'use strict'; +process.env.HFC_LOGGING = '{"debug": "console"}'; +var tape = require('tape'); +var _test = require('tape-promise'); +var test = _test(tape); + +var log4js = require('log4js'); +var logger = log4js.getLogger('E2E'); +logger.setLevel('DEBUG'); + +var path = require('path'); + +var hfc = require('fabric-client'); +hfc.setLogger(logger); + +var util = require('util'); +var testUtil = require('./util.js'); +var utils = require('fabric-client/lib/utils.js'); +var Peer = require('fabric-client/lib/Peer.js'); +var Orderer = require('fabric-client/lib/Orderer.js'); +var EventHub = require('fabric-client/lib/EventHub.js'); + +var client = new hfc(); +var chain_id = 'testchainid'; +var chain = client.newChain(chain_id); + +var webUser = null; +var chaincode_id = 'end2end'; // assumes end to end has run first and deployed this chain code +var tx_id = null; +var nonce = null; +var peer0 = new Peer('grpc://localhost:7051'), + peer1 = new Peer('grpc://localhost:7056'); + +var querys = []; +if (process.argv.length > 2) { + for (let i=2; i>>>> Query chain working <<<<<-----', function(t) { + hfc.newDefaultKeyValueStore({ + path: testUtil.KVS + }).then( function (store) { + client.setStateStore(store); + testUtil.getSubmitter(client, t) + .then( + function(admin) { + t.pass('Successfully enrolled user ' + admin); + webUser = admin; + // use default primary peer + // send query + return chain.queryBlock(0); + }, + function(err) { + t.fail('Failed to enroll user: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response) { + t.equal(response.Header.Number.toString(),'0','checking query results are correct that we got zero block back'); + chain.setPrimaryPeer(peer0); + // send query + return chain.queryTransaction('5678'); //assumes the end-to-end has run first + }, + function(err) { + t.fail('Failed to send query due to error: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response) { + t.pass('got back transaction '); // + JSON.stringify(response_payloads)); + chain.setPrimaryPeer(peer1); + // send query + return chain.queryInfo(); + }, + function(err) { + t.fail('Failed to send query due to error: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response) { + t.pass('got back blockchain info '); // + JSON.stringify(response_payloads[i])); + var block_hash = response.previousBlockHash; + chain.setPrimaryPeer(peer0); + // send query + return chain.queryBlockByHash(block_hash); + }, + function(err) { + t.fail('Failed to send query due to error: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response) { + t.pass('got back block number ' + response.Header.Number); + t.end(); + }, + function(err) { + t.fail('Failed to send query due to error: ' + err.stack ? err.stack : err); + t.end(); + } + ).catch( + function(err) { + t.fail('Failed to query with error:' + err.stack ? err.stack : err); + t.end(); + } + ); + }); +}); + +test(' ---->>>>> Query chain failing <<<<<-----', function(t) { + hfc.newDefaultKeyValueStore({ + path: testUtil.KVS + }).then( function (store) { + client.setStateStore(store); + var promise = testUtil.getSubmitter(client, t); + + if (querys.length === 0 || querys.indexOf('GetBlockByNumber') >= 0) { + logger.info('Executing GetBlockByNumber'); + promise = promise.then( + function(admin) { + t.pass('Successfully enrolled user ' + admin); + webUser = admin; + // send query + return chain.queryBlock(9999999); //should not find it + }, + function(err) { + t.fail('Failed to enroll user: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response_payloads) { + t.fail('Should not have found a block'); + t.end(); + }, + function(err) { + t.pass('Did not find a block with this number ::'+ err); + t.end(); + } + ).catch( + function(err) { + t.fail('Failed to query with error:' + err.stack ? err.stack : err); + t.end(); + } + ); + } + + if (querys.length === 0 || querys.indexOf('GetTransactionByID') >= 0) { + promise = promise.then( + function(admin) { + t.pass('Successfully enrolled user ' + admin); + if(admin) webUser = admin; + // send query + return chain.queryTransaction('99999'); //assumes the end-to-end has run first + }, + function(err) { + t.fail('Failed to enroll user: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response_payloads) { + t.fail('Should not have found a transaction with this ID'); + t.end(); + }, + function(err) { + t.pass('Did not find a transaction ::' + err); + t.end(); + } + ).catch( + function(err) { + t.fail('Failed to query with error:' + err.stack ? err.stack : err); + t.end(); + } + ); + } + + if (querys.length === 0 || querys.indexOf('GetChainInfo') >= 0) { + promise = promise.then( + function(admin) { + t.pass('Successfully enrolled user ' + admin); + if(admin) webUser = admin; + // send query + chain._name = 'dummy'; + return chain.queryInfo(); + }, + function(err) { + t.fail('Failed to enroll user: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response_payloads) { + t.fail('Should not have found chain info'); + t.end(); + }, + function(err) { + t.pass('Did not find chain info ::' + err); + t.end(); + } + ).catch( + function(err) { + t.fail('Failed to query with error:' + err.stack ? err.stack : err); + t.end(); + } + ); + } + + if (querys.length === 0 || querys.indexOf('GetBlockByHash') >= 0) { + promise = promise.then( + function(admin) { + t.pass('Successfully enrolled user ' + admin); + if(admin) webUser = admin; + // send query + chain._name = chain_id; //put it back + return chain.queryBlockByHash(Buffer.from('dummy')); + }, + function(err) { + t.fail('Failed to enroll user: ' + err.stack ? err.stack : err); + t.end(); + } + ).then( + function(response_payloads) { + t.fail('Should not have found block data'); + t.end(); + }, + function(err) { + t.pass('Did not find block data ::'+ err); + t.end(); + } + ).catch( + function(err) { + t.fail('Failed to query with error:' + err.stack ? err.stack : err); + t.end(); + } + ); + } + }); +});