updated chaincodes for asset-trnsfer-basic in order to show good example on how achieving determinism in json

Signed-off-by: fraVlaca <ocsenarf@outlook.com>
This commit is contained in:
fraVlaca 2021-09-06 11:01:22 +01:00
parent bd559c81c8
commit b933b35a95
7 changed files with 71 additions and 39 deletions

View file

@ -13,12 +13,14 @@ type SmartContract struct {
} }
// Asset describes basic details of what makes up a simple asset // Asset describes basic details of what makes up a simple asset
//Insert struct field in alphabetic order => to achieve determinism accross languages
// golang keeps the order when marshal to json but doesn't order automatically
type Asset struct { type Asset struct {
AppraisedValue int `json:"AppraisedValue"`
Color string `json:"Color"`
ID string `json:"ID"` ID string `json:"ID"`
Color string `json:"color"` Owner string `json:"Owner"`
Size int `json:"size"` Size int `json:"Size"`
Owner string `json:"owner"`
AppraisedValue int `json:"appraisedValue"`
} }
// InitLedger adds a base set of assets to the ledger // InitLedger adds a base set of assets to the ledger

View file

@ -7,6 +7,9 @@ package org.hyperledger.fabric.samples.assettransfer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import org.hyperledger.fabric.contract.Context; import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface; import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.Contact; import org.hyperledger.fabric.contract.annotation.Contact;
@ -51,7 +54,7 @@ public final class AssetTransfer implements ContractInterface {
* @param ctx the transaction context * @param ctx the transaction context
*/ */
@Transaction(intent = Transaction.TYPE.SUBMIT) @Transaction(intent = Transaction.TYPE.SUBMIT)
public void InitLedger(final Context ctx) { public void InitLedger(final Context ctx) throws JsonProcessingException {
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
CreateAsset(ctx, "asset1", "blue", 5, "Tomoko", 300); CreateAsset(ctx, "asset1", "blue", 5, "Tomoko", 300);
@ -76,7 +79,7 @@ public final class AssetTransfer implements ContractInterface {
*/ */
@Transaction(intent = Transaction.TYPE.SUBMIT) @Transaction(intent = Transaction.TYPE.SUBMIT)
public Asset CreateAsset(final Context ctx, final String assetID, final String color, final int size, public Asset CreateAsset(final Context ctx, final String assetID, final String color, final int size,
final String owner, final int appraisedValue) { final String owner, final int appraisedValue) throws JsonProcessingException {
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
if (AssetExists(ctx, assetID)) { if (AssetExists(ctx, assetID)) {
@ -86,8 +89,9 @@ public final class AssetTransfer implements ContractInterface {
} }
Asset asset = new Asset(assetID, color, size, owner, appraisedValue); Asset asset = new Asset(assetID, color, size, owner, appraisedValue);
String assetJSON = genson.serialize(asset); //Use Genson to convert the Asset into string, sort it alphabetically and serialize it into a json string
stub.putStringState(assetID, assetJSON); String sortedJson = genson.serialize(asset);
stub.putStringState(assetID, sortedJson);
return asset; return asset;
} }
@ -100,7 +104,7 @@ public final class AssetTransfer implements ContractInterface {
* @return the asset found on the ledger if there was one * @return the asset found on the ledger if there was one
*/ */
@Transaction(intent = Transaction.TYPE.EVALUATE) @Transaction(intent = Transaction.TYPE.EVALUATE)
public Asset ReadAsset(final Context ctx, final String assetID) { public Asset ReadAsset(final Context ctx, final String assetID) throws JsonProcessingException, IOException {
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
String assetJSON = stub.getStringState(assetID); String assetJSON = stub.getStringState(assetID);
@ -127,7 +131,7 @@ public final class AssetTransfer implements ContractInterface {
*/ */
@Transaction(intent = Transaction.TYPE.SUBMIT) @Transaction(intent = Transaction.TYPE.SUBMIT)
public Asset UpdateAsset(final Context ctx, final String assetID, final String color, final int size, public Asset UpdateAsset(final Context ctx, final String assetID, final String color, final int size,
final String owner, final int appraisedValue) { final String owner, final int appraisedValue) throws JsonProcessingException {
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
if (!AssetExists(ctx, assetID)) { if (!AssetExists(ctx, assetID)) {
@ -137,9 +141,9 @@ public final class AssetTransfer implements ContractInterface {
} }
Asset newAsset = new Asset(assetID, color, size, owner, appraisedValue); Asset newAsset = new Asset(assetID, color, size, owner, appraisedValue);
String newAssetJSON = genson.serialize(newAsset); //Use Genson to convert the Asset into string, sort it alphabetically and serialize it into a json string
stub.putStringState(assetID, newAssetJSON); String sortedJson = genson.serialize(newAsset);
stub.putStringState(assetID, sortedJson);
return newAsset; return newAsset;
} }
@ -186,7 +190,7 @@ public final class AssetTransfer implements ContractInterface {
* @return the updated asset * @return the updated asset
*/ */
@Transaction(intent = Transaction.TYPE.SUBMIT) @Transaction(intent = Transaction.TYPE.SUBMIT)
public Asset TransferAsset(final Context ctx, final String assetID, final String newOwner) { public Asset TransferAsset(final Context ctx, final String assetID, final String newOwner) throws JsonProcessingException {
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
String assetJSON = stub.getStringState(assetID); String assetJSON = stub.getStringState(assetID);
@ -199,8 +203,9 @@ public final class AssetTransfer implements ContractInterface {
Asset asset = genson.deserialize(assetJSON, Asset.class); Asset asset = genson.deserialize(assetJSON, Asset.class);
Asset newAsset = new Asset(asset.getAssetID(), asset.getColor(), asset.getSize(), newOwner, asset.getAppraisedValue()); Asset newAsset = new Asset(asset.getAssetID(), asset.getColor(), asset.getSize(), newOwner, asset.getAppraisedValue());
String newAssetJSON = genson.serialize(newAsset); //Use a Genson to conver the Asset into string, sort it alphabetically and serialize it into a json string
stub.putStringState(assetID, newAssetJSON); String sortedJson = genson.serialize(newAsset);
stub.putStringState(assetID, sortedJson);
return newAsset; return newAsset;
} }
@ -212,7 +217,7 @@ public final class AssetTransfer implements ContractInterface {
* @return array of assets found on the ledger * @return array of assets found on the ledger
*/ */
@Transaction(intent = Transaction.TYPE.EVALUATE) @Transaction(intent = Transaction.TYPE.EVALUATE)
public String GetAllAssets(final Context ctx) { public String GetAllAssets(final Context ctx) throws JsonProcessingException, IOException {
ChaincodeStub stub = ctx.getStub(); ChaincodeStub stub = ctx.getStub();
List<Asset> queryResults = new ArrayList<Asset>(); List<Asset> queryResults = new ArrayList<Asset>();
@ -225,8 +230,8 @@ public final class AssetTransfer implements ContractInterface {
for (KeyValue result: results) { for (KeyValue result: results) {
Asset asset = genson.deserialize(result.getStringValue(), Asset.class); Asset asset = genson.deserialize(result.getStringValue(), Asset.class);
System.out.println(asset);
queryResults.add(asset); queryResults.add(asset);
System.out.println(asset.toString());
} }
final String response = genson.serialize(queryResults); final String response = genson.serialize(queryResults);

View file

@ -24,6 +24,9 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
public final class AssetTransferTest { public final class AssetTransferTest {
private final class MockKeyValue implements KeyValue { private final class MockKeyValue implements KeyValue {
@ -109,7 +112,7 @@ public final class AssetTransferTest {
class InvokeReadAssetTransaction { class InvokeReadAssetTransaction {
@Test @Test
public void whenAssetExists() { public void whenAssetExists() throws JsonProcessingException, IOException {
AssetTransfer contract = new AssetTransfer(); AssetTransfer contract = new AssetTransfer();
Context ctx = mock(Context.class); Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class); ChaincodeStub stub = mock(ChaincodeStub.class);
@ -141,7 +144,7 @@ public final class AssetTransferTest {
} }
@Test @Test
void invokeInitLedgerTransaction() { void invokeInitLedgerTransaction() throws JsonProcessingException {
AssetTransfer contract = new AssetTransfer(); AssetTransfer contract = new AssetTransfer();
Context ctx = mock(Context.class); Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class); ChaincodeStub stub = mock(ChaincodeStub.class);
@ -180,7 +183,7 @@ public final class AssetTransferTest {
} }
@Test @Test
public void whenAssetDoesNotExist() { public void whenAssetDoesNotExist() throws JsonProcessingException {
AssetTransfer contract = new AssetTransfer(); AssetTransfer contract = new AssetTransfer();
Context ctx = mock(Context.class); Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class); ChaincodeStub stub = mock(ChaincodeStub.class);
@ -194,7 +197,7 @@ public final class AssetTransferTest {
} }
@Test @Test
void invokeGetAllAssetsTransaction() { void invokeGetAllAssetsTransaction() throws JsonProcessingException, IOException {
AssetTransfer contract = new AssetTransfer(); AssetTransfer contract = new AssetTransfer();
Context ctx = mock(Context.class); Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class); ChaincodeStub stub = mock(ChaincodeStub.class);
@ -216,7 +219,7 @@ public final class AssetTransferTest {
class TransferAssetTransaction { class TransferAssetTransaction {
@Test @Test
public void whenAssetExists() { public void whenAssetExists() throws JsonProcessingException {
AssetTransfer contract = new AssetTransfer(); AssetTransfer contract = new AssetTransfer();
Context ctx = mock(Context.class); Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class); ChaincodeStub stub = mock(ChaincodeStub.class);
@ -251,7 +254,7 @@ public final class AssetTransferTest {
class UpdateAssetTransaction { class UpdateAssetTransaction {
@Test @Test
public void whenAssetExists() { public void whenAssetExists() throws JsonProcessingException {
AssetTransfer contract = new AssetTransfer(); AssetTransfer contract = new AssetTransfer();
Context ctx = mock(Context.class); Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class); ChaincodeStub stub = mock(ChaincodeStub.class);

View file

@ -6,6 +6,9 @@
'use strict'; 'use strict';
// Deterministic JSON.stringify()
const stringify = require('json-stringify-deterministic');
const sortKeysRecursive = require('sort-keys-recursive');
const { Contract } = require('fabric-contract-api'); const { Contract } = require('fabric-contract-api');
class AssetTransfer extends Contract { class AssetTransfer extends Contract {
@ -58,8 +61,11 @@ class AssetTransfer extends Contract {
for (const asset of assets) { for (const asset of assets) {
asset.docType = 'asset'; asset.docType = 'asset';
await ctx.stub.putState(asset.ID, Buffer.from(JSON.stringify(asset))); // example of how to write to world state deterministically
console.info(`Asset ${asset.ID} initialized`); // use convetion of alphabetic order
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
// when retrieving data, in any lang, the order of data will be the same and consequently also the corresonding hash
await ctx.stub.putState(asset.ID, Buffer.from(stringify(sortKeysRecursive(asset))));
} }
} }
@ -77,7 +83,8 @@ class AssetTransfer extends Contract {
Owner: owner, Owner: owner,
AppraisedValue: appraisedValue, AppraisedValue: appraisedValue,
}; };
await ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); //we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
await ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
return JSON.stringify(asset); return JSON.stringify(asset);
} }
@ -105,7 +112,8 @@ class AssetTransfer extends Contract {
Owner: owner, Owner: owner,
AppraisedValue: appraisedValue, AppraisedValue: appraisedValue,
}; };
return ctx.stub.putState(id, Buffer.from(JSON.stringify(updatedAsset))); // we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
return ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(updatedAsset))));
} }
// DeleteAsset deletes an given asset from the world state. // DeleteAsset deletes an given asset from the world state.
@ -128,7 +136,8 @@ class AssetTransfer extends Contract {
const assetString = await this.ReadAsset(ctx, id); const assetString = await this.ReadAsset(ctx, id);
const asset = JSON.parse(assetString); const asset = JSON.parse(assetString);
asset.Owner = newOwner; asset.Owner = newOwner;
return ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); // we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
return ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
} }
// GetAllAssets returns all assets found in the world state. // GetAllAssets returns all assets found in the world state.
@ -146,7 +155,7 @@ class AssetTransfer extends Contract {
console.log(err); console.log(err);
record = strValue; record = strValue;
} }
allResults.push({ Key: result.value.key, Record: record }); allResults.push(record);
result = await iterator.next(); result = await iterator.next();
} }
return JSON.stringify(allResults); return JSON.stringify(allResults);

View file

@ -18,7 +18,9 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "^2.0.0", "fabric-contract-api": "^2.0.0",
"fabric-shim": "^2.0.0" "fabric-shim": "^2.0.0",
"json-stringify-deterministic": "^1.0.1",
"sort-keys-recursive": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.1.2", "chai": "^4.1.2",

View file

@ -22,7 +22,9 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "^2.0.0", "fabric-contract-api": "^2.0.0",
"fabric-shim": "^2.0.0" "fabric-shim": "^2.0.0",
"json-stringify-deterministic":"^1.0.0",
"sort-keys-recursive":"^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",

View file

@ -1,7 +1,9 @@
/* /*
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
//Deterministic JSON.stringify()
const stringify = require('json-stringify-deterministic')
const sortKeysRecursive = require('sort-keys-recursive')
import {Context, Contract, Info, Returns, Transaction} from 'fabric-contract-api'; import {Context, Contract, Info, Returns, Transaction} from 'fabric-contract-api';
import {Asset} from './asset'; import {Asset} from './asset';
@ -57,7 +59,11 @@ export class AssetTransferContract extends Contract {
for (const asset of assets) { for (const asset of assets) {
asset.docType = 'asset'; asset.docType = 'asset';
await ctx.stub.putState(asset.ID, Buffer.from(JSON.stringify(asset))); //example of how to write to world state deterministically
//use convetion of alphabetic order
//we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
//when retrieving data, in any lang, the order of data will be the same and consequently also the corresonding hash
await ctx.stub.putState(asset.ID, Buffer.from(stringify(sortKeysRecursive(asset))));
console.info(`Asset ${asset.ID} initialized`); console.info(`Asset ${asset.ID} initialized`);
} }
} }
@ -77,7 +83,8 @@ export class AssetTransferContract extends Contract {
Owner: owner, Owner: owner,
AppraisedValue: appraisedValue, AppraisedValue: appraisedValue,
}; };
await ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); //we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
await ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
} }
// ReadAsset returns the asset stored in the world state with given id. // ReadAsset returns the asset stored in the world state with given id.
@ -106,7 +113,8 @@ export class AssetTransferContract extends Contract {
Owner: owner, Owner: owner,
AppraisedValue: appraisedValue, AppraisedValue: appraisedValue,
}; };
return ctx.stub.putState(id, Buffer.from(JSON.stringify(updatedAsset))); //we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
return ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(updatedAsset))));
} }
// DeleteAsset deletes an given asset from the world state. // DeleteAsset deletes an given asset from the world state.
@ -133,7 +141,8 @@ export class AssetTransferContract extends Contract {
const assetString = await this.ReadAsset(ctx, id); const assetString = await this.ReadAsset(ctx, id);
const asset = JSON.parse(assetString); const asset = JSON.parse(assetString);
asset.Owner = newOwner; asset.Owner = newOwner;
await ctx.stub.putState(id, Buffer.from(JSON.stringify(asset))); //we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
await ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
} }
// GetAllAssets returns all assets found in the world state. // GetAllAssets returns all assets found in the world state.
@ -153,7 +162,7 @@ export class AssetTransferContract extends Contract {
console.log(err); console.log(err);
record = strValue; record = strValue;
} }
allResults.push({Key: result.value.key, Record: record}); allResults.push(record);
result = await iterator.next(); result = await iterator.next();
} }
return JSON.stringify(allResults); return JSON.stringify(allResults);