From 6e965ec55f0b5143c88681ba03f8e949bd172732 Mon Sep 17 00:00:00 2001 From: Gaurav Giri Date: Tue, 4 Aug 2020 10:26:14 +0530 Subject: [PATCH] fabric samples for state-based endorsement for chaincode typescript Signed-off-by: Gaurav Giri --- asset-transfer-sbe/README.md | 55 ++++++++++ .../chaincode-typescript/.gitignore | 20 ++++ .../chaincode-typescript/package.json | 65 +++++++++++ .../chaincode-typescript/src/asset.ts | 22 ++++ .../chaincode-typescript/src/assetContract.ts | 102 ++++++++++++++++++ .../chaincode-typescript/src/index.ts | 8 ++ .../chaincode-typescript/tsconfig.json | 18 ++++ .../chaincode-typescript/tslint.json | 21 ++++ 8 files changed, 311 insertions(+) create mode 100644 asset-transfer-sbe/README.md create mode 100644 asset-transfer-sbe/chaincode-typescript/.gitignore create mode 100644 asset-transfer-sbe/chaincode-typescript/package.json create mode 100644 asset-transfer-sbe/chaincode-typescript/src/asset.ts create mode 100644 asset-transfer-sbe/chaincode-typescript/src/assetContract.ts create mode 100644 asset-transfer-sbe/chaincode-typescript/src/index.ts create mode 100644 asset-transfer-sbe/chaincode-typescript/tsconfig.json create mode 100644 asset-transfer-sbe/chaincode-typescript/tslint.json diff --git a/asset-transfer-sbe/README.md b/asset-transfer-sbe/README.md new file mode 100644 index 00000000..d09e056d --- /dev/null +++ b/asset-transfer-sbe/README.md @@ -0,0 +1,55 @@ +# Sample to demonstrate State-Based Endorsements (SBE) + +## Introduction to SBE +Fabric allows different ways to set endorsements for transactions on the network. The default method is specified in chaincode definition, which is agreed by channel members and committed to the channel. However, there are cases where it may be necessary for a particular Key (public channel state or private data collection state) to have a different endorsement policy. State-Based endorsement allows endorsement policies to be overridden for the specified Key's. State-Based Endorsements are also known as Key Level Endorsements. To learn more about endorsement policies and State-Based endorsements, visit the [Fabric Endorsement Policies documentation](https://hyperledger-fabric.readthedocs.io/en/master/endorsement-policies.html). + + +Fabric Shim Method's for State-Based endorsements (below references from fabric-shim, ChaincodeStub for Node): +- setStateValidationParameter(key: string, ep: Uint8Array): Promise; +- getStateValidationParameter(key: string): Promise; +- setPrivateDataValidationParameter(collection: string, key: string, ep: Uint8Array): Promise; +- getPrivateDataValidationParameter(collection: string, key: string): Promise; + + +## About the SBE Asset Transfer Sample +Using this sample we demonstrate State-Based Endorsements (SBE), and set a different endorsement policy for a specified Key, instead of the default chaincode policy. This sample is deployed and demonstrated on the fabric-samples, Test network (2 Org network, Org1 and Org2). The endorsement policy for chaincodes deployed in this network defaults to majority, that is both Org's need to endorse transactions on the network. To bootstrap the Test Network, and deploy the SBE Asset Transfer Sample chaincode, visit the [Test Network documentation](https://hyperledger-fabric.readthedocs.io/en/latest/test_network.html). + +This sample demonstrates modifying the default chaincode policy (requiring endorsements from both Org Peers), and changing it to a less restrictive policy requiring endorsement only from a specific Org Peer for the specified Key. Endorsements can also be modified to be more restrictive for a particular Key in a similar way. + +On Asset creation, State-Based endorsements for the Asset Key is set to either Org1 or Org2 Peer, depending on the client Org creating the Asset. Creation of Asset still needs the default chaincode endorsement policy, and hence will need to be endorsed by both Org's (Org1 & Org2). However, all future updates for the Asset Key will use the modified endorsements as per the State-Based endorsements set during Asset creation. + +Hence transactions like UpdateAsset or TransferAsset will not succeed if endorsed by the non-owner Org Peer. + +Sample invokations to demonstrate State-Based Endorsements (SBE) on test-network, using client identity of Org1 +* Create Asset (requires Org1 & Org2 Peer to endorse as per the default chaincode endorsement policy, however future updates will need only Org1 Peer to endorse, as the State-Based endorsement policy is set for assetId Key during the createAsset transaction) +```bash +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer-sbe --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"CreateAsset","Args":["asset1","100","Org1User1"]}' +``` +* Read & Verify Asset (Use this to verify changes for UpdateAsset & TransferAsset) +```bash +peer chaincode query -C mychannel -n asset-transfer-sbe -c '{"Args":["ReadAsset","asset1"]}' +``` +* Update Asset (requires Org1 Peer to endorse as per SBE specified for assetId Key, but since Org2 Peer is endorsing it will result in ENDORSEMENT_POLICY_FAILURE by the peer during endorsement validations for the transaction, check peer logs) +```bash +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer-sbe --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","200"]}' +``` +* Update Asset succeeds (requires Org1 Peer to endorse as per SBE specified for assetId Key, and Org1 Peer is endorsing here) +```bash +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer-sbe --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","200"]}' +``` +* Transfer Asset (requires Org1 Peer to endorse as per SBE specified for assetId Key, but since Org2 Peer is endorsing it will result in ENDORSEMENT_POLICY_FAILURE by the peer during endorsement validations for the transaction, check peer logs) +```bash +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer-sbe --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"TransferAsset","Args":["asset1","Org2User1","Org2MSP"]}' +``` +* Transfer Asset succeeds (requires Org1 Peer to endorse as per SBE specified for assetId Key, and Org1 Peer is endorsing here) +```bash +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer-sbe --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -c '{"function":"TransferAsset","Args":["asset1","Org2User1","Org2MSP"]}' +``` +* Update Asset (now requires Org2 Peer to endorse as per SBE specified for assetId Key, but since Org1 Peer is endorsing it will result in ENDORSEMENT_POLICY_FAILURE by the peer during endorsement validations for the transaction, check peer logs) +```bash +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer-sbe --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","300"]}' +``` +* Update Asset succeeds (requires Org2 Peer to endorse as per SBE specified for assetId Key, and Org2 Peer is endorsing here) +```bash +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset-transfer-sbe --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"UpdateAsset","Args":["asset1","300"]}' +``` diff --git a/asset-transfer-sbe/chaincode-typescript/.gitignore b/asset-transfer-sbe/chaincode-typescript/.gitignore new file mode 100644 index 00000000..2a76b3d8 --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/.gitignore @@ -0,0 +1,20 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +# Compiled TypeScript files +dist + +# Editor Config +.editorconfig + +# npm ignore +.npmignore diff --git a/asset-transfer-sbe/chaincode-typescript/package.json b/asset-transfer-sbe/chaincode-typescript/package.json new file mode 100644 index 00000000..43be5e77 --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/package.json @@ -0,0 +1,65 @@ +{ + "name": "asset-transfer-sbe", + "version": "0.0.1", + "description": "Asset Transfer contract, using State Based Endorsement(SBE), implemented in TypeScript", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "pretest": "npm run lint", + "test": "nyc mocha -r ts-node/register src/**/*.spec.ts", + "start": "fabric-chaincode-node start", + "build": "tsc", + "build:watch": "tsc -w", + "prepublishOnly": "npm run build" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "@types/chai": "^4.2.11", + "@types/chai-as-promised": "^7.1.2", + "@types/mocha": "^7.0.2", + "@types/node": "^13.9.3", + "@types/sinon": "^7.5.2", + "@types/sinon-chai": "^3.2.3", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "mocha": "^7.1.1", + "nyc": "^15.0.0", + "sinon": "^9.0.1", + "sinon-chai": "^3.5.0", + "ts-node": "^8.8.1", + "tslint": "^6.1.0", + "typescript": "^3.8.3", + "winston": "^3.2.1" + }, + "nyc": { + "extension": [ + ".ts", + ".tsx" + ], + "exclude": [ + "coverage/**", + "dist/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/asset-transfer-sbe/chaincode-typescript/src/asset.ts b/asset-transfer-sbe/chaincode-typescript/src/asset.ts new file mode 100644 index 00000000..b2c85ecf --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/src/asset.ts @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Object, Property } from 'fabric-contract-api'; + +@Object() +export class Asset { + + @Property() + public ID: string; + + @Property() + public Value: string; + + @Property() + public Owner: string; + + @Property() + public OwnerOrg: string; + +} diff --git a/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts b/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts new file mode 100644 index 00000000..0c9237ff --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/src/assetContract.ts @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Context, Contract, Info, Transaction } from 'fabric-contract-api'; +import { Asset } from './asset'; +import { KeyEndorsementPolicy } from 'fabric-shim'; + +@Info({title: 'AssetContract', description: 'Asset Transfer Smart Contract, using State Based Endorsement(SBE), implemented in TypeScript' }) +export class AssetContract extends Contract { + + // CreateAsset creates a new asset + // CreateAsset sets the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates + @Transaction() + public async CreateAsset(ctx: Context, assetId: string, value: string, owner: string): Promise { + const exists = await this.AssetExists(ctx, assetId); + if (exists) { + throw new Error(`The asset ${assetId} already exists`); + } + const ownerOrg = this.getClientOrgId(ctx); + const asset = new Asset(); + asset.ID = assetId; + asset.Value = value; + asset.Owner = owner; + asset.OwnerOrg = ownerOrg; + const buffer = Buffer.from(JSON.stringify(asset)); + // Create the asset + await ctx.stub.putState(assetId, buffer); + // Set the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates + await this.setAssetStateBasedEndorsement(ctx, asset.ID, [ownerOrg]); + } + + // ReadAsset returns asset with given assetId + @Transaction(false) + public async ReadAsset(ctx: Context, assetId: string): Promise { + const exists = await this.AssetExists(ctx, assetId); + if (!exists) { + throw new Error(`The asset ${assetId} does not exist`); + } + // Read the asset + const assetJSON = await ctx.stub.getState(assetId); + return assetJSON.toString(); + } + + // UpdateAsset updates an existing asset + // UpdateAsset needs an endorsement of current owner Org Peer + @Transaction() + public async UpdateAsset(ctx: Context, assetId: string, newValue: string): Promise { + const assetString = await this.ReadAsset(ctx, assetId); + const asset = JSON.parse(assetString) as Asset; + asset.Value = newValue; + const buffer = Buffer.from(JSON.stringify(asset)); + // Update the asset + await ctx.stub.putState(assetId, buffer); + } + + // DeleteAsset deletes an given asset + // DeleteAsset needs an endorsement of current owner Org Peer + @Transaction() + public async DeleteAsset(ctx: Context, assetId: string): Promise { + const exists = await this.AssetExists(ctx, assetId); + if (!exists) { + throw new Error(`The asset ${assetId} does not exist`); + } + // Delete the asset + await ctx.stub.deleteState(assetId); + } + + // TransferAsset updates the Owner & OwnerOrg field of asset with given assetId, OwnerOrg must be a valid Org MSP Id + // TransferAsset needs an endorsement of current owner Org Peer + // TransferAsset re-sets the endorsement policy of the assetId Key, such that new owner Org Peer is required to endorse future updates + @Transaction() + public async TransferAsset(ctx: Context, assetId: string, newOwner: string, newOwnerOrg: string): Promise { + const assetString = await this.ReadAsset(ctx, assetId); + const asset = JSON.parse(assetString) as Asset; + asset.Owner = newOwner; + asset.OwnerOrg = newOwnerOrg; + // Update the asset + await ctx.stub.putState(assetId, Buffer.from(JSON.stringify(asset))); + // Re-Set the endorsement policy of the assetId Key, such that a new owner Org Peer is required to endorse future updates + await this.setAssetStateBasedEndorsement(ctx, asset.ID, [newOwnerOrg]); + } + + // AssetExists returns true when asset with given ID exists + public async AssetExists(ctx: Context, assetId: string): Promise { + const buffer = await ctx.stub.getState(assetId); + return (!!buffer && buffer.length > 0); + } + + // setAssetStateBasedEndorsement sets an endorsement policy to the assetId Key + // setAssetStateBasedEndorsement enforces that the owner Org Peers must endorse future update transactions for the specified assetId Key + private async setAssetStateBasedEndorsement(ctx: Context, assetId: string, ownerOrgs: string[]): Promise { + let ep = new KeyEndorsementPolicy(); + ep.addOrgs("MEMBER", ...ownerOrgs); + await ctx.stub.setStateValidationParameter(assetId, ep.getPolicy()); + } + + // getClientOrgId gets the client's OrgId (MSPID) + private getClientOrgId(ctx: Context): string { + return ctx.clientIdentity.getMSPID(); + } +} diff --git a/asset-transfer-sbe/chaincode-typescript/src/index.ts b/asset-transfer-sbe/chaincode-typescript/src/index.ts new file mode 100644 index 00000000..41a40ecb --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/src/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AssetContract } from './assetContract'; +export { AssetContract } from './assetContract'; + +export const contracts: any[] = [ AssetContract ]; diff --git a/asset-transfer-sbe/chaincode-typescript/tsconfig.json b/asset-transfer-sbe/chaincode-typescript/tsconfig.json new file mode 100644 index 00000000..7201e496 --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "es2017", + "moduleResolution": "node", + "module": "commonjs", + "declaration": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/asset-transfer-sbe/chaincode-typescript/tslint.json b/asset-transfer-sbe/chaincode-typescript/tslint.json new file mode 100644 index 00000000..2044fa69 --- /dev/null +++ b/asset-transfer-sbe/chaincode-typescript/tslint.json @@ -0,0 +1,21 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "indent": [true, "spaces", 4], + "quotemark": [true, "single"], + "semicolon": [true, "always"], + "no-console": false, + "curly": true, + "triple-equals": true, + "no-string-throw": true, + "no-var-keyword": true, + "no-trailing-whitespace": true, + "object-literal-key-quotes": [true, "as-needed"], + "max-line-length": false + }, + "rulesDirectory": [] +}