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 d2020d1f..e2210d65 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts @@ -108,7 +108,7 @@ 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. @@ -141,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({ assetId: assetKey, price: 110, tradeId: now}, mspIdOrg1, mspIdOrg2); + await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], 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}, mspIdOrg2); + await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now}); // Read the public details by org1. await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1); @@ -166,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({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2); + await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], 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({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2); + await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], 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 6771790e..82ac77fc 100644 --- a/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts @@ -136,7 +136,7 @@ export class ContractWrapper { console.log(`*** Result: committed, Desc: ${asset.publicDescription}`); } - public async agreeToSell(assetPrice: AssetPrice, buyerOrgID: string): Promise { + public async agreeToSell(assetPrice: AssetPrice): Promise { console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`); const assetPriceJSON: AssetPriceJSON = { @@ -146,16 +146,11 @@ export class ContractWrapper { }; await this.#contract.submit('AgreeToSell', { - arguments:[assetPrice.assetId, buyerOrgID], + arguments:[assetPrice.assetId], 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}`); } @@ -258,7 +253,7 @@ export class ContractWrapper { console.log('*** Result: GetAssetBidPrice', result); } - public async transferAsset(assetPrice: AssetPrice, ownerOrgID: string, buyerOrgID: string): Promise { + public async transferAsset(assetPrice: AssetPrice, endorsingOrganizations: string[], ownerOrgID: string, buyerOrgID: string): Promise { console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org} and ${buyerOrgID}.${RESET}`); @@ -273,9 +268,9 @@ export class ContractWrapper { await this.#contract.submit('TransferAsset', { arguments:[assetPrice.assetId, buyerOrgID], transientData: { asset_price: JSON.stringify(assetPriceJSON) }, - endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId] + endorsingOrganizations: endorsingOrganizations }); console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`); } -} \ No newline at end of file +} diff --git a/asset-transfer-secured-agreement/application-javascript/app.js b/asset-transfer-secured-agreement/application-javascript/app.js index e85b918b..b8a09582 100644 --- a/asset-transfer-secured-agreement/application-javascript/app.js +++ b/asset-transfer-secured-agreement/application-javascript/app.js @@ -312,8 +312,8 @@ async function main() { transaction.setTransient({ asset_price: Buffer.from(asset_price_string) }); - //call agree to sell with desired price and target buyer organization - await transaction.submit(assetKey, org2); + //call agree to sell with desired price + await transaction.submit(assetKey); console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 110`); } catch (sellError) { console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`); @@ -368,7 +368,7 @@ async function main() { 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(org1, org2); + transaction.setEndorsingOrganizations(org2); transaction.setTransient({ asset_price: Buffer.from(asset_price_string), asset_properties: Buffer.from(asset_properties_string) @@ -431,11 +431,11 @@ async function main() { 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, org2); + transaction.setEndorsingOrganizations(org1); transaction.setTransient({ asset_price: Buffer.from(asset_price_string) }); - await transaction.submit(assetKey, org2); + await transaction.submit(assetKey); console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 100`); } catch (sellError) { console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`); diff --git a/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go index cbafb27a..61c817d8 100644 --- a/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go +++ b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go @@ -57,17 +57,18 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, return "", fmt.Errorf("asset_properties key not found in the transient map") } + // AssetID will be the hash of the asset's properties 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. + // Get the clientOrgId from the input, will be used for implicit collection, owner, and state-based endorsement policy clientOrgID, err := getClientOrgID(ctx) if err != nil { return "", err } + // In this scenario, client is only authorized to read/write private data from its own peer, therefore verify client org id matches peer org id. err = verifyClientOrgMatchesPeerOrg(clientOrgID) if err != nil { return "", err @@ -89,7 +90,8 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, 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 + // Set the endorsement policy such that an owner org peer is required to endorse future updates. + // In practice, consider additional endorsers such as a trusted third party to further secure transfers. endorsingOrgs := []string{clientOrgID} err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs) if err != nil { @@ -108,13 +110,8 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, // 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) - if err != nil { - return err - } - err = verifyClientOrgMatchesPeerOrg(clientOrgID) + clientOrgID, err := getClientOrgID(ctx) if err != nil { return err } @@ -138,9 +135,8 @@ 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 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 { +// AgreeToSell adds seller's asking price to seller's implicit private data collection. +func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error { asset, err := s.ReadAsset(ctx, assetID) if err != nil { return err @@ -151,18 +147,17 @@ func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, return err } + // Verify that this client belongs to the peer's org + err = verifyClientOrgMatchesPeerOrg(clientOrgID) + if err != nil { + return err + } + // Verify that this clientOrgId actually owns the asset. if clientOrgID != asset.OwnerOrg { 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) } @@ -178,6 +173,12 @@ func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface, return err } + // Verify that this client belongs to the peer's org + err = verifyClientOrgMatchesPeerOrg(clientOrgID) + if err != nil { + return err + } + // Asset properties must be retrieved from the transient field as they are private immutablePropertiesJSON, ok := transientMap["asset_properties"] if !ok { @@ -232,8 +233,8 @@ func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, p return nil } -// VerifyAssetProperties Allows a buyer to validate the properties of -// an asset against the owner's implicit private data collection +// VerifyAssetProperties allows a buyer to validate the properties of +// an asset they intend to buy 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() @@ -241,7 +242,7 @@ func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContext return false, fmt.Errorf("error getting transient: %v", err) } - /// Asset properties must be retrieved from the transient field as they are private + // Asset properties must be retrieved from the transient field as they are private immutablePropertiesJSON, ok := transMap["asset_properties"] if !ok { return false, fmt.Errorf("asset_properties key not found in the transient map") @@ -342,7 +343,7 @@ 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 both buyers and seller on-chain asset defintion hash matches + // CHECK2: Verify that buyer and seller on-chain asset defintion hash matches collectionSeller := buildCollectionName(clientOrgID) collectionBuyer := buildCollectionName(buyerOrgID) @@ -361,7 +362,7 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface, return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID) } - // verify that the hash of the passed immutable properties matches the on-chain hash + // verify that buyer and seller on-chain asset defintion hash matches if !bytes.Equal(sellerPropertiesOnChainHash, buyerPropertiesOnChainHash) { return fmt.Errorf("on chain hash of seller %x does not match on-chain hash of buyer %x", sellerPropertiesOnChainHash, @@ -425,12 +426,13 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface, // transferAssetState performs the public and private state updates for the transferred asset // 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 { + + // Update ownership in public state asset.OwnerOrg = buyerOrgID updatedAsset, err := json.Marshal(asset) if err != nil { return err } - err = ctx.GetStub().PutState(asset.ID, updatedAsset) if err != nil { return fmt.Errorf("failed to write asset for buyer: %v", err) @@ -443,32 +445,29 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse return fmt.Errorf("failed setting state based endorsement for new owner: %v", err) } - // Transfer the private properties (delete from seller collection, create in buyer collection) + // Delete asset description from seller collection collectionSeller := buildCollectionName(clientOrgID) err = ctx.GetStub().DelPrivateData(collectionSeller, asset.ID) if err != nil { return fmt.Errorf("failed to delete Asset private details from seller: %v", err) } - collectionBuyer := buildCollectionName(buyerOrgID) - // Delete the price records for seller assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID}) if err != nil { return fmt.Errorf("failed to create composite key for seller: %v", err) } - err = ctx.GetStub().DelPrivateData(collectionSeller, assetPriceKey) if err != nil { return fmt.Errorf("failed to delete asset price from implicit private data collection for seller: %v", err) } // Delete the price records for buyer + collectionBuyer := buildCollectionName(buyerOrgID) assetPriceKey, err = ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID}) if err != nil { return fmt.Errorf("failed to create composite key for buyer: %v", err) } - err = ctx.GetStub().DelPrivateData(collectionBuyer, assetPriceKey) if err != nil { return fmt.Errorf("failed to delete asset price from implicit private data collection for buyer: %v", err) @@ -527,7 +526,22 @@ func getClientOrgID(ctx contractapi.TransactionContextInterface) (string, error) return clientOrgID, nil } -// verifyClientOrgMatchesPeerOrg checks the client org id matches the peer org id. +// getClientImplicitCollectionNameAndVerifyClientOrg gets the implicit collection for the client and checks that the client is from the same org as the peer +func getClientImplicitCollectionNameAndVerifyClientOrg(ctx contractapi.TransactionContextInterface) (string, error) { + clientOrgID, err := getClientOrgID(ctx) + if err != nil { + return "", err + } + + err = verifyClientOrgMatchesPeerOrg(clientOrgID) + if err != nil { + return "", err + } + + return buildCollectionName(clientOrgID), nil +} + +// verifyClientOrgMatchesPeerOrg checks that the client is from the same org as the peer func verifyClientOrgMatchesPeerOrg(clientOrgID string) error { peerOrgID, err := shim.GetMSPID() if err != nil { @@ -544,6 +558,11 @@ func verifyClientOrgMatchesPeerOrg(clientOrgID string) error { return nil } +// buildCollectionName returns the implicit collection name for an org +func buildCollectionName(clientOrgID string) string { + return fmt.Sprintf("_implicit_org_%s", clientOrgID) +} + // 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) @@ -566,8 +585,7 @@ 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 +// GetAssetHashId allows a potential buyer to validate the properties of an asset against the asset Id hash on chain and returns the hash func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterface) (string, error) { transientMap, err := ctx.GetStub().GetTransient() if err != nil { @@ -594,24 +612,6 @@ func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterfa 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) - if err != nil { - return "", err - } - - err = verifyClientOrgMatchesPeerOrg(clientOrgID) - if err != nil { - return "", err - } - - return buildCollectionName(clientOrgID), nil -} - func main() { chaincode, err := contractapi.NewChaincode(new(SmartContract)) if err != nil { diff --git a/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go b/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go index 7bc567fd..28eaa626 100644 --- a/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go +++ b/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go @@ -47,8 +47,8 @@ func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, a // GetAssetPrivateProperties returns the immutable asset properties from owner's private data collection func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { - // In this scenario, client is only authorized to read/write private data from its own peer. - collection, err := getClientImplicitCollectionName(ctx) + + collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx) if err != nil { return "", err } @@ -76,7 +76,8 @@ func (s *SmartContract) GetAssetBidPrice(ctx contractapi.TransactionContextInter // getAssetPrice gets the bid or ask price from caller's implicit private data collection func getAssetPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) (string, error) { - collection, err := getClientImplicitCollectionName(ctx) + + collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx) if err != nil { return "", err } @@ -108,7 +109,7 @@ func (s *SmartContract) QueryAssetBuyAgreements(ctx contractapi.TransactionConte } func queryAgreementsByType(ctx contractapi.TransactionContextInterface, agreeType string) ([]Agreement, error) { - collection, err := getClientImplicitCollectionName(ctx) + collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx) if err != nil { return nil, err }