diff --git a/build/tasks/eslint.js b/build/tasks/eslint.js new file mode 100644 index 0000000000..997d794df8 --- /dev/null +++ b/build/tasks/eslint.js @@ -0,0 +1,23 @@ +var gulp = require('gulp'); +var eslint = require('gulp-eslint'); + +gulp.task('lint', function () { + return gulp.src(['**/*.js', '!node_modules/**', '!docs/**']) + .pipe(eslint( + { + env: ['es6', 'node'], + extends: 'eslint:recommended', + parserOptions: { + sourceType: 'module' + }, + rules: { + indent: ['error', 'tab'], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'] + } + } + )) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); diff --git a/gulpfile.js b/gulpfile.js index 193ca03a77..4ed9b2c4eb 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,32 +1,9 @@ var requireDir = require('require-dir'); +var gulp = require('gulp'); // Require all tasks in gulp/tasks, including subfolders requireDir('./build/tasks', { recurse: true }); -var gulp = require('gulp'); -var eslint = require('gulp-eslint'); - -gulp.task('lint', function () { - return gulp.src(['**/*.js', '!node_modules/**', '!docs/**']) - .pipe(eslint( - { - env: ['es6', 'node'], - extends: 'eslint:recommended', - parserOptions: { - sourceType: 'module' - }, - rules: { - indent: ['error', 'tab'], - 'linebreak-style': ['error', 'unix'], - quotes: ['error', 'single'], - semi: ['error', 'always'] - } - } - )) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()); -}); - gulp.task('default', ['lint'], function () { // This will only run if the lint task is successful... }); \ No newline at end of file diff --git a/lib/Member.js b/lib/Member.js index 70c5b55295..4f3c5862eb 100644 --- a/lib/Member.js +++ b/lib/Member.js @@ -18,7 +18,14 @@ var api = require('./api.js'); var util = require('util'); +var fs = require('fs'); var stats = require('./stats.js'); +var Peer = require('./Peer.js'); +var sdkUtils = require('./utils.js'); +var grpc = require('grpc'); + +var _ccProto = grpc.load(__dirname + '/protos/chaincode.proto').protos; +var _fabricProto = grpc.load(__dirname + '/protos/fabric_next.proto').protos; /** * Represents an authenticated user of the application or an entity used by a Peer node. @@ -310,6 +317,91 @@ var Member = class { }); } + /** + * Sends a deployment proposal to an endorser. + * + * @param {Object} request An object containing the following fields: + * endorserUrl + * chaincodePath + * fcn + * args + * @returns Promise for a ProposalResponse + */ + sendDeploymentProposal(request) { + // Verify that chaincodePath is being passed + if (!request.chaincodePath || request.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]; + + // at this point, the targzFile has been successfully generated + + // step 1: construct a ChaincodeSpec + var args = []; + args.push(Buffer.from(request.fcn ? request.fcn : 'init', 'utf8')); + + for (let i=0; i Message2 -> payload +// +// The flow is as follows +// SDK -[Proposal]-> Endorser -[ProposalResponse]-> SDK -[Transaction2]-> +// Ordering Service -[batch of Transaction2] -> Committer +// +// The SDK constructs a Proposal and sends it to one or more endorsers for +// endorsement +// +// Endorsers create an Action for the given proposal and include the Action +// in a ProposalResponse which is sent back to the SDK. The ProposalResponse +// also contains an Endorsement which is the signature of the Action. +// +// When an SDK receives a ProposalResponse from and Endorser, it must look +// at the proposalHash within the Action to correlate it with the +// Proposal that was sent. After correlation, an SDK should +// 1. Inspect the response within the PrposalResponse to confirm the +// endorsement of the proposal was successful +// 2. Confirm that the 'actionBytes' match for all ProposalResponses that +// correlate to the same proposal hash. +// +// At this point, the SDK can construct an EndorsedAction. This is simply a +// combination of the Action, all Endorsements, and the Proposal as an +// optional field. One or more EndorsedActions can then be added to a +// Transaction2. This Transaction2 is then submitted to the ordering service. +// All endorsed actions contained in a single transaction are committed +// atomically, meaning that either all actions in the transaction will be +// committed or none will be committed. + +// Protocol Buffer Limitation: +// Something important to understand about protocol buffers is that while +// serialization is deterministic in that it will always produce the same +// bytes, deserialization is non-deterministic. This creates the following +// issue. If a message is serialized and a hash is calculated based on the +// bytes, then the message is deserialized, serialized, and the hash is +// calculated again, the hashes may not match! To avoid this problem, you +// will notice cases where the bytes of a message are included in a field +// instead of the message itself. In locations where this is done, you will +// see the acronym NDD (non-deterministic serialization). + +// Envelope is used to deliver a message +message Envelope { + + // Signature of the message. + bytes signature = 1; + + // The message. + Message2 message = 2; + +} + +// A Message2 encapsulates a payload of the indicated type in this message. +message Message2 { + + enum Type { + + // Undefined exists to prevent invalid message construction. + UNDEFINED = 0; + + // Handshake messages. + DISCOVERY = 1; + + // Sent to catch up with existing peers. + SYNC = 2; + + // Sent from SDK to endorser. Payload is a Proposal. + PROPOSAL = 3; + + // Reserved for future use. + PROPOSAL_SET = 4; + + // Sent from endorser to SDK. Payload is a ProposalResponse. + PROPOSAL_RESULT = 5; + + // Reserved for future use. + PROPOSAL_SET_RESULT = 6; + + // Sent from SDK to peer for relay or ordering service. Payload is a + // Transaction2. + TRANSACTION = 7; + + } + + // Type of this message. + Type type = 1; + + // Version indicates message protocol version. + int32 version = 2; + + // Timestamp is the time that the message was created as defined by the + // sender. + google.protobuf.Timestamp timestamp = 3; + + // The payload in this message. + bytes payload = 4; + +} + +// A Proposal is sent to an endorser for endorsement. The proposal contains +// a payload (such as a ChaincodeSpec) based on the type field. +message Proposal { + + enum Type { + + // Undefined exists to prevent invalid message construction. + UNDEFINED = 0; + + // A chaincode. The payload is a ChaincodeSpec. + CHAINCODE = 1; + + } + + // Type of this message. + Type type = 1; + + // Unique ID corresponding to this proposal + string id = 2; + + // The payload of the proposal as defined by the proposal type. + bytes payload = 3; + +} + +// A response with a representation similar to an HTTP response that can +// be used within another message. +message Response2 { + + // A status code that should follow the HTTP status codes. + int32 status = 1; + + // A message associated with the response code. + string message = 2; + + // A payload that can be used to include metadata with this response. + bytes payload = 3; + +} + +// A SystemChaincode is a chaincode compiled into the peer that cannot +// be modified at runtime. These are used to perform critical system level +// functions, including processing endorsements and validating transactions. +message SystemChaincode { + + // The ID used to identify a system chaincode. + string id = 1; + +} + +// An action to be taken against the ledger. +message Action { + + // Hash of proposal encoded in the Message2 payload. NDD. + bytes proposalHash = 1; + + // Uncommitted state changes (simulated) as calculated by the endorser. + // This generally would include MVCC and PostImage information for both the + // read set and write set. This is to be forwarded to the ordering + // service as part of the transaction and must match the simulationResult + // returned by other endorsers for the proposal. + bytes simulationResult = 2; + + // Events that should be sent by committers after the transaction is written + // to the ledger. This is to be forwarded to the ordering + // service as part of the transaction and must match the events + // returned by other endorsers for the proposal. + repeated bytes events = 3; + + // ESCC (Endorser System Chaincode) is logic that is run prior to the + // ProposalResponse being returned to the SDK. It can manipulate the + // ProposalResponse as needed. + SystemChaincode escc = 4; + + // VSCC (Validaing System Chaincode) is logic that is run to transform the + // raw ledger into the validated ledger. + SystemChaincode vscc = 5; + +} + +// Endorsement is included within a proposal response. +message Endorsement { + + // Signature of the actionBytes included in the Endorsement. + bytes signature = 1; + +} + +// A ProposalResponse is returned from an endorser to the proposal submitter. +message ProposalResponse { + + // A response message indicating whether the endorsement of the action + // was successful. Additional metadata can be included. This will not + // be forwarded from the SDK to the ordering service. + Response2 response = 1; + + // A serialized Action message. NDD. + bytes actionBytes = 2; + + // The endorsement of the action included in the proposal response + Endorsement endorsement = 3; + +} + +// An EndorsedAction describes a single action endorsed by one or more +// endorsers. Multiple endorsed actions can be included in a single +// transaction. The transaction is atomic meaning that either all +// actions in the transaction will be committed or none will be committed. +message EndorsedAction { + + // The action to be taken against the ledger. This is generally constructed + // by an endorser. NDD. + bytes actionBytes = 1; + + // The endorsements of the action. + repeated Endorsement endorsements = 2; + + // The proposal. This is optional and only needed if the SDK wants to store + // the Proposal on the ledger as opposed to just the hash. The proposal is + // not included within the Action because it is the SDK's decision whether + // or not they would like to include this information in the Transaction2. + // If it was in the Action and signed, either the Endorsers would be + // required to make the decision or the SDK would need to provide a hint + // in the Proposal about whether it should be included in the Action. + // TODO Revisit this decision. + bytes proposalBytes = 3; + +} + +// The transaction to be sent to the ordering service. A transaction contains +// one or more endorsed actions. The transaction is atomic meaning that either +// all actions in the transaction will be committed or none will be committed. +message Transaction2 { + + // One or more endorsed actions to be committed to the ledger. + repeated EndorsedAction endorsedActions = 1; + +} + +// This is used to wrap an invalid Transaction with the cause +message InvalidTransaction { + enum Cause { + TxIdAlreadyExists = 0; + RWConflictDuringCommit = 1; + } + Transaction2 transaction = 1; + Cause cause = 2; +} + +// Block contains a list of transactions and the crypto hash of previous block +message Block2 { + bytes PreviousBlockHash = 1; + // transactions are stored in serialized form so that the concenters can avoid marshaling of transactions + repeated bytes Transactions = 2; +} + +service Endorser { + rpc ProcessProposal(Proposal) returns (ProposalResponse) {} +} diff --git a/lib/utils.js b/lib/utils.js index 6400aa7d0f..0d13a8246e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -171,7 +171,7 @@ module.exports.GenerateDirectoryHash = function(rootDir, chaincodeDir, hash) { // saves them in a dest file. // -module.exports.GenerateTarGz = function(src, dest, cb) { +module.exports.GenerateTarGz = function(src, dest) { debug('GenerateTarGz'); // A list of file extensions that should be packaged into the .tar.gz. @@ -185,39 +185,41 @@ module.exports.GenerateTarGz = function(src, dest, cb) { '.h' ]; - // Create the pack stream specifying the ignore/filtering function - var pack = tar.pack(src, { - ignore: function(name) { - // Check whether the entry is a file or a directory - if (fs.statSync(name).isDirectory()) { - // If the entry is a directory, keep it in order to examine it further - return false; - } else { - // If the entry is a file, check to see if it's the Dockerfile - if (name.indexOf('Dockerfile') > -1) { + return new Promise(function(resolve, reject) { + // Create the pack stream specifying the ignore/filtering function + var pack = tar.pack(src, { + ignore: function(name) { + // Check whether the entry is a file or a directory + if (fs.statSync(name).isDirectory()) { + // If the entry is a directory, keep it in order to examine it further return false; - } - - // If it is not the Dockerfile, check its extension - var ext = path.extname(name); - - // Ignore any file who's extension is not in the keep list - if (keep.indexOf(ext) === -1) { - return true; } else { - return false; + // If the entry is a file, check to see if it's the Dockerfile + if (name.indexOf('Dockerfile') > -1) { + return false; + } + + // If it is not the Dockerfile, check its extension + var ext = path.extname(name); + + // Ignore any file who's extension is not in the keep list + if (keep.indexOf(ext) === -1) { + return true; + } else { + return false; + } } } - } - }) - .pipe(zlib.Gzip()) - .pipe(fs.createWriteStream(dest)); - - pack.on('close', function() { - return cb(null); - }); - pack.on('error', function() { - return cb(Error('Error on fs.createWriteStream')); + }) + .pipe(zlib.Gzip()) + .pipe(fs.createWriteStream(dest)); + + pack.on('close', function() { + return resolve(dest); + }); + pack.on('error', function() { + return reject(new Error('Error on fs.createWriteStream')); + }); }); }; diff --git a/test/unit/endorser-tests.js b/test/unit/endorser-tests.js new file mode 100644 index 0000000000..e80bf99c8d --- /dev/null +++ b/test/unit/endorser-tests.js @@ -0,0 +1,98 @@ +/** + * Copyright 2016 IBM All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var tape = require('tape'); +var _test = require('tape-promise'); +var test = _test(tape); + +var hfc = require('../..'); +var util = require('util'); +var fs = require('fs'); + +var keyValStorePath = '/tmp/keyValStore'; + +// +// Run the endorser test +// +test('endorser test', function(t) { + // + // Create and configure the test chain + // + var chain = hfc.newChain('testChain'); + var expect = ''; + var found = ''; + var webUser; + + chain.setKeyValueStore(hfc.newKeyValueStore({ + path: keyValStorePath + })); + + chain.setMemberServicesUrl('grpc://localhost:7054'); + + chain.enroll('admin', 'Xurw3yU9zI0l') + .then( + function(admin) { + t.pass('Successfully enrolled user \'admin\''); + + // send proposal to endorser + var request = { + endorserUrl: 'grpc://localhost:7051', + chaincodePath: 'github.com/chaincode_example02', + fcn: 'init', + args: ['a', '100', 'b', '200'] + }; + + return admin.sendDeploymentProposal(request); + }, + function(err) { + t.fail('Failed to enroll user \'admin\'. ' + err); + t.end(); + } + ).then( + function(status) { + if (status === 200) { + t.pass('Successfully obtained endorsement.'); + } else { + t.fail('Failed to obtain endorsement. Error code: ' + status); + } + + t.end(); + }, + function(err) { + t.fail('Failed to send deployment proposal due to error: ' + err.stack ? err.stack : err); + t.end(); + } + ).catch( + function(err) { + t.fail('Failed to send deployment proposal. ' + err.stack ? err.stack : err); + t.end(); + } + ); +}); + +function rmdir(path) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach(function(file, index) { + var curPath = path + '/' + file; + if (fs.lstatSync(curPath).isDirectory()) { // recurse + rmdir(curPath); + } else { // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } +}