From 475fc7d0f195680891e2016995d0e1e9e81a83e4 Mon Sep 17 00:00:00 2001 From: harrisob Date: Wed, 19 Aug 2020 08:49:28 -0400 Subject: [PATCH] FABN-1612 cherrypick from master to release-2.2 (#305) * FABN-1608 remove error message from discovery (#289) When a group has insufficient members after filtering based on user's requirements (specific mspids, specific peers, ledger height) an error message was posted. This will be changed to a debug message as this is not an error, but an expected result of the filtering. Signed-off-by: Bret Harrison * FABN-1607 Allow future startBlocks on EventService (#286) Allow the setting of startBlocks when setting up an EventService that have not happened and may not happen for sometime. We must keep the stream setup timer to be able to monitor for errors from the Peer event service, however we will check if the timer pops if we are waiting for a defined start block. In this case we will assume that the stream is all good and resolve the start service for the user call. Signed-off-by: Bret Harrison * FABN-1560 add disconnected peer to channel (#288) Allow for all service endpoints to be disconnected when added to a channel. When a service endpoint (peer, events, orderer, discovery) is added to a channel, it may not be active or it may go down between usages. The service connection will be checked and reset when an outbound request is made. Signed-off-by: Bret Harrison * FABN-1601 discovery required organizations (#291) When a discovery user knows the organizations that are needed to endorse a proposal, they may use the 'requiredOrgs' setting to have the DiscoveryHandler send it to peer that have been discovered for the required organizations. Signed-off-by: Bret Harrison * FABN-1596 add noPrivateReads (#293) Allow users to set the "noPrivateReads" when setting up a discovery hint that will be used as the interest when the discovery service builds an endorsement plan. Signed-off-by: Bret Harrison * FABN-1611 Blockdecoder needs kv_reads array (#297) The RangeQueryInfo raw_reads attribute is a QueryRead message object which has an array of KVRead objects and is not an array itself. Signed-off-by: Bret Harrison --- fabric-common/lib/BlockDecoder.js | 2 +- fabric-common/lib/Channel.js | 8 +- fabric-common/lib/Commit.js | 25 ++-- fabric-common/lib/DiscoveryHandler.js | 68 ++++++++--- fabric-common/lib/DiscoveryService.js | 59 ++++++---- fabric-common/lib/EventService.js | 27 +++-- fabric-common/lib/Eventer.js | 2 + fabric-common/lib/Proposal.js | 74 ++++++++++-- fabric-common/lib/ServiceEndpoint.js | 42 +++++-- fabric-common/test/BlockDecoder.js | 4 +- fabric-common/test/Channel.js | 4 +- fabric-common/test/Commit.js | 25 +++- fabric-common/test/DiscoveryHandler.js | 108 +++++++++++++++--- fabric-common/test/DiscoveryService.js | 26 ++++- fabric-common/test/EventService.js | 17 +++ fabric-common/test/Proposal.js | 77 +++++++++++-- fabric-common/test/ServiceEndpoint.js | 88 +++++++++++--- fabric-common/types/index.d.ts | 2 + fabric-network/src/contract.ts | 1 + fabric-network/test/contract.js | 17 ++- test/ts-scenario/features/discovery.feature | 6 + test/ts-scenario/steps/lib/gateway.ts | 10 +- .../steps/lib/utility/clientUtils.ts | 4 + test/ts-scenario/steps/network-model.ts | 4 + 24 files changed, 570 insertions(+), 130 deletions(-) diff --git a/fabric-common/lib/BlockDecoder.js b/fabric-common/lib/BlockDecoder.js index 49ac8f5dec..b848e38f38 100644 --- a/fabric-common/lib/BlockDecoder.js +++ b/fabric-common/lib/BlockDecoder.js @@ -1513,7 +1513,7 @@ function decodeRangeQueryInfo(rangeQueryInfoProto) { if (rangeQueryInfoProto.raw_reads) { range_query_info.raw_reads = {}; range_query_info.raw_reads.kv_reads = []; - for (const kVReadProto of rangeQueryInfoProto.raw_reads) { + for (const kVReadProto of rangeQueryInfoProto.raw_reads.kv_reads) { range_query_info.raw_reads.kv_reads.push(decodeKVRead(kVReadProto)); } } else if (rangeQueryInfoProto.reads_merkle_hashes) { diff --git a/fabric-common/lib/Channel.js b/fabric-common/lib/Channel.js index d14b49af1a..11feb516b8 100644 --- a/fabric-common/lib/Channel.js +++ b/fabric-common/lib/Channel.js @@ -255,8 +255,8 @@ const Channel = class { if (!(endorser.type === 'Endorser')) { throw Error('Missing valid endorser instance'); } - if (!(endorser.connected)) { - throw Error('Endorser must be connected'); + if (!endorser.isConnectable()) { + throw Error('Endorser must be connectable'); } const name = endorser.name; const check = this.endorsers.get(name); @@ -336,8 +336,8 @@ const Channel = class { if (!(committer.type === 'Committer')) { throw Error('Missing valid committer instance'); } - if (!(committer.connected)) { - throw Error('Committer must be connected'); + if (!committer.isConnectable()) { + throw Error('Committer must be connectable'); } const name = committer.name; const check = this.committers.get(name); diff --git a/fabric-common/lib/Commit.js b/fabric-common/lib/Commit.js index d9f5564ae9..37d6b101de 100644 --- a/fabric-common/lib/Commit.js +++ b/fabric-common/lib/Commit.js @@ -182,19 +182,28 @@ class Commit extends Proposal { } else if (targets) { logger.debug('%s - sending to the targets', method); const committers = this.channel.getTargetCommitters(targets); - let bad_result = {}; - bad_result.status = 'UNKNOWN'; + let result; for (const committer of committers) { - const result = await committer.sendBroadcast(envelope, requestTimeout); - if (result.status === 'SUCCESS') { - - return result; + const isConnected = await committer.checkConnection(); + if (isConnected) { + try { + result = await committer.sendBroadcast(envelope, requestTimeout); + if (result.status === 'SUCCESS') { + break; + } + } catch (error) { + logger.error('%s - Unable to commit on %s ::%s', method, committer.name, error); + result = error; + } } else { - bad_result = result; + result = new Error(`Committer ${committer.name} is not connected`); } } + if (result instanceof Error) { + throw result; + } - return bad_result; + return result; } else { throw checkParameter('targets'); } diff --git a/fabric-common/lib/DiscoveryHandler.js b/fabric-common/lib/DiscoveryHandler.js index 3e897b1be2..0e996bb94d 100644 --- a/fabric-common/lib/DiscoveryHandler.js +++ b/fabric-common/lib/DiscoveryHandler.js @@ -112,18 +112,23 @@ class DiscoveryHandler extends ServiceHandler { for (const committer of committers) { logger.debug('%s - sending to committer %s', method, committer.name); try { - const results = await committer.sendBroadcast(signedEnvelope, timeout); - if (results) { - if (results.status === 'SUCCESS') { - logger.debug('%s - Successfully sent transaction to the committer %s', method, committer.name); - return results; + const isConnected = await committer.checkConnection(); + if (isConnected) { + const results = await committer.sendBroadcast(signedEnvelope, timeout); + if (results) { + if (results.status === 'SUCCESS') { + logger.debug('%s - Successfully sent transaction to the committer %s', method, committer.name); + return results; + } else { + logger.debug('%s - Failed to send transaction successfully to the committer status:%s', method, results.status); + return_error = new Error('Failed to send transaction successfully to the committer status:' + results.status); + } } else { - logger.debug('%s - Failed to send transaction successfully to the committer status:%s', method, results.status); - return_error = new Error('Failed to send transaction successfully to the committer status:' + results.status); + return_error = new Error('Failed to send transaction to the committer'); + logger.debug('%s - Failed to send transaction to the committer %s', method, committer.name); } } else { - return_error = new Error('Failed to send transaction to the committer'); - logger.debug('%s - Failed to send transaction to the committer %s', method, committer.name); + return_error = new Error(`Committer ${committer.name} is not connected`); } } catch (error) { logger.debug('%s - Caught: %s', method, error.toString()); @@ -155,7 +160,20 @@ class DiscoveryHandler extends ServiceHandler { const results = await this.discovery.getDiscoveryResults(true); - if (results && results.endorsement_plan) { + if (results && request.requiredOrgs) { + // special case when user knows which organizations to send the endorsement + // let's build our own endorsement plan so that we can use the sorting and sending code + const endorsement_plan = this._buildRequiredOrgPlan(results.peers_by_org); + + // remove all org and peer + const orgs_request = { + sort: request.sort, + preferredHeightGap: request.preferredHeightGap + }; + + return this._endorse(endorsement_plan, orgs_request, signedProposal, timeout); + } else if (results && results.endorsement_plan) { + // normal processing of the discovery results const working_discovery = JSON.parse(JSON.stringify(results.endorsement_plan)); return this._endorse(working_discovery, request, signedProposal, timeout); @@ -259,7 +277,7 @@ class DiscoveryHandler extends ServiceHandler { if (required > group.peers.length) { results.success = false; const error = new Error(`Endorsement plan group does not contain enough peers (${group.peers.length}) to satisfy policy (required:${required})`); - logger.error(error); + logger.debug(error.message); results.endorsements.push(error); break; // no need to look at other groups, this layout failed } @@ -302,6 +320,23 @@ class DiscoveryHandler extends ServiceHandler { return responses; } + _buildRequiredOrgPlan(peers_by_org) { + const method = '_buildRequiredOrgPlan'; + logger.debug('%s - starting', method); + const endorsement_plan = {plan_id: 'required organizations'}; + endorsement_plan.groups = {}; + endorsement_plan.layouts = [{}]; // only one layout which will have all organizations + + for (const mspid in peers_by_org) { + logger.debug(`${method} - found org:${mspid}`); + endorsement_plan.groups[mspid] = {}; // make a group for each organization + endorsement_plan.groups[mspid].peers = peers_by_org[mspid].peers; // now put in all peers from that organization + endorsement_plan.layouts[0][mspid] = 1; // add this org to the one layout and require one peer to endorse + } + + return endorsement_plan; + } + /* * utility method to build a promise that will return one of the required * endorsements or an error object @@ -326,9 +361,14 @@ class DiscoveryHandler extends ServiceHandler { logger.debug('%s - send endorsement to %s', method, peer_info.name); peer_info.in_use = true; try { - endorsement = await peer.sendProposal(proposal, timeout); - // save this endorsement results in case we try this peer again - logger.debug('%s - endorsement completed to %s', method, peer_info.name); + const isConnected = await peer.checkConnection(); + if (isConnected) { + endorsement = await peer.sendProposal(proposal, timeout); + // save this endorsement results in case we try this peer again + logger.debug('%s - endorsement completed to %s', method, peer_info.name); + } else { + endorsement = new Error(`Peer ${peer.name} is not connected`); + } } catch (error) { endorsement = error; logger.error('%s - error on endorsement to %s error %s', method, peer_info.name, error); diff --git a/fabric-common/lib/DiscoveryService.js b/fabric-common/lib/DiscoveryService.js index 689c600b39..6ad8d8a5c4 100644 --- a/fabric-common/lib/DiscoveryService.js +++ b/fabric-common/lib/DiscoveryService.js @@ -70,10 +70,10 @@ class DiscoveryService extends ServiceAction { } for (const discoverer of targets) { - if (discoverer.connected || discoverer.isConnectable()) { - logger.debug('%s - target is or could be connected %s', method, discoverer.name); + if (discoverer.isConnectable()) { + logger.debug('%s - target is connectable%s', method, discoverer.name); } else { - throw Error(`Discoverer ${discoverer.name} is not connected`); + throw Error(`Discoverer ${discoverer.name} is not connectable`); } } // must be all targets are connected @@ -103,14 +103,18 @@ class DiscoveryService extends ServiceAction { * sending to the peer. * @property {Endorsement} [endorsement] - Optional. Include the endorsement * instance to build the discovery request based on the proposal. - * This will get the discovery interest (chaincode names and collections) + * This will get the discovery interest (chaincode names, collections and "no private reads") * from the endorsement instance. Use the {@link Proposal#addCollectionInterest} - * to add collections to the endorsement's chaincode. Use the - * {@link Proposal#addChaincodeCollectionsInterest} to add chaincodes - * and collections that will be called by the endorsement's chaincode. + * to add collections to the endorsement's chaincode. + * Use the {@link Proposal#setNoPrivateReads} to set the proposals "no private reads" + * setting of the discovery interest. + * Use the {@link Proposal#addCollectionInterest} to add chaincodes, + * collections, and no private reads that will be used to get an endorsement plan + * from the peer's discovery service. * @property {DiscoveryChaincode} [interest] - Optional. An - * array of {@link DiscoveryChaincodeInterest} that have chaincodes - * and collections to calculate the endorsement plans. + * array of {@link DiscoveryChaincodeInterest} that have chaincodes, collections, + * and "no private reads" to help the peer's discovery service calculate the + * endorsement plan. * @example "single chaincode" * [ * { name: "mychaincode"} @@ -121,17 +125,21 @@ class DiscoveryService extends ServiceAction { * ] * @example "single chaincode with a collection" * [ - * { name: "mychaincode", collection_names: ["mycollection"] } + * { name: "mychaincode", collectionNames: ["mycollection"] } + * ] + * @example "single chaincode with a collection allowing no private data reads" + * [ + * { name: "mychaincode", collectionNames: ["mycollection"], noPrivateReads: true } * ] * @example "chaincode to chaincode with a collection" * [ - * { name: "mychaincode", collection_names: ["mycollection"] }, - * { name: "myotherchaincode", collection_names: ["mycollection"] }} + * { name: "mychaincode", collectionNames: ["mycollection"] }, + * { name: "myotherchaincode", collectionNames: ["mycollection"] }} * ] * @example "chaincode to chaincode with collections" * [ - * { name: "mychaincode", collection_names: ["mycollection", "myothercollection"] }, - * { name: "myotherchaincode", collection_names: ["mycollection", "myothercollection"] }} + * { name: "mychaincode", collectionNames: ["mycollection", "myothercollection"] }, + * { name: "myotherchaincode", collectionNames: ["mycollection", "myothercollection"] }} * ] */ @@ -144,7 +152,8 @@ class DiscoveryService extends ServiceAction { /** * @typedef {Object} DiscoveryChaincodeCall * @property {string} name - The name of the chaincode - * @property {string[]} [collection_names] - The names of the related collections + * @property {string[]} [collectionNames] - The names of the related collections + * @property {boolean} [noPrivateReads] - Indicates we do not need to read from private data */ /** @@ -195,7 +204,7 @@ class DiscoveryService extends ServiceAction { queries.push(localQuery); } - // add a chaincode query to get endorsement plans + // add a discovery chaincode query to get endorsement plans if (endorsement || interest) { const interests = []; @@ -285,9 +294,12 @@ class DiscoveryService extends ServiceAction { for (const target of this.targets) { logger.debug(`${method} - about to discover on ${target.endpoint.url}`); try { - response = await target.sendDiscovery(signedEnvelope, this.requestTimeout); - this.currentTarget = target; - break; + const isConnected = await target.checkConnection(); + if (isConnected) { + response = await target.sendDiscovery(signedEnvelope, this.requestTimeout); + this.currentTarget = target; + break; + } } catch (error) { response = error; } @@ -376,6 +388,9 @@ class DiscoveryService extends ServiceAction { const chaincodeCall = fabproto6.discovery.ChaincodeCall.create(); if (typeof chaincode.name === 'string') { chaincodeCall.name = chaincode.name; + if (chaincode.noPrivateReads) { + chaincodeCall.no_private_reads = chaincode.noPrivateReads; + } // support both names if (chaincode.collection_names) { _getCollectionNames(chaincode.collection_names, chaincodeCall); @@ -738,7 +753,7 @@ class DiscoveryService extends ServiceAction { } } -function _getCollectionNames(names, chaincode_call) { +function _getCollectionNames(names, chaincodeCall) { if (Array.isArray(names)) { const collection_names = []; names.map(name => { @@ -748,7 +763,9 @@ function _getCollectionNames(names, chaincode_call) { throw Error('The collection name must be a string'); } }); - chaincode_call.collection_names = collection_names; + // this collection_names must be in snake case as it will + // be used by the gRPC create message + chaincodeCall.collection_names = collection_names; } else { throw Error('Collection names must be an array of strings'); } diff --git a/fabric-common/lib/EventService.js b/fabric-common/lib/EventService.js index c2c9abb2e8..6109feca48 100644 --- a/fabric-common/lib/EventService.js +++ b/fabric-common/lib/EventService.js @@ -89,6 +89,7 @@ class EventService extends ServiceAction { // will be set during the .build call this.blockType = FILTERED_BLOCK; this.replay = false; + this.startSpecified = false; this.myNumber = count++; } @@ -113,13 +114,13 @@ class EventService extends ServiceAction { } for (const eventer of targets) { - if (eventer.connected || eventer.isConnectable()) { - logger.debug('%s - target is or could be connected %s', method, eventer.name); + if (eventer.isConnectable()) { + logger.debug('%s - target is connectable %s', method, eventer.name); } else { throw Error(`Eventer ${eventer.name} is not connectable`); } } - // must be all targets are connected + // must be all targets are connectable this.targets = targets; return this; @@ -266,6 +267,7 @@ class EventService extends ServiceAction { number: this.startBlock }); this.replay = true; + this.startSpecified = true; } // build stop proto @@ -356,10 +358,6 @@ class EventService extends ServiceAction { logger.debug('%s - target has a stream, is already listening %s', method, target.toString()); startError = Error(`Event service ${target.name} is currently listening`); } else { - if (target.isConnectable()) { - logger.debug('%s - target needs to connect %s', method, target.toString()); - await target.connect(); // target endpoint has been previously assigned, but not connected yet - } const isConnected = await target.checkConnection(); if (!isConnected) { startError = Error(`Event service ${target.name} is not connected`); @@ -409,8 +407,19 @@ class EventService extends ServiceAction { logger.debug('%s - create stream setup timeout', method); const connectionSetupTimeout = setTimeout(() => { - logger.error(`EventService[${this.name}] timed out after:${requestTimeout}`); - reject(Error('Event service timed out - Unable to start listening')); + // this service may be waiting for a start block that has not happened + if (this.startSpecified) { + logger.debug(`EventService[${this.name}] timed out after:${requestTimeout}`); + logger.debug(`EventService[${this.name}] not stopping service, wait indefinitely`); + // resolve the promise as if we did get a good response from the peer, since we did + // not get an "end" or "error" back indicating that the request was invalid + // application should have a timer just in case this peer never gets this block + resolve(eventer); + } else { + logger.error(`EventService[${this.name}] timed out after:${requestTimeout}`); + reject(Error('Event service timed out - Unable to start listening')); + } + }, requestTimeout); logger.debug('%s - create stream based on blockType', method, this.blockType); diff --git a/fabric-common/lib/Eventer.js b/fabric-common/lib/Eventer.js index 48981ab0c7..eb885b756c 100644 --- a/fabric-common/lib/Eventer.js +++ b/fabric-common/lib/Eventer.js @@ -83,6 +83,8 @@ class Eventer extends ServiceEndpoint { const method = `checkConnection[${this.name}:${this.myCount}]`; logger.debug(`${method} - start`); + super.checkConnection(); + let result = false; if (this.service) { try { diff --git a/fabric-common/lib/Proposal.js b/fabric-common/lib/Proposal.js index 4569eb473c..b6fdf6bca7 100644 --- a/fabric-common/lib/Proposal.js +++ b/fabric-common/lib/Proposal.js @@ -39,6 +39,9 @@ class Proposal extends ServiceAction { this.chaincodeId = chaincodeId; this.channel = channel; + + // to be used to build a discovery interest + this.noPrivateReads = false; this.collectionsInterest = []; this.chaincodesCollectionsInterest = []; } @@ -59,18 +62,26 @@ class Proposal extends ServiceAction { } /** - * Returns a JSON object representing this proposals chaincodes - * and collections as an interest for the Discovery Service. - * The {@link Discovery} will use the interest to build a query + * Returns a JSON object representing this proposal's chaincodes, + * collections and the no private reads as an "interest" for the + * Discovery Service. + * The {@link Discovery} will use an interest to build a query * request for an endorsement plan to a Peer's Discovery service. * Use the {@link Proposal#addCollectionInterest} to add collections * for the chaincode of this proposal. + * Use the {@link Proposal#setNoPrivateReads} to set this "no private reads" + * setting for this proposal's chaincode. The default will be false + * when not set. * Use the {@link Proposal#addChaincodeCollectionInterest} to add * chaincodes and collections that this chaincode code will call. * @example * [ * { name: "mychaincode", collectionNames: ["mycollection"] } * ] + * @example + * [ + * { name: "mychaincode", collectionNames: ["mycollection"], noPrivateReads: true } + * ] */ buildProposalInterest() { const method = `buildProposalInterest[${this.chaincodeId}]`; @@ -83,6 +94,7 @@ class Proposal extends ServiceAction { if (this.collectionsInterest.length > 0) { chaincode.collectionNames = this.collectionsInterest; } + chaincode.noPrivateReads = this.noPrivateReads; if (this.chaincodesCollectionsInterest.length > 0) { interest = interest.concat(this.chaincodesCollectionsInterest); } @@ -109,18 +121,37 @@ class Proposal extends ServiceAction { } /** - * Use this method to add a chaincode name and collection names - * that this proposal's chaincode will call. These will be used - * to build a Discovery interest. {@link Proposal#buildProposalInterest} + * Use this method to set the "no private reads" of the discovery hint + * (interest) for the chaincode of this proposal. + * @param {boolean} noPrivateReads Indicates we do not need to read from private data + */ + setNoPrivateReads(noPrivateReads) { + const method = `setNoPrivateReads[${this.chaincodeId}]`; + logger.debug('%s - start', method); + + if (typeof noPrivateReads === 'boolean') { + this.noPrivateReads = noPrivateReads; + } else { + throw Error(`The "no private reads" setting must be boolean. :: ${noPrivateReads}`); + } + } + + /** + * Use this method to add a chaincode name and the collection names + * that this proposal's chaincode will call along with the no private read + * setting. These will be used to build a Discovery interest when this proposal + * is used with the Discovery Service. * @param {string} chaincodeId - chaincode name + * @param {boolean} noPrivateReads Indicates we do not need to read from private data * @param {...string} collectionNames - one or more collection names */ - addChaincodeCollectionsInterest(chaincodeId, ...collectionNames) { + addChaincodeNoPrivateReadsCollectionsInterest(chaincodeId, noPrivateReads, ...collectionNames) { const method = `addChaincodeCollectionsInterest[${this.chaincodeId}]`; logger.debug('%s - start', method); if (typeof chaincodeId === 'string') { const added_chaincode = {}; added_chaincode.name = chaincodeId; + added_chaincode.noPrivateReads = noPrivateReads ? true : false; if (collectionNames && collectionNames.length > 0) { added_chaincode.collectionNames = collectionNames; } @@ -131,6 +162,21 @@ class Proposal extends ServiceAction { return this; } + /** + * Use this method to add a chaincode name and collection names + * that this proposal's chaincode will call. These will be used + * to build a Discovery interest when this proposal is used with + * the Discovery Service. + * @param {string} chaincodeId - chaincode name + * @param {...string} collectionNames - one or more collection names + */ + addChaincodeCollectionsInterest(chaincodeId, ...collectionNames) { + const method = `addChaincodeCollectionsInterest[${this.chaincodeId}]`; + logger.debug('%s - start', method); + + return this.addChaincodeNoPrivateReadsCollectionsInterest(chaincodeId, false, ...collectionNames); + } + /** * @typedef {Object} BuildProposalRequest * @property {string} [fcn] - Optional. The function name. May be used by @@ -374,9 +420,17 @@ message Endorsement { } else if (targets) { logger.debug('%s - have targets', method); const peers = this.channel.getTargetEndorsers(targets); - const promises = peers.map(async (peer) => { - return peer.sendProposal(signedEnvelope, requestTimeout); - }); + const promises = []; + for (const peer of peers) { + const isConnected = await peer.checkConnection(); + if (isConnected) { + promises.push(peer.sendProposal(signedEnvelope, requestTimeout)); + } + } + if (promises.length === 0) { + logger.error('%s - no targets are connected', method); + throw Error('No targets are connected'); + } logger.debug('%s - about to send to all peers', method); const results = await settle(promises); diff --git a/fabric-common/lib/ServiceEndpoint.js b/fabric-common/lib/ServiceEndpoint.js index 4b0c970742..f30ae1a5ca 100644 --- a/fabric-common/lib/ServiceEndpoint.js +++ b/fabric-common/lib/ServiceEndpoint.js @@ -26,6 +26,7 @@ class ServiceEndpoint { this.service = null; this.serviceClass = null; this.type = TYPE; // will be overridden by subclass + this.options = {}; } /** @@ -55,10 +56,8 @@ class ServiceEndpoint { } /** - * Check that this ServiceEndpoint is not connected and has been assigned - * an endpoint so that it could be connected. If a previous attempt - * to conntect has been tried unsuccessfully it will be considered - * not to be connectable. + * Check that this ServiceEndpoint could be connected, even if it has + * failed a previous attempt. */ isConnectable() { const method = `isConnectable[${this.type}-${this.name}]`; @@ -67,8 +66,8 @@ class ServiceEndpoint { let result = false; if (this.connected) { logger.debug(`${method} - this servive endpoint has been connected`); - result = false; - } else if (this.endpoint && !this.connectAttempted) { + result = true; + } else if (this.endpoint && this.serviceClass) { logger.debug(`${method} - this service endpoint has been assigned an endpoint, connect may be run`); result = true; } @@ -152,8 +151,15 @@ class ServiceEndpoint { try { await this.waitForReady(); } catch (error) { - logger.error(`Peer ${this.endpoint.url} Connection failed :: ${error}`); - return false; + logger.error(`ServiceEndpoint ${this.endpoint.url} connection failed :: ${error}`); + } + } + + if (!this.connected && this.isConnectable()) { + try { + await this.resetConnection(); + } catch (error) { + logger.error(`ServiceEndpoint ${this.endpoint.url} reset connection failed :: ${error}`); } } @@ -161,6 +167,26 @@ class ServiceEndpoint { return this.connected; } + /** + * Reset the connection + */ + async resetConnection() { + const method = `resetConnection[${this.name}]`; + logger.debug('%s - start - connected:%s', method, this.connected); + + this.disconnect(); // clean up possible old service + this.connectAttempted = true; + logger.debug(`${method} - create the grpc service for ${this.name}`); + if (this.endpoint && this.serviceClass) { + this.service = new this.serviceClass(this.endpoint.addr, this.endpoint.creds, this.options); + await this.waitForReady(this.service); + } else { + throw Error(`ServiceEndpoint ${this.name} is missing endpoint information`); + } + + logger.debug('%s - end - connected:%s', method, this.connected); + } + waitForReady() { const method = 'waitForReady'; logger.debug(`${method} - start ${this.type}-${this.name} - ${this.endpoint.url}`); diff --git a/fabric-common/test/BlockDecoder.js b/fabric-common/test/BlockDecoder.js index f190469db2..83b8881c18 100644 --- a/fabric-common/test/BlockDecoder.js +++ b/fabric-common/test/BlockDecoder.js @@ -2018,13 +2018,13 @@ describe('BlockDecoder', () => { start_key: 'start_key', end_key: 'end_key', itr_exhausted: 'itr_exhausted', - raw_reads: ['raw_read'] + raw_reads: {kv_reads: ['kv_read']} }; const rangeQueryInfo = decodeRangeQueryInfo(mockProtoRangeQueryInfo); rangeQueryInfo.start_key.should.equal('start_key'); rangeQueryInfo.end_key.should.equal('end_key'); rangeQueryInfo.itr_exhausted.should.equal('itr_exhausted'); - sinon.assert.calledWith(decodeKVReadStub, 'raw_read'); + sinon.assert.calledWith(decodeKVReadStub, 'kv_read'); }); it('should return the correct range query info with reads merklehashes', () => { diff --git a/fabric-common/test/Channel.js b/fabric-common/test/Channel.js index 2e736155c0..38f372854e 100644 --- a/fabric-common/test/Channel.js +++ b/fabric-common/test/Channel.js @@ -231,7 +231,7 @@ describe('Channel', () => { const endorser1 = getEndorser('endorser'); endorser1.connected = false; channel.addEndorser(endorser1); - }).should.throw('Endorser must be connected'); + }).should.throw('Endorser must be connectable'); }); it('should find a endorser.name', () => { (() => { @@ -311,7 +311,7 @@ describe('Channel', () => { const committer1 = getCommitter('committer'); committer1.connected = false; channel.addCommitter(committer1); - }).should.throw('Committer must be connected'); + }).should.throw('Committer must be connectable'); }); it('should find a committer.name', () => { (() => { diff --git a/fabric-common/test/Commit.js b/fabric-common/test/Commit.js index b0c73238ca..4107baf8e0 100644 --- a/fabric-common/test/Commit.js +++ b/fabric-common/test/Commit.js @@ -9,8 +9,10 @@ const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const should = chai.should(); chai.use(chaiAsPromised); +const sinon = require('sinon'); const Commit = rewire('../lib/Commit'); +const Committer = require('../lib/Committer'); const Client = require('../lib/Client'); const User = rewire('../lib/User'); const TestUtils = require('./TestUtils'); @@ -36,12 +38,12 @@ describe('Commit', () => { } }; const committer_results = {status: 'SUCCESS'}; - const fakeCommitter = { - type: 'Committer', - sendBroadcast: () => { - return committer_results; - } - }; + const fakeCommitter = sinon.createStubInstance(Committer); + fakeCommitter.sendBroadcast.resolves(committer_results); + fakeCommitter.checkConnection.resolves(true); + fakeCommitter.type = 'Committer'; + fakeCommitter.name = 'mycommitter'; + beforeEach(() => { commit = endorsement.newCommit(); }); @@ -145,6 +147,17 @@ describe('Commit', () => { const results = await commit.send(request); should.equal(results.status, 'FAILED'); }); + it('uses a target with connection', async () => { + endorsement._proposalResponses = []; + endorsement._proposalResponses.push(proposalResponse); + commit.build(idx); + commit.sign(idx); + fakeCommitter.checkConnection.resolves(false); + const request = { + targets: [fakeCommitter] + }; + await commit.send(request).should.be.rejectedWith(/Committer mycommitter is not connected/); + }); }); describe('#toString', () => { diff --git a/fabric-common/test/DiscoveryHandler.js b/fabric-common/test/DiscoveryHandler.js index 73ad5320fa..0d16d901b7 100644 --- a/fabric-common/test/DiscoveryHandler.js +++ b/fabric-common/test/DiscoveryHandler.js @@ -34,8 +34,9 @@ describe('DiscoveryHandler', () => { const tmpSetDelete = Set.prototype.delete; let peer11, peer12, peer21, peer22, peer31, peer32, peer33; + let orderer1, orderer2, orderer3; - // const pem = '-----BEGIN CERTIFICATE----- -----END CERTIFICATE-----\n'; + const pem = '-----BEGIN CERTIFICATE----- -----END CERTIFICATE-----\n'; const org1 = [ 'Org1MSP', 'peer1.org1.example.com:7001', @@ -87,8 +88,8 @@ describe('DiscoveryHandler', () => { layouts: [{G0: 1, G1: 1}, {G3: 3, G1: 1}] }; - /* - const discovery_plan = { + + const config_results = { msps: { OrdererMSP: { id: 'OrdererMSP', @@ -128,7 +129,7 @@ describe('DiscoveryHandler', () => { Org2MSP: {endpoints: [{host: 'orderer.org2.example.com', port: 7150, name: 'orderer.org2.example.com'}]}, Org3MSP: {endpoints: [{host: 'orderer.org3.example.com', port: 7150, name: 'orderer.org3.example.com'}]} }, - peersByOrg: { + peers_by_org: { Org1MSP: { peers: [ {mspid: org1[0], endpoint: org1[1], ledgerHeight, chaincodes, name: org1[1]}, @@ -144,14 +145,38 @@ describe('DiscoveryHandler', () => { Org3MSP: { peers: [ {mspid: org3[0], endpoint: org3[1], ledgerHeight, chaincodes, name: org3[1]}, - {mspid: org3[0], endpoint: org3[2], ledgerHeight, chaincodes, name: org3[2]}, - {mspid: org3[0], endpoint: org3[3], ledgerHeight, chaincodes, name: org3[3]} + {mspid: org3[0], endpoint: org3[2], ledgerHeight: highest, chaincodes, name: org3[2]}, + {mspid: org3[0], endpoint: org3[3], ledgerHeight: smaller, chaincodes, name: org3[3]} + ] + } + } + }; + + const organization_plan = { + plan_id: 'required organizations', + groups: { + Org1MSP: { + peers: [ + {mspid: org1[0], endpoint: org1[1], ledgerHeight, chaincodes, name: org1[1]}, + {mspid: org1[0], endpoint: org1[2], ledgerHeight, chaincodes, name: org1[2]} + ] + }, + Org2MSP: { + peers: [ + {mspid: org2[0], endpoint: org2[1], ledgerHeight, chaincodes, name: org2[1]}, + {mspid: org2[0], endpoint: org2[2], ledgerHeight, chaincodes, name: org2[2]} + ] + }, + Org3MSP: { + peers: [ + {mspid: org3[0], endpoint: org3[1], ledgerHeight, chaincodes, name: org3[1]}, + {mspid: org3[0], endpoint: org3[2], ledgerHeight: highest, chaincodes, name: org3[2]}, + {mspid: org3[0], endpoint: org3[3], ledgerHeight: smaller, chaincodes, name: org3[3]} ] } }, - endorsement_plan: endorsement_plan + layouts: [{Org1MSP: 1, Org2MSP: 1, Org3MSP: 1}] }; - */ const good = {response: {status: 200}}; beforeEach(() => { @@ -173,54 +198,67 @@ describe('DiscoveryHandler', () => { peer11 = client.newEndorser(org1[1], org1[0]); peer11.endpoint = {url: 'grpcs://' + org1[1], addr: org1[1]}; peer11.sendProposal = sandbox.stub().resolves(good); + peer11.checkConnection = sandbox.stub().resolves(true); peer11.connected = true; channel.addEndorser(peer11); peer12 = client.newEndorser(org1[2], org1[0]); peer12.endpoint = {url: 'grpcs://' + org1[2], addr: org1[2]}; peer12.sendProposal = sandbox.stub().resolves(good); + peer12.checkConnection = sandbox.stub().resolves(true); peer12.connected = true; channel.addEndorser(peer12); peer21 = client.newEndorser(org2[1], org2[0]); peer21.endpoint = {url: 'grpcs://' + org2[1], addr: org2[1]}; peer21.sendProposal = sandbox.stub().resolves(good); + peer21.checkConnection = sandbox.stub().resolves(true); peer21.connected = true; channel.addEndorser(peer21); peer22 = client.newEndorser(org2[2], org2[0]); peer22.endpoint = {url: 'grpcs://' + org2[2], addr: org2[2]}; peer22.sendProposal = sandbox.stub().resolves(good); + peer22.checkConnection = sandbox.stub().resolves(true); peer22.connected = true; channel.addEndorser(peer22); peer31 = client.newEndorser(org3[1], org3[0]); peer31.endpoint = {url: 'grpcs://' + org3[1], addr: org3[1]}; peer31.sendProposal = sandbox.stub().resolves(good); + peer31.checkConnection = sandbox.stub().resolves(true); peer31.connected = true; channel.addEndorser(peer31); peer32 = client.newEndorser(org3[2], org3[0]); peer32.endpoint = {url: 'grpcs://' + org3[2], addr: org3[2]}; peer32.sendProposal = sandbox.stub().resolves(good); + peer32.checkConnection = sandbox.stub().resolves(true); peer32.connected = true; channel.addEndorser(peer32); peer33 = client.newEndorser(org3[3], org3[0]); peer33.endpoint = {url: 'grpcs://' + org3[3], addr: org3[3]}; peer33.sendProposal = sandbox.stub().resolves(good); + peer33.checkConnection = sandbox.stub().resolves(true); peer33.connected = true; channel.addEndorser(peer33); - const orderer1 = client.newCommitter('orderer1', 'msp1'); + orderer1 = client.newCommitter('orderer1', 'msp1'); orderer1.sendBroadcast = sandbox.stub().resolves({status: 'SUCCESS'}); + orderer1.checkConnection = sandbox.stub().resolves(true); orderer1.connected = true; + orderer1.endpoint = {url: 'grpc://orderer1.com'}; channel.addCommitter(orderer1); - const orderer2 = client.newCommitter('orderer2', 'msp2'); + orderer2 = client.newCommitter('orderer2', 'msp2'); orderer2.sendBroadcast = sandbox.stub().resolves({status: 'SUCCESS'}); + orderer2.checkConnection = sandbox.stub().resolves(true); orderer2.connected = true; + orderer2.endpoint = {url: 'grpc://orderer2.com'}; channel.addCommitter(orderer2); - const orderer3 = client.newCommitter('orderer3', 'msp1'); + orderer3 = client.newCommitter('orderer3', 'msp1'); orderer3.sendBroadcast = sandbox.stub().resolves({status: 'SUCCESS'}); + orderer3.checkConnection = sandbox.stub().resolves(true); orderer3.connected = true; + orderer3.endpoint = {url: 'grpc://orderer3.com'}; channel.addCommitter(orderer3); discovery = channel.newDiscoveryService('mydiscovery'); @@ -300,7 +338,9 @@ describe('DiscoveryHandler', () => { const results = await discoveryHandler.commit('signedEnvelope'); results.status.should.equal('SUCCESS'); }); - it('should run with orderers assigned', async () => { + it('should run with some bad orderers assigned', async () => { + orderer1.checkConnection = sandbox.stub().resolves(false); + orderer2.checkConnection = sandbox.stub().resolves(false); const results = await discoveryHandler.commit('signedEnvelope'); results.status.should.equal('SUCCESS'); }); @@ -321,6 +361,14 @@ describe('DiscoveryHandler', () => { await discoveryHandler.commit('signedEnvelope', {mspid: 'msp2'}) .should.be.rejectedWith(/FAILED with Error/); }); + it('should reject when all orderers are no connected', async () => { + orderer1.checkConnection = sandbox.stub().resolves(false); + orderer2.checkConnection = sandbox.stub().resolves(false); + orderer3.checkConnection = sandbox.stub().resolves(false); + + await discoveryHandler.commit('signedEnvelope') + .should.be.rejectedWith(/is not connected/); + }); }); describe('#endorse', () => { @@ -337,6 +385,14 @@ describe('DiscoveryHandler', () => { const results = await discoveryHandler.endorse('signedProposal', {requestTimeout: 2000}); results.should.equal('DONE'); }); + it('should run ok with required orgs', async () => { + discovery.getDiscoveryResults = sandbox.stub().resolves(config_results); + discoveryHandler._endorse = sandbox.stub().resolves('DONE'); + const results = await discoveryHandler.endorse('signedProposal', {requiredOrgs: ['Org1MSP', 'Org2MSP', 'Org3MSP'], sort: 'check'}); + results.should.equal('DONE'); + sinon.assert.calledWith(discoveryHandler._endorse, organization_plan, {sort: 'check', preferredHeightGap: undefined}); + + }); }); describe('#_endorse', () => { @@ -365,7 +421,7 @@ describe('DiscoveryHandler', () => { const results = await discoveryHandler._endorse({}, {preferredHeightGap: 0}, 'proposal'); results.should.equal('endorsements'); }); - it('should run ok', async () => { + it('should run - show failed', async () => { discovery.getDiscoveryResults = sandbox.stub().resolves({endorsement_plan: {something: 'plan a'}}); discoveryHandler._modify_groups = sinon.stub(); discoveryHandler._getRandom = sinon.stub().returns([{}]); @@ -492,6 +548,15 @@ describe('DiscoveryHandler', () => { }); }); + describe('#_buildRequiredOrgPlan', () => { + it('should run ok', async () => { + + // TEST CALL + const results = await discoveryHandler._buildRequiredOrgPlan(config_results.peers_by_org); + results.should.deep.equal(organization_plan); + }); + }); + describe('#_build_endorse_group_member', () => { it('should run ok', async () => { endorsement_plan.endorsements = {}; @@ -507,6 +572,23 @@ describe('DiscoveryHandler', () => { results.response.status.should.equal(200); sinon.assert.calledWith(FakeLogger.debug, '%s - start', '_build_endorse_group_member >> G0:0'); }); + it('should run - show reconnect error', async () => { + endorsement_plan.endorsements = {}; + peer11.checkConnection.resolves(false); + peer12.checkConnection.resolves(false); + // TEST CALL + const results = await discoveryHandler._build_endorse_group_member( + endorsement_plan, // endorsement plan + endorsement_plan.groups.G0, // group + 'proposal', // proposal + 2000, // timeout + 0, // endorser_process_index + 'G0' // group name + ); + results.message.should.equal('Peer peer2.org1.example.com:7002 is not connected'); + sinon.assert.notCalled(peer11.sendProposal); + sinon.assert.notCalled(peer12.sendProposal); + }); it('should run ok and return error when endorser rejects', async () => { endorsement_plan.endorsements = {}; peer11.sendProposal = sandbox.stub().rejects(Error('FAILED')); diff --git a/fabric-common/test/DiscoveryService.js b/fabric-common/test/DiscoveryService.js index 1ff987267b..c944f771a7 100644 --- a/fabric-common/test/DiscoveryService.js +++ b/fabric-common/test/DiscoveryService.js @@ -99,8 +99,13 @@ describe('DiscoveryService', () => { const endorser = sinon.createStubInstance(Endorser); endorser.type = 'Endorser'; + endorser.connected = true; + endorser.isConnectable = sinon.stub().returns(true); + const committer = sinon.createStubInstance(Committer); committer.type = 'Committer'; + committer.connected = true; + committer.isConnectable = sinon.stub().returns(true); let FakeLogger; @@ -121,6 +126,8 @@ describe('DiscoveryService', () => { discoverer = new Discoverer('mydiscoverer', client); endpoint = client.newEndpoint({url: 'grpc://somehost.com'}); discoverer.endpoint = endpoint; + discoverer.waitForReady = sinon.stub().resolves(true); + discoverer.checkConnection = sinon.stub().resolves(true); discovery = new DiscoveryService('mydiscovery', channel); client.getEndorser = sinon.stub().returns(endorser); client.newEndorser = sinon.stub().returns(endorser); @@ -178,7 +185,7 @@ describe('DiscoveryService', () => { (() => { discoverer.endpoint = undefined; discovery.setTargets([discoverer]); - }).should.throw('Discoverer mydiscoverer is not connected'); + }).should.throw('Discoverer mydiscoverer is not connectable'); }); it('should handle connected target', () => { discoverer.connected = true; @@ -415,6 +422,23 @@ describe('DiscoveryService', () => { const results = discovery._buildProtoChaincodeInterest(interest); should.exist(results.chaincodes); }); + it('should handle two chaincode four collection in camel case', () => { + const interest = [ + {name: 'chaincode1', collectionNames: ['collection1', 'collection3']}, + {name: 'chaincode2', collectionNames: ['collection2', 'collection4']} + ]; + const results = discovery._buildProtoChaincodeInterest(interest); + should.exist(results.chaincodes); + }); + it('should handle two chaincode four collection in camel case', () => { + const interest = [ + {name: 'chaincode1', collectionNames: ['collection1', 'collection3'], noPrivateReads: true}, + {name: 'chaincode2', collectionNames: ['collection2', 'collection4']} + ]; + const results = discovery._buildProtoChaincodeInterest(interest); + should.exist(results.chaincodes); + results.chaincodes[0].no_private_reads.should.be.true; + }); it('should handle two chaincodes same name', () => { const interest = [{name: 'chaincode1'}, {name: 'chaincode1'}]; const results = discovery._buildProtoChaincodeInterest(interest); diff --git a/fabric-common/test/EventService.js b/fabric-common/test/EventService.js index 629b9ec6a8..8c78ff8790 100644 --- a/fabric-common/test/EventService.js +++ b/fabric-common/test/EventService.js @@ -173,6 +173,7 @@ describe('EventService', () => { eventService2._closeRunning.should.be.false; eventService2.blockType.should.be.equal('filtered'); eventService2.replay.should.be.false; + eventService2.startSpecified.should.be.false; }); }); @@ -367,6 +368,7 @@ describe('EventService', () => { }; eventService.build(idx, options); should.exist(eventService._payload); + should.equal(eventService.startSpecified, true); }); it('should build with startBlock and endBlock as valid numbers', () => { const options = { @@ -517,6 +519,21 @@ describe('EventService', () => { eventService.blockType = 'full'; await eventService._startService(eventer1, {}, 10).should.be.rejectedWith('Event service timed out - Unable to start listening'); }); + it('throws timeout on stream', async () => { + const eventer1 = client.newEventer('eventer1'); + eventer1.endpoint = endpoint; + eventer1.checkConnection = sinon.stub().returns(true); + const stream = sinon.stub(); + stream.on = sinon.stub(); + stream.write = sinon.stub(); + eventer1.setStreamByType = function() { + this.stream = stream; + }; + eventService.blockType = 'full'; + eventService.startSpecified = true; + await eventService._startService(eventer1, {}, 10); + eventService.close(); + }); it('throws error on stream write', async () => { const eventer1 = client.newEventer('eventer1'); eventer1.endpoint = endpoint; diff --git a/fabric-common/test/Proposal.js b/fabric-common/test/Proposal.js index 24a179fcb4..cde1e5a7d9 100644 --- a/fabric-common/test/Proposal.js +++ b/fabric-common/test/Proposal.js @@ -41,6 +41,8 @@ describe('Proposal', () => { endorser.type = 'Endorser'; endpoint = client.newEndpoint({url: 'grpc://somehost.com'}); endorser.endpoint = endpoint; + endorser.waitForReady = sinon.stub().resolves(true); + endorser.checkConnection = sinon.stub().resolves(true); handler = new DiscoveryHandler('discovery'); }); @@ -78,19 +80,39 @@ describe('Proposal', () => { describe('#buildProposalInterest', () => { it('should return interest', () => { const interest = proposal.buildProposalInterest(); - interest.should.deep.equal([{name: 'chaincode'}]); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false}]); }); it('should return interest and collections', () => { const collections = ['col1', 'col2']; proposal.collectionsInterest = collections; const interest = proposal.buildProposalInterest(); - interest.should.deep.equal([{name: 'chaincode', collectionNames: collections}]); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false, collectionNames: collections}]); + }); + it('should return interest and collections with noPrivateReads', () => { + const collections = ['col1', 'col2']; + proposal.collectionsInterest = collections; + proposal.noPrivateReads = true; + const interest = proposal.buildProposalInterest(); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: true, collectionNames: collections}]); }); it('should return interest and chaincode and chaincode collections ', () => { const chaincode_collection = {name: 'chain2', collectionNames: ['col1', 'col2']}; proposal.chaincodesCollectionsInterest = [chaincode_collection]; const interest = proposal.buildProposalInterest(); - interest.should.deep.equal([{name: 'chaincode'}, chaincode_collection]); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false}, chaincode_collection]); + }); + it('should return interest and chaincode and chaincode collections with no private reads ', () => { + const chaincode_collection = {name: 'chain2', collectionNames: ['col1', 'col2'], noPrivateReads: true}; + proposal.chaincodesCollectionsInterest = [chaincode_collection]; + const interest = proposal.buildProposalInterest(); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false}, chaincode_collection]); + }); + it('should return interest and chaincode and chaincode collections with no private reads ', () => { + const chaincode_collection = {name: 'chain2', collectionNames: ['col1', 'col2'], noPrivateReads: true}; + proposal.noPrivateReads = true; + proposal.chaincodesCollectionsInterest = [chaincode_collection]; + const interest = proposal.buildProposalInterest(); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: true}, chaincode_collection]); }); }); @@ -105,7 +127,7 @@ describe('Proposal', () => { proposal.addCollectionInterest('col2'); proposal.collectionsInterest.should.deep.equal(collections); const interest = proposal.buildProposalInterest(); - interest.should.deep.equal([{name: 'chaincode', collectionNames: collections}]); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false, collectionNames: collections}]); }); it('should require a string collection name', () => { (() => { @@ -114,20 +136,37 @@ describe('Proposal', () => { }); }); + describe('#setNoPrivateReads', () => { + it('should set no private reads', () => { + proposal.setNoPrivateReads(true); + proposal.noPrivateReads.should.equal(true); + }); + it('should set no private reads false', () => { + proposal.setNoPrivateReads(false); + proposal.noPrivateReads.should.equal(false); + }); + + it('should require a boolean', () => { + (() => { + proposal.setNoPrivateReads({}); + }).should.throw(/The "no private reads" setting must be boolean/); + }); + }); + describe('#addChaincodeCollectionsInterest', () => { it('should save chaincode collection interest', () => { - const chaincode_collection = {name: 'chain2', collectionNames: ['col1', 'col2']}; + const chaincode_collection = {name: 'chain2', noPrivateReads: false, collectionNames: ['col1', 'col2']}; proposal.addChaincodeCollectionsInterest('chain2', 'col1', 'col2'); proposal.chaincodesCollectionsInterest.should.deep.equal([chaincode_collection]); const interest = proposal.buildProposalInterest(); - interest.should.deep.equal([{name: 'chaincode'}, chaincode_collection]); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false}, chaincode_collection]); }); it('should save chaincode only chaincode collection interest', () => { - const chaincode_collection = {name: 'chain2'}; + const chaincode_collection = {name: 'chain2', noPrivateReads: false}; proposal.addChaincodeCollectionsInterest('chain2'); proposal.chaincodesCollectionsInterest.should.deep.equal([chaincode_collection]); const interest = proposal.buildProposalInterest(); - interest.should.deep.equal([{name: 'chaincode'}, chaincode_collection]); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false}, chaincode_collection]); }); it('should require a string chaincode name', () => { (() => { @@ -136,6 +175,28 @@ describe('Proposal', () => { }); }); + describe('#addChaincodeNoPrivateReadsCollectionsInterest', () => { + it('should save chaincode collection interest', () => { + const chaincode_collection = {name: 'chain2', noPrivateReads: true, collectionNames: ['col1', 'col2']}; + proposal.addChaincodeNoPrivateReadsCollectionsInterest('chain2', true, 'col1', 'col2'); + proposal.chaincodesCollectionsInterest.should.deep.equal([chaincode_collection]); + const interest = proposal.buildProposalInterest(); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false}, chaincode_collection]); + }); + it('should save chaincode only chaincode collection interest', () => { + const chaincode_collection = {name: 'chain2', noPrivateReads: false}; + proposal.addChaincodeNoPrivateReadsCollectionsInterest('chain2'); + proposal.chaincodesCollectionsInterest.should.deep.equal([chaincode_collection]); + const interest = proposal.buildProposalInterest(); + interest.should.deep.equal([{name: 'chaincode', noPrivateReads: false}, chaincode_collection]); + }); + it('should require a string chaincode name', () => { + (() => { + proposal.addChaincodeNoPrivateReadsCollectionsInterest({}); + }).should.throw('Invalid chaincodeId parameter'); + }); + }); + describe('#build', () => { it('should require a idContext', () => { (() => { diff --git a/fabric-common/test/ServiceEndpoint.js b/fabric-common/test/ServiceEndpoint.js index 17ff137c32..a85868eba2 100644 --- a/fabric-common/test/ServiceEndpoint.js +++ b/fabric-common/test/ServiceEndpoint.js @@ -62,24 +62,24 @@ describe('ServiceEndpoint', () => { it('should be true if connected', () => { serviceEndpoint.connected = true; const result = serviceEndpoint.isConnectable(); - should.equal(result, false); + should.equal(result, true); }); it('should be true if not connected and have endpoint assigned', () => { serviceEndpoint.connected = false; serviceEndpoint.endpoint = endpoint; + serviceEndpoint.serviceClass = sinon.stub(); const result = serviceEndpoint.isConnectable(); should.equal(result, true); }); - it('should be false if not connected and have endpoint assigned but already tried to connect', () => { + it('should be false if not connected and no endpoint assigned', () => { serviceEndpoint.connected = false; - serviceEndpoint.endpoint = endpoint; - serviceEndpoint.connectAttempted = true; + serviceEndpoint.endpoint = undefined; const result = serviceEndpoint.isConnectable(); should.equal(result, false); }); - it('should be false if not connected and no endpoint assigned', () => { + it('should be false if not connected and no service class assigned', () => { serviceEndpoint.connected = false; - serviceEndpoint.endpoint = undefined; + serviceEndpoint.endpoint = endpoint; const result = serviceEndpoint.isConnectable(); should.equal(result, false); }); @@ -120,6 +120,59 @@ describe('ServiceEndpoint', () => { }); }); + describe('#checkConnection', () => { + it('should resolve true if connected', async () => { + serviceEndpoint.connected = true; + sinon.stub(serviceEndpoint, 'waitForReady').resolves(true); + sinon.stub(serviceEndpoint, 'resetConnection').resolves(true); + sinon.stub(serviceEndpoint, 'isConnectable').resolves(true); + + const result = await serviceEndpoint.checkConnection(); + should.equal(result, true); + }); + it('should resolve false if not connected and not able to reset', async () => { + serviceEndpoint.connected = false; + sinon.stub(serviceEndpoint, 'resetConnection').resolves(false); + sinon.stub(serviceEndpoint, 'isConnectable').resolves(true); + + const result = await serviceEndpoint.checkConnection(); + should.equal(result, false); + sinon.assert.calledOnce(serviceEndpoint.resetConnection); + }); + it('should resolve true if not connected and able to reset', async () => { + serviceEndpoint.connected = false; + serviceEndpoint.resetConnection = () => { + return new Promise((resolve, reject) => { + serviceEndpoint.connected = true; + resolve(true); + }); + }; + sinon.stub(serviceEndpoint, 'isConnectable').resolves((true)); + + const result = await serviceEndpoint.checkConnection(); + should.equal(result, true); + }); + it('should resolve true if connection fails, but able to reset', async () => { + serviceEndpoint.connected = true; + sinon.stub(serviceEndpoint, 'waitForReady').resolves(false); + sinon.stub(serviceEndpoint, 'resetConnection').resolves(true); + sinon.stub(serviceEndpoint, 'isConnectable').resolves(true); + + const result = await serviceEndpoint.checkConnection(); + should.equal(result, true); + }); + it('should resolve false if not connectable', async () => { + serviceEndpoint.connected = false; + sinon.stub(serviceEndpoint, 'waitForReady').resolves(false); + sinon.stub(serviceEndpoint, 'resetConnection').resolves(false); + sinon.stub(serviceEndpoint, 'isConnectable').resolves(false); + + const result = await serviceEndpoint.checkConnection(); + should.equal(result, false); + sinon.assert.notCalled(serviceEndpoint.waitForReady); + }); + }); + describe('#disconnect', () => { it('should run if no service', () => { serviceEndpoint.service = null; @@ -134,21 +187,18 @@ describe('ServiceEndpoint', () => { }); }); - describe('#checkConnection', () => { - it('should run if connected', async () => { - serviceEndpoint.connected = false; - const results = await serviceEndpoint.checkConnection(); - results.should.be.false; - }); - it('should run if connected', async () => { + describe('#resetConnection', () => { + it('should run', async () => { + sinon.stub(serviceEndpoint, 'disconnect').returns(true); sinon.stub(serviceEndpoint, 'waitForReady').resolves(true); - const results = await serviceEndpoint.checkConnection(); - results.should.be.true; + serviceEndpoint.endpoint = sinon.stub(); + serviceEndpoint.serviceClass = sinon.stub(); + await serviceEndpoint.resetConnection(); }); - it('should get false if waitForReady fails', async () => { - sinon.stub(serviceEndpoint, 'waitForReady').rejects(new Error('Failed to connect')); - const results = await serviceEndpoint.checkConnection(); - results.should.be.false; + it('should fail', async () => { + sinon.stub(serviceEndpoint, 'disconnect').returns(true); + sinon.stub(serviceEndpoint, 'waitForReady').resolves(true); + await serviceEndpoint.resetConnection().should.be.rejectedWith(/is missing endpoint information/); }); }); diff --git a/fabric-common/types/index.d.ts b/fabric-common/types/index.d.ts index bc8a21cf7c..659cbaceb8 100644 --- a/fabric-common/types/index.d.ts +++ b/fabric-common/types/index.d.ts @@ -213,7 +213,9 @@ export class Proposal extends ServiceAction { public getTransactionId(): string; public buildProposalInterest(): any; public addCollectionInterest(collectionName: string): Proposal; + public setNoPrivateReads(noPrivateReads: boolean): Proposal; public addChaincodeCollectionsInterest(collectionName: string, collectionNames: string[]): Proposal; + public addChaincodeNoPrivateReadsCollectionsInterest(collectionName: string, noPrivateReads: boolean, collectionNames: string[]): Proposal; public build(idContext: IdentityContext, request?: BuildProposalRequest): Buffer; public send(request?: SendProposalRequest): Promise; public verifyProposalResponse(proposalResponse?: any): boolean; diff --git a/fabric-network/src/contract.ts b/fabric-network/src/contract.ts index ba41469760..8ff808ca2b 100644 --- a/fabric-network/src/contract.ts +++ b/fabric-network/src/contract.ts @@ -46,6 +46,7 @@ function verifyNamespace(namespace?: string): void { export interface DiscoveryInterest { name: string; collectionNames?: string[]; + noPrivateReads?: boolean; } export interface Contract { diff --git a/fabric-network/test/contract.js b/fabric-network/test/contract.js index a3e2c84756..5e609d6eb3 100644 --- a/fabric-network/test/contract.js +++ b/fabric-network/test/contract.js @@ -146,13 +146,20 @@ describe('Contract', () => { it ('throws when not an interest', () => { (() => contract.addDiscoveryInterest('intersts')).should.throw('"interest" parameter must be a DiscoveryInterest object'); }); - it('add collection', async () => { + it('update collection', async () => { const interest = {name: chaincodeId, collectionNames: ['c1', 'c2']}; contract.addDiscoveryInterest(interest); expect(contract.discoveryInterests).to.deep.equal([ interest ]); }); + it('update collection with no private reads', async () => { + const interest = {name: chaincodeId, collectionNames: ['c1', 'c2'], noPrivateReads: true}; + contract.addDiscoveryInterest(interest); + expect(contract.discoveryInterests).to.deep.equal([ + interest + ]); + }); it('add chaincode', async () => { const other = {name: 'other'}; contract.addDiscoveryInterest(other); @@ -169,6 +176,14 @@ describe('Contract', () => { other ]); }); + it('add chaincode and collection with no private reads', async () => { + const other = {name: 'other', collectionNames: ['c1', 'c2'], noPrivateReads: true}; + contract.addDiscoveryInterest(other); + expect(contract.discoveryInterests).to.deep.equal([ + {name: chaincodeId}, + other + ]); + }); }); describe('#getDiscoveryInterests', () => { diff --git a/test/ts-scenario/features/discovery.feature b/test/ts-scenario/features/discovery.feature index 7d8d4529bf..63e2ed5949 100644 --- a/test/ts-scenario/features/discovery.feature +++ b/test/ts-scenario/features/discovery.feature @@ -63,3 +63,9 @@ Feature: Configure Fabric using CLI and submit/evaluate using a network Gateway Then The gateway named myDiscoveryGateway has a submit type response matching {"key0":"value1","key1":"value2"} When I modify myDiscoveryGateway to evaluate a transaction with transient data using args [getTransient,valueA,valueB] for contract fabcar instantiated on channel discoverychannel Then The gateway named myDiscoveryGateway has a evaluate type response matching {"key0":"valueA","key1":"valueB"} + + Scenario: Using a Gateway I can submit and evaluate transactions on instantiated node smart contract with specific organizations + When I use the discovery gateway named myDiscoveryGateway to submit a transaction with args [createCar,2001,Ford,F350,red,Sam] for contract fabcar instantiated on channel discoverychannel using requiredOrgs ["Org1MSP","Org2MSP"] + Then The gateway named myDiscoveryGateway has a submit type response + When I use the gateway named myDiscoveryGateway to evaluate a transaction with args [queryCar,2001] for contract fabcar instantiated on channel discoverychannel + Then The gateway named myDiscoveryGateway has a evaluate type response matching {"color":"red","docType":"car","make":"Ford","model":"F350","owner":"Sam"} diff --git a/test/ts-scenario/steps/lib/gateway.ts b/test/ts-scenario/steps/lib/gateway.ts index c8e01f4b35..f8000d872b 100644 --- a/test/ts-scenario/steps/lib/gateway.ts +++ b/test/ts-scenario/steps/lib/gateway.ts @@ -5,7 +5,7 @@ 'use strict'; import * as FabricCAClient from 'fabric-ca-client'; -import { Contract, DefaultEventHandlerStrategies, DefaultQueryHandlerStrategies, Gateway, GatewayOptions, HsmOptions, HsmX509Provider, Identity, IdentityProvider, Network, QueryHandlerFactory, Transaction, TransientMap, TxEventHandlerFactory, Wallet, Wallets } from 'fabric-network'; +import { Contract, DefaultEventHandlerStrategies, DefaultQueryHandlerStrategies, Gateway, GatewayOptions, HsmOptions, HsmX509Provider, Identity, IdentityProvider, Network, QueryHandlerFactory, Transaction, TransientMap, TxEventHandlerFactory, Wallet, Wallets, DiscoveryInterest } from 'fabric-network'; import * as fs from 'fs'; import * as path from 'path'; import { createQueryHandler as sampleQueryStrategy } from '../../config/handlers/sample-query-handler'; @@ -333,7 +333,7 @@ async function createHSMUser(wallet: Wallet, ccp: CommonConnectionProfileHelper, * @param {String} txnType the type of transaction (submit/evaluate) * @param {String} handlerOption Optional: the handler option to use */ -export async function performGatewayTransaction(gatewayName: string, contractName: string, channelName: string, collectionName: string, args: string, txnType: string, handlerOption?: string): Promise { +export async function performGatewayTransaction(gatewayName: string, contractName: string, channelName: string, collectionName: string, args: string, txnType: string, handlerOption?: string, requiredOrgs?: string[]): Promise { const gatewayObj = getGatewayObject(gatewayName); const gateway = gatewayObj.gateway; @@ -382,7 +382,7 @@ export async function performGatewayTransaction(gatewayName: string, contractNam BaseUtils.logMsg(` -- adding a discovery interest colletion name to the contrace ${collectionName}`); const chaincodeId = contract.chaincodeId; contract.resetDiscoveryInterests(); - contract.addDiscoveryInterest({name: chaincodeId, collectionNames: [collectionName]}); + contract.addDiscoveryInterest({name: chaincodeId, collectionNames: [collectionName], noPrivateReads: false}); } // Split args @@ -393,6 +393,10 @@ export async function performGatewayTransaction(gatewayName: string, contractNam // Submit/evaluate transaction try { const transaction: Transaction = contract.createTransaction(func); + if (requiredOrgs) { + transaction.setEndorsingOrganizations(...requiredOrgs); + } + let resultBuffer: Buffer; if (submit) { resultBuffer = await transaction.submit(...funcArgs); diff --git a/test/ts-scenario/steps/lib/utility/clientUtils.ts b/test/ts-scenario/steps/lib/utility/clientUtils.ts index dd8ab83155..d064a99cc7 100644 --- a/test/ts-scenario/steps/lib/utility/clientUtils.ts +++ b/test/ts-scenario/steps/lib/utility/clientUtils.ts @@ -125,6 +125,10 @@ export async function buildChannelRequest(requestName: string, contractName: str // centralize the endorsement operation, including the endorsement results. // Proposals must be built from channel and chaincode name const endorsement: Endorsement = channel.newEndorsement(contractName); + + // ---- setup DISCOVERY + endorsement.setNoPrivateReads(true); + endorsement.addCollectionInterest('_implicit_org_Org1MSP'); const discovery: DiscoveryService = channel.newDiscoveryService('mydiscovery'); const discoverer: Discoverer = client.newDiscoverer('peer1-discovery'); diff --git a/test/ts-scenario/steps/network-model.ts b/test/ts-scenario/steps/network-model.ts index d11226fb45..d2b1cd92b9 100644 --- a/test/ts-scenario/steps/network-model.ts +++ b/test/ts-scenario/steps/network-model.ts @@ -38,6 +38,10 @@ Given(/^I have a (.+?) backed gateway named (.+?) with discovery set to (.+?) fo } }); +When(/^I use the discovery gateway named (.+?) to (.+?) a transaction with args (.+?) for contract (.+?) instantiated on channel (.+?) using requiredOrgs (.+?)$/, { timeout: Constants.STEP_MED as number }, async (gatewayName: string, txnType: string, txnArgs: string, ccName: string, channelName: string, requiredOrgs: string) => { + return await Gateway.performGatewayTransaction(gatewayName, ccName, channelName, '', txnArgs, txnType, '', JSON.parse(requiredOrgs)); +}); + When(/^I use the discovery gateway named (.+?) to (.+?) a transaction with args (.+?) for contract (.+?) instantiated on channel (.+?) using collection (.+?)$/, { timeout: Constants.STEP_MED as number }, async (gatewayName: string, txnType: string, txnArgs: string, ccName: string, channelName: string, collectionName: string) => { return await Gateway.performGatewayTransaction(gatewayName, ccName, channelName, collectionName, txnArgs, txnType); });