From 94867dd517c55da0b6c4cfe4add0f49c4feab80e Mon Sep 17 00:00:00 2001 From: fraVlaca <86831094+fraVlaca@users.noreply.github.com> Date: Thu, 26 May 2022 13:42:01 +0100 Subject: [PATCH] updated and improved asset-transfer-secured-agreement sample (#751) Signed-off-by: fraVlaca --- asset-transfer-secured-agreement/README.md | 1 + .../application-gateway-typescript/src/app.ts | 21 +- .../src/contractWrapper.ts | 82 ++++---- .../application-javascript/app.js | 67 ++---- .../chaincode-go/asset_transfer.go | 192 ++++++++++++------ 5 files changed, 203 insertions(+), 160 deletions(-) diff --git a/asset-transfer-secured-agreement/README.md b/asset-transfer-secured-agreement/README.md index 7ebae1d8..2cf2acfa 100644 --- a/asset-transfer-secured-agreement/README.md +++ b/asset-transfer-secured-agreement/README.md @@ -24,6 +24,7 @@ The smart contract (in folder `chaincode-go`) implements the following functions - GetAssetPrivateProperties - GetAssetSalesPrice - GetAssetBidPrice +- GetAssetHashId - QueryAssetSaleAgreements - QueryAssetBuyAgreements - QueryAssetHistory diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts index 70ed7383..d2020d1f 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts @@ -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 { @@ -57,9 +57,8 @@ async function main(): Promise { 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 { 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 { // 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 { // 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); diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts index 626e99f5..6771790e 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts @@ -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 { - 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 { + 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 { @@ -113,7 +115,6 @@ export class ContractWrapper { const resultString = this.#utf8Decoder.decode(resultBytes); const json = parse(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 { + public async agreeToSell(assetPrice: AssetPrice, buyerOrgID: string): Promise { 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 { - console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetProperties.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`); + public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise { + 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(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 { + public async agreeToBuy(assetPrice: AssetPrice, privateData: AssetPrivateData): Promise { + + 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 { + public async transferAsset(assetPrice: AssetPrice, ownerOrgID: string, buyerOrgID: string): Promise { - 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}`); diff --git a/asset-transfer-secured-agreement/application-javascript/app.js b/asset-transfer-secured-agreement/application-javascript/app.js index fca85799..f81bcc83 100644 --- a/asset-transfer-secured-agreement/application-javascript/app.js +++ b/asset-transfer-secured-agreement/application-javascript/app.js @@ -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); diff --git a/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go index cce1c2cd..5cf6ec68 100644 --- a/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go +++ b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go @@ -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) }