Skip to content

Commit

Permalink
Add prototype node sdk and sample client
Browse files Browse the repository at this point in the history
Signed-off-by: andrew-coleman <andrew_coleman@uk.ibm.com>
andrew-coleman committed Aug 28, 2020
1 parent 3521681 commit 41dd44c
Showing 6 changed files with 340 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.vscode/**
node_modules
scenario/fixtures/crypto-material/crypto-config/
scenario/fixtures/crypto-material/genesis.block
scenario/fixtures/crypto-material/mychannel.block
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -11,9 +11,15 @@ To use
- `export DISCOVERY_AS_LOCALHOST=TRUE`
- `go run gateway.go -h peer0.org1.example.com -p 7051 -m Org1MSP -id ../../fabric-samples/fabcar/javascript/wallet/gateway.id -tlscert ../../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/tlsca/tlsca.org1.example.com-cert.pem`
- where the `id` flag points to the wallet id file created for the gateway identity
- In a separate command window:
- `cd ..../fabric-gateway/client/go`
- `go run client2.go -id ../../../fabric-samples/fabcar/javascript/wallet/appUser.id`
- In a separate command window, run the sample client:
- Either Go:
- `cd ..../fabric-gateway/client/go`
- `go run client2.go -id ../../../fabric-samples/fabcar/javascript/wallet/appUser.id`
- Or Node
- `cd ..../fabric-gateway/client/node/sdk`
- `npm install`
- `cd ..`
- `node client.js`

Running the scenario tests
- Install Godog (https://github.com/cucumber/godog#install)
35 changes: 35 additions & 0 deletions client/node/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright 2020 IBM All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

'use strict';

const {Gateway, Signer} = require('./sdk/sdk');
const fs = require('fs');

(async() => {
try {
const mspid = "Org1MSP"
const certPath = "../../scenario/fixtures/crypto-material/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem"
const keyPath = "../../scenario/fixtures/crypto-material/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/key.pem"
const cert = fs.readFileSync(certPath);
const key = fs.readFileSync(keyPath);
const signer = new Signer(mspid, cert, key);
const gateway = new Gateway();
gateway.connect('localhost:1234', signer);
const network = gateway.getNetwork('mychannel');
const contract = network.getContract('fabcar');
let result = await contract.evaluateTransaction('queryAllCars');
console.log(result);
await contract.submitTransaction("createCar", "CAR12", "VW", "Polo", "Grey", "Mary");
result = await contract.evaluateTransaction("queryCar", "CAR12");
console.log(result);
await contract.submitTransaction("changeCarOwner", "CAR12", "Archie");
result = await contract.evaluateTransaction("queryCar", "CAR12");
console.log(result);
} catch(err) {
console.log(err);
}
})()
18 changes: 18 additions & 0 deletions client/node/sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "fabric-gateway",
"version": "0.0.1",
"description": "Node SDK client library for Hyperledger Fabric Gateway",
"main": "sdk.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@grpc/grpc-js": "^1.1.5",
"@grpc/proto-loader": "^0.5.5",
"elliptic": "^6.5.3",
"jsrsasign": "^9.1.3",
"protobufjs": "^6.10.1"
}
}
274 changes: 274 additions & 0 deletions client/node/sdk/sdk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/*
Copyright 2020 IBM All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

'use strict';

const PROTO_PATH = [
__dirname + '/../../../protos/gateway.proto',
__dirname + '/../../../../fabric-protos/peer/proposal.proto',
__dirname + '/../../../../fabric-protos/peer/proposal_response.proto',
__dirname + '/../../../../fabric-protos/peer/chaincode.proto',
__dirname + '/../../../../fabric-protos/common/common.proto',
__dirname + '/../../../../fabric-protos/common/policies.proto',
__dirname + '/../../../../fabric-protos/msp/identities.proto',
__dirname + '/../../../../fabric-protos/msp/msp_principal.proto',
];
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const protobuf = require('protobufjs');
const fs = require('fs');
const crypto = require('crypto');
const elliptic = require('elliptic');
const EC = elliptic.ec;
const ecdsaCurve = elliptic.curves['p256'];
const ecdsa = new EC(ecdsaCurve);
const { KEYUTIL } = require('jsrsasign');

const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});

const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
//console.log(protoDescriptor.protos.Gateway);
// The protoDescriptor object has the full package hierarchy
const protos = protoDescriptor.protos;

const root = protobuf.loadSync(PROTO_PATH)
//console.log(root)

class Gateway {
constructor() { }

async connect(url, signer) {
this.url = url;
this.signer = signer;
this.stub = new protos.Gateway(url, grpc.credentials.createInsecure());
this.evaluate = signedProposal => {
return new Promise((resolve, reject) => {
this.stub.evaluate(signedProposal, function(err, result) {
if (err) reject(err);
resolve(result.value.toString());
});
})
};
this.prepare = signedProposal => {
return new Promise((resolve, reject) => {
this.stub.prepare(signedProposal, function(err, result) {
if (err) reject(err);
resolve(result);
});
})
};
this.commit = preparedTransaction => {
return new Promise((resolve, reject) => {
const call = this.stub.commit(preparedTransaction);
call.on('data', function(event) {
console.log('Event received: ', event.value.toString());
});
call.on('end', function() {
resolve()
});
call.on('error', function(e) {
// An error has occurred and the stream has been closed.
reject(e);
});
call.on('status', function(status) {
// process status
});
})
};
}

getNetwork(networkName) {
return new Network(networkName, this);
}
}

class Network {
constructor(name, gateway) {
this.name = name;
this.gateway = gateway;
}

getContract(contractName) {
return new Contract(contractName, this);
}
}

class Contract {
constructor(name, network) {
this.name = name;
this.network = network;
}

createTransaction(transactionName) {
return new Transaction(transactionName, this);
}

async evaluateTransaction(name, ...args) {
return this.createTransaction(name).evaluate(...args);
}

async submitTransaction(name, ...args) {
return this.createTransaction(name).submit(...args);
}
}

class Transaction {
constructor(name, contract) {
this.name = name;
this.contract = contract;
}

async evaluate(...args) {
const gw = this.contract.network.gateway;
const proposal = createProposal(this, args, gw.signer);
const signedProposal = signProposal(proposal, gw.signer);
return gw.evaluate(signedProposal);
}

async submit(...args) {
const gw = this.contract.network.gateway;
const proposal = createProposal(this, args, gw.signer);
const signedProposal = signProposal(proposal, gw.signer);
const preparedTxn = await gw.prepare(signedProposal);
preparedTxn.envelope.signature = gw.signer.sign(preparedTxn.envelope.payload);
await gw.commit(preparedTxn);
return preparedTxn.response.value.toString();
}
}

class Signer {
constructor(mspid, certPem, keyPem) {
this.mspid = mspid;
this.cert = certPem;
const { prvKeyHex } = KEYUTIL.getKey(keyPem.toString()); // convert the pem encoded key to hex encoded private key
this.signKey = ecdsa.keyFromPrivate(prvKeyHex, 'hex');
this.serialized = marshal('msp.SerializedIdentity', {
mspid: mspid,
idBytes: certPem
})
}

sign(msg) {
const hash = crypto.createHash('sha256');
hash.update(msg);
const digest = hash.digest();
const sig = ecdsa.sign(Buffer.from(digest, 'hex'), this.signKey);
_preventMalleability(sig, ecdsaCurve);
return sig.toDER();
}

serialize() {
return this.serialized;
}
}

function _preventMalleability(sig, curve) {

const halfOrder = curve.n.shrn(1);
if (!halfOrder) {
throw new Error('Can not find the half order needed to calculate "s" value for immalleable signatures. Unsupported curve name: ' + curve);
}

if (sig.s.cmp(halfOrder) === 1) {
const bigNum = curve.n;
sig.s = bigNum.sub(sig.s);
}

return sig;
}

function createProposal(txn, args, signer) {
const creator = signer.serialize();
const nonce = crypto.randomBytes(24);
const hash = crypto.createHash('sha256');
hash.update(nonce);
hash.update(creator);
const txid = hash.digest('hex');

const hdr = {
channelHeader: marshal('common.ChannelHeader', {
type: 3, // ENDORSER_TRANSACTION - TODO lookup enum
txId: txid,
timestamp: create('google.protobuf.Timestamp', {
timestamp: Date.now()
}),
channelId: txn.contract.network.name,
extension: marshal('protos.ChaincodeHeaderExtension', {
chaincodeId: create('protos.ChaincodeID', {
name: txn.contract.name
})
}),
epoch: 0
}),
signatureHeader: marshal('common.SignatureHeader', {
creator: signer.serialize(),
nonce: nonce
})
}

const allArgs = [Buffer.from(txn.name)];
args.forEach(arg => allArgs.push(Buffer.from(arg)));

const ccis = marshal('protos.ChaincodeInvocationSpec', {
chaincodeSpec: create('protos.ChaincodeSpec', {
type: 2,
chaincodeId: create('protos.ChaincodeID', {
name: txn.contract.name
}),
input: create('protos.ChaincodeInput', {
args: allArgs
})
})
})

const proposal = {
header: marshal('common.Header', hdr),
payload: marshal('protos.ChaincodeProposalPayload', {
input: ccis,
transientMap: null
})
}

return proposal;
}

function signProposal(proposal, signer) {
const payload = marshal('protos.Proposal', proposal);
const signature = signer.sign(payload);
return {
proposal_bytes: payload,
signature: signature
};
}

function create(name, payload) {
const type = root.lookupType(name);
const errMsg = type.verify(payload);
if (errMsg) console.log('ERROR: ', errMsg);
const message = type.create(payload);
//console.log('message:', message);
return message;
}

function marshal(name, payload) {
const type = root.lookupType(name);
const errMsg = type.verify(payload);
if (errMsg) console.log('ERROR: ', errMsg);
const message = type.create(payload);
//console.log('message:', message);
const buffer = type.encode(message).finish();
//console.log('buffer: ', buffer)
return buffer;
}

module.exports = { Gateway, Network, Contract, Transaction, Signer }
3 changes: 3 additions & 0 deletions pkg/gateway/functions2.go
Original file line number Diff line number Diff line change
@@ -27,6 +27,9 @@ func (gs *GatewayServer) Evaluate(ctx context.Context, signedProposal *peer.Sign
return nil, errors.Wrap(err, "Failed to unpack channel header: ")
}
endorsers := gs.registry.getEndorsers(channelHeader.ChannelId)
if len(endorsers) == 0 {
return nil, errors.New("No endorsing peers found for channel: " + channelHeader.ChannelId)
}
response, err := endorsers[0].ProcessProposal(ctx, signedProposal) // choose suitable peer
if err != nil {
return nil, errors.Wrap(err, "Failed to evaluate transaction: ")

0 comments on commit 41dd44c

Please sign in to comment.