Skip to content

Commit

Permalink
Fixed sendDeploymentProposal() promise chaining issue
Browse files Browse the repository at this point in the history
Due to promise chaining, the data returned by Peer.sendProposal() was
not properly returned to calling code, causing the promise chaining
to be broken. Result is the sendDeploymentProposal() API returns
prematurely without any data.

The changeset also includes these changes:
    - more unit tests for this API
    - more debug logging
    - better inline documentations especially in explaining the "enroll"
    process with the CA

Change-Id: Ibbe4b54d8c9ff91db0c6a3e18960bdaefa5f0122
Signed-off-by: Jim Zhang <jzhang@us.ibm.com>
  • Loading branch information
jimthematrix committed Oct 26, 2016
1 parent 0dbf4a7 commit 7ba3992
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 157 deletions.
2 changes: 1 addition & 1 deletion build/tasks/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ var gulp = require('gulp');
var eslint = require('gulp-eslint');

gulp.task('lint', function () {
return gulp.src(['**/*.js', '!node_modules/**', '!docs/**'])
return gulp.src(['**/*.js', '!node_modules/**', '!docs/**', '!coverage/**', '!tmp/**'])
.pipe(eslint(
{
env: ['es6', 'node'],
Expand Down
144 changes: 83 additions & 61 deletions lib/Member.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ var Member = class {

return new Promise(function(resolve, reject) {
var enrollment = self._enrollment;
if (enrollment) {
return resolve(enrollment);
if (self.isEnrolled()) {
return resolve(self.getEnrollment());
} else {
var req = {
enrollmentID: self.getName(),
Expand All @@ -237,13 +237,16 @@ var Member = class {
self._enrollment.queryStateKey = self._chain.cryptoPrimitives.generateNonce();

// Save state
return self.saveState();
return self.saveState()
.then(function() {
return resolve(enrollment);
});
}
).then(
function(data) {
function(enrollment) {
// Unmarshall chain key
// TODO: during restore, unmarshall enrollment.chainKey
var ecdsaChainKey = self._chain.cryptoPrimitives.ecdsaPEMToPublicKey(self._enrollment.chainKey);
// TODO: during restore, unmarshall enrollment.chainEncryptionKey
var ecdsaChainKey = self._chain.cryptoPrimitives.getPublicKeyFromPEM(self._enrollment.chainEncryptionKey);
self._enrollment.enrollChainKey = ecdsaChainKey;

return resolve(enrollment);
Expand Down Expand Up @@ -361,50 +364,62 @@ var Member = class {
* @returns Promise for a ProposalResponse
*/
sendDeploymentProposal(request) {
// Verify that chaincodePath is being passed
if (!request.endorserUrl || request.endorserUrl === '') {
logger.error('Invalid input parameter to "sendDeploymentProposal": must have "endorserUrl"');
return Promise.reject(new Error('missing endorserUrl in Deployment proposal request'));
}

if (!request.chaincodePath || request.chaincodePath === '') {
logger.error('Invalid input parameter to "sendDeploymentProposal": must have "chaincodePath"');
return Promise.reject(new Error('missing chaincodePath in Deployment proposal request'));
}

return new Promise(
function(resolve, reject) {
packageChaincode(request.chaincodePath, request.fcn, request.args)
.then(
function(data) {
var targzFilePath = data[0];
var hash = data[1];
if (!request.fcn || request.fnc === '') {
logger.error('Invalid input parameter to "sendDeploymentProposal": must have "fcn" for the target function to call during chaincode initialization');
return Promise.reject(new Error('missing fcn in Deployment proposal request'));
}

logger.debug('Successfully generated chaincode deploy archive and name hash (%s)', hash);
// args is optional because some chaincode may not need any input parameters during initialization
if (!request.args) {
request.args = [];
}

// at this point, the targzFile has been successfully generated
return packageChaincode(request.chaincodePath, request.fcn, request.args)
.then(
function(data) {
var targzFilePath = data[0];
var hash = data[1];

// step 1: construct a ChaincodeSpec
var args = [];
args.push(Buffer.from(request.fcn ? request.fcn : 'init', 'utf8'));
logger.debug('Successfully generated chaincode deploy archive and name hash (%s)', hash);

for (let i=0; i<request.args.length; i++)
args.push(Buffer.from(request.args[i], 'utf8'));
// at this point, the targzFile has been successfully generated

let ccSpec = {
type: _ccProto.ChaincodeSpec.Type.GOLANG,
chaincodeID: {
name: hash
},
ctorMsg: {
args: args
}
};
// step 1: construct a ChaincodeSpec
var args = [];
args.push(Buffer.from(request.fcn ? request.fcn : 'init', 'utf8'));

// // step 2: construct the ChaincodeDeploymentSpec
let chaincodeDeploymentSpec = new _ccProto.ChaincodeDeploymentSpec();
chaincodeDeploymentSpec.setChaincodeSpec(ccSpec);
for (let i=0; i<request.args.length; i++)
args.push(Buffer.from(request.args[i], 'utf8'));

fs.readFile(targzFilePath, function(err, data) {
if(err) {
reject(new Error(util.format('Error reading deployment archive [%s]: %s', targzFilePath, err)));
}
let ccSpec = {
type: _ccProto.ChaincodeSpec.Type.GOLANG,
chaincodeID: {
name: hash
},
ctorMsg: {
args: args
}
};

// // step 2: construct the ChaincodeDeploymentSpec
let chaincodeDeploymentSpec = new _ccProto.ChaincodeDeploymentSpec();
chaincodeDeploymentSpec.setChaincodeSpec(ccSpec);

return new Promise(function(resolve, reject) {
fs.readFile(targzFilePath, function(err, data) {
if(err) {
reject(new Error(util.format('Error reading deployment archive [%s]: %s', targzFilePath, err)));
} else {
chaincodeDeploymentSpec.setCodePackage(data);

let lcccSpec = {
Expand All @@ -428,13 +443,19 @@ var Member = class {
};

let peer = new Peer(request.endorserUrl);
return peer.sendProposal(proposal);
});
},
function(err) {
reject(err);
}
);
return peer.sendProposal(proposal)
.then(
function(status) {
resolve(status);
}
);
}
});
});
}
).catch(
function(err) {
return Promise.reject(err);
}
);
}
Expand Down Expand Up @@ -493,10 +514,10 @@ function packageChaincode(chaincodePath, fcn, args) {
hash = sdkUtils.generateDirectoryHash(goPath + '/src/', chaincodePath, hash);

// Compose the Dockerfile commands
let dockerFileContents = 'from hyperledger/fabric-baseimage\n' +
let dockerFileContents = 'from hyperledger/fabric-ccenv\n' +
'COPY . $GOPATH/src/build-chaincode/\n' +
'WORKDIR $GOPATH\n\n' +
'RUN go install build-chaincode && cp src/build-chaincode/vendor/github.com/hyperledger/fabric/peer/core.yaml $GOPATH/bin && mv $GOPATH/bin/build-chaincode $GOPATH/bin/%s';
'RUN go install build-chaincode && mv $GOPATH/bin/build-chaincode $GOPATH/bin/%s';

// Substitute the hashStrHash for the image name
dockerFileContents = util.format(dockerFileContents, hash);
Expand All @@ -506,22 +527,23 @@ function packageChaincode(chaincodePath, fcn, args) {
let dockerFilePath = projDir + '/Dockerfile';
fs.writeFile(dockerFilePath, dockerFileContents, function(err) {
if (err) {
return reject(new Error(util.format('Error writing file [%s]: %s', dockerFilePath, err)));
reject(new Error(util.format('Error writing file [%s]: %s', dockerFilePath, err)));
} else {
// Create the .tar.gz file of the chaincode package
let targzFilePath = '/tmp/deployment-package.tar.gz';
// Create the compressed archive
return sdkUtils.generateTarGz(projDir, targzFilePath)
.then(
function(targzFilePath) {
// return both the hash and the tar.gz file path as resolved data
resolve([targzFilePath, hash]);
}
).catch(
function(err) {
reject(err);
}
);
}

// Create the .tar.gz file of the chaincode package
let targzFilePath = '/tmp/deployment-package.tar.gz';
// Create the compressed archive
sdkUtils.generateTarGz(projDir, targzFilePath)
.then(
function(targzFilePath) {
// return both the hash and the tar.gz file path as resolved data
return resolve([targzFilePath, hash]);
},
function(err) {
return reject(err);
}
);
});
});
}
Expand Down
13 changes: 9 additions & 4 deletions lib/Peer.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ var Peer = class {
// The rpc specification on the peer side is:
// rpc ProcessProposal(Proposal) returns (ProposalResponse) {}
return new Promise(function(resolve, reject) {
self._endorserClient.processProposal(proposal, function (err, response) {
self._endorserClient.processProposal(proposal, function(err, response) {
if (err) {
reject(new Error(err));
} else {
if (response) {
logger.info('Received proposal response: code - %s', JSON.stringify(response.response.status));
resolve(response.response.status);
} else {
logger.error('GRPC client failed to get a proper response from the peer.');
reject(new Error('GRPC client failed to get a proper response from the peer.'));
}
}

logger.info('Received proposal response: code - %s', JSON.stringify(response.response.status));
resolve(response.response.status);
});
});
}
Expand Down
35 changes: 27 additions & 8 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,15 @@ module.exports.CryptoSuite = class {
*/
sign(key, msg) {}

/**
* Loads a public key object from a PEM encoded certificate
*
* @param {string} pem HEX encoded string of a PEM certificate
* @returns {Object} public key object specific to the algorithm
*/
getPublicKeyFromPEM(pem) {}

ecdsaPrivateKeyToASN1(prvKeyHex /*string*/ ) {}
ecdsaPEMToPublicKey(chainKey) {}
eciesEncryptECDSA(ecdsaRecipientPublicKey, msg) {}
eciesKeyGen() {}
eciesEncrypt(recipientPublicKey, msg) {}
Expand All @@ -196,24 +202,25 @@ module.exports.CryptoSuite = class {
*/
module.exports.Enrollment = class {

constructor() {
constructor(key, cert, chainKey) {
/**
* @member {Buffer} key private key generated locally by the SDK
* @member {Buffer} privateKey private key generated locally by the SDK
* @memberof module:api.Enrollment.prototype
*/
this.key = null;
this.privateKey = key;

/**
* @member {string} cert certificate issued by member services after successful enrollment
* @member {string} certificate HEX encoded string for the certificate issued by member services after successful enrollment
* @memberof module:api.Enrollment.prototype
*/
this.cert = '';
this.certificate = cert;

/**
* @member {string} chainKey what's the best description for this?
* @member {string} chainEncryptionKey HEX encoded string for public encryption key used to optionally encrypt content
* sent to endorsers and validators
* @memberof module:api.Enrollment.prototype
*/
this.chainKey = '';
this.chainEncryptionKey = chainKey;
}
};

Expand Down Expand Up @@ -282,6 +289,18 @@ module.exports.MemberServices = class {
/**
* Exchange the one-time password, a.k.a the enrollment secrete, for enrollment certificate.
*
* User enrollment is a two step process.
* 1) Send a create Certificate request to the server with two public keys:
* - A signature key that will be used to verify the signature of the keys
* - An encryption key that will be used to encrypt the data
*
* In response to this request, server creates a challenge and sends it to the client after encrypting it with
* the encryption public key to ensure that the client is in possession of the encryption private key
*
* 2) Client decrypts the token returned by the server in step 1, adds it to the request, creates the signature
* with the signing private key and sends another Create Certificate request to the server. Server verifies that
* signature and decrypted token is correct, server sends enrollment certificates to the client.
*
* @param {Object} req Enrollment request with the following fields:
*
* enrollmentID: {string} The enrollment ID,
Expand Down
Loading

0 comments on commit 7ba3992

Please sign in to comment.