Skip to content

Commit

Permalink
[FABN-1148] Allow offline singer for token
Browse files Browse the repository at this point in the history
- Add the following APIs to TokenClient
  generateUnsignedTokenCommand
  generateUnsignedTokenTransaction
  sendSignedTokenCommand
  sendSignedTokenTransaction

Change-Id: I0efef872b05f46c6c7673b3dde1678b08d0ca1c5
Signed-off-by: Wenjian Qiao <[email protected]>
  • Loading branch information
wenjianqiao committed Feb 20, 2019
1 parent e1af608 commit e29bcd9
Show file tree
Hide file tree
Showing 5 changed files with 582 additions and 19 deletions.
79 changes: 79 additions & 0 deletions fabric-client/lib/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -3263,6 +3263,50 @@ const Channel = class {
return client_utils.toEnvelope(client_utils.signProposal(signer, payload));
}

/**
* Send signed token command to peer
*
* @param {SignedCommandRequest} request - Required. Signed token command
* that will be sent to peer. Must contain a signedCommand object.
* @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 Orderer instance and the global
* timeout in the config settings.
* @returns {Promise} A Promise for the {@link CommandResponse}
*/
static async sendSignedTokenCommand(request, timeout) {
const method = 'sendSignedTokenCommand';
logger.debug('%s - start', method);

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

const signedCommand = token_utils.toSignedCommand(request.signature, request.command_bytes);

if (this._prover_handler) {
const params = {
request: request,
signed_command: signedCommand,
timeout: timeout,
};

const response = await this._prover_handler.processCommand(params);
return response;
} else {
// 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);
return await token_utils.sendTokenCommandToPeer(targetPeers, signedCommand, timeout);
}
}

/**
* 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.
Expand Down Expand Up @@ -3354,6 +3398,41 @@ const Channel = class {
return signed_command;
}

/**
* send the signed token transaction
*
* @param {SignedTokenTransactionRequest} request - Required. The signed token transaction.
* Must contain 'signature' and 'tokentx_bytes' properties.
* @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 {BroadcastResponse} 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}.
* This will be an acknowledgement from the orderer of a successfully
* submitted transaction.
*/
async sendSignedTokenTransaction(request, timeout) {
const method = 'sendSignedTokenTransaction';
logger.debug('%s - start', method);

const signed_envelope = token_utils.toEnvelope(request.signature, request.tokentx_bytes);
if (this._commit_handler) {
const params = {
signed_envelope,
request: request,
timeout: timeout,
};

return this._commit_handler.commit(params);
} else {
// verify that we have an orderer configured
const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name);
return orderer.sendBroadcast(signed_envelope, timeout);
}
}

/**
* @typedef {Object} TokenTransactionRequest
* This object contains properties that will be used for broadcasting token transaction.
Expand Down
186 changes: 182 additions & 4 deletions fabric-client/lib/TokenClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@

'use strict';

const Channel = require('./Channel');
const TransactionID = require('./TransactionID');
const util = require('util');
const clientUtils = require('./client-utils.js');
const tokenUtils = require('./token-utils.js');
const utils = require('./utils.js');
const logger = utils.getLogger('TokenClient.js');
const fabprotos = require('fabric-protos');
const {Identity} = require('fabric-common');
const {HashPrimitives} = require('fabric-common');

/**
* @classdesc
Expand Down Expand Up @@ -179,7 +185,7 @@ const TokenClient = class {
An array of token ids that will be spent by the token command.
* @property {TokenParam[]} params - Required. One or multiple TokenParam
* for the token command.
* @property {TransactionID} txId - Required. Transaction ID to use for this request.
* @property {TransactionID} txId - Optional. Required for issue, transfer and redeem.
*/

