updated and improved asset-transfer-secured-agreement sample (#751)

Signed-off-by: fraVlaca <ocsenarf@outlook.com>
This commit is contained in:
fraVlaca 2022-05-26 13:42:01 +01:00 committed by GitHub
parent 4681fe7865
commit 94867dd517
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 203 additions and 160 deletions

View file

@ -24,6 +24,7 @@ The smart contract (in folder `chaincode-go`) implements the following functions
- GetAssetPrivateProperties
- GetAssetSalesPrice
- GetAssetBidPrice
- GetAssetHashId
- QueryAssetSaleAgreements
- QueryAssetBuyAgreements
- QueryAssetHistory

View file

@ -15,7 +15,7 @@ const chaincodeName = 'secured';
//Use a random key so that we can run multiple times
const now = Date.now().toString();
const assetKey = `asset${now}`;
let assetKey: string;
async function main(): Promise<void> {
@ -57,9 +57,8 @@ async function main(): Promise<void> {
const contractWrapperOrg2 = new ContractWrapper(contractOrg2, mspIdOrg2);
// Create an asset by organization Org1, this only requires the owning organization to endorse.
await contractWrapperOrg1.createAsset({ assetId: assetKey,
ownerOrg: mspIdOrg1,
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg1} is not for sale`}, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
assetKey = await contractWrapperOrg1.createAsset(mspIdOrg1,
`Asset owned by ${mspIdOrg1} is not for sale`, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
@ -109,16 +108,16 @@ async function main(): Promise<void> {
assetId: assetKey,
price: 110,
tradeId: now,
});
}, mspIdOrg2);
// Check the private information about the asset from Org2. Org1 would have to send Org2 asset details,
// so the hash of the details may be checked by the chaincode.
await contractWrapperOrg2.verifyAssetProperties({ assetId:assetKey, color:'blue', size:35});
await contractWrapperOrg2.verifyAssetProperties(assetKey, {color:'blue', size:35});
// Agree to a buy by org2.
await contractWrapperOrg2.agreeToBuy( {assetId: assetKey,
price: 100,
tradeId: now});
tradeId: now}, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
// Org1 should be able to read the sale price of this asset.
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
@ -142,12 +141,12 @@ async function main(): Promise<void> {
// Org1 will try to transfer the asset to Org2
// This will fail due to the sell price and the bid price are not the same.
try{
await contractWrapperOrg1.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, mspIdOrg1, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
}
// Agree to a sell by Org1, the seller will agree to the bid price of Org2.
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now});
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now}, mspIdOrg2);
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
@ -167,14 +166,14 @@ async function main(): Promise<void> {
// Org2 user will try to transfer the asset to Org1.
// This will fail as the owner is Org1.
try{
await contractWrapperOrg2.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
}
// Org1 will transfer the asset to Org2.
// This will now complete as the sell price and the bid price are the same.
await contractWrapperOrg1.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2);
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);

View file

@ -7,6 +7,7 @@ import { Contract } from '@hyperledger/fabric-gateway';
import { TextDecoder } from 'util';
import { GREEN, parse, RED, RESET } from './utils';
import crpto from 'crypto';
import { mspIdOrg2 } from './connect';
const randomBytes = crpto.randomBytes(256).toString('hex');
@ -19,7 +20,6 @@ interface AssetJSON {
interface AssetPropertiesJSON {
objectType: string;
assetID: string;
color: string;
size: number;
salt: string;
@ -44,7 +44,6 @@ export interface Asset {
}
export interface AssetProperties {
assetId: string;
color: string;
size: number;
}
@ -61,27 +60,30 @@ export class ContractWrapper {
readonly #org: string;
readonly #utf8Decoder = new TextDecoder();
readonly #randomBytes: string = randomBytes;
#endorsingOrgs: { [id: string]: string[] };
public constructor(contract: Contract, org: string) {
this.#contract = contract;
this.#org = org;
this.#endorsingOrgs = {};
}
public async createAsset(asset: Asset, privateData: AssetPrivateData): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${asset.assetId} as ${asset.ownerOrg} - endorsed by Org1.${RESET}`);
public async createAsset(ownerOrg: string, publicDescription: string, privateData: AssetPrivateData): Promise<string> {
console.log(`${GREEN}--> Submit Transaction: CreateAsset as ${ownerOrg} - endorsed by Org1.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = {
objectType: 'asset_properties',
assetID: asset.assetId,
color: privateData.Color,
size: privateData.Size,
salt: this.#randomBytes };
await this.#contract.submit('CreateAsset', {
arguments: [asset.assetId, asset.publicDescription],
const resultBytes = await this.#contract.submit('CreateAsset', {
arguments: [publicDescription],
transientData: { asset_properties: JSON.stringify(assetPropertiesJSON)},
});
console.log(`*** Result: committed, asset ${asset.assetId} is owned by Org1`);
const assetID = this.#utf8Decoder.decode(resultBytes);
this.#endorsingOrgs[assetID] = [ownerOrg];
console.log(`*** Result: committed, asset ${assetID} is owned by ${ownerOrg}`);
return assetID;
}
public async readAsset(assetKey: string, ownerOrg: string): Promise<void> {
@ -113,7 +115,6 @@ export class ContractWrapper {
const resultString = this.#utf8Decoder.decode(resultBytes);
const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = {
assetId: json.assetID,
color: json.color,
size: json.size,
};
@ -129,12 +130,13 @@ export class ContractWrapper {
await this.#contract.submit('ChangePublicDescription', {
arguments:[asset.assetId, asset.publicDescription],
endorsingOrganizations: this.#endorsingOrgs[asset.assetId]
});
console.log(`*** Result: committed, Desc: ${asset.publicDescription}`);
}
public async agreeToSell(assetPrice: AssetPrice): Promise<void> {
public async agreeToSell(assetPrice: AssetPrice, buyerOrgID: string): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
const assetPriceJSON: AssetPriceJSON = {
@ -144,23 +146,28 @@ export class ContractWrapper {
};
await this.#contract.submit('AgreeToSell', {
arguments:[assetPrice.assetId],
transientData: {asset_price: JSON.stringify(assetPriceJSON)}
arguments:[assetPrice.assetId, buyerOrgID],
transientData: {asset_price: JSON.stringify(assetPriceJSON)},
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
});
//update local record of sbe to inlcude buyer org if not already
if (this.#endorsingOrgs[assetPrice.assetId].indexOf('buyerOrgID') == -1){
this.#endorsingOrgs[assetPrice.assetId].push(buyerOrgID);
}
console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`);
}
public async verifyAssetProperties(assetProperties: AssetProperties): Promise<void> {
console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetProperties.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise<void> {
console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetId} as ${this.#org} - endorsed by ${this.#org} and ${mspIdOrg2}.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
assetID: assetProperties.assetId,
color: assetProperties.color,
size: assetProperties.size,
salt: this.#randomBytes };
const resultBytes = await this.#contract.evaluate('VerifyAssetProperties', {
arguments:[assetPropertiesJSON.assetID],
arguments:[assetId],
transientData: {asset_properties: JSON.stringify(assetPropertiesJSON)},
});
@ -168,24 +175,29 @@ export class ContractWrapper {
if (resultString.length !== 0) {
const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = {
assetId: json.assetID,
color: json.color,
size: json.size
};
if (result) {
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetProperties.assetId} has been verified by ${this.#org}`);
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`);
} else {
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetProperties.assetId} has not been verified by ${this.#org}`);
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`);
}
} else {
throw new Error(`Private information about asset ${assetProperties.assetId} has not been verified by ${this.#org}`);
throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`);
}
}
public async agreeToBuy(assetPrice: AssetPrice, ): Promise<void> {
public async agreeToBuy(assetPrice: AssetPrice, privateData: AssetPrivateData): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org} and ${mspIdOrg2}.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = {
objectType: 'asset_properties',
color: privateData.Color,
size: privateData.Size,
salt: this.#randomBytes };
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
const assetPriceJSON: AssetPriceJSON = {
assetID: assetPrice.assetId,
price: assetPrice.price,
@ -194,7 +206,11 @@ export class ContractWrapper {
await this.#contract.submit('AgreeToBuy', {
arguments:[assetPrice.assetId],
transientData: {asset_price: JSON.stringify(assetPriceJSON)}
transientData: {
asset_price: JSON.stringify(assetPriceJSON),
asset_properties: JSON.stringify(assetPropertiesJSON)
},
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
});
console.log(`*** Result: committed, ${this.#org} has agreed to buy asset ${assetPrice.assetId} for 100`);
@ -242,9 +258,9 @@ export class ContractWrapper {
console.log('*** Result: GetAssetBidPrice', result);
}
public async transferAsset( privateData: AssetPrivateData, assetPrice: AssetPrice, endorsingOrganizations: string[], ownerOrgID: string, buyerOrgID: string): Promise<void> {
public async transferAsset(assetPrice: AssetPrice, ownerOrgID: string, buyerOrgID: string): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org}.${RESET}`);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org} and ${buyerOrgID}.${RESET}`);
if (this.#org !== ownerOrgID) {
console.log(`${GREEN}* Expected to fail as the owner is ${ownerOrgID}.${RESET}`);
@ -252,20 +268,12 @@ export class ContractWrapper {
console.log(`${GREEN}* Expected to fail as sell price and the bid price are not the same.${RESET}`);
}
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
assetID: assetPrice.assetId,
color: privateData.Color,
size: privateData.Size,
salt: this.#randomBytes };
const assetPriceJSON: AssetPriceJSON = { assetID: assetPrice.assetId, price:assetPrice.price, tradeID:assetPrice.tradeId};
await this.#contract.submit('TransferAsset', {
arguments:[assetPropertiesJSON.assetID, buyerOrgID],
transientData: {
asset_properties: JSON.stringify(assetPropertiesJSON),
asset_price: JSON.stringify(assetPriceJSON)},
endorsingOrganizations:endorsingOrganizations
arguments:[assetPrice.assetId, buyerOrgID],
transientData: { asset_price: JSON.stringify(assetPriceJSON) },
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
});
console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`);

View file

@ -207,8 +207,7 @@ async function main() {
console.log(`${GREEN} **** START ****${RESET}`);
try {
const randomNumber = Math.floor(Math.random() * 100) + 1;
// use a random key so that we can run multiple times
const assetKey = `asset-${randomNumber}`;
let assetKey;
/** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */
const gatewayOrg1 = await initGatewayForOrg1();
@ -231,20 +230,19 @@ async function main() {
// the actual peers that may be active at any given time.
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
console.log(`${GREEN}--> Submit Transaction: CreateAsset as Org1 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg1.createTransaction('CreateAsset');
transaction.setEndorsingOrganizations(org1);
transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string)
});
await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org1} is not for sale`);
assetKey = await transaction.submit( `Asset owned by ${org1} is not for sale`);
console.log(`*** Result: committed, asset ${assetKey} is owned by Org1`);
} catch (createError) {
console.log(`${RED}*** Failed: CreateAsset - ${createError}${RESET}`);
@ -314,7 +312,8 @@ async function main() {
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey);
//call agree to sell with desired price and target buyer organization
await transaction.submit(assetKey, org2);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 110`);
} catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);
@ -326,7 +325,6 @@ async function main() {
// details may be checked by the chaincode.
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
@ -356,16 +354,24 @@ async function main() {
try {
// Agree to a buy by Org2
const asset_price = {
asset_id: assetKey,
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
const asset_properties = {
object_type: 'asset_properties',
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetKey} as Org2 - endorsed by Org2${RESET}`);
transaction = contractOrg2.createTransaction('AgreeToBuy');
transaction.setEndorsingOrganizations(org2);
transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
asset_price: Buffer.from(asset_price_string),
asset_properties: Buffer.from(asset_properties_string)
});
await transaction.submit(assetKey);
console.log(`*** Result: committed, Org2 has agreed to buy asset ${assetKey} for 100`);
@ -395,14 +401,6 @@ async function main() {
// Org1 will try to transfer the asset to Org2
// This will fail due to the sell price and the bid price
// are not the same
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
const asset_price = {
asset_id: assetKey,
price: 110,
@ -411,11 +409,9 @@ async function main() {
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg1.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1);
transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string),
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey, org2);
@ -428,18 +424,18 @@ async function main() {
// Agree to a sell by Org1
// Org1, the seller will agree to the bid price of Org2
const asset_price = {
asset_id: assetKey,
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
transaction = contractOrg1.createTransaction('AgreeToSell');
transaction.setEndorsingOrganizations(org1);
transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey);
await transaction.submit(assetKey, org2);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 100`);
} catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);
@ -460,27 +456,17 @@ async function main() {
try {
// Org2 user will try to transfer the asset to Org2
// This will fail as the owner is Org1
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
const asset_price = {
asset_id: assetKey,
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org2 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg2.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string),
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey, org2);
@ -492,27 +478,18 @@ async function main() {
try {
// Org1 will transfer the asset to Org2
// This will now complete as the sell price and the bid price are the same
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
const asset_price = {
asset_id: assetKey,
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg1.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string),
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey, org2);

View file

@ -7,6 +7,7 @@ package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
@ -42,24 +43,29 @@ type receipt struct {
timestamp time.Time
}
// CreateAsset creates an asset and sets it as owned by the client's org
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, assetID, publicDescription string) error {
// CreateAsset creates an asset, sets it as owned by the client's org and returns its id
// the id of the asset corresponds to the hash of the properties of the asset that are passed by transiet field
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, publicDescription string) (string, error) {
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return fmt.Errorf("error getting transient: %v", err)
return "", fmt.Errorf("error getting transient: %v", err)
}
// Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transientMap["asset_properties"]
if !ok {
return fmt.Errorf("asset_properties key not found in the transient map")
return "", fmt.Errorf("asset_properties key not found in the transient map")
}
hash := sha256.New()
hash.Write(immutablePropertiesJSON)
assetID := hex.EncodeToString(hash.Sum(nil))
// Get client org id and verify it matches peer org id.
// In this scenario, client is only authorized to read/write private data from its own peer.
clientOrgID, err := getClientOrgID(ctx, true)
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err)
return "", fmt.Errorf("failed to get verified OrgID: %v", err)
}
asset := Asset{
@ -70,34 +76,35 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface,
}
assetBytes, err := json.Marshal(asset)
if err != nil {
return fmt.Errorf("failed to create asset JSON: %v", err)
return "", fmt.Errorf("failed to create asset JSON: %v", err)
}
err = ctx.GetStub().PutState(asset.ID, assetBytes)
err = ctx.GetStub().PutState(assetID, assetBytes)
if err != nil {
return fmt.Errorf("failed to put asset in public data: %v", err)
return "", fmt.Errorf("failed to put asset in public data: %v", err)
}
// Set the endorsement policy such that an owner org peer is required to endorse future updates
err = setAssetStateBasedEndorsement(ctx, asset.ID, clientOrgID)
endorsingOrgs := []string{clientOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
return fmt.Errorf("failed setting state based endorsement for owner: %v", err)
return "", fmt.Errorf("failed setting state based endorsement for buyer and seller: %v", err)
}
// Persist private immutable asset properties to owner's private data collection
collection := buildCollectionName(clientOrgID)
err = ctx.GetStub().PutPrivateData(collection, asset.ID, immutablePropertiesJSON)
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
if err != nil {
return fmt.Errorf("failed to put Asset private details: %v", err)
return "", fmt.Errorf("failed to put Asset private details: %v", err)
}
return nil
return assetID, nil
}
// ChangePublicDescription updates the assets public description. Only the current owner can update the public description
func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionContextInterface, assetID string, newDescription string) error {
// No need to check client org id matches peer org id, rely on the asset ownership check instead.
clientOrgID, err := getClientOrgID(ctx, false)
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err)
}
@ -121,14 +128,15 @@ func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionConte
return ctx.GetStub().PutState(assetID, updatedAssetJSON)
}
// AgreeToSell adds seller's asking price to seller's implicit private data collection
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error {
// AgreeToSell adds seller's asking price to seller's implicit private data collection and requires to specify the next possible buyer
// Set the endorsement policy such that seller org and passed target buyer org peers are both required to endorse the tranfer
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return err
}
clientOrgID, err := getClientOrgID(ctx, true)
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err)
}
@ -138,18 +146,48 @@ func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg)
}
// Set the endorsement policy such that owner org and seller org peers are both required to endorse the tranfer
endorsingOrgs := []string{clientOrgID, buyerOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
return fmt.Errorf("failed setting state based endorsement for buyer and future seller: %v", err)
}
return agreeToPrice(ctx, assetID, typeAssetForSale)
}
// AgreeToBuy adds buyer's bid price to buyer's implicit private data collection
// AgreeToBuy adds buyer's bid price and asset properties to buyer's implicit private data collection
func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface, assetID string) error {
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return fmt.Errorf("error getting transient: %v", err)
}
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err)
}
// Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transientMap["asset_properties"]
if !ok {
return fmt.Errorf("asset_properties key not found in the transient map")
}
// Persist private immutable asset properties to seller's private data collection
collection := buildCollectionName(clientOrgID)
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
if err != nil {
return fmt.Errorf("failed to put Asset private details: %v", err)
}
return agreeToPrice(ctx, assetID, typeAssetBid)
}
// agreeToPrice adds a bid or ask price to caller's implicit private data collection
func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error {
// In this scenario, client is only authorized to read/write private data from its own peer.
clientOrgID, err := getClientOrgID(ctx, true)
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err)
}
@ -186,6 +224,7 @@ func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, p
// VerifyAssetProperties Allows a buyer to validate the properties of
// an asset against the owner's implicit private data collection
// and verifies that the asset properties never changed from the origin of the asset by checking their hash against the assetID
func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
transMap, err := ctx.GetStub().GetTransient()
if err != nil {
@ -225,13 +264,22 @@ func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContext
)
}
// verify that the hash of the passed immutable properties and on chain hash matches the assetID
if !(hex.EncodeToString(immutablePropertiesOnChainHash) == assetID) {
return false, fmt.Errorf("hash %x for passed immutable properties %s does match on-chain hash %x but do not match assetID %s: asset was altered from its initial form",
calculatedPropertiesHash,
immutablePropertiesJSON,
immutablePropertiesOnChainHash,
assetID)
}
return true, nil
}
// TransferAsset checks transfer conditions and then transfers asset state to buyer.
// TransferAsset can only be called by current owner
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
clientOrgID, err := getClientOrgID(ctx, false)
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err)
}
@ -241,11 +289,6 @@ func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterfac
return fmt.Errorf("error getting transient data: %v", err)
}
immutablePropertiesJSON, ok := transMap["asset_properties"]
if !ok {
return fmt.Errorf("asset_properties key not found in the transient map")
}
priceJSON, ok := transMap["asset_price"]
if !ok {
return fmt.Errorf("asset_price key not found in the transient map")
@ -262,12 +305,12 @@ func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterfac
return fmt.Errorf("failed to get asset: %v", err)
}
err = verifyTransferConditions(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, priceJSON)
err = verifyTransferConditions(ctx, asset, clientOrgID, buyerOrgID, priceJSON)
if err != nil {
return fmt.Errorf("failed transfer verification: %v", err)
}
err = transferAssetState(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, agreement.Price)
err = transferAssetState(ctx, asset, clientOrgID, buyerOrgID, agreement.Price)
if err != nil {
return fmt.Errorf("failed asset transfer: %v", err)
}
@ -279,7 +322,6 @@ func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterfac
// verifyTransferConditions checks that client org currently owns asset and that both parties have agreed on price
func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
asset *Asset,
immutablePropertiesJSON []byte,
clientOrgID string,
buyerOrgID string,
priceJSON []byte) error {
@ -290,27 +332,30 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("a client from %s cannot transfer a asset owned by %s", clientOrgID, asset.OwnerOrg)
}
// CHECK2: Verify that the hash of the passed immutable properties matches the on-chain hash
// CHECK2: Verify that both buyers and seller on-chain asset defintion hash matches
collectionSeller := buildCollectionName(clientOrgID)
immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID)
collectionBuyer := buildCollectionName(buyerOrgID)
sellerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID)
if err != nil {
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
}
if immutablePropertiesOnChainHash == nil {
if sellerPropertiesOnChainHash == nil {
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
}
buyerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, asset.ID)
if err != nil {
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
}
if buyerPropertiesOnChainHash == nil {
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
}
hash := sha256.New()
hash.Write(immutablePropertiesJSON)
calculatedPropertiesHash := hash.Sum(nil)
// verify that the hash of the passed immutable properties matches the on-chain hash
if !bytes.Equal(immutablePropertiesOnChainHash, calculatedPropertiesHash) {
return fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x",
calculatedPropertiesHash,
immutablePropertiesJSON,
immutablePropertiesOnChainHash,
if !bytes.Equal(sellerPropertiesOnChainHash, buyerPropertiesOnChainHash) {
return fmt.Errorf("on chain hash of seller %x does not match on-chain hash of buyer %x",
sellerPropertiesOnChainHash,
buyerPropertiesOnChainHash,
)
}
@ -330,7 +375,6 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
}
// Get buyers bid price
collectionBuyer := buildCollectionName(buyerOrgID)
assetBidKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key: %v", err)
@ -343,7 +387,7 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("buyer price for %s does not exist", asset.ID)
}
hash = sha256.New()
hash := sha256.New()
hash.Write(priceJSON)
calculatedPriceHash := hash.Sum(nil)
@ -369,7 +413,8 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
}
// transferAssetState performs the public and private state updates for the transferred asset
func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, immutablePropertiesJSON []byte, clientOrgID string, buyerOrgID string, price int) error {
// changes the endorsement for the transferred asset sbe to the new owner org
func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, clientOrgID string, buyerOrgID string, price int) error {
asset.OwnerOrg = buyerOrgID
updatedAsset, err := json.Marshal(asset)
if err != nil {
@ -381,8 +426,9 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse
return fmt.Errorf("failed to write asset for buyer: %v", err)
}
// Change the endorsement policy to the new owner
err = setAssetStateBasedEndorsement(ctx, asset.ID, buyerOrgID)
// Changes the endorsement policy to the new owner org
endorsingOrgs := []string{buyerOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
return fmt.Errorf("failed setting state based endorsement for new owner: %v", err)
}
@ -395,10 +441,6 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse
}
collectionBuyer := buildCollectionName(buyerOrgID)
err = ctx.GetStub().PutPrivateData(collectionBuyer, asset.ID, immutablePropertiesJSON)
if err != nil {
return fmt.Errorf("failed to put Asset private properties for buyer: %v", err)
}
// Delete the price records for seller
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
@ -466,23 +508,12 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse
}
// getClientOrgID gets the client org ID.
// The client org ID can optionally be verified against the peer org ID, to ensure that a client
// from another org doesn't attempt to read or write private data from this peer.
// The only exception in this scenario is for TransferAsset, since the current owner
// needs to get an endorsement from the buyer's peer.
func getClientOrgID(ctx contractapi.TransactionContextInterface, verifyOrg bool) (string, error) {
func getClientOrgID(ctx contractapi.TransactionContextInterface) (string, error) {
clientOrgID, err := ctx.GetClientIdentity().GetMSPID()
if err != nil {
return "", fmt.Errorf("failed getting client's orgID: %v", err)
}
if verifyOrg {
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return "", err
}
}
return clientOrgID, nil
}
@ -503,14 +534,13 @@ func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
return nil
}
// setAssetStateBasedEndorsement adds an endorsement policy to a asset so that only a peer from an owning org
// can update or transfer the asset.
func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgToEndorse string) error {
// setAssetStateBasedEndorsement adds an endorsement policy to an asset so that the passed orgs need to agree upon transfer
func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgsToEndorse []string) error {
endorsementPolicy, err := statebased.NewStateEP(nil)
if err != nil {
return err
}
err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgToEndorse)
err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgsToEndorse...)
if err != nil {
return fmt.Errorf("failed to add org to endorsement policy: %v", err)
}
@ -526,12 +556,40 @@ func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface,
return nil
}
// GetAssetHash Allows a buyer to validate the properties of
// an asset against the asset Id and return the hash
func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterface) (string, error) {
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return "", fmt.Errorf("error getting transient: %v", err)
}
// Asset properties must be retrieved from the transient field as they are private
propertiesJSON, ok := transientMap["asset_properties"]
if !ok {
return "", fmt.Errorf("asset_properties key not found in the transient map")
}
hash := sha256.New()
hash.Write(propertiesJSON)
assetID := hex.EncodeToString(hash.Sum(nil))
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return "", fmt.Errorf("failed to get asset: %v, asset properies provided do not represent any on chain asset", err)
}
if asset.ID != assetID {
return "", fmt.Errorf("Asset properies provided do not correpond to any on chain asset")
}
return asset.ID, nil
}
func buildCollectionName(clientOrgID string) string {
return fmt.Sprintf("_implicit_org_%s", clientOrgID)
}
func getClientImplicitCollectionName(ctx contractapi.TransactionContextInterface) (string, error) {
clientOrgID, err := getClientOrgID(ctx, true)
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return "", fmt.Errorf("failed to get verified OrgID: %v", err)
}