From f32f77b129472c06e54c7eab6aa96141234001f7 Mon Sep 17 00:00:00 2001 From: Dave Enyeart Date: Wed, 13 Jul 2022 14:09:14 -0400 Subject: [PATCH] Update secured asset transfer sample (#781) A recent commit added the potential buyer to an asset's state based endorsement policy. That change was problematic because if the transfer fell through, the buyer lost control of the asset, in that they could no longer update the asset or change the sell price or sell to somebody else. The asset state based endorsement policy is now based on the seller only, and we document that additional parties could be added such as a trusted third party (although no such party exists in test network at this time). This commit also re-adds some necessary verifications, and make other minor edits and comments to help users understand the sample. Signed-off-by: David Enyeart --- .../application-gateway-typescript/src/app.ts | 10 +- .../src/contractWrapper.ts | 15 +-- .../application-javascript/app.js | 10 +- .../chaincode-go/asset_transfer.go | 102 +++++++++--------- .../chaincode-go/asset_transfer_queries.go | 9 +- 5 files changed, 71 insertions(+), 75 deletions(-) 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 }