/**
Expand All @@ -200,7 +206,7 @@ const TokenClient = class {
*/
async issue(request, timeout) {
logger.debug('issue - Start');
tokenUtils.checkTokenRequest(request, 'issue');
tokenUtils.checkTokenRequest(request, 'issue', true);

// copy request so that we can make update
const sendRequest = Object.assign({}, request);
Expand Down Expand Up @@ -228,7 +234,7 @@ const TokenClient = class {
*/
async transfer(request, timeout) {
logger.debug('transfer - Start');
tokenUtils.checkTokenRequest(request, 'transfer');
tokenUtils.checkTokenRequest(request, 'transfer', true);

// copy request so that we can make update
const sendRequest = Object.assign({}, request);
Expand All @@ -255,7 +261,7 @@ const TokenClient = class {
*/
async redeem(request, timeout) {
logger.debug('redeem - Start');
tokenUtils.checkTokenRequest(request, 'redeem');
tokenUtils.checkTokenRequest(request, 'redeem', true);

// copy request so that we can make update
const sendRequest = Object.assign({}, request);
Expand Down Expand Up @@ -328,6 +334,178 @@ const TokenClient = class {
const result = await this._channel.sendTokenTransaction(request, timeout);
return result;
}

/**
* @typedef {Object} SignedCommandRequest
* The object contains properties that will be used for sending a token command.
* @property {Buffer} command_bytes - Required. The token command bytes.
* @property {Buffer} signature - Required. The signer's signature for the command_bytes.
* @property {Peer[]|string[]} targets - Optional. The peers that will receive this
* request, when not provided the list of peers added to this channel
* object will be used.
*/

/**
* Sends signed token command to peer
*
* @param {SignedCommandRequest} request containing signedCommand and targets.
* The signed command would be sent to peer directly.
* @param {number} timeout the timeout setting passed on sendSignedCommand
* @returns {Promise} A Promise for a "CommandResponse" message returned by
* the prover peer.
*/
async sendSignedTokenCommand(request, timeout) {
// copy request to protect user input
const sendRequest = Object.assign({}, request);
if (!sendRequest.targets) {
sendRequest.targets = this._targets;
}
return Channel.sendSignedTokenCommand(sendRequest, timeout);
}

/**
* @typedef {Object} SignedTokenTransactionRequest
* This object contains properties that will be used for broadcasting a token transaction.
* @property {Buffer} signature - Required. The signature.
* @property {Buffer} payload_bytes - Required. The token transaction payload bytes.
* @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 signed token transaction
*
* @param {SignedTokenTransactionRequest} request - Required. The signed token transaction.
* Must contain 'signature' and 'tokentx_bytes' properties.
* @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 {BroadcastResponse} 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}.
* This will be an acknowledgement from the orderer of a successfully
* submitted transaction.
*/
async sendSignedTokenTransaction(request, timeout) {
return this._channel.sendSignedTokenTransaction(request, timeout);
}

/**
* Generates the unsigned token command
*
* Currently the [sendTokenCommand]{@link Channel#sendTokenCommand}
* signs a token command using the user identity from SDK's context (which
* contains the user's private key).
*
* This method is designed to build the token command at SDK side,
* and user can sign this token command with their private key, and send
* the signed token command to peer by [sendSignedTokenCommand]
* so the user's private key would not be required at SDK side.
*
* @param {TokenRequest} request token request
* @param {string} mspId the mspId for this identity
* @param {string} certificate PEM encoded certificate
* @param {boolean} admin if this transaction is invoked by admin
* @returns {Command} protobuf message for token command
*/
generateUnsignedTokenCommand(request, mspId, certificate, admin) {
const method = 'generateUnsignedTokenCommand';
logger.debug('%s - start', method);

logger.debug('%s - token command name is %s', method, request.commandName);
tokenUtils.checkTokenRequest(request, method, false);

let tokenCommand;
if (request.commandName === 'issue') {
tokenCommand = tokenUtils.buildIssueCommand(request);
} else if (request.commandName === 'transfer') {
tokenCommand = tokenUtils.buildTransferCommand(request);
} else if (request.commandName === 'redeem') {
tokenCommand = tokenUtils.buildRedeemCommand(request);
} else if (request.commandName === 'list') {
tokenCommand = tokenUtils.buildListCommand(request);
} else if (!request.commandName) {
throw new Error(utils.format('Missing commandName on the %s call', method));
} else {
throw new Error(utils.format('Invalid commandName (%s) on the %s call', request.commandName, method));
}

// create identity using certificate, publicKey (null), and mspId
const identity = new Identity(certificate, null, mspId);
const txId = new TransactionID(identity, admin);
const header = tokenUtils.buildTokenCommandHeader(
identity,
this._channel._name,
txId.getNonce(),
this._client.getClientCertHash()
);

tokenCommand.setHeader(header);
return tokenCommand;
}

/**
* @typedef {Object} TokenTransactionRequest
* This object contains properties that will be used build token transaction payload.
* @property {Buffer} tokentx_bytes - Required. The token transaction bytes.
* @property {Command} tokenCommand - Required. The token command that is used to receive the token transaction.
*/

/**
* Generates unsigned payload for the token transaction.
* The tokenCommand used to generate the token transaction must be passed in the request.
*
* @param {TokenTransactionRequest} request - required.
* @returns {Payload} protobuf message for token transaction payload
*/
generateUnsignedTokenTransaction(request) {
const method = 'generateUnsignedTokenTransaction';
logger.debug('%s - start', method);

if (!request) {
throw new Error(util.format('Missing input request parameter on the %s call', method));
}
if (!request.tokenTransaction) {
throw new Error(util.format('Missing required "tokenTransaction" 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));
}
if (!request.tokenCommand.header) {
throw new Error(util.format('Missing required "header" in tokenCommand on the %s call', method));
}

const commandHeader = request.tokenCommand.header;
const trans_bytes = Buffer.concat([commandHeader.nonce.toBuffer(), commandHeader.creator.toBuffer()]);
const trans_hash = HashPrimitives.SHA2_256(trans_bytes);
const txId = Buffer.from(trans_hash).toString();

const channel_header = clientUtils.buildChannelHeader(
fabprotos.common.HeaderType.TOKEN_TRANSACTION,
this._channel._name,
txId,
null, // no epoch
'',
clientUtils.buildCurrentTimestamp(),
this._client.getClientCertHash()
);

const signature_header = new fabprotos.common.SignatureHeader();
signature_header.setCreator(commandHeader.creator);
signature_header.setNonce(commandHeader.nonce);

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());

return payload;
}
};

module.exports = TokenClient;
36 changes: 31 additions & 5 deletions fabric-client/lib/token-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const utils = require('./utils.js');
const logger = utils.getLogger('token-utils.js');
const fabprotos = require('fabric-protos');

const valid_command_names = ['issue', 'transfer', 'redeem', 'list'];

/*
* This function will build a token command for issue request
*/
Expand Down Expand Up @@ -181,21 +183,31 @@ module.exports.buildTokenCommandHeader = (creator, channelId, nonce, tlsCertHash
};

/*
* Checks token request and throw an error if any required parameter is missing.
* Checks token request and throw an error if any required parameter is missing or invalid.
*/
module.exports.checkTokenRequest = (request, command_name) => {
module.exports.checkTokenRequest = (request, command_name, txIdRequired) => {
logger.debug('checkTokenRequest - start');

if (!request) {
logger.error('Missing required "request" parameter on %s call', command_name);
throw new Error(util.format('Missing required "request" parameter on %s call', command_name));
}
if (!request.txId) {
if (txIdRequired && !request.txId) {
logger.error('Missing required "txId" in request on %s call', command_name);
throw new Error(util.format('Missing required "txId" in request on %s call', command_name));
}
if (request.commandName !== undefined && request.commandName !== command_name) {
throw new Error(util.format('Wrong "commandName" in request on %s call: %s', command_name, request.commandName));
if (request.commandName !== undefined && !valid_command_names.includes(request.commandName)) {
throw new Error(util.format('Invalid "commandName" in request on %s call: %s', command_name, request.commandName));
}
if (!request.commandName && !valid_command_names.includes(command_name)) {
throw new Error(util.format('Missing "commandName" in request on %s call', command_name));
}
if (valid_command_names.includes(command_name) && request.commandName !== undefined && request.commandName !== command_name) {
throw new Error(util.format('Invalid "commandName" in request on %s call: %s', command_name, request.commandName));
}

if (request.commandName === 'list' || command_name === 'list') {
return;
}

// check parameters for non-list commands
Expand Down Expand Up @@ -230,3 +242,17 @@ module.exports.checkTokenRequest = (request, command_name) => {
}
});
};

/**
* convert to protos.token.SignedCommand
* @param signature
* @param command_bytes
*/
exports.toSignedCommand = (signature, command_bytes) => ({signature: signature, command: command_bytes});

/**
* convert to protos.common.Envelope
* @param signature
* @param tokentx_bytes
*/
exports.toEnvelope = (signature, tokentx_bytes) => ({signature: signature, payload: tokentx_bytes});
Loading

0 comments on commit e29bcd9

Please sign in to comment.