Hyperledger Fabric, how to share private data with another peer node? - hyperledger-fabric

I am exploring a bit more about Hyperledger Fabric, and have become interested in private data. From what I understand, when a peer node creates private data, it is stored on that node, together with the hash of that private data. Unauthorized nodes will only store the hash of the private data so they can verify its existence.
I am working with a 4 nodes (three peers, one ordered), Where Org1 (peer node) has unique privilidges to create value which has to be made visible to Org2 (peer node) but must not be made visible to Org3 (peer node). Is there a way this can be done. This is a Typescript chaincode sample taken from the IBM Blockchain Platform vscode extension tutorials to create private data.
#Transaction()
public async createMyPrivateAsset(ctx: Context, myPrivateAssetId: string): Promise<void> {
const exists: boolean = await this.myPrivateAssetExists(ctx, myPrivateAssetId);
if (exists) {
throw new Error(`The asset my private asset ${myPrivateAssetId} already exists`);
}
const privateAsset: MyPrivateAsset = new MyPrivateAsset();
const transientData: Map<string, Uint8Array> = ctx.stub.getTransient();
if (transientData.size === 0 || !transientData.has('privateValue')) {
throw new Error('The privateValue key was not specified in transient data. Please try again.');
}
privateAsset.privateValue = transientData.get('privateValue').toString();
const collectionName: string = await getCollectionName(ctx);
await ctx.stub.putPrivateData(collectionName, myPrivateAssetId, Buffer.from(JSON.stringify(privateAsset)));
}
Would I have to change something here for it?
Thanks in advance!

Yes, private data is a good fit for this scenario.
There are two basic approaches.
Use a static collection - Create a collection with Org1 and Org2 as members and putPrivateData to that collection.
Use implicit collections - putPrivateData to Org1 implicit collection and putPrivateData to Org2 implicit collection.
With the latter approach, you do not need to create a static collection ahead of time. It also allows you to share the private data with Org3 implicit collection at a later time if Org3 gets authorized for the data in the future.
See the private data documentation and private data tutorial and sample for more details.
Note that with this approach Org3 will know that Org1 and Org2 are sharing some data, but will not see the private data itself. If you don't want Org3 to have any knowledge that the transaction occurred, then you would use a channel for Org1 and Org2 instead of using private data.

Related

Hyperledger Fabric Smart Contract with private data works in vscode but not in external app. How can I write data to multiple implicit collections?

