Skip to content

Commit

Permalink
FABN-929: Transient data with evaluateTransaction()
Browse files Browse the repository at this point in the history
- Add Transaction.evaluate()
- Refactor Contract.evaluateTransaction() to use Transaction.evaluate()

Change-Id: I7aca331351aec8201b3bba2bb0f2ddef141b6f89
Signed-off-by: Mark S. Lewis <mark_lewis@uk.ibm.com>
  • Loading branch information
bestbeforetoday committed Oct 31, 2018
1 parent 5b7c7cd commit 1b14029
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 241 deletions.
29 changes: 16 additions & 13 deletions fabric-client/lib/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2359,10 +2359,10 @@ const Channel = class {
* and nonce
* @property {string} collections-config - Optional. The path to the collections
* config. More details can be found at this [tutorial]{@link https://fabric-sdk-node.github.io/tutorial-private-data.html}
* @property {map} transientMap - Optional. <string, byte[]> map that can be
* used by the chaincode during initialization, but not saved in the
* ledger. Data such as cryptographic information for encryption can
* be passed to the chaincode using this technique.
* @property {object} [transientMap] - Optional. Object with String property names
* and Buffer property values that can be used by the chaincode but not
* saved in the ledger. Data such as cryptographic information for
* encryption can be passed to the chaincode using this technique.
* @property {string} fcn - Optional. The function name to be returned when
* calling <code>stub.GetFunctionAndParameters()</code> in the target
* chaincode. Default is 'init'.
Expand Down Expand Up @@ -2563,9 +2563,10 @@ const Channel = class {
* @property {TransactionID} txId - Optional. TransactionID object with the
* transaction id and nonce. txId is required for [sendTransactionProposal]{@link Channel#sendTransactionProposal}
* and optional for [generateUnsignedProposal]{@link Channel#generateUnsignedProposal}
* @property {map} transientMap - Optional. <string, Buffer> map that can be
* used by the chaincode but not
* saved in the ledger, such as cryptographic information for encryption
* @property {object} [transientMap] - Optional. Object with String property names
* and Buffer property values that can be used by the chaincode but not
* saved in the ledger. Data such as cryptographic information for
* encryption can be passed to the chaincode using this technique.
* @property {string} fcn - Optional. The function name to be returned when
* calling <code>stub.GetFunctionAndParameters()</code>
* in the target chaincode. Default is 'invoke'
Expand Down Expand Up @@ -2832,9 +2833,10 @@ const Channel = class {
* @property {string[]} args - Required. Arguments to send to chaincode.
* @property {string} chaincodeId - Required. ChaincodeId.
* @property {Buffer} argbytes - Optional. Include when an argument must be included as bytes.
* @property {map} transientMap - Optional. <sting, byte[]> The Map that can be
* used by the chaincode but not saved in the ledger, such as
* cryptographic information for encryption.
* @property {object} [transientMap] - Optional. Object with String property names
* and Buffer property values that can be used by the chaincode but not
* saved in the ledger. Data such as cryptographic information for
* encryption can be passed to the chaincode using this technique.
*/


Expand Down Expand Up @@ -3067,9 +3069,10 @@ const Channel = class {
* object will be used.
* @property {string} chaincodeId - Required. The id of the chaincode to process
* the transaction proposal
* @property {map} transientMap - Optional. <string, byte[]> map that can be
* used by the chaincode but not saved in the ledger, such as cryptographic
* information for encryption
* @property {object} [transientMap] - Optional. Object with String property names
* and Buffer property values that can be used by the chaincode but not
* saved in the ledger. Data such as cryptographic information for
* encryption can be passed to the chaincode using this technique.
* @property {string} fcn - Optional. The function name to be returned when
* calling <code>stub.GetFunctionAndParameters()</code>
* in the target chaincode. Default is 'invoke'
Expand Down
29 changes: 13 additions & 16 deletions fabric-network/lib/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ class Contract {
return this.gateway.getOptions().eventHandlerOptions;
}

getQueryHandler() {
return this.queryHandler;
}

/**
* Submit a transaction to the ledger. The transaction function <code>transactionName</code>
* will be evaluated on the endorsing peers and then submitted to the ordering service
Expand Down Expand Up @@ -105,33 +109,26 @@ class Contract {
* for committing to the ledger.
* @async
* @param {string} name Transaction function name.
* @param {...string} args Transaction function arguments.
* @param {...string} [args] Transaction function arguments.
* @returns {Buffer} Payload response from the transaction function.
*/
async submitTransaction(name, ...args) {
const transaction = this.createTransaction(name);
return transaction.submit(...args);
return this.createTransaction(name).submit(...args);
}

/**
* Evaluate a transaction function and return its results.
* The transaction function <code>transactionName</code>
* will be evaluated on the endorsing peers but the responses will not be sent to to
* will be evaluated on the endorsing peers but the responses will not be sent to
* the ordering service and hence will not be committed to the ledger.
* This is used for querying the world state.
* @param {string} name Transaction function name
* @param {...string} parameters Transaction function parameters
* @returns {Buffer} Payload response from the transaction function
* @async
* @param {string} name Transaction function name.
* @param {...string} [args] Transaction function arguments.
* @returns {Buffer} Payload response from the transaction function.
*/
async evaluateTransaction(name, ...parameters) {
verifyTransactionName(name);
Transaction.verifyArguments(parameters);

// form the transaction name with the namespace
const qualifiedName = this._getQualifiedName(name);
const txId = this.gateway.getClient().newTransactionID();
const result = await this.queryHandler.queryChaincode(this.chaincodeId, txId, qualifiedName, parameters);
return result ? result : null;
async evaluateTransaction(name, ...args) {
return this.createTransaction(name).evaluate(...args);
}
}

Expand Down
13 changes: 9 additions & 4 deletions fabric-network/lib/impl/query/defaultqueryhandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ class DefaultQueryHandler extends QueryHandler {
* @param {string} functionName the function name to invoke
* @param {string[]} args the arguments
* @param {TransactionID} txId the transaction id to use
* @param {object} [transientMap] transient data
* @returns {object} asynchronous response or async error.
*/
async queryChaincode(chaincodeId, txId, functionName, args) {
async queryChaincode(chaincodeId, txId, functionName, args, transientMap) {
const method = 'queryChaincode';
let success = false;
let payload;
Expand All @@ -67,7 +68,7 @@ class DefaultQueryHandler extends QueryHandler {
const peer = this.allQueryablePeers[this.queryPeerIndex];
try {
logger.debug('%s - querying previously successful peer: %s', method, peer.getName());
payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args);
payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args, transientMap);
success = true;
} catch (error) {
logger.warn('%s - error response trying previously successful peer: %s. Error: %s', method, peer.getName(), error);
Expand All @@ -88,7 +89,7 @@ class DefaultQueryHandler extends QueryHandler {
const peer = this.allQueryablePeers[i];
try {
logger.debug('%s - querying new peer: %s', method, peer.getName());
payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args);
payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args, transientMap);
this.queryPeerIndex = i;
success = true;
break;
Expand Down Expand Up @@ -120,9 +121,10 @@ class DefaultQueryHandler extends QueryHandler {
* @param {string} functionName the function name of the query
* @param {array} args the arguments to ass
* @param {TransactionID} txId the transaction id to use
* @param {object} [transientMap] transient data
* @returns {Buffer} asynchronous response to query
*/
async _querySinglePeer(peer, chaincodeId, txId, functionName, args) {
async _querySinglePeer(peer, chaincodeId, txId, functionName, args, transientMap) {
const method = '_querySinglePeer';
const request = {
targets: [peer],
Expand All @@ -131,6 +133,9 @@ class DefaultQueryHandler extends QueryHandler {
fcn: functionName,
args: args
};
if (transientMap) {
request.transientMap = transientMap;
}

const payloads = await this.channel.queryByChaincode(request);
if (!payloads.length) {
Expand Down
56 changes: 37 additions & 19 deletions fabric-network/lib/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ function getResponsePayload(peerResponse) {
return (payload && payload.length > 0) ? payload : null;
}

/**
* Ensure supplied transaction arguments are not strings.
* @private
* @static
* @param {Array} args transaction arguments.
* @throws {Error} if any arguments are invalid.
*/
function verifyArguments(args) {
const isInvalid = args.some((arg) => typeof arg !== 'string');
if (isInvalid) {
const argsString = args.map((arg) => util.format('%j', arg)).join(', ');
const msg = util.format('Transaction arguments must be strings: %s', argsString);
logger.error('verifyArguments:', msg);
throw new Error(msg);
}
}

class Transaction {
/**
* Constructor.
Expand All @@ -34,23 +51,6 @@ class Transaction {
};
}

/**
* Ensure supplied transaction arguments are not strings.
* @private
* @static
* @param {Array} args transaction argument.
* @throws {Error} if any arguments are invalid.
*/
static verifyArguments(args) {
const isInvalid = args.some((arg) => typeof arg !== 'string');
if (isInvalid) {
const argsString = args.map((arg) => util.format('%j', arg)).join(', ');
const msg = util.format('Transaction arguments must be strings: %s', argsString);
logger.error('_verifyTransactionArguments:', msg);
throw new Error(msg);
}
}

getName() {
return this._name;
}
Expand All @@ -72,11 +72,11 @@ class Transaction {
* will be evaluated on the endorsing peers and then submitted to the ordering service
* for committing to the ledger.
* @async
* @param {...string} args Transaction function arguments.
* @param {...string} [args] Transaction function arguments.
* @returns {Buffer} Payload response from the transaction function.
*/
async submit(...args) {
Transaction.verifyArguments(args);
verifyArguments(args);

const network = this._contract.getNetwork();
const channel = network.getChannel();
Expand Down Expand Up @@ -166,6 +166,24 @@ class Transaction {

return { validResponses, invalidResponses };
}

/**
* Evaluate a transaction function and return its results.
* The transaction function will be evaluated on the endorsing peers but
* the responses will not be sent to the ordering service and hence will
* not be committed to the ledger.
* This is used for querying the world state.
* @async
* @param {...string} [args] Transaction function arguments.
* @returns {Buffer} Payload response from the transaction function.
*/
async evaluate(...args) {
verifyArguments(args);

const queryHandler = this._contract.getQueryHandler();
const chaincodeId = this._contract.getChaincodeId();
return queryHandler.queryChaincode(chaincodeId, this._transactionId, this._name, args, this._transientMap);
}
}

module.exports = Transaction;
61 changes: 15 additions & 46 deletions fabric-network/test/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ describe('Contract', () => {
});
});

describe('#getQueryhandler', () => {
it('returns the query handler', () => {
const result = contract.getQueryHandler();
result.should.equal(mockQueryHandler);
});
});

describe('#createTransaction', () => {
it('returns a transaction with only a name', () => {
const name = 'name';
Expand Down Expand Up @@ -150,54 +157,16 @@ describe('Contract', () => {
});

describe('#evaluateTransaction', () => {
it('should query chaincode and handle a good response without return data', async () => {
mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).resolves();

const result = await contract.evaluateTransaction('myfunc', 'arg1', 'arg2');
sinon.assert.calledOnce(mockQueryHandler.queryChaincode);
should.equal(result, null);
});

it('should query chaincode and handle a good response with return data', async () => {
const response = Buffer.from('hello world');
mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).resolves(response);

const result = await contract.evaluateTransaction('myfunc', 'arg1', 'arg2');
sinon.assert.calledOnce(mockQueryHandler.queryChaincode);
result.equals(response).should.be.true;
});

it('should query chaincode and handle an error response', () => {
const response = new Error('such error');
mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).rejects(response);
return contract.evaluateTransaction('myfunc', 'arg1', 'arg2')
.should.be.rejectedWith(/such error/);

});

it('should query chaincode with namespace added to the function', async () => {
const nscontract = new Contract(network, chaincodeId, mockGateway, mockQueryHandler, 'my.name.space');

mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).resolves();

await nscontract.evaluateTransaction('myfunc', 'arg1', 'arg2');
sinon.assert.calledOnce(mockQueryHandler.queryChaincode);
sinon.assert.calledWith(mockQueryHandler.queryChaincode,
sinon.match.any,
sinon.match.any,
'my.name.space:myfunc',
sinon.match.any);
});
it('evaluates a transaction with supplied arguments', async () => {
const args = [ 'a', 'b', 'c' ];
const expected = Buffer.from('result');
const stubTransaction = sinon.createStubInstance(Transaction);
stubTransaction.evaluate.withArgs(...args).resolves(expected);
sinon.stub(contract, 'createTransaction').returns(stubTransaction);

it('throws if name is an empty string', () => {
const promise = contract.evaluateTransaction('');
return promise.should.be.rejectedWith('name');
});
const result = await contract.evaluateTransaction('name', ...args);

it('throws is name is not a string', () => {
const promise = contract.evaluateTransaction(123);
return promise.should.be.rejectedWith('name');
result.should.equal(expected);
});

});
});
Loading

0 comments on commit 1b14029

Please sign in to comment.