Skip to content

Commit

Permalink
[FABN-1142] Node sdk: add token support
Browse files Browse the repository at this point in the history
- Add TokenClient class to provide APIs for token operations
- Add token-utils.js for helper methods
- Create a new TokenClient in Client.js
- Update Channel.js to send token command and transaction
- Update Peer.js to send grpc request to a prover service
- Define proverPeer role in Constants.js
- Implement unit tests

Change-Id: I1c3dc96b1f4383ea62b262eb66d85f40361a818d
Signed-off-by: Wenjian Qiao <[email protected]>
  • Loading branch information
wenjianqiao committed Feb 20, 2019
1 parent 46080b7 commit e1af608
Show file tree
Hide file tree
Showing 12 changed files with 2,567 additions and 1 deletion.
204 changes: 204 additions & 0 deletions fabric-client/lib/Channel.js
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const sdk_utils = require('./utils.js');
const client_utils = require('./client-utils.js');
const token_utils = require('./token-utils.js');
const util = require('util');
const Peer = require('./Peer.js');
const ChannelEventHub = require('./ChannelEventHub.js');
Expand Down Expand Up @@ -110,6 +111,7 @@ const Channel = class {
this._as_localhost = sdk_utils.getConfigSetting('discovery-as-localhost', true);
this._endorsement_handler = null;
this._commit_handler = null;
this._prover_handler = null;

logger.debug('Constructed Channel instance: name - %s, network mode: %s', this._name, !this._devMode);
}
Expand Down Expand Up @@ -179,6 +181,7 @@ const Channel = class {

let endorsement_handler_path = null;
let commit_handler_path = null;
let prover_handler_path = null;

if (request) {
if (request.configUpdate) {
Expand Down Expand Up @@ -211,12 +214,17 @@ const Channel = class {
logger.debug('%s - user requested commitHandler %s', method, request.commitHandler);
commit_handler_path = request.commitHandler;
}
if (request.proverHandler) {
logger.debug('%s - user requested proverHandler %s', method, request.proverHandler);
prover_handler_path = request.proverHandler;
}
}
}

// setup the handlers
this._endorsement_handler = await this._build_handler(endorsement_handler_path, 'endorsement-handler');
this._commit_handler = await this._build_handler(commit_handler_path, 'commit-handler');
this._prover_handler = await this._build_handler(prover_handler_path, 'prover-handler');

let results = null;
try {
Expand Down Expand Up @@ -3254,6 +3262,202 @@ const Channel = class {
const signer = clientContext._getSigningIdentity(use_admin_signer);
return client_utils.toEnvelope(client_utils.signProposal(signer, payload));
}

/**
* Sends a token command to a prover peer and returns a [CommandResponse] {@link CommandResponse}
* that contains either a token transaction or unspent tokens depending on the command.
*
* If multiple prover peers are configured, the function will send to one peer
* at a time. If a peer returns the response, it will return the response
* and skip the remaining peers.
*
* @param {TokenRequest} request - Required. A [TokenRequest]{@link TokenClient#TokenRequest}.
* @param {Number} timeout - Optional. A number indicating milliseconds to wait on the
* response before rejecting the promise with a timeout error. This
* overrides the default timeout of the Peer instance and the global
* timeout in the config settings.
* @returns {Promise} A Promise for the {@link CommandResponse}
*/
async sendTokenCommand(request, timeout) {
const method = 'sendTokenCommand';
logger.debug('%s - start', method);

if (!request) {
throw Error(util.format('Missing required "request" parameter on the %s call', method));
}
if (!request.txId) {
throw new Error(util.format('Missing required "txId" in request on the %s call', method));
}
if (!request.tokenCommand) {
throw new Error(util.format('Missing required "tokenCommand" in request on the %s call', method));
}

// convert any names into peer objects or if empty find all
// prover peers added to this channel
const targetPeers = this._getTargets(request.targets, Constants.NetworkConfig.PROVER_PEER_ROLE);
logger.debug('sendTokenCommand, number targetPeers=%d', targetPeers.length);

if (this._prover_handler) {
logger.debug('%s - running with prover handler', method);
const signedCommand = Channel._buildSignedTokenCommand(request, this._name, this._clientContext);

const params = {
request: request,
signed_command: signedCommand,
timeout: timeout,
};

const response = await this._prover_handler.processCommand(params);
return response;
} else {
return Channel.sendTokenCommand(request, targetPeers, this._name, this._clientContext, timeout);
}
}

/*
* Internal static method to allow token command to be sent without
* creating a new channel
*/
static async sendTokenCommand(request, targets, channelId, client_context, timeout) {
const method = 'sendTokenCommand(static)';
logger.debug('%s - start', method);

const signedCommand = Channel._buildSignedTokenCommand(request, channelId, client_context);
const responses = await token_utils.sendTokenCommandToPeer(targets, signedCommand, timeout);
return responses;
}

/*
* Internal static method to create header and sign the token command
* without creating a channel.
*/
static _buildSignedTokenCommand(request, channelId, client_context) {
const method = '_buildSignedTokenCommand';
logger.debug('%s - start, channelId %s', method, channelId);

let signer = null;
if (request.signer) {
signer = request.signer;
} else {
signer = client_context._getSigningIdentity(request.txId.isAdmin());
}

const header = token_utils.buildTokenCommandHeader(
signer, channelId, request.txId.getNonce(), client_context.getClientCertHash());

// copy tokenCommand to a local variable so that we can update it
const command = Object.assign(request.tokenCommand);
command.setHeader(header);
logger.debug('%s - before signCommand', method);
const signed_command = token_utils.signCommand(signer, command);

return signed_command;
}

/**
* @typedef {Object} TokenTransactionRequest
* This object contains properties that will be used for broadcasting token transaction.
* @property {TokenTransaction} tokenTransaction - Required. The response from sendTokenCommand.
* @property {TransactionID} txId - Required. Transaction ID to use for this request.
* @property {Orderer | string} orderer - Optional. The orderer that will receive this request,
* when not provided, the transaction will be sent to the orderers assigned to this channel instance.
*/

/**
* Send the token transaction to the orderer for further processing. When the committing peers
* successfully validate the transactions, it will mark the transaction as valid inside
* the block. After all transactions in a block have been validated, and marked either as
* valid or invalid (with a [reason code]{@link https://github.com/hyperledger/fabric/blob/v1.0.0/protos/peer/transaction.proto#L125}),
* the block will be appended (committed) to the channel's ledger on the peer.
* <br><br>
* The caller of this method must use the response returned from the
* [sendTokenCommand()]{@link Channel#sendTokenCommand}.
*
* @param {TokenTransactionRequest} request - {@link TokenTransactionRequest}
* @param {Number} timeout - A number indicating milliseconds to wait on the
* response before rejecting the promise with a timeout error. This
* overrides the default timeout of the Orderer instance and the global
* timeout in the config settings.
* @returns {Promise} A Promise for a "BroadcastResponse" message returned by
* the orderer that contains a single "status" field for a
* standard [HTTP response code]{@link https://github.com/hyperledger/fabric/blob/v1.0.0/protos/common/common.proto#L27}.
* submitted transaction.
* This will be an acknowledgement from the orderer of a successfully
* submitted transaction.
*/
async sendTokenTransaction(request, timeout) {
const method = 'sendTokenTransaction';
logger.debug('%s - start', method);

if (!request) {
throw Error(util.format('Missing required "request" parameter on the %s call', method));
}
if (!request.txId) {
throw new Error(util.format('Missing required "txId" in request on the %s call', method));
}
if (!request.tokenTransaction) {
throw new Error(util.format('Missing required "tokenTransaction" in request on the %s call', method));
}

const isAdmin = request.txId.isAdmin();
const signer = this._clientContext._getSigningIdentity(isAdmin);
const envelope = Channel._buildTokenTxEnvelope(request, this._name, this._clientContext, signer, isAdmin);

if (this._commit_handler) {
// protect the users input
const param_request = Object.assign({}, request);
if (param_request.orderer) {
// check and convert to orderer object if a name
param_request.orderer = this._clientContext.getTargetOrderer(param_request.orderer, this.getOrderers(), this._name);
}
const params = {
signed_envelope: envelope,
request: param_request,
timeout: timeout
};
return this._commit_handler.commit(params);
} else {
const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name);
return orderer.sendBroadcast(envelope, timeout);
}
}

/*
* Internal static method to allow transaction envelope to be built without
* creating a new channel
*/
static _buildTokenTxEnvelope(request, channelId, clientContext, signer, use_admin_signer) {
const txId = request.txId;
const channel_header = client_utils.buildChannelHeader(
fabprotos.common.HeaderType.TOKEN_TRANSACTION,
channelId,
txId.getTransactionID(),
null, // no epoch
'',
client_utils.buildCurrentTimestamp(),
clientContext.getClientCertHash()
);

const signature_header = new fabprotos.common.SignatureHeader();
signature_header.setCreator(signer.serialize());
signature_header.setNonce(txId.getNonce());

const header = new fabprotos.common.Header();
header.setChannelHeader(channel_header.toBuffer());
header.setSignatureHeader(signature_header.toBuffer());

const payload = new fabprotos.common.Payload();
payload.setHeader(header);
payload.setData(request.tokenTransaction.toBuffer());
const payloadBytes = payload.toBuffer();
const signature = Buffer.from(signer.sign(payloadBytes));

const envelope = new fabprotos.common.Envelope();
envelope.setPayload(payloadBytes);
envelope.setSignature(signature);
return envelope;
}

/**
* @typedef {Object} ChaincodeQueryRequest
* @property {Peer[]} targets - Optional. The peers that will receive this
Expand Down
19 changes: 19 additions & 0 deletions fabric-client/lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const Package = require('./Package.js');
const Peer = require('./Peer.js');
const ChannelEventHub = require('./ChannelEventHub');
const Orderer = require('./Orderer.js');
const TokenClient = require('./TokenClient.js');
const TransactionID = require('./TransactionID.js');
const crypto = require('crypto');

Expand Down Expand Up @@ -282,6 +283,24 @@ const Client = class extends BaseClient {
return chaincode;
}

/**
* Returns a {@link TokenClient} instance for the specific channel and targets.
* The TokenClient provides APIs for applications to perform token functions.
*
* @param {Channel} channel - Required. A channel object.
* @param {Peer[]/string[]} targets - Optional. One or multiple prover peers.
* @returns {TokenClient} a new instance of {@link TokenClient}.
*/
newTokenClient(channel, targets) {
if (!channel) {
throw new Error('Missing required "channel" parameter on newTokenClient() call');
}

const tokenClient = new TokenClient(this, channel, targets);

return tokenClient;
}

/**
* Returns a {@link Channel} instance with the given name. This represents a channel and its associated ledger.
*
Expand Down
2 changes: 2 additions & 0 deletions fabric-client/lib/Constants.js
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ module.exports.NetworkConfig.CHAINCODE_QUERY_ROLE = 'chaincodeQuery';
module.exports.NetworkConfig.LEDGER_QUERY_ROLE = 'ledgerQuery';
module.exports.NetworkConfig.EVENT_SOURCE_ROLE = 'eventSource';
module.exports.NetworkConfig.DISCOVERY_ROLE = 'discover';
module.exports.NetworkConfig.PROVER_PEER_ROLE = 'proverPeer';
module.exports.NetworkConfig.ALL_ROLES = 'all';
module.exports.NetworkConfig.ROLES = [
module.exports.NetworkConfig.ENDORSING_PEER_ROLE,
module.exports.NetworkConfig.CHAINCODE_QUERY_ROLE,
module.exports.NetworkConfig.LEDGER_QUERY_ROLE,
module.exports.NetworkConfig.EVENT_SOURCE_ROLE,
module.exports.NetworkConfig.DISCOVERY_ROLE,
module.exports.NetworkConfig.PROVER_PEER_ROLE,
module.exports.NetworkConfig.ALL_ROLES
];
68 changes: 68 additions & 0 deletions fabric-client/lib/Peer.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Peer extends Remote {
logger.debug('Peer.const - url: %s timeout: %s name:%s', url, this._request_timeout, this.getName());
this._endorserClient = null;
this._discoveryClient = null;
this._proverClient = null;
this._createClients();
}

Expand All @@ -62,6 +63,10 @@ class Peer extends Remote {
logger.debug('_createClients - create peer discovery connection ' + this._endpoint.addr);
this._discoveryClient = new fabprotos.discovery.Discovery(this._endpoint.addr, this._endpoint.creds, this._options);
}
if (!this._proverClient) {
logger.debug('_createClients - create peer prover connection ' + this._endpoint.addr);
this._proverClient = new fabprotos.token.Prover(this._endpoint.addr, this._endpoint.creds, this._options);
}
}

/**
Expand All @@ -78,6 +83,11 @@ class Peer extends Remote {
this._discoveryClient.close();
this._discoveryClient = null;
}
if (this._proverClient) {
logger.debug('close - closing peer prover connection ' + this._endpoint.addr);
this._proverClient.close();
this._proverClient = null;
}
}

/**
Expand Down Expand Up @@ -155,6 +165,64 @@ class Peer extends Remote {
});
}

/**
* Send a token command to a prover peer.
*
* @param {Command} tokenCommand - A protobuf message for the token operation
* @param {Number} timeout - A number indicating milliseconds to wait on the
* response before rejecting the promise with a timeout error. This
* overrides the default timeout of the Peer instance and the global
* timeout in the config settings.
* @returns {Promise} A Promise for a {@link SignedCommandResponse}
*/
async sendTokenCommand(command, timeout) {
const method = 'sendTokenCommand';
logger.debug('%s - Start ----%s %s', method, this.getName(), this.getUrl());
const self = this;
let rto = self._request_timeout;

if (!command) {
throw new Error('Missing command parameter to send to peer');
}
if (typeof timeout === 'number') {
rto = timeout;
}

this._createClients();

await this.waitForReady(this._proverClient);

return new Promise((resolve, reject) => {
const send_timeout = setTimeout(() => {
clearTimeout(send_timeout);
logger.error('%s - timed out after:%s', method, rto);
return reject(new Error('REQUEST_TIMEOUT'));
}, rto);

self._proverClient.processCommand(command, (err, signedCommandResp) => {
clearTimeout(send_timeout);
if (err) {
logger.error('%s - Received error %s from peer %s', method, err, self._url);
if (err instanceof Error) {
reject(err);
} else {
reject(new Error(err));
}
} else {
if (signedCommandResp) {
logger.debug('%s - Received signed command response %s from peer "%s"', method, signedCommandResp, self._url);
resolve(signedCommandResp);
} else {
const return_error = new Error(util.format('GRPC client got a null or undefined response from the peer "%s".', self._url));
return_error.peer = self.getCharacteristics();
logger.error('%s - rejecting with error response: %s', method, return_error);
reject(return_error);
}
}
});
});
}

/**
* Send an discovery request to this peer.
*
Expand Down
Loading

0 comments on commit e1af608

Please sign in to comment.