Summary: An external app can submit a transaction that writes to a single implicit private collection but fails when writing to 2 implicit collections. Everything works OK when using the vscode blockchain extension instead of the external app.
Details:
I am using the vscode blockchain extension (v2.0.8) When used, it installs microfab version 0.0.11.
I am using a 2-org network created in the vscode extension using the 2-org template.
I have a Smart Contract that writes data to 2 implicit private collections (for Org1 and Org2).
Here is the relevant portion of the smart contract (typescript):
#Transaction()
public async createMyPrivateAsset(ctx: Context, myPrivateAssetId: string): Promise<void> {
const exists: boolean = await this.myPrivateAssetExists(ctx, myPrivateAssetId);
if (exists) {
throw new Error(`The asset my private asset ${myPrivateAssetId} already exists`);
}
const privateAsset: MyPrivateAsset = new MyPrivateAsset();
const transientData: Map<string, Uint8Array> = ctx.stub.getTransient();
if (transientData.size === 0 || !transientData.has('privateValue')) {
throw new Error('The privateValue key was not specified in transient data. Please try again.');
}
privateAsset.privateValue = transientData.get('privateValue').toString();
const collectionName: string = await getCollectionName(ctx, ctx.clientIdentity.getMSPID());
await ctx.stub.putPrivateData(collectionName, myPrivateAssetId, Buffer.from(JSON.stringify(privateAsset)));
}
#Transaction()
public async createMyPrivateAssetMultiple(ctx: Context, myPrivateAssetId: string): Promise<void> {
const exists: boolean = await this.myPrivateAssetExists(ctx, myPrivateAssetId);
if (exists) {
throw new Error(`The asset my private asset ${myPrivateAssetId} already exists`);
}
const privateAsset: MyPrivateAsset = new MyPrivateAsset();
const transientData: Map<string, Uint8Array> = ctx.stub.getTransient();
if (transientData.size === 0 || !transientData.has('privateValue')) {
throw new Error('The privateValue key was not specified in transient data. Please try again.');
}
privateAsset.privateValue = transientData.get('privateValue').toString();
for (const mspid of ['Org1MSP', 'Org2MSP']) {
var collectionName: string = await getCollectionName(ctx, mspid);
await ctx.stub.putPrivateData(collectionName, myPrivateAssetId, Buffer.from(JSON.stringify(privateAsset)));
}
}
#Transaction(false)
#Returns('MyPrivateAsset')
public async readMyPrivateAsset(ctx: Context, myPrivateAssetId: string): Promise<string> {
const exists: boolean = await this.myPrivateAssetExists(ctx, myPrivateAssetId);
if (!exists) {
throw new Error(`The asset my private asset ${myPrivateAssetId} does not exist`);
}
let privateDataString: string;
const collectionName: string = await getCollectionName(ctx, ctx.clientIdentity.getMSPID());
const privateData: Uint8Array = await ctx.stub.getPrivateData(collectionName, myPrivateAssetId);
privateDataString = JSON.parse(privateData.toString());
return privateDataString;
}
createMyPrivateAsset writes to a single implicit collection: everything OK.
createMyPrivateAssetMultiple writes to 2 implicit collections: fails in external app.
Both transactions work perfectly when I use the vscode Transaction View to submit transactions.
For createMyPrivateAssetMultiple, I submit using the Org1 gateway and then call readMyPrivateAsset using the Org1 gateway and also using the Org2 gateway and the private data is returned correctly.
Now, when I use an external app, the transaction createMyPrivateAsset works but createMyPrivateAssetMultiple does not.
Here is the relevant portion of the app (typescript):
// connection
const gateway: Gateway = new Gateway();
const connectionProfilePath: string = path.resolve(__dirname, '..', connectionFile);
const connectionProfile = JSON.parse(fs.readFileSync(connectionProfilePath, 'utf8'));
const connectionOptions: GatewayOptions = { wallet, identity: identity, discovery: { enabled: true, asLocalhost: true } };
await gateway.connect(connectionProfile, connectionOptions);
// Get the network (channel) our contract is deployed to.
const network = await gateway.getNetwork('mychannel');
// Get the contract from the network.
const contract = network.getContract('private-contract');
Here is the transaction submission for createMyPrivateAsset
let transientData = {
'privateValue': Buffer.from(`Private value for asset ${assetId}`)
};
const trans:Transaction = contract.createTransaction('createMyPrivateAsset');
const buffer: Buffer = await trans.setTransient(transientData).submit(assetId);
This works fine in the app.
Here is the code for createMyPrivateAssetMultiple
let transientData = {
'privateValue': Buffer.from(`Private value for asset ${assetId}`)
};
const trans:Transaction = contract.createTransaction('createMyPrivateAssetMultiple');
const buffer: Buffer = await trans.setTransient(transientData).submit(assetId);
For this transaction, the app throws this (using Org1 gateway):
2022-06-07T13:21:50.727Z - warn: [TransactionEventHandler]: strategyFail: commit failure for transaction "4e9921b590a361ae01bba673e1d3d204d106522780c820055cec0345e1e67e6f": TransactionError: Commit of transaction 4e9921b590a361ae01bba673e1d3d204d106522780c820055cec0345e1e67e6f failed on peer org1peer-api.127-0-0-1.nip.io:8084 with status ENDORSEMENT_POLICY_FAILURE
The microfab docker container log includes this:
> WARN 0ff Failed fetching private data from remote peers for dig2src:[map[{b16526e4cd2ac3f431103cda23a6f64adc12acab0550eff18c1f25f1cc0d8bc1 private-contract _implicit_org_Org2MSP 6 0}:[]]], err: Empty membership channel=mychannel
...
[ org2peer] 2022-06-07 13:22:50.794 UTC [gossip.privdata] RetrievePvtdata -> WARN 220 Could not fetch all 1 eligible collection private write sets for block [20] (0 from local cache, 0 from transient store, 0 from other peers). Will commit block with missing private write sets:[txID: 4e9921b590a361ae01bba673e1d3d204d106522780c820055cec0345e1e67e6f, seq: 0, namespace: private-contract, collection: _implicit_org_Org2MSP, hash: 3e0f263d2edcfaf29df346504a40fdbadce0807938f204fe3e6bf753b751d9a3
Also, package.json includes this:
"dependencies": {
"fabric-network": "~2.1.0"
},
Can anyone shed light on this problem?
You should definitely be using fabric-network#2.2.x, not 2.1.x.
I suspect what is happening is the VS Code client is not using service discovery and sending proposals for endorsement to all network peers, whereas the standalone client application is using service discovery by default and only sending proposals to orgs required by the chaincode endorsement policy. To get it to consider the collection endorsement policy you would need to add a chaincode interest to the Contract object before submitting transactions:
https://hyperledger.github.io/fabric-sdk-node/release-2.2/module-fabric-network.Contract.html#addDiscoveryInterest
There is a working example of this in the Fabric samples:
https://github.com/hyperledger/fabric-samples/blob/8ca50df4ffec311e59451c2a7ebe210d9e6f0004/asset-transfer-private-data/application-javascript/app.js#L166-L178
Alternatively you could either:
Disable service discovery in the Gateway connection options.
Explicitly set the endorsing orgs for a given transaction invocation:
https://hyperledger.github.io/fabric-sdk-node/release-2.2/module-fabric-network.Transaction.html#setEndorsingOrganizations
In general it's much better to be using service discovery so I would not recommend option 1.
The best approach would actually be to use Fabric v2.4+ and the Fabric Gateway client API:
https://hyperledger.github.io/fabric-gateway/
With this API the client (generally) does not need to worry about the organizations required for endorsement when using private data collections or state-/key-based endorsement policies. Things just work automagically.

ENDORSEMENT_POLICY_FAILURE while invoking chain code using even if the transaction object contains enough endorsements

I have a network on hyperledger fabric 1.4.4 with 5 organizations A , B, C , D , E. I created a channel with these 5 orgs and installed my chaincode on org A and org B because only they are apart of the endorsement policy.
This is the endorsement policy :
{"identities":[{"role":{"name":"member","mspId":"AMSP"}},{"role":{"name":"member","mspId":"BMSP"}},{"role":{"name":"member","mspId":"CMSP"}},{"role":{"name":"member","mspId":"DMSP"}},{"role":{"name":"member","mspId":"EMSP"}}],"policy":{"2-of":[{"signed-by":0},{"signed-by":1}]}}
I am using a gateway with the below configuration to invoke the chain code
const walletPath = path.join('wallet' );
const wallet = new FileSystemWallet(walletPath);
let connectionOptions = {
identity: userName,
wallet: wallet,
discovery: { enabled:true, asLocalhost: true },
eventHandlerOptions: {
commitTimeout: 100,
strategy: DefaultEventHandlerStrategies.NETWORK_SCOPE_ALLFORTX
}
};
logger.debug('Connecting to Fabric gateway');
await gateway.connect(clientConnectionProfileJson, connectionOptions);
const network = await gateway.getNetwork(channelName);
const contract = await network.getContract(chaincodeName , contractName);
const transaction = contract.createTransaction(functionName);
await transaction.submit(<arguments>);
This is the error which I a getting , at the client level
2021-02-17T05:28:13.063Z - warn: [TransactionEventHandler]: _strategyFail: strategy fail for transaction "9be4da8b1d52ddde804d6c7c08d134ef4b6ac2043cbe0258b5b4c921424c9f04": TransactionError: Peer a-org-peer1.a-org.com:7051 has rejected transaction "9be4da8b1d52ddde804d6c7c08d134ef4b6ac2043cbe0258b5b4c921424c9f04" with code "ENDORSEMENT_POLICY_FAILURE"
This is what I see in all the peer logs
2021-02-17 05:28:12.313 UTC [vscc] Validate -> ERRO 0db VSCC error: stateBasedValidator.Validate failed, err validation of endorsement policy for chaincode {chaincodeName} in tx 26:0 failed: signature set did not satisfy policy
After some research , I found that this is a failure that is occurring when the org peer is trying to commit the transaction to the ledger , and finds that the signature set did not satisfy the policy.
I have gone ahead and looked at the transaction object using the getTransactionByID method. I see that there are two endorsers MSP with the correct sign certificates , these certificates belong to the one of the peers of A and B orgs. So the discovery service correctly identified the peers and even the peers have endorsed the transaction , but not sure why the transaction is not getting committed.
What am I missing here ?
How can I verify if the signatures are correct ?
To explicitly say to the gateway that the request should go to specific endorsing peers , I have used the below code.
const walletPath = path.join('wallet' );
const wallet = new FileSystemWallet(walletPath);
let connectionOptions = {
identity: userName,
wallet: wallet,
discovery: { enabled: true , asLocalhost: true },
eventHandlerOptions: {
commitTimeout: 100,
strategy: DefaultEventHandlerStrategies.NETWORK_SCOPE_ALLFORTX
}
};
logger.debug('Connecting to Fabric gateway');
await gateway.connect(clientConnectionProfileJson, connectionOptions);
const network = await gateway.getNetwork(channelName);
const channel = network.getChannel();
let endorsingPeers = [];
endorsingPeers.push(channel.getChannelPeer('a-org-peer1.a-org.com'));
endorsingPeers.push(channel.getChannelPeer('b-org-peer1.b-org.com'));
// Get addressability to org.cargoesnetwork.ebilloflading contract
// Use chaincodeName that is used for installing
const contract = await network.getContract(chaincodeName , contractName);
const transaction = contract.createTransaction(functionName).setEndorsingPeers(endorsingPeers);
await transaction.submit(<arguments>);
No luck , the transaction still fails with the same endorsement policy failure. I verified the transaction object if the endorser sign certs are correctly present. They are present , but still got the same error.
Out of curiosity , I changed the endorsement policy to only one org from two orgs , every thing worked as expected. The issue exists only when the policy contains more than one endorsing organisations.
Please help in debugging this issue.
An ENDORSEMENT_POLICY_FAILURE can occur for a number of reasons. The first being you don't have enough signatures which is what you have said you have already checked. Another reason is that not all the signatures match to the proposal that was sent.
In the 1.4 gateway apis the proposals are received and not compared to see if they all match before a proposal is sent to the orderer. The SDK will send all the signatures and one of the proposals that was received back. The signatures are created over the each peer's individual proposal response.
If those proposals don't match (which would mean that your chaincode is not deterministic) then one of those signatures will be ok, but the other one won't because it won't match the proposal that was sent to the orderer.
I would check that your chaincode is deterministic because it's possible that each peer is generating different responses. An example of non-deterministic chaincode for example is where it creates a new date and stores that in the world state. Each peer would create a slightly different date value resulting in differing responses.

Is it possible to use invokeChaincode method to retrieve data from the chaincode that is on different channel

I'm currently running a project with 2 chaincodes, 2 channels and 2 organizations. I want to install my first chaincode (CC1) on first channel (channel-1) consisting of both org1 and org2. The second chaincode(CC2) will be installed on channel-2 consisting of only org2.
I want to retrieve data from first chaincode and the retrieved data will be used in the second chaincode. Is it possible to retrieve data from a chaincode which is on different channel using invokeChaincode method?
If not possible what are then what are the ways to retrieve data from other chaincodes which is installed on different channel?
I'm using hyperledger fabric version 2.0 and node js to build my chaincode.
You can use the invokeChaincode function to invoke chaincode from a difference channel:
//get data from channel 1
const cc1Args = ['arg1', 'arg2'];
const cc1Res = await ctx.stub.invokeChaincode('CC1', cc1Args, 'channel-1');
if (cc1Res.status !== 200) {
throw new Error(cc1Res.message);
}
const cc1Asset = JSON.parse(cc1Res.payload.toString('utf8'));
//save data to channel 2
const cc2Args = [cc1Asset.arg1, cc1Asset.arg2];
const cc2Res = await ctx.stub.invokeChaincode('CC2', cc2Args, 'channel-2');
if (cc2Res.status !== 200) {
throw new Error(cc2Res.message);
}
const cc2ResObj = JSON.parse(cc1Res.payload.toString('utf8'));
You may also read the document of invokeChaincode: https://hyperledger.github.io/fabric-chaincode-node/master/api/fabric-shim.ChaincodeStub.html#invokeChaincode__anchor
Yes, you can call other chaincode using invokechaincode method but your peer should be joined on that channel. for more information please these links
https://kctheservant.medium.com/cross-chaincode-invoking-in-hyperledger-fabric-8b8df1183c04
https://github.com/hyperledger-archives/fabric/blob/master/examples/chaincode/go/chaincode_example04/chaincode_example04.go
https://golang.hotexamples.com/examples/github.com.hyperledger.fabric.core.chaincode.shim/ChaincodeStubInterface/InvokeChaincode/golang-chaincodestubinterface-invokechaincode-method-examples.html

How to select endorsing peers when submitting transaction which uses private data

I have a Hyperledger Fabric network with two organisations: Org1 and Org2. Discovery is enabled. Private data collections are used to protect sensitive data. In particular there is one private data collection accessible only to members of Org2. When I try to submit transaction which requires access to Org2-only private data, I observe that it is sent to peers of Org1 for endorsement as well:
const gateway = new Gateway();
await gateway.connect(ccp, { wallet, identity: userName, discovery: { enabled: true, asLocalhost: false } });
const network = await gateway.getNetwork(channelName);
const contract = await network.getContract(contractName);
await contract.createTransaction(command).setTransient(transient).submit();
Connection profile used does not list peers from Org2, but my guess is that the Node SDK finds peers from Org1 through discovery and sends transaction proposal to them. Peer logs in Org1 show that it cannot access private data, which is expected:
2019-11-21T12:03:03.684Z ERROR [contracts-spi/chaincodefromcontract.js]
{"message":"GET_STATE failed: transaction ID: 25f22c0abd0318b2ec1da06ae28b90a8e6af55e6d0ea825461938cce8b2d0801: private data matching public hash version is not available. Public hash version = {BlockNum: 951, TxNum: 4}, Private data version = <nil>","stack":"Error: GET_STATE failed: transaction ID: 25f22c0abd0318b2ec1da06ae28b90a8e6af55e6d0ea825461938cce8b2d0801: private data matching public hash version is not available. Public hash version = {BlockNum: 951, TxNum: 4}, Private data version = <nil>\n at parseResponse (/usr/local/src/node_modules/fabric-shim/lib/handler.js:751:15)\n at MsgQueueHandler.handleMsgResponse (/usr/local/src/node_modules/fabric-shim/lib/handler.js:136:40)\n at ClientDuplexStream.<anonymous> (/usr/local/src/node_modules/fabric-shim/lib/handler.js:290:46)\n at emitOne (events.js:116:13)\n at ClientDuplexStream.emit (events.js:211:7)\n at addChunk (_stream_readable.js:263:12)\n at readableAddChunk (_stream_readable.js:250:11)\n at ClientDuplexStream.Readable.push (_stream_readable.js:208:10)\n at Object.onReceiveMessage (/usr/local/src/node_modules/grpc/src/client_interceptors.js:1292:19)\n at InterceptingListener.recvMessageWithContext (/usr/local/src/node_modules/grpc/src/client_interceptors.js:607:19)"}
2019-11-21T12:03:03.684Z ERROR [lib/handler.js] [channel123-25f22c0a]Calling chaincode Invoke() returned error response [Error: GET_STATE failed: transaction ID: 25f22c0abd0318b2ec1da06ae28b90a8e6af55e6d0ea825461938cce8b2d0801: private data matching public hash version is not available. Public hash version = {BlockNum: 951, TxNum: 4}, Private data version = <nil>]. Sending ERROR message back to peer
Similar output is also displayed at the client side. Only in client application, it is a Warning instead of an Error.
2019-11-21T15:15:53.165Z - warn: [DiscoveryEndorsementHandler]: _build_endorse_group_member >> G2:0 - endorsement failed - Error: transaction returned with failure: Error: GET_STATE failed: transaction ID: 0b3e90c745535af7520ffab7b82b041394d409850cb5efff96071c24f5f75817: private data matching public hash version is not available. Public hash version = {BlockNum: 957, TxNum: 0}, Private data version = <nil>
Despite the above errors/warnings, the transaction is successful. Expected updates to the ledger and private data collections are performed.
The question is: is it possible to control from the client side, which peers are used for endorsement of particular transaction, taking into account private data?
I found Channel.getEndorsementPlan(endorsement_hint) which correctly recognises which peers have access to specific chaincode and private data collections. Is it possible to use output from this function to control the behaviour of Transaction.submit()?
The code to allow you to target specific peers is in the gateway (high-level api) code base but unfortunately it is not in the current release of the node sdk which currently is 1.4.4. Hopefully at some point a 1.4.5 version will be released that will have this in. There are probably newer snapshot releases of the fabric-node-sdk on npm that you could try out for now. For reference if you look at the reference documentation here https://fabric-sdk-node.github.io/release-1.4/module-fabric-network.Transaction.html you should see a method called setEndorsingPeers. This should allow you to perform peer targeting for your transaction. An example used in the tests can be found here https://github.com/hyperledger/fabric-sdk-node/blob/bf8c663fbbb9adeeb872b27eb8ccec60c03af6de/test/typescript/integration/network-e2e/invoke.ts#L954
The node-sdk low-level api (Client/Channel interface) does have the capability to discover and determine collections, but that is not available through the gateway/network/contract interface (and no code for it exists at the moment). Here is a reference to a how to use it https://fabric-sdk-node.github.io/release-1.4/tutorial-discovery.html but the Client/Channel apis don't provide support for wallets or handle events for you so you need to do identity handling and event handling yourself.
I Think that the better way to do it is creating a specif queryhandler (FABRIC 2):
import { QueryHandler, QueryHandlerFactory, Query, QueryResults } from 'fabric-network';
import { Endorser } from 'fabric-common';
import * as util from 'util';
/**
* Query handler implementation
*/
class SameOrgQueryHandler implements QueryHandler {
private readonly peers: Endorser[];
constructor(peers: Endorser[]) {
this.peers = peers;
}
public async evaluate(query: Query): Promise<Buffer> {
const errorMessages: string[] = [];
for (const peer of this.peers) {
const results: QueryResults = await query.evaluate([peer]);
const result = results[peer.name];
if (result instanceof Error) {
errorMessages.push(result.toString());
} else {
if (result.isEndorsed) {
console.log(`QueryHandler: ${result.payload}`);
return result.payload;
}
throw new Error(result.message);
}
}
const message = util.format('Query failed. Errors: %j', errorMessages);
throw new Error(message);
}
}
/**
* Factory function for creating sample query handlers.
* #param {Network} network The network where transactions are to be evaluated.
* #returns {QueryHandler} A query handler implementation.
*/
export const createQueryHandler: QueryHandlerFactory = network => {
const mspId = network.getGateway().getIdentity().mspId;
//const mspIdOrg2 = 'Org2MSP';
const channel = network.getChannel();
const orgPeers = channel.getEndorsers(mspId);
const otherPeers = channel.getEndorsers().filter(peer => !orgPeers.includes(peer));
const allPeers = orgPeers.concat(otherPeers);
return new SameOrgQueryHandler(allPeers);
};

How to insert documents to couchdb in bulk by chaincode

How to insert/write documents to couchdb in bulk by chaincode? It seems that the chaincode shim library(https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim) doesn't have such API.
For reading documents, it seems that there is an API named "GetQueryResult"(https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim#ChaincodeStub.GetQueryResult). In the string parameter "query", we can construct bulk get request.
But for inserting/writing documents, is there any bulk API for chaincode? Thanks in advance.
When chaincode executes, each PutState() adds a key/value to the transaction's proposed writeset. You can call PutState() multiple times in a chaincode, and the final set of keys/values will appear in the transaction's proposed writeset. Note that nothing is written to CouchDB at chaincode execution time.
Only when the transaction is submitted for ordering, will the transaction appear in a block that gets processed by all peers. Each peer validates the transactions and then applies the writesets of all valid transactions in the block to the CouchDB state database. Note that the state database commit is indeed using the CouchDB bulk update API (HTTP _bulk_docs), so you automatically get the desired bulk update performance in CouchDB.
If there are many key/value updates in the block, Fabric will actually group them into batches of 1000 (configurable using core.yaml maxBatchUpdateSize property) when committing to CouchDB to avoid any issues with excessively large payloads. Finally, to ensure all writes appear to Fabric as an atomic commit, Fabric writes a final savepoint per block to the CouchDB state database and flushes the CouchDB data to disk. This ensures any chaincode executions get a consistent view of the state data, and ensures Fabric can recover from any peer crash with full data integrity.
If you mean you want to batch multiple transactions in one batch then it is not advisable to do so because if they are updating the same value then while looking at the history, the final change can only be tracked. So, it is better to submit individual transactions.
Not sure if this answers your question but by document, lets assume you mean a PDF file.
This has two functions, read a key value pair and write one. Nothing special here.
'use strict';
const { Contract } = require('fabric-contract-api');
Class SomeName extends Contract {
async initLedger(ctx) { }
async writeDocument(ctx, documentId, documentAsString) {
await ctx.stub.putState(documentId, Buffer.from(documentAsString));
};
async getDocument(ctx, documentId) {
const docAsBytes = await ctx.stub.getState(documentId);
return carAsBytes.toString();
};
}
Now comes the server side code
const { FileSystemWallet, Gateway } = require('fabric-network');
async main() {
const walletPath = path.join(process.cwd(), 'wallet');
const wallet = new FileSystemWallet(walletPath); // can be wherever your path is
const gateway = new Gateway();
await gateway.connect(ccp, { wallet, identity: 'user1', discovery: { enabled: false } });
const network = await gateway.getNetwork('mychannel');
// Get the contract from the network.
const contract = network.getContract('SomeName');
// Now over here lets assume you get the PDF file from a POST request using Multer
// We get the PDF file as a buffer, then convert to a string and send it
let pdfFile = req.file;
await contract.submitTransaction('writeDocument', req.file.buffer.toString('utf-8);
}
and if you want to retrieve the PDF file
const getDocument = async (req, res, next) => {
const walletPath = path.join(process.cwd(), 'wallet');
const wallet = new FileSystemWallet(walletPath); // can be wherever your path is
const gateway = new Gateway();
await gateway.connect(ccp, { wallet, identity: 'user1', discovery: { enabled: false } });
const network = await gateway.getNetwork('mychannel');
// Get the contract from the network.
const contract = network.getContract('SomeName');
const pdfAsString= await contract.evaluateTransaction('getDocument', req.body.documentId);
// Now do whatever with this
};
So I hope this answers your question regarding document upload, that being said its not a good practice to upload documents on a blockchain as it can slow down the transaction speed significantly.
In that case, you can store the document on a local server such as mongoDB and then refer a sha256 hash of the document on the blockchain to cross check the authenticity of the document at a later stage.

Resources