From ffa075ed3c5203fe87cf9c017d4ece6b6fddb76b Mon Sep 17 00:00:00 2001 From: fraVlaca Date: Tue, 12 Jul 2022 15:11:06 +0100 Subject: [PATCH] added purgePrivateData function with tests Signed-off-by: fraVlaca --- apis/fabric-shim-api/types/index.d.ts | 1 + ci/azure-pipelines.yml | 3 +- common/config/rush/command-line.json | 2 +- libraries/fabric-shim/lib/handler.js | 13 +++++ libraries/fabric-shim/lib/stub.js | 24 +++++++++ .../fabric-shim/test/typescript/chaincode.ts | 2 + libraries/fabric-shim/test/unit/handler.js | 50 +++++++++++++++++++ libraries/fabric-shim/test/unit/stub.js | 35 +++++++++++++ libraries/fabric-shim/types/index.d.ts | 1 + test/chaincodes/privateData/chaincode.js | 8 +++ test/fv/privateData.js | 11 ++++ tools/getEdgeDocker.sh | 8 +-- 12 files changed, 152 insertions(+), 6 deletions(-) diff --git a/apis/fabric-shim-api/types/index.d.ts b/apis/fabric-shim-api/types/index.d.ts index fac575ed..98f176ea 100644 --- a/apis/fabric-shim-api/types/index.d.ts +++ b/apis/fabric-shim-api/types/index.d.ts @@ -87,6 +87,7 @@ declare module 'fabric-shim-api' { getPrivateDataHash(collection: string, key: string): Promise; putPrivateData(collection: string, key: string, value: Uint8Array): Promise; deletePrivateData(collection: string, key: string): Promise; + purgePrivateData(collection: string, key: string): Promise; setPrivateDataValidationParameter(collection: string, key: string, ep: Uint8Array): Promise; getPrivateDataValidationParameter(collection: string, key: string): Promise; getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise & AsyncIterable; diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 8ff89ea7..027d71f6 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -43,7 +43,7 @@ variables: - name: rewire_node_version_spec # see https://github.com/jhnns/rewire/issues/178 value: '16.4.0' - name: FABRIC_VERSION - value: 2.4 + value: 2.5 # Build on Ubuntu pool: @@ -147,6 +147,7 @@ stages: set -ev ./tools/getEdgeDocker.sh # essential to get main docker images of peer etc. docker image load --input build/fabric-nodeenv.tar.gz # gets the build image of nodeenv + docker tag hyperledger/fabric-nodeenv:latest hyperledger/fabric-nodeenv:$(FABRIC_VERSION) docker images node common/scripts/install-run-rush.js install node common/scripts/install-run-rush.js update # should the tests need 'building' this will need to go here diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index e7c122ec..758051c2 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -60,7 +60,7 @@ "name": "start-fabric", "summary": "Starts local Fabric test network ", "description": "Run this command to start local Fabric network for testing", - "shellCommand": "rm -rf ./fabric-samples && curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.4.0-beta 1.5.1 && cd ./fabric-samples/test-network && ./network.sh down && ./network.sh up createChannel -ca -s couchdb && cd -" + "shellCommand": "rm -rf ./fabric-samples && curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.4.5 1.5.5 -d && cd ./fabric-samples/test-network && ./network.sh down && ./network.sh up createChannel -ca -s couchdb && cd -" }, { "commandKind": "global", diff --git a/libraries/fabric-shim/lib/handler.js b/libraries/fabric-shim/lib/handler.js index b27206d5..907b6159 100644 --- a/libraries/fabric-shim/lib/handler.js +++ b/libraries/fabric-shim/lib/handler.js @@ -415,6 +415,19 @@ class ChaincodeMessageHandler { return await this._askPeerAndListen(msg, 'DeleteState'); } + async handlePurgeState(collection, key, channel_id, txId) { + const msgPb = new peer.PurgePrivateState(); + msgPb.setKey(key); + msgPb.setCollection(collection); + const msg = mapToChaincodeMessage({ + type: peer.ChaincodeMessage.Type.PURGE_PRIVATE_DATA, + payload: msgPb.serializeBinary(), + txid: txId, + channel_id: channel_id + }); + return await this._askPeerAndListen(msg, 'PurgePrivateState'); + } + async handlePutStateMetadata(collection, key, metakey, ep, channel_id, txId) { const msgPb = new peer.PutStateMetadata(); msgPb.setCollection(collection); diff --git a/libraries/fabric-shim/lib/stub.js b/libraries/fabric-shim/lib/stub.js index a478012a..65275cb7 100644 --- a/libraries/fabric-shim/lib/stub.js +++ b/libraries/fabric-shim/lib/stub.js @@ -1007,6 +1007,30 @@ class ChaincodeStub { return this.handler.handleDeleteState(collection, key, this.channel_id, this.txId); } + /** + * PurgePrivateData records the specified `key` to be purged in the private writeset + * of the transaction. Note that only hash of the private writeset goes into the + * transaction proposal response (which is sent to the client who issued the + * transaction) and the actual private writeset gets temporarily stored in a + * transient store. The `key` and its value will be deleted from the collection + * when the transaction is validated and successfully committed, and will + * subsequently be completely removed from the private data store (that maintains + * the historical versions of private writesets) as a background operation. + * @param {string} collection The collection name + * @param {string} key Private data variable key to delete from the state store + */ + async purgePrivateData(collection, key) { + // Access public data by setting the collection to empty string + logger.debug('purgePrivateData called with collection:%s, key:%s', collection, key); + if (!collection || typeof collection !== 'string') { + throw new Error('collection must be a valid string'); + } + if (!key || typeof key !== 'string') { + throw new Error('key must be a valid string'); + } + return await this.handler.handlePurgeState(collection, key, this.channel_id, this.txId); + } + /** * SetPrivateDataValidationParameter sets the key-level endorsement policy * for the private data specified by `key`. diff --git a/libraries/fabric-shim/test/typescript/chaincode.ts b/libraries/fabric-shim/test/typescript/chaincode.ts index 590e7302..7bcd8bd2 100644 --- a/libraries/fabric-shim/test/typescript/chaincode.ts +++ b/libraries/fabric-shim/test/typescript/chaincode.ts @@ -188,6 +188,8 @@ class TestTS implements ChaincodeInterface { await putPrivData; const delPrivateData: Promise = stub.deletePrivateData(collection, key); await delPrivateData; + const purgePrivData: Promise = stub.purgePrivateData(collection, key); + await purgePrivData; } diff --git a/libraries/fabric-shim/test/unit/handler.js b/libraries/fabric-shim/test/unit/handler.js index e02d8370..fb8b2389 100644 --- a/libraries/fabric-shim/test/unit/handler.js +++ b/libraries/fabric-shim/test/unit/handler.js @@ -1053,6 +1053,56 @@ describe('Handler', () => { }); }); + describe('handlePurgeState', () => { + const key = 'theKey'; + const collection = ''; + + let expectedMsg; + + before(() => { + const payloadPb = new peer.PurgePrivateState(); + payloadPb.setKey(key); + payloadPb.setCollection(collection); + expectedMsg = mapToChaincodeMessage({ + type: peer.ChaincodeMessage.Type.PURGE_PRIVATE_DATA, + payload: payloadPb.serializeBinary(), + channel_id: 'theChannelID', + txid: 'theTxID' + }); + }); + + afterEach(() => { + Handler = rewire('../../../fabric-shim/lib/handler.js'); + sandbox.restore(); + }); + + it ('should resolve when _askPeerAndListen resolves', async () => { + 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.handlePurgeState(collection, key, 'theChannelID', 'theTxID'); + + expect(result).to.deep.equal('some response'); + expect(_askPeerAndListenStub.firstCall.args.length).to.deep.equal(2); + expect(_askPeerAndListenStub.firstCall.args[0]).to.deep.equal(expectedMsg); + expect(_askPeerAndListenStub.firstCall.args[1]).to.deep.equal('PurgePrivateState'); + }); + + it ('should reject when _askPeerAndListen rejects', async () => { + 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.handlePurgeState(collection, key, 'theChannelID', 'theTxID'); + + await expect(result).to.eventually.be.rejected; + expect(_askPeerAndListenStub.firstCall.args.length).to.deep.equal(2); + expect(_askPeerAndListenStub.firstCall.args[0]).to.deep.equal(expectedMsg); + expect(_askPeerAndListenStub.firstCall.args[1]).to.deep.equal('PurgePrivateState'); + }); + }); + describe('handlePutStateMetadata', () => { const key = 'theKey'; const collection = ''; diff --git a/libraries/fabric-shim/test/unit/stub.js b/libraries/fabric-shim/test/unit/stub.js index b77509e7..c256c7fa 100644 --- a/libraries/fabric-shim/test/unit/stub.js +++ b/libraries/fabric-shim/test/unit/stub.js @@ -1210,6 +1210,41 @@ describe('Stub', () => { }); }); + describe('purgePrivateData', () => { + let handlePurgeStateStub; + let stub; + + beforeEach(() => { + handlePurgeStateStub = sinon.stub().resolves('some state'); + stub = new Stub({ + handlePurgeState: handlePurgeStateStub + }, 'dummyChannelId', 'dummyTxid', chaincodeInput); + }); + + it ('should throw an error if no arguments supplied', async () => { + const result = stub.purgePrivateData(); + await expect(result).to.eventually.be.rejectedWith(Error, 'collection must be a valid string'); + }); + + it ('should throw an error if one argument supplied', async () => { + const result = stub.purgePrivateData('some arg'); + await expect(result).to.eventually.be.rejectedWith(Error, 'key must be a valid string'); + }); + + it ('should throw an error if collection null', async () => { + const result = stub.purgePrivateData(null, 'some key'); + await expect(result).to.eventually.be.rejectedWith(Error, 'collection must be a valid string'); + }); + + it ('should return handler.handlePurgeState', async () => { + const result = await stub.purgePrivateData('some collection', 'some key'); + + expect(result).to.deep.equal('some state'); + expect(handlePurgeStateStub.calledOnce).to.be.true; + expect(handlePurgeStateStub.firstCall.args).to.deep.equal(['some collection', 'some key', 'dummyChannelId', 'dummyTxid']); + }); + }); + describe('setPrivateDataValidationParameter', () => { it('should return handler.handlePutStateMetadata', async () => { const handlePutStateMetadataStub = sinon.stub().resolves('nothing'); diff --git a/libraries/fabric-shim/types/index.d.ts b/libraries/fabric-shim/types/index.d.ts index c939a8d3..2ebf37c7 100644 --- a/libraries/fabric-shim/types/index.d.ts +++ b/libraries/fabric-shim/types/index.d.ts @@ -126,6 +126,7 @@ declare module 'fabric-shim' { getPrivateDataHash(collection: string, key: string): Promise; putPrivateData(collection: string, key: string, value: Uint8Array): Promise; deletePrivateData(collection: string, key: string): Promise; + purgePrivateData(collection: string, key: string): Promise; setPrivateDataValidationParameter(collection: string, key: string, ep: Uint8Array): Promise; getPrivateDataValidationParameter(collection: string, key: string): Promise; getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise & AsyncIterable; diff --git a/test/chaincodes/privateData/chaincode.js b/test/chaincodes/privateData/chaincode.js index a8635163..2d5d3ad4 100644 --- a/test/chaincodes/privateData/chaincode.js +++ b/test/chaincodes/privateData/chaincode.js @@ -66,6 +66,14 @@ class privateDataContract extends Contract { await ctx.stub.deletePrivateData("collection", assetId); } + async purgeAsset(ctx, assetId) { + const exists = await this.assetExists(ctx, assetId); + if (!exists) { + throw new Error(`The asset asset ${assetId} does not exist`); + } + await ctx.stub.purgePrivateData("collection", assetId); + } + async verifyAsset(ctx, mspid, assetId, objectToVerify) { const hashToVerify = crypto.createHash('sha256').update(objectToVerify).digest('hex'); diff --git a/test/fv/privateData.js b/test/fv/privateData.js index e6a47a78..fff3ef4e 100644 --- a/test/fv/privateData.js +++ b/test/fv/privateData.js @@ -48,4 +48,15 @@ describe('Chaincode privateData', () => { expect(payload).to.eql('false'); }); + it('should call purge private data without failing', async function () { + this.timeout(MED_STEP); + const privateData = Buffer.from('privateData').toString('base64'); + await utils.invoke(suite, 'privateDataContract:createAsset', ['4'], `{"privateValue":"${privateData}"}`); + let payload = await utils.query(suite, 'privateDataContract:readAsset', ['4']); + expect(payload).to.eql('{"privateValue":"privateData"}'); + console.log(await utils.invoke(suite, 'privateDataContract:purgeAsset', ['4'])); + payload = await utils.query(suite, 'privateDataContract:assetExists', ['4']); + expect(payload).to.eql('false'); + }); + }); \ No newline at end of file diff --git a/tools/getEdgeDocker.sh b/tools/getEdgeDocker.sh index de9474c0..fce8aad1 100755 --- a/tools/getEdgeDocker.sh +++ b/tools/getEdgeDocker.sh @@ -6,7 +6,7 @@ # set -euo pipefail -version=${FABRIC_VERSION:-2.4} +version=${FABRIC_VERSION:-2.5} artifactory_url=hyperledger-fabric.jfrog.io for image in peer orderer ca baseos ccenv tools; do @@ -17,6 +17,6 @@ for image in peer orderer ca baseos ccenv tools; do done docker pull -q couchdb:3.1 -docker pull -q hyperledger/fabric-ca:1.5 -docker tag hyperledger/fabric-ca:1.5 hyperledger/fabric-ca -docker rmi hyperledger/fabric-ca:1.5 >/dev/null +docker pull -q hyperledger/fabric-ca:1.5.5 +docker tag hyperledger/fabric-ca:1.5.5 hyperledger/fabric-ca +docker rmi hyperledger/fabric-ca:1.5.5 >/dev/null