diff --git a/libraries/fabric-shim/lib/chaincode.js b/libraries/fabric-shim/lib/chaincode.js index 1fb726e5..2ae51b67 100644 --- a/libraries/fabric-shim/lib/chaincode.js +++ b/libraries/fabric-shim/lib/chaincode.js @@ -14,7 +14,8 @@ const Logger = require('./logger'); const utils = require('./utils/utils'); const logger = Logger.getLogger('lib/chaincode.js'); -const Handler = require('./handler'); +const {ChaincodeSupportClient} = require('./handler'); +const ChaincodeServer = require('./server'); const Iterators = require('./iterators'); const ChaincodeStub = require('./stub'); const KeyEndorsementPolicy = require('./utils/statebased'); @@ -122,7 +123,7 @@ class Shim { } const chaincodeName = opts['chaincode-id-name']; - const client = new Handler(chaincode, url, optsCpy); + const client = new ChaincodeSupportClient(chaincode, url, optsCpy); const chaincodeID = { name: chaincodeName }; @@ -194,6 +195,28 @@ class Shim { return Logger.getLogger(name); } + + /** + * @interface ChaincodeServerTLSProperties + * @property {Buffer} key Private key for TLS + * @property {Buffer} cert Certificate for TLS + * @property {Buffer} [clientCACerts] CA certificate for client certificates if mutual TLS is used. + */ + /** + * @interface ChaincodeServerOpts + * @property {string} ccid Chaincode ID + * @property {string} address Listen address for the server + * @property {ChaincodeServerTLSProperties} [tlsProps] TLS properties if TLS is required. + */ + /** + * Returns a new Chaincode server. Should be called when the chaincode is launched in a server mode. + * @static + * @param {ChaincodeInterface} chaincode User-provided object that must implement ChaincodeInterface + * @param {ChaincodeServerOpts} serverOpts Chaincode server options + */ + static server(chaincode, serverOpts) { + return new ChaincodeServer(chaincode, serverOpts); + } } // special OID used by Fabric to save attributes in X.509 certificates diff --git a/libraries/fabric-shim/lib/cmds/serverCommand.js b/libraries/fabric-shim/lib/cmds/serverCommand.js new file mode 100644 index 00000000..d23b72db --- /dev/null +++ b/libraries/fabric-shim/lib/cmds/serverCommand.js @@ -0,0 +1,106 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const fs = require('fs'); + +exports.command = 'server [options]'; +exports.desc = 'Start the chaincode as a server'; + +const validOptions = { + 'chaincode-address': {type: 'string', required: true}, + 'grpc.max_send_message_length': {type: 'number', default: -1}, + 'grpc.max_receive_message_length': {type: 'number', default: -1}, + 'grpc.keepalive_time_ms': {type: 'number', default: 110000}, + 'grpc.http2.min_time_between_pings_ms': {type: 'number', default: 110000}, + 'grpc.keepalive_timeout_ms': {type: 'number', default: 20000}, + 'grpc.http2.max_pings_without_data': {type: 'number', default: 0}, + 'grpc.keepalive_permit_without_calls': {type: 'number', default: 1}, + 'chaincode-id': {type: 'string', required: true}, + 'chaincode-tls-cert-file': {type: 'string', conflicts: 'chaincode-tls-cert-path'}, + 'chaincode-tls-cert-path': {type: 'string', conflicts: 'chaincode-tls-cert-file'}, + 'chaincode-tls-key-file': {type: 'string', conflicts: 'chaincode-tls-key-path'}, + 'chaincode-tls-key-path': {type: 'string', conflicts: 'chaincode-tls-key-file'}, + 'chaincode-tls-client-cacert-file': {type: 'string', conflicts: 'chaincode-tls-client-cacert-path'}, + 'chaincode-tls-client-cacert-path': {type: 'string', conflicts: 'chaincode-tls-client-cacert-file'}, + 'module-path': {type: 'string', default: process.cwd()} +}; + +exports.validOptions = validOptions; + +exports.builder = function (yargs) { + yargs.options(validOptions); + + yargs.usage('fabric-chaincode-node server --chaincode-address 0.0.0.0:9999 --chaincode-id mycc_v0:abcdef12345678...'); + + yargs.check((argv) => { + if (argv['chaincode-tls-key-file'] || argv['chaincode-tls-key-path'] || + argv['chaincode-tls-cert-file'] || argv['chaincode-tls-cert-path']) { + // TLS should be enabled + if (!argv['chaincode-tls-key-file'] && !argv['chaincode-tls-key-path']) { + throw new Error('A TLS option is set but no key is specified'); + } + if (!argv['chaincode-tls-cert-file'] && !argv['chaincode-tls-cert-path']) { + throw new Error('A TLS option is set but no cert is specified'); + } + } + return true; + }); + + return yargs; +}; + +exports.handler = function (argv) { + const Bootstrap = require('../contract-spi/bootstrap'); + + return argv.thePromise = Bootstrap.bootstrap(true); +}; + +exports.getArgs = function (yargs) { + const argv = {}; + + for (const name in validOptions) { + argv[name] = yargs.argv[name]; + } + + // Load the cryptographic files if TLS is enabled + if (argv['chaincode-tls-key-file'] || argv['chaincode-tls-key-path'] || + argv['chaincode-tls-cert-file'] || argv['chaincode-tls-cert-path']) { + + const tlsProps = {}; + + if (argv['chaincode-tls-key-file']) { + tlsProps.key = fs.readFileSync(argv['chaincode-tls-key-file']); + } else { + tlsProps.key = Buffer.from(fs.readFileSync(argv['chaincode-tls-key-path']).toString(), 'base64'); + } + + if (argv['chaincode-tls-cert-file']) { + tlsProps.cert = fs.readFileSync(argv['chaincode-tls-cert-file']); + } else { + tlsProps.cert = Buffer.from(fs.readFileSync(argv['chaincode-tls-cert-path']).toString(), 'base64'); + } + + // If cacert option is specified, enable client certificate validation + if (argv['chaincode-tls-client-cacert-file']) { + tlsProps.clientCACerts = fs.readFileSync(argv['chaincode-tls-client-cacert-file']); + } else if (argv['chaincode-tls-client-cacert-path']) { + tlsProps.clientCACerts = Buffer.from(fs.readFileSync(argv['chaincode-tls-client-cacert-path']).toString(), 'base64'); + } + + argv.tlsProps = tlsProps; + } + + // Translate the options to server options + argv.ccid = argv['chaincode-id']; + argv.address = argv['chaincode-address']; + + delete argv['chaincode-id']; + delete argv['chaincode-address']; + + return argv; +}; diff --git a/libraries/fabric-shim/lib/contract-spi/bootstrap.js b/libraries/fabric-shim/lib/contract-spi/bootstrap.js index f682018e..12affc42 100644 --- a/libraries/fabric-shim/lib/contract-spi/bootstrap.js +++ b/libraries/fabric-shim/lib/contract-spi/bootstrap.js @@ -14,6 +14,7 @@ const shim = require('../chaincode'); const ChaincodeFromContract = require('./chaincodefromcontract'); const Logger = require('../logger'); const StartCommand = require('../cmds/startCommand.js'); +const ServerCommand = require('../cmds/serverCommand.js'); const logger = Logger.getLogger('contracts-spi/bootstrap.js'); @@ -28,25 +29,31 @@ class Bootstrap { * @ignore * @param {Contract} contracts contract to register to use */ - static register(contracts, serializers, fileMetadata, title, version) { + static register(contracts, serializers, fileMetadata, title, version, opts, serverMode = false) { // load up the meta data that the user may have specified // this will need to passed in and rationalized with the // code as implemented const chaincode = new ChaincodeFromContract(contracts, serializers, fileMetadata, title, version); - // say hello to the peer - shim.start(chaincode); + if (serverMode) { + const server = shim.server(chaincode, opts); + server.start(); + } else { + // say hello to the peer + shim.start(chaincode); + } } /** * * @ignore + * @param {boolean} serverMode set true if the chaincode should be started as a server */ - static async bootstrap() { - const opts = StartCommand.getArgs(yargs); + static async bootstrap(serverMode = false) { + const opts = serverMode ? ServerCommand.getArgs(yargs) : StartCommand.getArgs(yargs); const {contracts, serializers, title, version} = this.getInfoFromContract(opts['module-path']); const fileMetadata = await Bootstrap.getMetadata(opts['module-path']); - Bootstrap.register(contracts, serializers, fileMetadata, title, version); + Bootstrap.register(contracts, serializers, fileMetadata, title, version, opts, serverMode); } static getInfoFromContract(modulePath) { diff --git a/libraries/fabric-shim/lib/handler.js b/libraries/fabric-shim/lib/handler.js index 899fc132..e521f8aa 100644 --- a/libraries/fabric-shim/lib/handler.js +++ b/libraries/fabric-shim/lib/handler.js @@ -205,7 +205,7 @@ class MsgQueueHandler { /* - * The ChaincodeSupportClient class represents a the base class for all remote nodes, Peer, Orderer , and MemberServicespeer. + * The ChaincodeSupportClient class represents a chaincode gRPC client to the peer. */ class ChaincodeSupportClient { @@ -269,11 +269,37 @@ class ChaincodeSupportClient { this._stream.end(); } + chat(convStarterMsg) { + this._stream = this._client.register(); + + this._handler = new ChaincodeMessageHandler(this._stream, this.chaincode); + this._handler.chat(convStarterMsg); + } + + /* + return a printable representation of this object + */ + toString() { + return 'ChaincodeSupportClient : {' + + 'url:' + + this._url + + '}'; + } +} + +/** + * The ChaincodeMessageHandler class handles messages between peer and chaincode both in the chaincode server and client model. + */ +class ChaincodeMessageHandler { + constructor (stream, chaincode) { + this._stream = stream; + this.chaincode = chaincode; + } + // this is a long-running method that does not return until // the conversation b/w the chaincode program and the target // peer has been completed chat(convStarterMsg) { - this._stream = this._client.register(); this.msgQueueHandler = new MsgQueueHandler(this); const stream = this._stream; @@ -528,15 +554,11 @@ class ChaincodeSupportClient { }); } - /* - * return a printable representation of this object - */ + return a printable representation of this object + */ toString() { - return 'ChaincodeSupportClient : {' + - 'url:' + - this._url + - '}'; + return 'ChaincodeMessageHandler : {}'; } } @@ -723,7 +745,10 @@ function parseResponse(handler, res, method) { } } -module.exports = ChaincodeSupportClient; +module.exports = { + ChaincodeSupportClient, + ChaincodeMessageHandler +}; // // The Endpoint class represents a remote grpc or grpcs target diff --git a/libraries/fabric-shim/lib/server.js b/libraries/fabric-shim/lib/server.js new file mode 100644 index 00000000..005c5a47 --- /dev/null +++ b/libraries/fabric-shim/lib/server.js @@ -0,0 +1,134 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const protoLoader = require('@grpc/proto-loader'); +const grpc = require('@grpc/grpc-js'); +const path = require('path'); + +const fabprotos = require('../bundle'); +const {ChaincodeMessageHandler} = require('./handler'); +const logger = require('./logger').getLogger('lib/server.js'); + +const PROTO_PATH = path.resolve(__dirname, '..', 'protos', 'peer', 'chaincode_shim.proto'); +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [ + path.resolve(__dirname, '..', 'google-protos'), + path.resolve(__dirname, '..', 'protos') + ] + } +); +const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); + +/** + * The ChaincodeServer class represents a chaincode gRPC server, which waits for connections from peers. + */ +class ChaincodeServer { + constructor(chaincode, serverOpts) { + // Validate arguments + if (typeof chaincode !== 'object' || chaincode === null) { + throw new Error('Missing required argument: chaincode'); + } + if (typeof serverOpts !== 'object' || serverOpts === null) { + throw new Error('Missing required argument: serverOpts'); + } + if (typeof chaincode.Init !== 'function' || typeof chaincode.Invoke !== 'function') { + throw new Error('The "chaincode" argument must implement Init() and Invoke() methods'); + } + if (typeof serverOpts.ccid !== 'string') { + throw new Error('Missing required property in serverOpts: ccid'); + } + if (typeof serverOpts.address !== 'string') { + throw new Error('Missing required property in serverOpts: address'); + } + if (typeof serverOpts.tlsProps === 'object' && serverOpts.tlsProps !== null) { + if (typeof serverOpts.tlsProps.key !== 'object' || serverOpts.tlsProps.key === null) { + throw new Error('Missing required property in serverOpts.tlsProps: key'); + } + if (typeof serverOpts.tlsProps.cert !== 'object' || serverOpts.tlsProps.cert === null) { + throw new Error('Missing required property in serverOpts.tlsProps: cert'); + } + + let clientCACerts; + if (typeof serverOpts.tlsProps.clientCACerts === 'object' && serverOpts.tlsProps.clientCACerts !== null) { + clientCACerts = serverOpts.tlsProps.clientCACerts; + } else { + clientCACerts = null; + } + + this._credentials = grpc.ServerCredentials.createSsl(clientCACerts, [ + { + private_key: serverOpts.tlsProps.key, + cert_chain: serverOpts.tlsProps.cert + } + ], clientCACerts === null ? false : true); + } else { + this._credentials = grpc.ServerCredentials.createInsecure(); + } + + // Create GRPC Server and register RPC handler + this._server = new grpc.Server(); + const self = this; + + this._server.addService(protoDescriptor.protos.Chaincode.service, { + connect: (stream) => { + self.connect(stream); + } + }); + + this._serverOpts = serverOpts; + this._chaincode = chaincode; + } + + start() { + return new Promise((resolve, reject) => { + logger.debug('ChaincodeServer trying to bind to ' + this._serverOpts.address); + + this._server.bindAsync(this._serverOpts.address, this._credentials, (error, port) => { + if (!error) { + logger.debug('ChaincodeServer successfully bound to ' + port); + + this._server.start(); + logger.debug('ChaincodeServer started.'); + + resolve(); + } else { + logger.error('ChaincodeServer failed to bind to ' + this._serverOpts.address); + reject(error); + } + }); + }); + } + + connect(stream) { + logger.debug('ChaincodeServer.connect called.'); + + try { + const client = new ChaincodeMessageHandler(stream, this._chaincode); + const chaincodeID = { + name: this._serverOpts.ccid + }; + + logger.debug('Start chatting with a peer through a new stream. Chaincode ID = ' + this._serverOpts.ccid); + client.chat({ + type: fabprotos.protos.ChaincodeMessage.Type.REGISTER, + payload: fabprotos.protos.ChaincodeID.encode(chaincodeID).finish() + }); + } catch (e) { + logger.warn('connection from peer failed: ' + e); + } + } +} + +module.exports = ChaincodeServer; diff --git a/libraries/fabric-shim/test/unit/chaincode.js b/libraries/fabric-shim/test/unit/chaincode.js index 3f87ea2d..2e7170b1 100644 --- a/libraries/fabric-shim/test/unit/chaincode.js +++ b/libraries/fabric-shim/test/unit/chaincode.js @@ -21,8 +21,8 @@ const chaincodePath = '../../lib/chaincode.js'; const StartCommand = require('../../lib/cmds/startCommand.js'); const caPath = path.join(__dirname, 'test-ca.pem'); -const certPath = path.join(__dirname, 'test-cert.pem'); -const keyPath = path.join(__dirname, 'test-key.pem'); +const certPath = path.join(__dirname, 'test-cert.base64'); +const keyPath = path.join(__dirname, 'test-key.base64'); const ca = fs.readFileSync(caPath, 'utf8'); const key = fs.readFileSync(keyPath, 'utf8'); @@ -97,7 +97,7 @@ describe('Chaincode', () => { }); it ('should start when passed init and invoke', () => { - const handlerClass = Chaincode.__get__('Handler'); + const handlerClass = Chaincode.__get__('ChaincodeSupportClient'); const chat = sandbox.stub(handlerClass.prototype, 'chat'); const myYargs = {'argv': {'$0': 'fabric-chaincode-node', 'peer.address': 'localhost:7051', 'chaincode-id-name': 'mycc'}}; @@ -140,8 +140,8 @@ describe('Chaincode', () => { const myYargs = {'argv': {'$0': 'fabric-chaincode-node', 'peer.address': 'localhost:7051', 'chaincode-id-name': 'mycc', 'some-other-arg': 'another-arg', 'yet-another-bad-arg': 'arg'}}; Chaincode.__set__('yargs', myYargs); - const handlerClass = Chaincode.__get__('Handler'); - Chaincode.__set__('Handler', MockHandler); + const handlerClass = Chaincode.__get__('ChaincodeSupportClient'); + Chaincode.__set__('ChaincodeSupportClient', MockHandler); const getArgsStub = sandbox.stub(StartCommand, 'getArgs').returns({ 'peer.address': 'localhost:7051', @@ -162,7 +162,7 @@ describe('Chaincode', () => { expect(testOpts.hasOwnProperty('module-path')).to.be.false; expect(testOpts.hasOwnProperty('peer.address')).to.be.true; - Chaincode.__set__('Handler', handlerClass); + Chaincode.__set__('ChaincodeSupportClient', handlerClass); getArgsStub.restore(); }); @@ -213,7 +213,7 @@ describe('Chaincode', () => { it ('should call handler.chat() with the correct object and output a message', () => { - const handlerClass = Chaincode.__get__('Handler'); + const handlerClass = Chaincode.__get__('ChaincodeSupportClient'); const chat = sandbox.stub(handlerClass.prototype, 'chat'); process.env.CORE_TLS_CLIENT_KEY_PATH = keyPath; @@ -247,8 +247,8 @@ describe('Chaincode', () => { } } - const handlerClass = Chaincode.__get__('Handler'); - Chaincode.__set__('Handler', MockHandler); + const handlerClass = Chaincode.__get__('ChaincodeSupportClient'); + Chaincode.__set__('ChaincodeSupportClient', MockHandler); process.env.CORE_TLS_CLIENT_KEY_PATH = keyPath; process.env.CORE_TLS_CLIENT_CERT_PATH = certPath; @@ -262,7 +262,7 @@ describe('Chaincode', () => { testOpts.cert.should.equal(cert); testOpts.key.should.equal(key); - Chaincode.__set__('Handler', handlerClass); + Chaincode.__set__('ChaincodeSupportClient', handlerClass); }); }); }); @@ -371,4 +371,23 @@ describe('Chaincode', () => { Logger.getLogger.restore(); }); }); + + describe('server()', () => { + before(() => { + Chaincode = rewire(chaincodePath); + }); + + it ('should create a ChaincodeServer instance', () => { + const mockObj = {_chaincode: {}, _serverOpts: {}}; + const serverStub = sinon.stub().returns(mockObj); + Chaincode.__set__('ChaincodeServer', serverStub); + + const mockChaincode = new Chaincode.ChaincodeInterface(); + const serverOpts = {ccid: 'example-cc-id:1', address: '0.0.0.0:9999'}; + + expect(Chaincode.server(mockChaincode, serverOpts)).to.deep.equal(mockObj); + expect(serverStub.calledOnce).to.be.true; + expect(serverStub.firstCall.args).to.deep.equal([mockChaincode, serverOpts]); + }); + }); }); diff --git a/libraries/fabric-shim/test/unit/cmds/serverCommand.js b/libraries/fabric-shim/test/unit/cmds/serverCommand.js new file mode 100644 index 00000000..14426b2f --- /dev/null +++ b/libraries/fabric-shim/test/unit/cmds/serverCommand.js @@ -0,0 +1,211 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const sinon = require('sinon'); + +const chai = require('chai'); +const expect = chai.expect; +const fs = require('fs'); +const path = require('path'); + +const yargs = require('yargs'); +const Bootstrap = require('../../../lib/contract-spi/bootstrap'); +const chaincodeServerCommand = require('../../../lib/cmds/serverCommand.js'); + +describe('server cmd', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('.builder', () => { + it('should configure the builder function', () => { + sandbox.stub(yargs, 'options'); + sandbox.stub(yargs, 'usage'); + sandbox.stub(yargs, 'check'); + + chaincodeServerCommand.builder(yargs); + + expect(yargs.options.calledOnce).to.be.true; + + const args = yargs.options.getCall(0).args[0]; + + expect(args['chaincode-address'].required).to.be.true; + expect(args['chaincode-id'].required).to.be.true; + expect(args['grpc.max_send_message_length'].default).to.deep.equal(-1); + expect(args['grpc.max_receive_message_length'].default).to.deep.equal(-1); + expect(args['grpc.keepalive_time_ms'].default).to.deep.equal(110000); + expect(args['grpc.http2.min_time_between_pings_ms'].default).to.deep.equal(110000); + expect(args['grpc.keepalive_timeout_ms'].default).to.deep.equal(20000); + expect(args['grpc.http2.max_pings_without_data'].default).to.deep.equal(0); + expect(args['grpc.keepalive_permit_without_calls'].default).to.deep.equal(1); + expect(args['module-path'].default).to.deep.equal(process.cwd()); + + expect(yargs.usage.calledOnce).to.be.true; + expect(yargs.check.calledOnce).to.be.true; + }); + }); + + describe('.handle', () => { + it('should handle properly and call bootstrap', () => { + sandbox.stub(Bootstrap, 'bootstrap'); + + const argv = {}; + chaincodeServerCommand.handler(argv); + + expect(Bootstrap.bootstrap.calledOnce).to.be.true; + }); + }); + + describe('.getArgs', () => { + const certFileEncoded = path.join(__dirname, '..', 'test-cert.base64'); + const keyFileEncoded = path.join(__dirname, '..', 'test-key.base64'); + const caFileEncoded = path.join(__dirname, '..', 'test-ca.base64'); + const certFile = path.join(__dirname, '..', 'test-cert.pem'); + const keyFile = path.join(__dirname, '..', 'test-key.pem'); + const caFile = path.join(__dirname, '..', 'test-ca.pem'); + const cert = Buffer.from(fs.readFileSync(certFileEncoded).toString(), 'base64'); + const key = Buffer.from(fs.readFileSync(keyFileEncoded).toString(), 'base64'); + const ca = Buffer.from(fs.readFileSync(caFileEncoded).toString(), 'base64'); + + it('should return the arguments properly', () => { + const argv = { + 'chaincode-address': '0.0.0.0:9999', + 'chaincode-id': 'test_id:1', + 'grpc.keepalive_time_ms': 1000, + 'module-path': '/tmp/example', + 'extra-options': 'something' + }; + + const ret = chaincodeServerCommand.getArgs({argv}); + + expect(ret.address).to.equal('0.0.0.0:9999'); + expect(ret.ccid).to.equal('test_id:1'); + expect(ret['grpc.keepalive_time_ms']).to.equal(1000); + expect(ret['module-path']).to.equal('/tmp/example'); + expect(ret['chaincode-address']).to.be.undefined; + expect(ret['chaincode-id']).to.be.undefined; + expect(ret['extra-options']).to.be.undefined; + }); + + it('should return the TLS arguments properly', () => { + const argv = { + 'chaincode-address': '0.0.0.0:9999', + 'chaincode-id': 'test_id:1', + 'module-path': '/tmp/example', + 'chaincode-tls-cert-path': certFileEncoded, + 'chaincode-tls-key-path': keyFileEncoded + }; + + const ret = chaincodeServerCommand.getArgs({argv}); + + expect(ret.address).to.equal('0.0.0.0:9999'); + expect(ret.ccid).to.equal('test_id:1'); + + expect(ret.tlsProps).to.deep.equal({ + cert, + key + }); + }); + + it('should return the mutual TLS arguments properly', () => { + const argv = { + 'chaincode-address': '0.0.0.0:9999', + 'chaincode-id': 'test_id:1', + 'module-path': '/tmp/example', + 'chaincode-tls-cert-path': certFileEncoded, + 'chaincode-tls-key-path': keyFileEncoded, + 'chaincode-tls-client-cacert-path': caFileEncoded + }; + + const ret = chaincodeServerCommand.getArgs({argv}); + + expect(ret.address).to.equal('0.0.0.0:9999'); + expect(ret.ccid).to.equal('test_id:1'); + + expect(ret.tlsProps).to.deep.equal({ + cert, + key, + clientCACerts: ca + }); + }); + + it('should return the TLS arguments with PEM files properly', () => { + const argv = { + 'chaincode-address': '0.0.0.0:9999', + 'chaincode-id': 'test_id:1', + 'module-path': '/tmp/example', + 'chaincode-tls-cert-file': certFile, + 'chaincode-tls-key-file': keyFile, + 'chaincode-tls-client-cacert-file': caFile + }; + + const ret = chaincodeServerCommand.getArgs({argv}); + + expect(ret.address).to.equal('0.0.0.0:9999'); + expect(ret.ccid).to.equal('test_id:1'); + + expect(ret.tlsProps).to.deep.equal({ + cert, + key, + clientCACerts: ca + }); + }); + }); + + describe('parse arguments', () => { + it('should parse the arguments successfully', () => { + expect(() => { + chaincodeServerCommand.builder(yargs) + .exitProcess(false) + .parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999'); + }).not.to.throw(); + }); + + it('should parse the arguments successfully with TLS options', () => { + expect(() => { + chaincodeServerCommand.builder(yargs) + .exitProcess(false) + .parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' + + '--chaincode-tls-key-file tls.key --chaincode-tls-cert-file tls.pem'); + }).not.to.throw(); + }); + + it('should throw when conflicting arguments are passed', () => { + expect(() => { + chaincodeServerCommand.builder(yargs) + .exitProcess(false) + .parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' + + '--chaincode-tls-key-file tls.key --chaincode-tls-key-path tls.pem'); + }).to.throw(); + }); + + it('should throw when only TLS key is passed', () => { + expect(() => { + chaincodeServerCommand.builder(yargs) + .exitProcess(false) + .parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' + + '--chaincode-tls-key-file tls.key'); + }).to.throw(); + }); + + it('should throw when only TLS cert is passed', () => { + expect(() => { + chaincodeServerCommand.builder(yargs) + .exitProcess(false) + .parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' + + '--chaincode-tls-cert-file tls.pem'); + }).to.throw(); + }); + }); +}); diff --git a/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js b/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js index bd1698b7..aa7d5cdf 100644 --- a/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js +++ b/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js @@ -59,6 +59,7 @@ describe('bootstrap.js', () => { let sandbox; let mockShim; + let mockServer; let mockCmd; let readFileStub; let pathExistsStub; @@ -73,7 +74,8 @@ describe('bootstrap.js', () => { useCleanCache: true }); sandbox = sinon.createSandbox(); - mockShim = {start : sandbox.stub()}; + mockServer = {start: sandbox.stub()}; + mockShim = {start : sandbox.stub(), server: sandbox.stub().returns(mockServer)}; getArgsStub = sandbox.stub(); mockCmd = {getArgs : getArgsStub}; @@ -84,6 +86,7 @@ describe('bootstrap.js', () => { mockery.registerMock('yargs', {}); mockery.registerMock('../chaincode', mockShim); mockery.registerMock('../cmds/startCommand.js', mockCmd); + mockery.registerMock('../cmds/serverCommand.js', mockCmd); mockery.registerMock('./chaincodefromcontract', MockChaincodeFromContract); mockery.registerMock('fs-extra', {pathExists:pathExistsStub, readFileSync : readFileStub}); @@ -103,6 +106,16 @@ describe('bootstrap.js', () => { sinon.assert.calledOnce(mockShim.start); }); + it('should pass on the register to the shim in the server mode', async () => { + const opts = {ccid: 'abcdef', address: '0.0.0.0:9999'}; + await Bootstrap.register([sc], {}, {}, 'some title', 'some version', opts, true); + + sinon.assert.calledOnce(mockShim.server); + sinon.assert.calledOnce(mockServer.start); + + expect(mockShim.server.getCall(0).args[1]).to.deep.equal(opts); + }); + }); describe('#bootstrap', () => { @@ -129,8 +142,24 @@ describe('bootstrap.js', () => { sinon.assert.calledOnce(getMetadataStub); sinon.assert.calledOnce(getInfoFromContractStub); sinon.assert.calledOnce(registerStub); - sinon.assert.calledWith(registerStub, [sc], {}, {}, 'some title', 'some version'); + sinon.assert.calledWith(registerStub, [sc], {}, {}, 'some title', 'some version', {'module-path':'fakepath'}, false); }); + + it ('should correctly call the register method in the server mode', async () => { + getMetadataStub.resolves({}); + mockery.registerMock(path.resolve(process.cwd(), 'fakepath', 'entrypoint'), {contracts: [sc]}); + const registerStub = sandbox.stub(); + Bootstrap.register = registerStub; + getInfoFromContractStub.returns({contracts: [sc], serializers : {}, title: 'some title', version: 'some version'}); + + await Bootstrap.bootstrap(true); + + sinon.assert.calledOnce(getMetadataStub); + sinon.assert.calledOnce(getInfoFromContractStub); + sinon.assert.calledOnce(registerStub); + sinon.assert.calledWith(registerStub, [sc], {}, {}, 'some title', 'some version', {'module-path':'fakepath'}, true); + }); + }); describe('#getInfoFromContract', () => { diff --git a/libraries/fabric-shim/test/unit/handler.js b/libraries/fabric-shim/test/unit/handler.js index 7b622af6..5bcd5042 100644 --- a/libraries/fabric-shim/test/unit/handler.js +++ b/libraries/fabric-shim/test/unit/handler.js @@ -33,8 +33,8 @@ const mockChaincodeImpl = { }; const ca = fs.readFileSync(path.join(__dirname, 'test-ca.pem'), 'utf8'); -const key = fs.readFileSync(path.join(__dirname, 'test-key.pem'), 'utf8'); -const cert = fs.readFileSync(path.join(__dirname, 'test-cert.pem'), 'utf8'); +const key = fs.readFileSync(path.join(__dirname, 'test-key.base64'), 'utf8'); +const cert = fs.readFileSync(path.join(__dirname, 'test-cert.base64'), 'utf8'); const mockOpts = { pem: ca, @@ -369,19 +369,19 @@ describe('Handler', () => { describe('ChaincodeSupportClient', () => { it ('should throw an error when chaincode not passed', () => { expect(() => { - new Handler(); + new Handler.ChaincodeSupportClient(); }).to.throw(/Missing required argument: chaincode/); }); it ('should throw an error if argument does not match chaincode format', () => { expect(() => { - new Handler({}); + new Handler.ChaincodeSupportClient({}); }).to.throw(/The chaincode argument must implement the mandatory "Init\(\)" method/); }); it ('should throw an error if argument only part matches chaincode format', () => { expect(() => { - new Handler({ + new Handler.ChaincodeSupportClient({ Init: function() {} }); }).to.throw(/The chaincode argument must implement the mandatory "Invoke\(\)" method/); @@ -389,20 +389,20 @@ describe('Handler', () => { it ('should throw an error if argument missing URL argument', () => { expect(() => { - new Handler(mockChaincodeImpl); + new Handler.ChaincodeSupportClient(mockChaincodeImpl); }).to.throw(/Invalid URL: undefined/); }); it ('should throw an error if URL argument does not use grpc as protocol', () => { expect(() => { - new Handler(mockChaincodeImpl, 'https://' + mockPeerAddress.base); + new Handler.ChaincodeSupportClient(mockChaincodeImpl, 'https://' + mockPeerAddress.base); }).to.throw(/Invalid protocol: https. {2}URLs must begin with grpc:\/\/ or grpcs:\/\//); }); it ('should set endpoint, client and default timeout', () => { const credsSpy = sinon.spy(grpc.credentials, 'createInsecure'); - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.unsecure); expect(handler._request_timeout).to.deep.equal(30000); expect(handler._endpoint.addr).to.deep.equal(mockPeerAddress.base); @@ -414,7 +414,7 @@ describe('Handler', () => { }); it ('should override the default request timeout if value passed', () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure, { + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.unsecure, { 'request-timeout': 123456 }); @@ -422,7 +422,7 @@ describe('Handler', () => { }); it ('should store additional grpc options', () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure, { + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.unsecure, { 'grpc.max_send_message_length': 1, 'grpc.max_receive_message_length': 2, 'grpc.keepalive_time_ms': 3, @@ -442,20 +442,20 @@ describe('Handler', () => { }); it ('should preserve casing in handler addr', () => { - const handler = new Handler(mockChaincodeImpl, 'grpc://' + mockPeerAddress.base.toUpperCase()); + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, 'grpc://' + mockPeerAddress.base.toUpperCase()); expect(handler._endpoint.addr).to.deep.equal(mockPeerAddress.base.toUpperCase()); }); it ('should throw an error if connection secure and certificate not passed', () => { expect(() => { - new Handler(mockChaincodeImpl, mockPeerAddress.secure); + new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.secure); }).to.throw(/PEM encoded certificate is required./); }); it ('should throw an error if connection secure encoded private key not passed as opt', () => { expect(() => { - new Handler(mockChaincodeImpl, mockPeerAddress.secure, { + new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.secure, { pem: ca }); }).to.throw(/encoded Private key is required./); @@ -463,7 +463,7 @@ describe('Handler', () => { it ('should throw an error if connection secure encoded private key not passed as opt', () => { expect(() => { - new Handler(mockChaincodeImpl, mockPeerAddress.secure, { + new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.secure, { pem: ca, key: key }); @@ -473,7 +473,7 @@ describe('Handler', () => { it ('should set endpoint, client and default timeout for a secure connection', () => { const credsSpy = sinon.spy(grpc.credentials, 'createSsl'); - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.secure, mockOpts); + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.secure, mockOpts); expect(handler._options.cert).to.deep.equal(mockOpts.cert); expect(handler._request_timeout).to.deep.equal(30000); @@ -488,7 +488,7 @@ describe('Handler', () => { const opts = Object.assign({}, mockOpts); opts['ssl-target-name-override'] = 'dummy override'; - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.secure, opts); + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.secure, opts); expect(handler._options['grpc.ssl_target_name_override']).to.deep.equal('dummy override'); expect(handler._options['grpc.default_authority']).to.deep.equal('dummy override'); @@ -496,7 +496,7 @@ describe('Handler', () => { describe('close', () => { it ('should call end on the stream', () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.unsecure); handler._stream = {end: sinon.stub()}; handler.close(); @@ -505,6 +505,43 @@ describe('Handler', () => { }); }); + describe('chat', () => { + afterEach(() => { + Handler = rewire('../../../fabric-shim/lib/handler.js'); + }); + + it ('should create an instance of ChaincodeMessageHandler and pass the argument', () => { + const mockChaincodeMessageHandler = sinon.spy(() => { + return sinon.createStubInstance(Handler.ChaincodeMessageHandler); + }); + Handler.__set__('ChaincodeMessageHandler', mockChaincodeMessageHandler); + + const mockStream = {write: sinon.stub(), on: sinon.stub()}; + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.unsecure); + handler._client.register = sinon.stub().returns(mockStream); + + handler.chat('starter message example'); + + expect(handler._client.register.calledOnce).to.be.true; + expect(mockChaincodeMessageHandler.calledWithNew).to.be.ok; // believe wrong + expect(mockChaincodeMessageHandler.calledWithNew()).to.be.false; + expect(handler._stream).to.deep.equal(mockStream); + expect(handler._handler).to.deep.equal(new mockChaincodeMessageHandler(mockStream, mockChaincodeImpl)); + expect(handler._handler.chat.calledOnce).to.be.true; + expect(handler._handler.chat.firstCall.args).to.deep.equal(['starter message example']); + }); + }); + + describe('toString', () => { + it ('should return ChaincodeSupportClient object as a string with the URL', () => { + const handler = new Handler.ChaincodeSupportClient(mockChaincodeImpl, mockPeerAddress.unsecure); + + expect(handler.toString()).to.deep.equal(`ChaincodeSupportClient : {url:${mockPeerAddress.unsecure}}`); + }); + }); + }); + + describe('ChaincodeMessageHandler', () => { describe('chat', () => { afterEach(() => { Handler = rewire('../../../fabric-shim/lib/handler.js'); @@ -519,12 +556,9 @@ describe('Handler', () => { const mockStream = {write: sinon.stub(), on: sinon.stub()}; - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); - handler._client.register = sinon.stub().returns(mockStream); - + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.chat('some starter message'); - expect(handler._client.register.calledOnce).to.be.true; expect(mockMsgQueueHandler.calledWithNew).to.be.ok; // believe wrong expect(mockMsgQueueHandler.calledWithNew()).to.be.false; expect(handler._stream).to.deep.equal(mockStream); @@ -584,8 +618,7 @@ describe('Handler', () => { mockStream = {write: (sinon.stub()), on: mockEventEmitter, cancel: sinon.stub(), end: sinon.stub()}; - handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); - handler._client.register = sinon.stub().returns(mockStream); + handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.chat('some starter message'); handleInitSpy = sinon.spy(); @@ -758,8 +791,7 @@ describe('Handler', () => { const mockStream = {write: sinon.stub(), on: mockEventEmitter, cancel: sinon.stub(), end: sinon.stub()}; - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); - handler._client.register = sinon.stub().returns(mockStream); + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.chat('some starter message'); eventReg.end(); @@ -778,8 +810,7 @@ describe('Handler', () => { const mockStream = {write: sinon.stub(), on: mockEventEmitter, end: sinon.stub()}; - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); - handler._client.register = sinon.stub().returns(mockStream); + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.chat('some starter message'); eventReg.error({}); @@ -795,8 +826,7 @@ describe('Handler', () => { const mockStream = {write: sinon.stub(), on: mockEventEmitter, end: sinon.stub()}; - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); - handler._client.register = sinon.stub().returns(mockStream); + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.chat('some starter message'); const error = new Error(); eventReg.error(error); @@ -814,7 +844,8 @@ describe('Handler', () => { const handleMessage = sinon.spy(); Handler.__set__('handleMessage', handleMessage); - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.handleInit('some message'); expect(handleMessage.calledOnce).to.be.true; @@ -831,7 +862,8 @@ describe('Handler', () => { const handleMessage = sinon.spy(); Handler.__set__('handleMessage', handleMessage); - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.handleTransaction('some message'); expect(handleMessage.calledOnce).to.be.true; @@ -861,7 +893,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleGetState(collection, key, 'theChannelID', 'theTxID'); @@ -873,7 +906,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleGetState(collection, key, 'theChannelID', 'theTxID'); @@ -907,7 +941,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handlePutState(collection, key, value, 'theChannelID', 'theTxID'); @@ -919,7 +954,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handlePutState(collection, key, value, 'theChannelID', 'theTxID'); @@ -952,7 +988,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleDeleteState(collection, key, 'theChannelID', 'theTxID'); @@ -964,7 +1001,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen rejects', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleDeleteState(collection, key, 'theChannelID', 'theTxID'); @@ -1005,7 +1043,8 @@ describe('Handler', () => { }); it('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handlePutStateMetadata(collection, key, metadataKey, ep, 'theChannelID', 'theTxID'); @@ -1017,7 +1056,8 @@ describe('Handler', () => { }); it('should reject when _askPeerAndListen rejects', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handlePutStateMetadata(collection, key, metadataKey, ep, 'theChannelID', 'theTxID'); @@ -1049,7 +1089,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleGetPrivateDataHash(collection, key, 'theChannelID', 'theTxID'); @@ -1061,7 +1102,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen rejects', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleGetPrivateDataHash(collection, key, 'theChannelID', 'theTxID'); @@ -1093,7 +1135,8 @@ describe('Handler', () => { }); it('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleGetStateMetadata(collection, key, 'theChannelID', 'theTxID'); @@ -1105,7 +1148,8 @@ describe('Handler', () => { }); it('should reject when _askPeerAndListen rejects', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleGetStateMetadata(collection, key, 'theChannelID', 'theTxID'); @@ -1138,7 +1182,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleGetStateByRange(collection, startKey, endKey, 'theChannelID', 'theTxID'); @@ -1150,7 +1195,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleGetStateByRange(collection, startKey, endKey, 'theChannelID', 'theTxID'); @@ -1162,7 +1208,8 @@ describe('Handler', () => { }); it ('should resolve with metadata when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const metadata = Buffer.from('metadata'); @@ -1201,7 +1248,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleQueryStateNext(id, 'theChannelID', 'theTxID'); @@ -1213,7 +1261,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleQueryStateNext(id, 'theChannelID', 'theTxID'); @@ -1244,7 +1293,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleQueryStateClose(id, 'theChannelID', 'theTxID'); @@ -1256,7 +1306,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleQueryStateClose(id, 'theChannelID', 'theTxID'); @@ -1288,7 +1339,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleGetQueryResult(collection, query, null, 'theChannelID', 'theTxID'); @@ -1300,7 +1352,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen rejects', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleGetQueryResult(collection, query, null, 'theChannelID', 'theTxID'); @@ -1312,7 +1365,8 @@ describe('Handler', () => { }); it ('handleGetQueryResult with metadata should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const metadata = Buffer.from('some metadata'); @@ -1351,7 +1405,8 @@ describe('Handler', () => { }); it ('should resolve when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves('some response'); const result = await handler.handleGetHistoryForKey(key, 'theChannelID', 'theTxID'); @@ -1363,7 +1418,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const result = handler.handleGetHistoryForKey(key, 'theChannelID', 'theTxID'); @@ -1404,7 +1460,8 @@ describe('Handler', () => { }); it ('should return decoded response when chaincode message type COMPLETED', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves({type: fabprotos.protos.ChaincodeMessage.Type.COMPLETED, payload: 'some payload'}); const decodeStub = sandbox.stub(fabprotos.protos.Response, 'decode').returns('some response'); @@ -1419,7 +1476,8 @@ describe('Handler', () => { }); it ('should throw an error when _askPeerAndListen resolves with an error', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves({type: fabprotos.protos.ChaincodeMessage.Type.ERROR, payload: 'some payload'}); const decodeStub = sandbox.stub(fabprotos.protos.Response, 'decode').returns('some response'); @@ -1433,7 +1491,8 @@ describe('Handler', () => { }); it ('should reject when _askPeerAndListen resolves', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').rejects(); const decodeStub = sandbox.stub(fabprotos.protos.Response, 'decode').returns('some response'); @@ -1447,7 +1506,8 @@ describe('Handler', () => { }); it ('should return nothing chaincode message type not COMPLETED or ERROR', async () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); const _askPeerAndListenStub = sandbox.stub(handler, '_askPeerAndListen').resolves({type: fabprotos.protos.ChaincodeMessage.Type.SOMETHING_ELSE, payload: 'some payload'}); const decodeStub = sandbox.stub(fabprotos.protos.Response, 'decode').returns('some response'); @@ -1466,7 +1526,8 @@ describe('Handler', () => { const msg = 'some message'; const method = 'SomeMethod'; - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); handler.msgQueueHandler = sinon.createStubInstance(MsgQueueHandler); handler.msgQueueHandler.queueMsg.callsFake((qMsg) => { @@ -1484,9 +1545,10 @@ describe('Handler', () => { describe('toString', () => { it ('should return ChaincodeSupportClient object as a string with the URL', () => { - const handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + const mockStream = {write: sinon.stub(), end: sinon.stub()}; + const handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); - expect(handler.toString()).to.deep.equal(`ChaincodeSupportClient : {url:${mockPeerAddress.unsecure}}`); + expect(handler.toString()).to.deep.equal('ChaincodeMessageHandler : {}'); }); }); }); @@ -1767,6 +1829,7 @@ describe('Handler', () => { let saveStateQueryIterator; let saveHistoryQueryIterator; + let mockStream; before(() => { saveStateQueryIterator = Handler.__get__('StateQueryIterator'); @@ -1789,7 +1852,8 @@ describe('Handler', () => { txid: 'aTx' }; - handler = new Handler(mockChaincodeImpl, mockPeerAddress.unsecure); + mockStream = {write: sinon.stub(), end: sinon.stub()}; + handler = new Handler.ChaincodeMessageHandler(mockStream, mockChaincodeImpl); }); after(() => { diff --git a/libraries/fabric-shim/test/unit/iterators.js b/libraries/fabric-shim/test/unit/iterators.js index 5d208108..4c3b768f 100644 --- a/libraries/fabric-shim/test/unit/iterators.js +++ b/libraries/fabric-shim/test/unit/iterators.js @@ -14,7 +14,7 @@ const rewire = require('rewire'); const Iterator = rewire('../../../fabric-shim/lib/iterators.js'); const StateQueryIterator = Iterator.StateQueryIterator; const HistoryQueryIterator = Iterator.HistoryQueryIterator; -const handler = require('../../../fabric-shim/lib/handler.js'); +const {ChaincodeMessageHandler} = require('../../../fabric-shim/lib/handler.js'); const fabprotos = require('../../bundle'); const channel_id = 'theChannelId'; @@ -26,7 +26,7 @@ describe('Iterator', () => { let sandbox; beforeEach(() => { sandbox = sinon.createSandbox(); - mockHandler = sandbox.createStubInstance(handler); + mockHandler = sandbox.createStubInstance(ChaincodeMessageHandler); mockResponse = {}; }); diff --git a/libraries/fabric-shim/test/unit/server.js b/libraries/fabric-shim/test/unit/server.js new file mode 100644 index 00000000..04c3520a --- /dev/null +++ b/libraries/fabric-shim/test/unit/server.js @@ -0,0 +1,227 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ +/* global describe it beforeEach afterEach before after */ +'use strict'; + +const sinon = require('sinon'); +const chai = require('chai'); +chai.use(require('chai-as-promised')); +const expect = chai.expect; +const fs = require('fs'); +const path = require('path'); +const rewire = require('rewire'); + +const fabprotos = require('../../bundle'); +const grpc = require('@grpc/grpc-js'); + +const serverPath = '../../lib/server'; +let ChaincodeServer = rewire(serverPath); + +const mockChaincode = {Init: () => {}, Invoke: () => {}}; + +describe('ChaincodeServer', () => { + const tlsKey = Buffer.from(fs.readFileSync(path.join(__dirname, 'test-key.pem')).toString(), 'base64'); + const tlsCert = Buffer.from(fs.readFileSync(path.join(__dirname, 'test-cert.pem')).toString(), 'base64'); + const tlsClientCA = fs.readFileSync(path.join(__dirname, 'test-ca.pem')); + + let grpcServerStub; + const serverOpts = { + ccid: 'example-chaincode-id', + address: '0.0.0.0:9999' + }; + const serverTLSOpts = { + ccid: 'example-chaincode-id', + address: '0.0.0.0:9999', + tlsProps: { + // test-cert.pem and test-key.pem are base64-encoded and need to decode to make Buffer + key: tlsKey, + cert: tlsCert + } + }; + const serverMutualTLSOpts = { + ccid: 'example-chaincode-id', + address: '0.0.0.0:9999', + tlsProps: { + key: tlsKey, + cert: tlsCert, + clientCACerts: tlsClientCA + } + }; + const mockCredentials = {type: 'insecure'}; + const mockTLSCredentials = {type: 'secure'}; + let insecureCredentialsStub; + let sslCredentialsStub; + + let mockGrpcServerInstance; + + beforeEach(() => { + mockGrpcServerInstance = { + addService: sinon.stub() + }; + + grpcServerStub = sinon.stub(grpc, 'Server').returns(mockGrpcServerInstance); + insecureCredentialsStub = sinon.stub(grpc.ServerCredentials, 'createInsecure').returns(mockCredentials); + sslCredentialsStub = sinon.stub(grpc.ServerCredentials, 'createSsl').returns(mockTLSCredentials); + }); + afterEach(() => { + grpcServerStub.restore(); + insecureCredentialsStub.restore(); + sslCredentialsStub.restore(); + }); + + describe('constructor', () => { + it('should create a gRPC server instance and call addService in the constructor', () => { + const server = new ChaincodeServer(mockChaincode, serverOpts); + + expect(grpcServerStub.calledOnce).to.be.ok; + expect(server._server).to.deep.equal(mockGrpcServerInstance); + expect(server._server.addService.calledOnce).to.be.ok; + expect(server._chaincode).to.deep.equal(mockChaincode); + expect(server._serverOpts).to.deep.equal(serverOpts); + expect(server._credentials).to.deep.equal(mockCredentials); + + expect(insecureCredentialsStub.calledOnce).to.be.ok; + }); + it('should create a gRPC server instance with TLS credentials and call addService in the constructor', () => { + const server = new ChaincodeServer(mockChaincode, serverTLSOpts); + + expect(grpcServerStub.calledOnce).to.be.ok; + expect(server._server).to.deep.equal(mockGrpcServerInstance); + expect(server._server.addService.calledOnce).to.be.ok; + expect(server._chaincode).to.deep.equal(mockChaincode); + expect(server._serverOpts).to.deep.equal(serverTLSOpts); + expect(server._credentials).to.deep.equal(mockTLSCredentials); + + expect(sslCredentialsStub.calledOnce).to.be.ok; + expect(sslCredentialsStub.firstCall.args[0]).to.be.null; + expect(sslCredentialsStub.firstCall.args[1]).to.deep.equal([{ + private_key: tlsKey, + cert_chain: tlsCert + }]); + expect(sslCredentialsStub.firstCall.args[2]).to.be.false; + }); + it('should create a gRPC server instance with mutual TLS credentials and call addService in the constructor', () => { + const server = new ChaincodeServer(mockChaincode, serverMutualTLSOpts); + + expect(grpcServerStub.calledOnce).to.be.ok; + expect(server._server).to.deep.equal(mockGrpcServerInstance); + expect(server._server.addService.calledOnce).to.be.ok; + expect(server._chaincode).to.deep.equal(mockChaincode); + expect(server._serverOpts).to.deep.equal(serverMutualTLSOpts); + expect(server._credentials).to.deep.equal(mockTLSCredentials); + + expect(sslCredentialsStub.calledOnce).to.be.ok; + expect(sslCredentialsStub.firstCall.args[0]).to.deep.equal(tlsClientCA); + expect(sslCredentialsStub.firstCall.args[1]).to.deep.equal([{ + private_key: tlsKey, + cert_chain: tlsCert, + }]); + expect(sslCredentialsStub.firstCall.args[2]).to.be.true; + }); + + it('should throw an error when chaincode is missing', () => { + expect(() => new ChaincodeServer(null, serverOpts)).to.throw('Missing required argument: chaincode'); + }); + it('should throw an error when chaincode implements only Invoke', () => { + expect(() => new ChaincodeServer({Invoke: sinon.stub()}, serverOpts)) + .to.throw('The "chaincode" argument must implement Init() and Invoke() methods'); + }); + it('should throw an error when chaincode implements only Init', () => { + expect(() => new ChaincodeServer({Init: sinon.stub()}, serverOpts)) + .to.throw('The "chaincode" argument must implement Init() and Invoke() methods'); + }); + it('should throw an error when serverOpts is missing', () => { + expect(() => new ChaincodeServer(mockChaincode)).to.throw('Missing required argument: serverOpts'); + }); + it('should throw an error when serverOpts.ccid is missing', () => { + expect(() => new ChaincodeServer(mockChaincode, {})).to.throw('Missing required property in serverOpts: ccid'); + }); + it('should throw an error when serverOpts.address is missing', () => { + expect(() => new ChaincodeServer(mockChaincode, {ccid: 'some id'})).to.throw('Missing required property in serverOpts: address'); + }); + it('should throw an error when serverOpts.tlsProps.key is missing', () => { + expect(() => new ChaincodeServer(mockChaincode, {ccid: 'some id', address: '0.0.0.0:9999', tlsProps: {}})). + to.throw('Missing required property in serverOpts.tlsProps: key'); + }); + it('should throw an error when serverOpts.tlsProps.cert is missing', () => { + expect(() => new ChaincodeServer(mockChaincode, {ccid: 'some id', address: '0.0.0.0:9999', tlsProps: {key: Buffer.from('a')}})). + to.throw('Missing required property in serverOpts.tlsProps: cert'); + }); + }); + + describe('start()', () => { + it('should call bindAsync and start', async () => { + const server = new ChaincodeServer(mockChaincode, serverOpts); + + server._server = { + bindAsync: sinon.stub().callsFake((address, credentials, callback) => { + callback(null, 9999); + }), + start: sinon.stub() + }; + + expect(await server.start()).not.to.throw; + expect(server._server.bindAsync.calledOnce).to.be.ok; + expect(server._server.bindAsync.firstCall.args[0]).to.equal(serverOpts.address); + expect(server._server.bindAsync.firstCall.args[1]).to.equal(mockCredentials); + expect(server._server.start.calledOnce).to.be.ok; + }); + + it('should throw if bindAsync fails', async () => { + const server = new ChaincodeServer(mockChaincode, serverOpts); + + server._server = { + bindAsync: sinon.stub().callsFake((address, credentials, callback) => { + callback('failed to bind', 9999); + }), + start: sinon.stub() + }; + expect(server.start()).to.eventually.be.rejectedWith('failed to bind'); + }); + }); + + describe('connect()', () => { + it('should call connect', () => { + const mockHandler = { + chat: sinon.stub() + }; + const mockHandlerStub = sinon.stub().returns(mockHandler); + ChaincodeServer.__set__('ChaincodeMessageHandler', mockHandlerStub); + + const server = new ChaincodeServer(mockChaincode, serverOpts); + + const serviceImpl = server._server.addService.firstCall.args[1]; + const mockStream = {on: sinon.stub(), write: sinon.stub()}; + + expect(serviceImpl.connect(mockStream)).not.to.throw; + expect(mockHandlerStub.calledOnce).to.be.ok; + expect(mockHandler.chat.calledOnce).to.be.ok; + expect(mockHandler.chat.firstCall.args).to.deep.equal([{ + type: fabprotos.protos.ChaincodeMessage.Type.REGISTER, + payload: fabprotos.protos.ChaincodeID.encode({ + name: 'example-chaincode-id' + }).finish() + }]); + }); + + it('should not throw even if chat fails', () => { + const mockHandler = { + chat: sinon.stub().throws(new Error('Some error from chat')) + }; + const mockHandlerStub = sinon.stub().returns(mockHandler); + ChaincodeServer.__set__('ChaincodeMessageHandler', mockHandlerStub); + + const server = new ChaincodeServer(mockChaincode, serverOpts); + + const serviceImpl = server._server.addService.firstCall.args[1]; + const mockStream = {on: sinon.stub(), write: sinon.stub()}; + + expect(serviceImpl.connect(mockStream)).not.to.throw; + expect(mockHandlerStub.calledOnce).to.be.ok; + expect(mockHandler.chat.calledOnce).to.be.ok; + }); + }); +}); diff --git a/libraries/fabric-shim/test/unit/test-ca.base64 b/libraries/fabric-shim/test/unit/test-ca.base64 new file mode 100644 index 00000000..fc88d63f --- /dev/null +++ b/libraries/fabric-shim/test/unit/test-ca.base64 @@ -0,0 +1,11 @@ +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrekNCOVFJSkFKMFo4akZpUFBpOU1Bb0dD +Q3FHU000OUJBTUNNQTR4RERBS0JnTlZCQU1NQTNSc2N6QWUKRncweU1EQTFNRGN4TmpNME1qRmFG +dzB6TURBMU1EVXhOak0wTWpGYU1BNHhEREFLQmdOVkJBTU1BM1JzY3pDQgptekFRQmdjcWhrak9Q +UUlCQmdVcmdRUUFJd09CaGdBRUFDb1VTM3pnOVFqNUNnUWVOQ1krOXNQTTJZV1lIVVVRClNCRS9v +WXBncldWOEU4VHF0V2tXY2h3WFA0T29aQXE3Yk1KMmJOUUU1U3E2SVkrYlpyWXBPS2pmQVNweVM0 +cVIKNHhKZkN1bjdCSVpBallIdlZxbWN1RjhhSmFmaDhGOTNHQmprSUxIZ0hUcnRMTHNBcTZzQnB6 +RXVWSmxzdWYxaApMaEtuQ0FxdmZFdEMxSUJWTUFvR0NDcUdTTTQ5QkFNQ0E0R01BRENCaUFKQ0FO +R1VwNDU5UDNhTWh0VFpkWEZxCm1jOFFWTTdySFIzWmxpOWttV3NHVmRKdmJVYnVIY1g2KzBBVTFT +OFIyRGhQQThvZUJ1UVQ3ZGJvdllmd2V1VmIKUWZSM0FrSUFwYUtlc2lOOUxOeTlhclNCR1hURktK +cXVCVDYzdjRiaTRmeUNOaGhkQzN3eEtldHNKMURDcElkcgpCbDhabmNKeFVNakNyZDdCTmxrQk5Q +N2pDR2RLcFBrPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t diff --git a/libraries/fabric-shim/test/unit/test-cert.base64 b/libraries/fabric-shim/test/unit/test-cert.base64 new file mode 100644 index 00000000..0181cedc --- /dev/null +++ b/libraries/fabric-shim/test/unit/test-cert.base64 @@ -0,0 +1 @@ +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrakNCOVFJSkFPZTRqL3NqV3ltRE1Bb0dDQ3FHU000OUJBTUNNQTR4RERBS0JnTlZCQU1NQTNSc2N6QWUKRncweU1EQTFNRGN4TmpNek5UUmFGdzB6TURBMU1EVXhOak16TlRSYU1BNHhEREFLQmdOVkJBTU1BM1JzY3pDQgptekFRQmdjcWhrak9QUUlCQmdVcmdRUUFJd09CaGdBRUFPcllIem5BZmVXZ3pVM3dVZ1Q1Ylk5NkUvT2ZMb3p4CitlUUFIL2Y5cDV3TUxuUzliMTJZczBHWitTNEdjSEYva1FBNlpoMVZBcFdKYnJ6MjFYVWhSbU5QQVRibDd3K2cKbytyazJ2cVZ5Y0E3dU1tUERlL0J2MVlldEh1WXZCd05vajVLVm9vVnVpUnJPOUlkU2N3ZUxkai9WOXoyQUkvQgpmUC9iN01aYWFwbUdUQjVFTUFvR0NDcUdTTTQ5QkFNQ0E0R0xBRENCaHdKQmVZQ1ZPclFWR3dmb1Q3OWRqRWpqCm1YVkVIL3hWcGk4b1ZhWkxVRm0yN2RldkYwb1ViZHowZSt2MzhZdkx6aERnWWh2MUtMQzhnYWxxaFdleTI2MmkKVVcwQ1FnRWRrUHFOYUZlbjF0WEQ5RWJoTjhSdVRyQWE3RGphNzZ3SWVMZUZSdFloZ0hCZlMvZmM0VWR3N1hWbgpQcG9nQ0xhM0ZMNkNDSUZIQWEyTU9kc3VMeld4V2c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t \ No newline at end of file diff --git a/libraries/fabric-shim/test/unit/test-cert.pem b/libraries/fabric-shim/test/unit/test-cert.pem index 0181cedc..11719b04 100644 --- a/libraries/fabric-shim/test/unit/test-cert.pem +++ b/libraries/fabric-shim/test/unit/test-cert.pem @@ -1 +1,11 @@ -LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrakNCOVFJSkFPZTRqL3NqV3ltRE1Bb0dDQ3FHU000OUJBTUNNQTR4RERBS0JnTlZCQU1NQTNSc2N6QWUKRncweU1EQTFNRGN4TmpNek5UUmFGdzB6TURBMU1EVXhOak16TlRSYU1BNHhEREFLQmdOVkJBTU1BM1JzY3pDQgptekFRQmdjcWhrak9QUUlCQmdVcmdRUUFJd09CaGdBRUFPcllIem5BZmVXZ3pVM3dVZ1Q1Ylk5NkUvT2ZMb3p4CitlUUFIL2Y5cDV3TUxuUzliMTJZczBHWitTNEdjSEYva1FBNlpoMVZBcFdKYnJ6MjFYVWhSbU5QQVRibDd3K2cKbytyazJ2cVZ5Y0E3dU1tUERlL0J2MVlldEh1WXZCd05vajVLVm9vVnVpUnJPOUlkU2N3ZUxkai9WOXoyQUkvQgpmUC9iN01aYWFwbUdUQjVFTUFvR0NDcUdTTTQ5QkFNQ0E0R0xBRENCaHdKQmVZQ1ZPclFWR3dmb1Q3OWRqRWpqCm1YVkVIL3hWcGk4b1ZhWkxVRm0yN2RldkYwb1ViZHowZSt2MzhZdkx6aERnWWh2MUtMQzhnYWxxaFdleTI2MmkKVVcwQ1FnRWRrUHFOYUZlbjF0WEQ5RWJoTjhSdVRyQWE3RGphNzZ3SWVMZUZSdFloZ0hCZlMvZmM0VWR3N1hWbgpQcG9nQ0xhM0ZMNkNDSUZIQWEyTU9kc3VMeld4V2c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t \ No newline at end of file +-----BEGIN CERTIFICATE----- +MIIBkjCB9QIJAOe4j/sjWymDMAoGCCqGSM49BAMCMA4xDDAKBgNVBAMMA3RsczAe +Fw0yMDA1MDcxNjMzNTRaFw0zMDA1MDUxNjMzNTRaMA4xDDAKBgNVBAMMA3RsczCB +mzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAOrYHznAfeWgzU3wUgT5bY96E/OfLozx ++eQAH/f9p5wMLnS9b12Ys0GZ+S4GcHF/kQA6Zh1VApWJbrz21XUhRmNPATbl7w+g +o+rk2vqVycA7uMmPDe/Bv1YetHuYvBwNoj5KVooVuiRrO9IdScweLdj/V9z2AI/B +fP/b7MZaapmGTB5EMAoGCCqGSM49BAMCA4GLADCBhwJBeYCVOrQVGwfoT79djEjj +mXVEH/xVpi8oVaZLUFm27devF0oUbdz0e+v38YvLzhDgYhv1KLC8galqhWey262i +UW0CQgEdkPqNaFen1tXD9EbhN8RuTrAa7Dja76wIeLeFRtYhgHBfS/fc4Udw7XVn +PpogCLa3FL6CCIFHAa2MOdsuLzWxWg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/libraries/fabric-shim/test/unit/test-key.base64 b/libraries/fabric-shim/test/unit/test-key.base64 new file mode 100644 index 00000000..d9368608 --- /dev/null +++ b/libraries/fabric-shim/test/unit/test-key.base64 @@ -0,0 +1 @@ +LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQmZXcmJQam9pdHcwd3AwUjA3dHdKeDhxaDZGenhpVFArekpFNEZHZ3EvTXh6Sy9kdUhIN2YKRjNzOWtmM1dkcWxYNlkwTnM2K3VRR2hmK2laODZtd01zaU9nQndZRks0RUVBQ09oZ1lrRGdZWUFCQURxMkI4NQp3SDNsb00xTjhGSUUrVzJQZWhQem55Nk04Zm5rQUIvMy9hZWNEQzUwdlc5ZG1MTkJtZmt1Qm5CeGY1RUFPbVlkClZRS1ZpVzY4OXRWMUlVWmpUd0UyNWU4UG9LUHE1TnI2bGNuQU83akpqdzN2d2I5V0hyUjdtTHdjRGFJK1NsYUsKRmJva2F6dlNIVW5NSGkzWS8xZmM5Z0NQd1h6LzIrekdXbXFaaGt3ZVJBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQ== \ No newline at end of file diff --git a/libraries/fabric-shim/test/unit/test-key.pem b/libraries/fabric-shim/test/unit/test-key.pem index d9368608..f42cbdc2 100644 --- a/libraries/fabric-shim/test/unit/test-key.pem +++ b/libraries/fabric-shim/test/unit/test-key.pem @@ -1 +1,7 @@ -LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQmZXcmJQam9pdHcwd3AwUjA3dHdKeDhxaDZGenhpVFArekpFNEZHZ3EvTXh6Sy9kdUhIN2YKRjNzOWtmM1dkcWxYNlkwTnM2K3VRR2hmK2laODZtd01zaU9nQndZRks0RUVBQ09oZ1lrRGdZWUFCQURxMkI4NQp3SDNsb00xTjhGSUUrVzJQZWhQem55Nk04Zm5rQUIvMy9hZWNEQzUwdlc5ZG1MTkJtZmt1Qm5CeGY1RUFPbVlkClZRS1ZpVzY4OXRWMUlVWmpUd0UyNWU4UG9LUHE1TnI2bGNuQU83akpqdzN2d2I5V0hyUjdtTHdjRGFJK1NsYUsKRmJva2F6dlNIVW5NSGkzWS8xZmM5Z0NQd1h6LzIrekdXbXFaaGt3ZVJBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQ== \ No newline at end of file +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIBfWrbPjoitw0wp0R07twJx8qh6FzxiTP+zJE4FGgq/MxzK/duHH7f +F3s9kf3WdqlX6Y0Ns6+uQGhf+iZ86mwMsiOgBwYFK4EEACOhgYkDgYYABADq2B85 +wH3loM1N8FIE+W2PehPzny6M8fnkAB/3/aecDC50vW9dmLNBmfkuBnBxf5EAOmYd +VQKViW689tV1IUZjTwE25e8PoKPq5Nr6lcnAO7jJjw3vwb9WHrR7mLwcDaI+SlaK +FbokazvSHUnMHi3Y/1fc9gCPwXz/2+zGWmqZhkweRA== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/libraries/fabric-shim/types/index.d.ts b/libraries/fabric-shim/types/index.d.ts index b11dee63..64749079 100644 --- a/libraries/fabric-shim/types/index.d.ts +++ b/libraries/fabric-shim/types/index.d.ts @@ -45,6 +45,24 @@ declare module 'fabric-shim' { static newLogger(name: string): Logger; static start(chaincode: ChaincodeInterface): any; static success(payload?: Uint8Array): ChaincodeResponse; + static server(chaincode: ChaincodeInterface, serverOpts: ChaincodeServerOpts): ChaincodeServer; + } + + export class ChaincodeServer { + constructor(chaincode: ChaincodeInterface, serverOpts: ChaincodeServerOpts); + start(): Promise; + } + + export interface ChaincodeServerOpts { + ccid: string; + address: string; + tlsProps: ChaincodeServerTLSProperties; + } + + export interface ChaincodeServerTLSProperties { + key: Buffer; + cert: Buffer; + clientCACerts: Buffer; } export class ClientIdentity implements IClientIdentity { diff --git a/test/chaincodes/server/.dockerignore b/test/chaincodes/server/.dockerignore new file mode 100644 index 00000000..f8b6bbd6 --- /dev/null +++ b/test/chaincodes/server/.dockerignore @@ -0,0 +1,3 @@ +node_modules +package.lock.json +package diff --git a/test/chaincodes/server/Dockerfile b/test/chaincodes/server/Dockerfile new file mode 100644 index 00000000..8dbb1a7e --- /dev/null +++ b/test/chaincodes/server/Dockerfile @@ -0,0 +1,7 @@ +FROM hyperledger/fabric-nodeenv:latest + +ADD . /opt/chaincode +RUN cd /opt/chaincode; npm install + +WORKDIR /opt/chaincode +ENTRYPOINT ["npm", "start"] diff --git a/test/chaincodes/server/index.js b/test/chaincodes/server/index.js new file mode 100644 index 00000000..7d1accc5 --- /dev/null +++ b/test/chaincodes/server/index.js @@ -0,0 +1,30 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ +"use strict"; + +const { Contract } = require('fabric-contract-api'); + +class ServerTestChaincode extends Contract { + async unknownTransaction({stub}) { + const {fcn, params} = stub.getFunctionAndParameters(); + throw new Error(`Could not find chaincode function: ${fcn}`); + } + + constructor() { + super('org.mynamespace.server'); + } + + async putValue(ctx, value) { + await ctx.stub.putState('state1', Buffer.from(JSON.stringify(value))); + } + + async getValue(ctx) { + const value = await ctx.stub.getState('state1'); + return JSON.parse(value.toString()); + } +} + +exports.contracts = [ ServerTestChaincode ]; diff --git a/test/chaincodes/server/package.json b/test/chaincodes/server/package.json new file mode 100644 index 00000000..997f8322 --- /dev/null +++ b/test/chaincodes/server/package.json @@ -0,0 +1,21 @@ +{ + "name": "chaincode", + "description": "Chaincode server", + "engines": { + "node": "^12.13.0", + "npm": ">=5.3.0" + }, + "scripts": { + "start": "fabric-chaincode-node server" + }, + "main": "index.js", + "engine-strict": true, + "engineStrict": true, + "version": "1.0.0", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "fabric-shim": "2.1.3-unstable", + "fabric-contract-api": "2.1.3-unstable" + } +} diff --git a/test/chaincodes/server/package/connection.json b/test/chaincodes/server/package/connection.json new file mode 100644 index 00000000..96f2571f --- /dev/null +++ b/test/chaincodes/server/package/connection.json @@ -0,0 +1,5 @@ +{ + "address": "cc-server:9999", + "dial_timeout": "10s", + "tls_required": false +} diff --git a/test/chaincodes/server/package/metadata.json b/test/chaincodes/server/package/metadata.json new file mode 100644 index 00000000..31b9bba2 --- /dev/null +++ b/test/chaincodes/server/package/metadata.json @@ -0,0 +1,5 @@ +{ + "path": "", + "type": "external", + "label": "server_v0" +} diff --git a/test/e2e/scenario.js b/test/e2e/scenario.js index 86c7aafd..a825327c 100644 --- a/test/e2e/scenario.js +++ b/test/e2e/scenario.js @@ -185,4 +185,7 @@ const installChaincode = async () => { ]); }; -exports.default = series(installChaincode, instantiateChaincode, invokeFunctions, queryFunctions); +const clientTests = series(installChaincode, instantiateChaincode, invokeFunctions, queryFunctions); +const serverTests = require('./server').default; + +exports.default = series(clientTests, serverTests); diff --git a/test/e2e/server.js b/test/e2e/server.js new file mode 100644 index 00000000..e3cddc28 --- /dev/null +++ b/test/e2e/server.js @@ -0,0 +1,165 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const {series} = require('gulp'); + +const util = require('util'); +const path = require('path'); + +const { shell: runcmds , getTLSArgs, getPeerAddresses } = require('toolchain'); +const ip = require('ip'); + +const CHANNEL_NAME = 'mychannel'; + +const chaincodeDir = path.join(__dirname, '..', '..', 'test', 'chaincodes', 'server'); + +async function packageChaincode() { + await runcmds([ + util.format( + 'tar -C %s/package -cvzf %s/package/code.tar.gz connection.json', + chaincodeDir, chaincodeDir + ), + util.format( + 'tar -C %s/package -cvzf %s/package/chaincode.tar.gz code.tar.gz metadata.json', + chaincodeDir, chaincodeDir + ), + ]); +} + +async function buildChaincode() { + const npmrc = path.join(chaincodeDir, '.npmrc'); + + await runcmds([ + `echo "registry=http://${ip.address()}:4873" > ${npmrc}`, + util.format( + 'docker build --no-cache -t chaincode-e2e-server %s', + chaincodeDir + ), + `rm -f ${npmrc}` + ]); +} + +async function installChaincode() { + const peerInstall = 'peer lifecycle chaincode install /opt/gopath/src/github.com/chaincode/server/package/chaincode.tar.gz'; + + await runcmds([ + util.format( + 'docker exec %s %s', + 'org1_cli', + peerInstall + ), + util.format( + 'docker exec %s %s', + 'org2_cli', + peerInstall + ) + ]); +}; + +function findPackageId(queryOutput, label) { + const output = JSON.parse(queryOutput); + + const cc = output.installed_chaincodes.filter((chaincode) => chaincode.label === label); + if (cc.length !== 1) { + throw new Error('Failed to find installed chaincode'); + } + + return cc[0].package_id; +} + +async function instantiateChaincode() { + const endorsementPolicy = '"OR (\'Org1MSP.member\', \'Org2MSP.member\')"'; + const queryInstalled = util.format( + 'peer lifecycle chaincode queryinstalled --output json' + ); + const sequence = 1; + + const approveChaincode = util.format( + 'peer lifecycle chaincode approveformyorg -o %s %s -C %s -n %s -v %s --package-id %s --sequence %d --signature-policy %s', + 'orderer.example.com:7050', + getTLSArgs(), + CHANNEL_NAME, + 'server', + 'v0', + '%s', // To be filled in for each org + sequence, + endorsementPolicy + ); + + const outputs = await runcmds([ + util.format( + 'docker exec %s %s', + 'org1_cli', + queryInstalled + ), + util.format( + 'docker exec %s %s', + 'org2_cli', + queryInstalled + ), + ]); + + const packageIdOrg1 = findPackageId(outputs[0], 'server_v0'); + const packageIdOrg2 = findPackageId(outputs[1], 'server_v0'); + + // TODO: Assuming the two package IDs are the same + await runcmds([ + // Start the CC Server container + `docker run -e CORE_CHAINCODE_ID=${packageIdOrg1} -e CORE_CHAINCODE_ADDRESS=0.0.0.0:9999 -h cc-server --name cc-server -d --network node_default chaincode-e2e-server`, + // Approve the chaincode definition by each org + util.format('docker exec %s %s', + 'org1_cli', + util.format(approveChaincode, packageIdOrg1) + ), + util.format('docker exec %s %s', + 'org2_cli', + util.format(approveChaincode, packageIdOrg2) + ), + // Commit the chaincode definition + util.format('docker exec org1_cli peer lifecycle chaincode commit -o %s %s -C %s -n %s -v %s --sequence %d --signature-policy %s %s', + 'orderer.example.com:7050', + getTLSArgs(), + CHANNEL_NAME, + 'server', + 'v0', + sequence, + endorsementPolicy, + getPeerAddresses() + ) + ]); +} + +const invokeFunctions = async () => { + const args = util.format('docker exec org1_cli peer chaincode invoke %s -C %s -n %s -c %s --waitForEvent', + getTLSArgs(), + CHANNEL_NAME, + 'server', + '\'{"Args":["putValue","\'42\'"]}\''); + + await runcmds([args]); +}; + +const queryFunctions = async () => { + const args = util.format('docker exec org1_cli peer chaincode query %s -C %s -n %s -c %s', + getTLSArgs(), + CHANNEL_NAME, + 'server', + '\'{"Args":["getValue"]}\''); + + const ret = await runcmds([args]); + + const response = JSON.parse(ret[0]); + + if (response !== 42) { + throw new Error("Unexpected result from chaincode"); + } +} + +exports.default = series( + packageChaincode, buildChaincode, installChaincode, instantiateChaincode, + invokeFunctions, queryFunctions +); diff --git a/tools/toolchain/fabric.js b/tools/toolchain/fabric.js index 66b79846..d1b44f44 100644 --- a/tools/toolchain/fabric.js +++ b/tools/toolchain/fabric.js @@ -78,9 +78,12 @@ const _docker_clean = async () => { // stop and remove chaincode docker instances 'docker kill $(docker ps | grep "dev-peer0.org[12].example.com" | awk \'{print $1}\') || echo ok', 'docker rm $(docker ps -a | grep "dev-peer0.org[12].example.com" | awk \'{print $1}\') || echo ok', + 'docker kill $(docker ps | grep "cc-server" | awk \'{print $1}\') || echo ok', + 'docker rm $(docker ps -a | grep "cc-server" | awk \'{print $1}\') || echo ok', // remove chaincode images so that they get rebuilt during test 'docker rmi $(docker images | grep "^dev-peer0.org[12].example.com" | awk \'{print $3}\') || echo ok', + 'docker rmi $(docker images | grep "^chaincode-e2e-server" | awk \'{print $3}\') || echo ok', // clean up all the containers created by docker-compose util.format('docker-compose -f %s down --volumes', fs.realpathSync(path.join(dockerComposeDir, 'docker-compose-cli.yaml'))), @@ -140,6 +143,10 @@ const _generate_config = async () => { 'docker exec cli cp /etc/hyperledger/fabric/core.yaml %s', dockerCfgPath ), + util.format( + 'docker exec cli sed -i \'s/externalBuilders: \\[\\]/externalBuilders: [{path: \\/opt\\/chaincode, name: test}]/\' %s/core.yaml', + dockerCfgPath + ), util.format( 'docker exec cli sh %s/rename_sk.sh', dockerCfgPath @@ -197,10 +204,18 @@ async function _channel_create() { ]); } +async function _peer_setup() { + // Install the 'jq' command in the peer containers to run external builder scripts. + await runcmds([ + 'docker exec peer0.org1.example.com apk add jq', + 'docker exec peer0.org2.example.com apk add jq', + ]); +} + const channelSetup = series(_channel_create, _channel_init); // -- -const startFabric = series(dockerReady, channelSetup); +const startFabric = series(dockerReady, _peer_setup, channelSetup); exports.default = startFabric; exports.stopFabric = series(_docker_clean); diff --git a/tools/toolchain/network/docker-compose/docker-compose-base.yaml b/tools/toolchain/network/docker-compose/docker-compose-base.yaml index a1047260..02e4a6eb 100644 --- a/tools/toolchain/network/docker-compose/docker-compose-base.yaml +++ b/tools/toolchain/network/docker-compose/docker-compose-base.yaml @@ -99,6 +99,8 @@ services: command: peer node start --peer-chaincodedev=${DOCKER_DEVMODE} volumes: - /var/run/:/host/var/run/ + - ../external:/opt/chaincode/bin:ro + - ../crypto-material/core.yaml:/etc/hyperledger/fabric/core.yaml:ro clibase: extends: diff --git a/tools/toolchain/network/external/build b/tools/toolchain/network/external/build new file mode 100755 index 00000000..6773ec16 --- /dev/null +++ b/tools/toolchain/network/external/build @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +SOURCE="$1" +OUTPUT="$3" + +if [ ! -f "${SOURCE}/connection.json" ]; then + echo "Error: ${SOURCE}/connection.json not found" 1>&2 + exit 1 +fi + +cp "${SOURCE}/connection.json" "${OUTPUT}/connection.json" + +if [ -d "${SOURCE}/metadata" ]; then + cp -a ${SOURCE}/metadata ${OUTPUT}/metadata +fi + +exit 0 diff --git a/tools/toolchain/network/external/detect b/tools/toolchain/network/external/detect new file mode 100755 index 00000000..6ef69b54 --- /dev/null +++ b/tools/toolchain/network/external/detect @@ -0,0 +1,15 @@ +#!/bin/sh +# +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +METADIR="$2" + +if [ `jq -r .type "${METADIR}/metadata.json"` = "external" ]; then + exit 0 +fi + +exit 1 diff --git a/tools/toolchain/network/external/release b/tools/toolchain/network/external/release new file mode 100755 index 00000000..b4ea54c1 --- /dev/null +++ b/tools/toolchain/network/external/release @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +BUILD="$1" +RELEASE="$2" + +if [ -d "${BUILD}/metadata" ]; then + cp -a "${BUILD}/metadata/*" "${RELEASE}/" +fi + +if [ -f "${BUILD}/connection.json" ]; then + mkdir -p "${RELEASE}/chaincode/server" + cp "${BUILD}/connection.json" "${RELEASE}/chaincode/server" + + # TODO: TLS + + exit 0 +fi