Skip to content

Commit

Permalink
FABN-929: Transient data support in fabric-network
Browse files Browse the repository at this point in the history
- Refactor transaction submit to use a Transaction class.
- Add Transaction.setTransient()

Change-Id: I889b419eefee0e6d6b6c7818512bf90be9727b80
Signed-off-by: Mark S. Lewis <[email protected]>
  • Loading branch information
bestbeforetoday committed Oct 30, 2018
1 parent 52c82cc commit 967eee1
Show file tree
Hide file tree
Showing 7 changed files with 608 additions and 375 deletions.
2 changes: 1 addition & 1 deletion fabric-client/lib/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2563,7 +2563,7 @@ 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, byte[]> map that can be
* @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 {string} fcn - Optional. The function name to be returned when
Expand Down
230 changes: 72 additions & 158 deletions fabric-network/lib/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,198 +6,111 @@

'use strict';

const Transaction = require('fabric-network/lib/transaction');

const logger = require('./logger').getLogger('Contract');
const util = require('util');

/**
* Ensure transaction name is a non-empty string.
* @private
* @param {*} name Transaction name.
* @throws {Error} if the name is invalid.
*/
function verifyTransactionName(name) {
if(typeof name !== 'string' || name.length === 0) {
const msg = util.format('Transaction name must be a non-empty string: %j', name);
logger.error('verifyTransactionName:', msg);
throw new Error(msg);
}
}

/**
* Ensure that, if a namespace is defined, it is a non-empty string
* @private
* @param {*} namespace Transaction namespace.
* @throws {Error} if the namespace is invalid.
*/
function verifyNamespace(namespace) {
if(namespace && typeof namespace !== 'string') {
const msg = util.format('Namespace must be a non-empty string: %j', namespace);
logger.error('verifyNamespace:', msg);
throw new Error(msg);
}
}

/**
* Represents a smart contract (chaincode) instance in a network.
* Applications should get a Contract instance using the
* networks's [getContract]{@link Network#getContract} method.
*/
class Contract {

constructor(network, chaincodeId, gateway, queryHandler, namespace='') {
constructor(network, chaincodeId, gateway, queryHandler, namespace) {
logger.debug('in Contract constructor');

verifyNamespace(namespace);

this.network = network;
this.channel = network.getChannel();
this.chaincodeId = chaincodeId;
this.gateway = gateway;
this.queryHandler = queryHandler;
this.eventHandlerOptions = gateway.getOptions().eventHandlerOptions;
this.namespace = namespace;
}

/**
* Check for proposal response errors.
* @private
* @param {any} responses the responses from the install, instantiate or invoke
* @return {Object} number of ignored errors and valid responses
* @throws if there are no valid responses at all.
*/
_validatePeerResponses(responses) {
logger.debug('in _validatePeerResponses');
getNetwork() {
return this.network;
}

if (!responses.length) {
logger.error('_validatePeerResponses: No results were returned from the request');
throw new Error('No results were returned from the request');
}
createTransactionID() {
return this.gateway.getClient().newTransactionID();
}

const validResponses = [];
const invalidResponses = [];
const invalidResponseMsgs = [];

responses.forEach((responseContent) => {
if (responseContent instanceof Error) {

// this is either an error from the sdk, peer response or chaincode response.
// we can distinguish between sdk vs peer/chaincode by the isProposalResponse flag in the future.
// TODO: would be handy to know which peer the response is from and include it here.
const warning = util.format('Response from attempted peer comms was an error: %j', responseContent);
logger.warn('_validatePeerResponses: ' + warning);
invalidResponseMsgs.push(warning);
invalidResponses.push(responseContent);
} else {

// anything else is a successful response ie status will be less the 400.
// in the future we can do things like verifyProposalResponse and compareProposalResponseResults
// as part of an extended client side validation strategy but for now don't perform any client
// side checks as the peers will have to do this anyway and it impacts client performance
validResponses.push(responseContent);
}
});

if (validResponses.length === 0) {
const errorMessages = [ 'No valid responses from any peers.' ];
invalidResponseMsgs.forEach(invalidResponse => errorMessages.push(invalidResponse));
const msg = errorMessages.join('\n');
logger.error('_validatePeerResponses: ' + msg);
throw new Error(msg);
}
getChaincodeId() {
return this.chaincodeId;
}

return {validResponses, invalidResponses};
getEventHandlerOptions() {
return this.gateway.getOptions().eventHandlerOptions;
}

/**
* 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
* for committing to the ledger.
* @param {string} transactionName Transaction function name
* @param {...string} parameters Transaction function parameters
* @returns {Buffer} Payload response from the transaction function
* @param {string} name Transaction function name
*/
async submitTransaction(transactionName, ...parameters) {
logger.debug('in submitTransaction: ' + transactionName);

// form the transaction name with the namespace
const fullTxName = (this.namespace==='') ? transactionName : `${this.namespace}:${transactionName}`;
createTransaction(name) {
verifyTransactionName(name);

this._verifyTransactionDetails('submitTransaction', fullTxName, parameters);
const qualifiedName = this._getQualifiedName(name);
const transaction = new Transaction(this, qualifiedName);

const txId = this.gateway.getClient().newTransactionID();
const eventHandler = this._createTxEventHandler(txId.getTransactionID());

// Submit the transaction to the endorsers.
const request = {
chaincodeId: this.chaincodeId,
txId,
fcn: fullTxName,
args: parameters
};

// node sdk will target all peers on the channel that are endorsingPeer or do something special for a discovery environment
const results = await this.channel.sendTransactionProposal(request);
const proposalResponses = results[0];
const proposal = results[1];

// get only the valid responses to submit to the orderer
const {validResponses} = this._validatePeerResponses(proposalResponses);

eventHandler && (await eventHandler.startListening());

// Submit the endorsed transaction to the primary orderers.
const response = await this.channel.sendTransaction({
proposalResponses: validResponses,
proposal
});

if (response.status !== 'SUCCESS') {
const msg = util.format('Failed to send peer responses for transaction \'%j\' to orderer. Response status: %j', txId.getTransactionID(), response.status);
logger.error('submitTransaction:', msg);
eventHandler && eventHandler.cancelListening();
throw new Error(msg);
const eventHandlerStrategy = this.getEventHandlerOptions().strategy;
if (eventHandlerStrategy) {
transaction.setEventHandlerStrategy(eventHandlerStrategy);
}

eventHandler && (await eventHandler.waitForEvents());

// return the payload from the invoked chaincode
let result = null;
if (validResponses[0].response.payload && validResponses[0].response.payload.length > 0) {
result = validResponses[0].response.payload;
}
return result;
return transaction;
}

/**
* Create an appropriate transaction event handler, if one is configured.
* @private
* @param {String} transactionId The transation ID to listen for.
* @returns {TransactionEventHandler|null} A transactionevent handler, or null if non is configured.
*/
_createTxEventHandler(transactionId) {
const eventHandlerFactoryFn = this.eventHandlerOptions.strategy;
if (eventHandlerFactoryFn) {
return eventHandlerFactoryFn(transactionId, this.network, this.eventHandlerOptions);
} else {
return null;
}
_getQualifiedName(name) {
return (this.namespace ? `${this.namespace}:${name}` : name);
}

/**
* Verify the supplied transaction details.
* @private
* @param {String} methodName Requesting method name, used for logging.
* @param {String} transactionName Name of a transaction.
* @param {String[]} parameters transaction parameters.
* @throws {Error} if the details are not acceptable.
*/
_verifyTransactionDetails(methodName, transactionName, parameters) {
this._verifyTransactionName(methodName, transactionName);
this._verifyTransactionParameters(methodName, parameters);
}

/**
* Ensure a supplied transaction name is valid.
* @private
* @param {String} methodName Requesting method name, used for logging.
* @param {String} transactionName Name of a transaction.
* @throws {Error} if the name is not valid.
*/
_verifyTransactionName(methodName, transactionName) {
if(typeof transactionName !== 'string' || transactionName.length === 0) {
const msg = util.format('Transaction name must be a non-empty string: %j', transactionName);
logger.error(methodName + ':', msg);
throw new Error(msg);
}
}

/**
* Ensure supplied transaction parameters are valid.
* @private
* @param {String} methodName Requesting method name, used for logging.
* @param {String[]} parameters transaction parameters.
* @throws {Error} if any parameters are invalid.
*/
_verifyTransactionParameters(methodName, parameters) {
const invalidParameters = parameters.filter((parameter) => typeof parameter !== 'string');
if (invalidParameters.length > 0) {
const invalidParamString = invalidParameters
.map((parameter) => util.format('%j', parameter))
.join(', ');
const msg = 'Transaction parameters must be strings: ' + invalidParamString;
logger.error(methodName + ':', msg);
throw new Error(msg);
}
* Submit a transaction to the ledger. The transaction function <code>name</code>
* will be evaluated on the endorsing peers and then submitted to the ordering service
* for committing to the ledger.
* @async
* @param {string} name Transaction function name.
* @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);
}

/**
Expand All @@ -206,17 +119,18 @@ class Contract {
* will be evaluated on the endorsing peers but the responses will not be sent to to
* the ordering service and hence will not be committed to the ledger.
* This is used for querying the world state.
* @param {string} transactionName Transaction function name
* @param {string} name Transaction function name
* @param {...string} parameters Transaction function parameters
* @returns {Buffer} Payload response from the transaction function
*/
async evaluateTransaction(transactionName, ...parameters) {
async evaluateTransaction(name, ...parameters) {
verifyTransactionName(name);
Transaction.verifyArguments(parameters);

// form the transaction name with the namespace
const fullTxName = (this.namespace==='') ? transactionName : `${this.namespace}:${transactionName}`;
this._verifyTransactionDetails('evaluateTransaction', fullTxName, parameters);
const qualifiedName = this._getQualifiedName(name);
const txId = this.gateway.getClient().newTransactionID();
const result = await this.queryHandler.queryChaincode(this.chaincodeId, txId, fullTxName, parameters);
const result = await this.queryHandler.queryChaincode(this.chaincodeId, txId, qualifiedName, parameters);
return result ? result : null;
}
}
Expand Down
Loading

0 comments on commit 967eee1

Please sign in to comment.