Skip to content
This repository was archived by the owner on Apr 22, 2025. It is now read-only.

Commit 7b05d75

Browse files
committed
[FABN-896] sign transaction offline
This CR split the "sign" a transaction from SDK. So that a user can use his private key offline. By using the new Channel interface, the endorse/commit will be: 1. build endorse proposal 2. sign the proposal 3. submit the signed proposal Change-Id: I044fb9d0a963a0a2ea295e4a01cfe318126b40b2 Signed-off-by: zhaochy <zhaochy_2015@hotmail.com>
1 parent eb56c95 commit 7b05d75

File tree

8 files changed

+599
-125
lines changed

8 files changed

+599
-125
lines changed

build/tasks/test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ gulp.task('run-full', ['clean-up', 'lint', 'pre-test', 'compile', 'docker-ready'
177177
'test/integration/network-e2e/e2e.js',
178178
// channel: mychannel, chaincode: end2endnodesdk:v0/v1
179179
'test/integration/e2e.js',
180+
'test/integration/signTransactionOffline.js',
180181
'test/integration/query.js',
181182
'test/integration/fabric-ca-affiliation-service-tests.js',
182183
'test/integration/fabric-ca-identity-service-tests.js',

fabric-client/lib/Channel.js

Lines changed: 232 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const MSPManager = require('./msp/msp-manager.js');
2222
const Policy = require('./Policy.js');
2323
const Constants = require('./Constants.js');
2424
const CollectionConfig = require('./SideDB.js');
25+
const { Identity } = require('./msp/identity.js');
26+
const ChannelHelper = require('./utils/ChannelHelper');
2527

2628
const _ccProto = grpc.load(__dirname + '/protos/peer/chaincode.proto').protos;
2729
const _transProto = grpc.load(__dirname + '/protos/peer/transaction.proto').protos;
@@ -43,7 +45,7 @@ const _discoveryProto = grpc.load(__dirname + '/protos/discovery/protocol.proto'
4345
const _gossipProto = grpc.load(__dirname + '/protos/gossip/message.proto').gossip;
4446
const _collectionProto = grpc.load(__dirname + '/protos/common/collection.proto').common;
4547

46-
const ImplicitMetaPolicy_Rule = {0: 'ANY', 1: 'ALL', 2: 'MAJORITY'};
48+
const ImplicitMetaPolicy_Rule = { 0: 'ANY', 1: 'ALL', 2: 'MAJORITY' };
4749

4850
const PEER_NOT_ASSIGNED_MSG = 'Peer with name "%s" not assigned to this channel';
4951
const ORDERER_NOT_ASSIGNED_MSG = 'Orderer with name "%s" not assigned to this channel';
@@ -92,7 +94,7 @@ const Channel = class {
9294
}
9395
const channelNameRegxChecker = sdk_utils.getConfigSetting('channel-name-regx-checker');
9496
if (channelNameRegxChecker) {
95-
const {pattern, flags} = channelNameRegxChecker;
97+
const { pattern, flags } = channelNameRegxChecker;
9698
const namePattern = new RegExp(pattern ? pattern : '', flags ? flags : '');
9799
if (!(name.match(namePattern))) {
98100
throw new Error(util.format('Failed to create Channel. channel name should match Regex %s, but got %j', namePattern, name));
@@ -485,7 +487,7 @@ const Channel = class {
485487
const keys = Object.keys(msps);
486488
for (const key in keys) {
487489
const msp = msps[keys[key]];
488-
const msp_org = {id: msp.getId()};
490+
const msp_org = { id: msp.getId() };
489491
logger.debug('%s - found %j', method, msp_org);
490492
orgs.push(msp_org);
491493
}
@@ -871,7 +873,7 @@ const Channel = class {
871873
seekPayload.setHeader(seekHeader);
872874
seekPayload.setData(seekInfo.toBuffer());
873875
// building manually or will get protobuf errors on send
874-
const envelope = client_utils.toEnvelope(client_utils.signProposal(signer,seekPayload));
876+
const envelope = client_utils.toEnvelope(client_utils.signProposal(signer, seekPayload));
875877

876878
return orderer.sendDeliver(envelope);
877879
}
@@ -985,7 +987,7 @@ const Channel = class {
985987
discovery_request.setQueries(queries);
986988

987989
// build up the outbound request object
988-
const signed_request = client_utils.toEnvelope(client_utils.signProposal(signer,discovery_request));
990+
const signed_request = client_utils.toEnvelope(client_utils.signProposal(signer, discovery_request));
989991

990992
const response = await target_peer.sendDiscovery(signed_request);
991993
logger.debug('%s - processing discovery response', method);
@@ -1479,7 +1481,7 @@ const Channel = class {
14791481
seekPayload.setData(seekInfo.toBuffer());
14801482

14811483
// building manually or will get protobuf errors on send
1482-
let envelope = client_utils.toEnvelope(client_utils.signProposal(signer,seekPayload));
1484+
let envelope = client_utils.toEnvelope(client_utils.signProposal(signer, seekPayload));
14831485
// This will return us a block
14841486
let block = await orderer.sendDeliver(envelope);
14851487
logger.debug('%s - good results from seek block ', method); // :: %j',results);
@@ -1537,7 +1539,7 @@ const Channel = class {
15371539
seekPayload.setData(seekInfo.toBuffer());
15381540

15391541
// building manually or will get protobuf errors on send
1540-
envelope = client_utils.toEnvelope(client_utils.signProposal(signer,seekPayload));
1542+
envelope = client_utils.toEnvelope(client_utils.signProposal(signer, seekPayload));
15411543
// this will return us a block
15421544
block = await orderer.sendDeliver(envelope);
15431545
if (!block) {
@@ -2217,8 +2219,8 @@ const Channel = class {
22172219
const lcccSpec = {
22182220
// type: _ccProto.ChaincodeSpec.Type.GOLANG,
22192221
type: client_utils.translateCCType(request.chaincodeType),
2220-
chaincode_id: {name: Constants.LSCC},
2221-
input: {args: lcccSpec_args}
2222+
chaincode_id: { name: Constants.LSCC },
2223+
input: { args: lcccSpec_args }
22222224
};
22232225

22242226
const channelHeader = client_utils.buildChannelHeader(
@@ -2247,8 +2249,9 @@ const Channel = class {
22472249
* discovery service if no targets are specified.
22482250
* @property {string} chaincodeId - Required. The id of the chaincode to process
22492251
* the transaction proposal
2250-
* @property {TransactionID} txId - Required. TransactionID object with the
2251-
* transaction id and nonce
2252+
* @property {TransactionID} txId - Optional. TransactionID object with the
2253+
* transaction id and nonce. txId is required for [sendTransactionProposal]{@link Channel#sendTransactionProposal}
2254+
* and optional for [generateUnsignedProposal]{@link Channel#generateUnsignedProposal}
22522255
* @property {map} transientMap - Optional. <string, byte[]> map that can be
22532256
* used by the chaincode but not
22542257
* saved in the ledger, such as cryptographic information for encryption
@@ -2375,8 +2378,8 @@ const Channel = class {
23752378

23762379
const invokeSpec = {
23772380
type: _ccProto.ChaincodeSpec.Type.GOLANG,
2378-
chaincode_id: {name: request.chaincodeId},
2379-
input: {args: args}
2381+
chaincode_id: { name: request.chaincodeId },
2382+
input: { args: args }
23802383
};
23812384

23822385
let signer = null;
@@ -2400,7 +2403,7 @@ const Channel = class {
24002403
const proposal = client_utils.buildProposal(invokeSpec, header, request.transientMap);
24012404
const signed_proposal = client_utils.signProposal(signer, proposal);
24022405

2403-
return {signed: signed_proposal, source: proposal};
2406+
return { signed: signed_proposal, source: proposal };
24042407
}
24052408

24062409
/**
@@ -2452,7 +2455,7 @@ const Channel = class {
24522455
async sendTransaction(request, timeout) {
24532456
logger.debug('sendTransaction - start :: channel %s', this);
24542457

2455-
if(!request){
2458+
if (!request) {
24562459
throw Error('Missing input request object on the transaction request');
24572460
}
24582461
// Verify that data is being passed in
@@ -2490,7 +2493,7 @@ const Channel = class {
24902493
use_admin_signer = request.txId.isAdmin();
24912494
}
24922495

2493-
const envelope = Channel.buildEnvelope(this._clientContext,chaincodeProposal,endorsements,proposalResponse,use_admin_signer);
2496+
const envelope = Channel.buildEnvelope(this._clientContext, chaincodeProposal, endorsements, proposalResponse, use_admin_signer);
24942497

24952498
if (this._commit_handler) {
24962499
const params = {
@@ -2503,15 +2506,225 @@ const Channel = class {
25032506
} else {
25042507
// verify that we have an orderer configured
25052508
const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name);
2506-
return orderer.sendBroadcast(envelope,timeout);
2509+
return orderer.sendBroadcast(envelope, timeout);
2510+
}
2511+
}
2512+
2513+
2514+
/**
2515+
* @typedef {Object} ProposalRequest
2516+
* @property {string} fcn - Required. The function name.
2517+
* @property {string[]} args - Required. Arguments to send to chaincode.
2518+
* @property {string} chaincodeId - Required. ChaincodeId.
2519+
* @property {Buffer} argbytes - Optional. Include when an argument must be included as bytes.
2520+
* @property {map} transientMap - Optional. <sting, byte[]> The Map that can be
2521+
* used by the chaincode but not saved in the ledger, such as
2522+
* cryptographic information for encryption.
2523+
*/
2524+
2525+
2526+
/**
2527+
* Generates the endorse proposal bytes for a transaction
2528+
*
2529+
* Current the [sendTransactionProposal]{@link Channel#sendTransactionProposal}
2530+
* sign a transaction using the user identity from SDK's context (which
2531+
* contains the user's private key).
2532+
*
2533+
* This method is designed to build the proposal bytes at SDK side,
2534+
* and user can sign this proposal with their private key, and send
2535+
* the signed proposal to peer by [sendSignedProposal]
2536+
*
2537+
* so the user's private
2538+
* key would not be required at SDK side.
2539+
*
2540+
* @param {ProposalRequest} request chaincode invoke request
2541+
* @param {string} mspId the mspId for this identity
2542+
* @param {string} certificate PEM encoded certificate
2543+
* @param {boolean} admin if this transaction is invoked by admin
2544+
* @returns {Proposal}
2545+
*/
2546+
generateUnsignedProposal(request, mspId, certificate, admin) {
2547+
const method = 'generateUnsignedProposal';
2548+
logger.debug('%s - start', method);
2549+
2550+
const args = [];
2551+
args.push(Buffer.from(request.fcn ? request.fcn : 'invoke', 'utf8'));
2552+
logger.debug('%s - adding function arg:%s', method, request.fcn ? request.fcn : 'invoke');
2553+
2554+
// check request && request.chaincodeId
2555+
let errorMsg = client_utils.checkProposalRequest(request, false);
2556+
2557+
if (!request.args) {
2558+
errorMsg = 'Missing "args" in Transaction proposal request';
2559+
}
2560+
if (!Array.isArray(request.args)) {
2561+
errorMsg = 'Param "args" in Transaction proposal request should be a string array';
2562+
}
2563+
if (!request.channelId) {
2564+
errorMsg = 'Missing Required param "channelId" in Transaction proposal';
2565+
}
2566+
2567+
if (errorMsg) {
2568+
logger.error('%s error %s', method, errorMsg);
2569+
throw new Error(errorMsg);
2570+
}
2571+
2572+
request.args.forEach(arg => {
2573+
logger.debug('%s - adding arg %s', method, arg);
2574+
args.push(Buffer.from(arg, 'utf8'));
2575+
});
2576+
//special case to support the bytes argument of the query by hash
2577+
if (request.argbytes) {
2578+
logger.debug('%s - adding the argument :: argbytes', method);
2579+
args.push(request.argbytes);
2580+
} else {
2581+
logger.debug('%s - not adding the argument :: argbytes', method);
2582+
}
2583+
2584+
const invokeSpec = {
2585+
type: _ccProto.ChaincodeSpec.Type.GOLANG,
2586+
chaincode_id: { name: request.chaincodeId },
2587+
input: { args }
2588+
};
2589+
2590+
// certificate, publicKey, mspId, cryptoSuite
2591+
const signer = new Identity(certificate, null, mspId);
2592+
const txId = new TransactionID(signer, admin);
2593+
2594+
const channelHeader = client_utils.buildChannelHeader(
2595+
_commonProto.HeaderType.ENDORSER_TRANSACTION,
2596+
request.channelId,
2597+
txId.getTransactionID(),
2598+
null,
2599+
request.chaincodeId,
2600+
client_utils.buildCurrentTimestamp(),
2601+
this._clientContext.getClientCertHash()
2602+
);
2603+
2604+
const header = client_utils.buildHeader(signer, channelHeader, txId.getNonce());
2605+
const proposal = client_utils.buildProposal(invokeSpec, header, request.transientMap);
2606+
return { proposal, txId };
2607+
}
2608+
2609+
/**
2610+
* @typedef {Object} SignedProposal
2611+
* @property {Peer[]} targets - Required. The function name.
2612+
* @property {Buffer} signedProposal - Required. The signed endorse proposal
2613+
*/
2614+
2615+
/**
2616+
* Send signed transaction proposal to peer
2617+
*
2618+
* @param {SignedProposal} request signed endorse transaction proposal, this signed
2619+
* proposal would be send to peer directly.
2620+
* @param {number} timeout the timeout setting passed on sendSignedProposal
2621+
*/
2622+
async sendSignedProposal(request, timeout) {
2623+
return Channel.sendSignedProposal(request, timeout);
2624+
}
2625+
2626+
/**
2627+
* Send signed transaction proposal to peer
2628+
*
2629+
* @param {SignedProposal} request signed endorse transaction proposal, this signed
2630+
* proposal would be send to peer directly.
2631+
* @param {number} timeout the timeout setting passed on sendSignedProposal
2632+
*/
2633+
static async sendSignedProposal(request, timeout) {
2634+
const responses = await client_utils.sendPeersProposal(request.targets, request.signedProposal, timeout);
2635+
return responses;
2636+
}
2637+
2638+
/**
2639+
* generate the commit proposal for a transaction
2640+
*
2641+
* @param {TransactionRequest} request
2642+
*/
2643+
async generateUnsignedTransaction(request) {
2644+
logger.debug('generateUnsignedTransaction - start :: channel %s', this._name);
2645+
2646+
if (!request) {
2647+
throw Error('Missing input request object on the generateUnsignedTransaction() call');
2648+
}
2649+
// Verify that data is being passed in
2650+
if (!request.proposalResponses) {
2651+
throw Error('Missing "proposalResponses" parameter in transaction request');
2652+
}
2653+
if (!request.proposal) {
2654+
throw Error('Missing "proposal" parameter in transaction request');
2655+
}
2656+
let proposalResponses = request.proposalResponses;
2657+
const chaincodeProposal = request.proposal;
2658+
2659+
const endorsements = [];
2660+
if (!Array.isArray(proposalResponses)) {
2661+
//convert to array
2662+
proposalResponses = [proposalResponses];
2663+
}
2664+
for (const proposalResponse of proposalResponses) {
2665+
// make sure only take the valid responses to set on the consolidated response object
2666+
// to use in the transaction object
2667+
if (proposalResponse && proposalResponse.response && proposalResponse.response.status === 200) {
2668+
endorsements.push(proposalResponse.endorsement);
2669+
}
2670+
}
2671+
2672+
if (endorsements.length < 1) {
2673+
logger.error('sendTransaction - no valid endorsements found');
2674+
throw new Error('no valid endorsements found');
2675+
}
2676+
const proposalResponse = proposalResponses[0];
2677+
2678+
let use_admin_signer = false;
2679+
if (request.txId) {
2680+
use_admin_signer = request.txId.isAdmin();
2681+
}
2682+
2683+
const proposal = ChannelHelper.buildTransactionProposal(
2684+
chaincodeProposal,
2685+
endorsements,
2686+
proposalResponse,
2687+
use_admin_signer
2688+
);
2689+
return proposal;
2690+
}
2691+
2692+
/**
2693+
* @typedef {Object} SignedCommitProposal
2694+
* @property {TransactionRequest} request - Required. The commit request
2695+
* @property {Buffer} signedTransaction - Required. The signed transaction
2696+
* @property {Orderer|string} orderer - Optional. The orderer instance or string name
2697+
* of the orderer to operate. See {@link Client.getTargetOrderer}
2698+
*/
2699+
2700+
/**
2701+
* send the signed commit proposal for a transaction
2702+
*
2703+
* @param {SignedCommitProposal} request the signed commit proposal
2704+
* @param {number} timeout the timeout setting passed on sendSignedProposal
2705+
*/
2706+
async sendSignedTransaction(request, timeout) {
2707+
const signed_envelope = client_utils.toEnvelope(request.signedProposal);
2708+
if (this._commit_handler) {
2709+
const params = {
2710+
signed_envelope,
2711+
request: request.request,
2712+
timeout: timeout
2713+
};
2714+
2715+
return this._commit_handler.commit(params);
2716+
} else {
2717+
// verify that we have an orderer configured
2718+
const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name);
2719+
return orderer.sendBroadcast(signed_envelope, timeout);
25072720
}
25082721
}
25092722

25102723
/*
25112724
* Internal static method to allow transaction envelop to be built without
25122725
* creating a new channel
25132726
*/
2514-
static buildEnvelope(clientContext, chaincodeProposal, endorsements, proposalResponse, use_admin_signer){
2727+
static buildEnvelope(clientContext, chaincodeProposal, endorsements, proposalResponse, use_admin_signer) {
25152728

25162729
const header = _commonProto.Header.decode(chaincodeProposal.getHeader());
25172730

@@ -2548,7 +2761,7 @@ const Channel = class {
25482761
payload.setData(transaction.toBuffer());
25492762

25502763
const signer = clientContext._getSigningIdentity(use_admin_signer);
2551-
return client_utils.toEnvelope(client_utils.signProposal(signer,payload));
2764+
return client_utils.toEnvelope(client_utils.signProposal(signer, payload));
25522765
}
25532766
/**
25542767
* @typedef {Object} ChaincodeQueryRequest

0 commit comments

Comments
 (0)