From a401bc92bc926a017b5f63ad392f34babc4a73ac Mon Sep 17 00:00:00 2001 From: Brett Logan Date: Tue, 7 Jul 2020 17:43:19 -0400 Subject: [PATCH 1/3] Refactor Asset Secured Chaincode Into Idiomatic Go Rewrites the chaincode in idiomatic Go and cleans up the general implementation. A future commit should push the chaincode logic itself into a separate package as chaincode cannot be tested when the logic is part of the main package. Signed-off-by: Brett Logan --- .../chaincode-go/asset_transfer.go | 296 +++++++++--------- .../chaincode-go/asset_transfer_queries.go | 83 ++--- 2 files changed, 181 insertions(+), 198 deletions(-) diff --git a/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go index 7ea397ae..cce1c2cd 100644 --- a/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go +++ b/asset-transfer-secured-agreement/chaincode-go/asset_transfer.go @@ -1,21 +1,6 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ + SPDX-License-Identifier: Apache-2.0 +*/ package main @@ -24,8 +9,10 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "log" "time" + "github.com/golang/protobuf/ptypes" "github.com/hyperledger/fabric-chaincode-go/pkg/statebased" "github.com/hyperledger/fabric-chaincode-go/shim" "github.com/hyperledger/fabric-contract-api-go/contractapi" @@ -44,10 +31,10 @@ type SmartContract struct { // Asset struct and properties must be exported (start with capitals) to work with contract api metadata type Asset struct { - ObjectType string `json:"object_type"` // ObjectType is used to distinguish different object types in the same chaincode namespace - ID string `json:"asset_id"` - OwnerOrg string `json:"owner_org"` - PublicDescription string `json:"public_description"` + ObjectType string `json:"objectType"` // ObjectType is used to distinguish different object types in the same chaincode namespace + ID string `json:"assetID"` + OwnerOrg string `json:"ownerOrg"` + PublicDescription string `json:"publicDescription"` } type receipt struct { @@ -55,16 +42,15 @@ type receipt struct { timestamp time.Time } -// CreateAsset creates a asset and sets it as owned by the client's org +// 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 { - - transMap, err := ctx.GetStub().GetTransient() + transientMap, err := ctx.GetStub().GetTransient() if err != nil { - return fmt.Errorf("Error getting transient: " + err.Error()) + return fmt.Errorf("error getting transient: %v", err) } - // Asset properties are private, therefore they get passed in transient field - immutablePropertiesJSON, ok := transMap["asset_properties"] + // 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") } @@ -73,68 +59,63 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, // In this scenario, client is only authorized to read/write private data from its own peer. clientOrgID, err := getClientOrgID(ctx, true) if err != nil { - return fmt.Errorf("failed to get verified OrgID: %s", err.Error()) + return fmt.Errorf("failed to get verified OrgID: %v", err) } - // Create and persist asset asset := Asset{ ObjectType: "asset", ID: assetID, OwnerOrg: clientOrgID, PublicDescription: publicDescription, } - - assetJSON, err := json.Marshal(asset) + assetBytes, err := json.Marshal(asset) if err != nil { - return fmt.Errorf("failed to create asset JSON: %s", err.Error()) + return fmt.Errorf("failed to create asset JSON: %v", err) } - err = ctx.GetStub().PutState(asset.ID, assetJSON) + err = ctx.GetStub().PutState(asset.ID, assetBytes) if err != nil { - return fmt.Errorf("failed to put Asset in public data: %s", err.Error()) + 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) if err != nil { - return fmt.Errorf("failed setting state based endorsement for owner: %s", err.Error()) + return fmt.Errorf("failed setting state based endorsement for owner: %v", err) } // Persist private immutable asset properties to owner's private data collection collection := buildCollectionName(clientOrgID) - err = ctx.GetStub().PutPrivateData(collection, asset.ID, []byte(immutablePropertiesJSON)) + err = ctx.GetStub().PutPrivateData(collection, asset.ID, immutablePropertiesJSON) if err != nil { - return fmt.Errorf("failed to put Asset private details: %s", err.Error()) + return fmt.Errorf("failed to put Asset private details: %v", err) } return nil } -// ChangePublicDescription updates the asset public description. Only the current owner can update the public description +// 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 { - - // Get client org id // No need to check client org id matches peer org id, rely on the asset ownership check instead. clientOrgID, err := getClientOrgID(ctx, false) if err != nil { - return fmt.Errorf("failed to get verified OrgID: %s", err.Error()) + return fmt.Errorf("failed to get verified OrgID: %v", err) } asset, err := s.ReadAsset(ctx, assetID) if err != nil { - return fmt.Errorf("failed to get asset: %s", err.Error()) + return fmt.Errorf("failed to get asset: %v", err) } - // auth check to ensure that client's org actually owns the asset + // Auth check to ensure that client's org actually owns the asset if clientOrgID != asset.OwnerOrg { return fmt.Errorf("a client from %s cannot update the description of a asset owned by %s", clientOrgID, asset.OwnerOrg) } asset.PublicDescription = newDescription - updatedAssetJSON, err := json.Marshal(asset) if err != nil { - return fmt.Errorf("failed to marshal asset: %s", err.Error()) + return fmt.Errorf("failed to marshal asset: %v", err) } return ctx.GetStub().PutState(assetID, updatedAssetJSON) @@ -142,7 +123,6 @@ func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionConte // AgreeToSell adds seller's asking price to seller's implicit private data collection func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error { - // Query asset and verify that this clientOrgId actually owns the asset. asset, err := s.ReadAsset(ctx, assetID) if err != nil { return err @@ -150,11 +130,12 @@ func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, clientOrgID, err := getClientOrgID(ctx, true) if err != nil { - return fmt.Errorf("failed to get verified OrgID: %s", err.Error()) + return fmt.Errorf("failed to get verified OrgID: %v", err) } + // Verify that this clientOrgId actually owns the asset. if clientOrgID != asset.OwnerOrg { - return fmt.Errorf("a client from %s cannot sell a asset owned by %s", clientOrgID, asset.OwnerOrg) + return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg) } return agreeToPrice(ctx, assetID, typeAssetForSale) @@ -167,23 +148,19 @@ func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface, // agreeToPrice adds a bid or ask price to caller's implicit private data collection func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error { - - // 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) if err != nil { - return fmt.Errorf("failed to get verified OrgID: %s", err.Error()) + return fmt.Errorf("failed to get verified OrgID: %v", err) } - // price is private, therefore it gets passed in transient field transMap, err := ctx.GetStub().GetTransient() if err != nil { - return fmt.Errorf("Error getting transient: " + err.Error()) + return fmt.Errorf("error getting transient: %v", err) } - // Price hash will get verfied later, therefore always pass and persist the JSON bytes as-is, - // so that there is no risk of nondeterministic marshaling. - priceJSON, ok := transMap["asset_price"] + // Asset price must be retrieved from the transient field as they are private + price, ok := transMap["asset_price"] if !ok { return fmt.Errorf("asset_price key not found in the transient map") } @@ -194,26 +171,28 @@ func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, p // to avoid collisions between private asset properties, sell price, and buy price assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID}) if err != nil { - return fmt.Errorf("failed to create composite key: %s", err.Error()) + return fmt.Errorf("failed to create composite key: %v", err) } - err = ctx.GetStub().PutPrivateData(collection, assetPriceKey, priceJSON) + // The Price hash will be verified later, therefore always pass and persist price bytes as is, + // so that there is no risk of nondeterministic marshaling. + err = ctx.GetStub().PutPrivateData(collection, assetPriceKey, price) if err != nil { - return fmt.Errorf("failed to put asset bid: %s", err.Error()) + return fmt.Errorf("failed to put asset bid: %v", err) } return nil } -// VerifyAssetProperties implement function to verify asset properties using the hash -// 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 against the owner's implicit private data collection func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) { transMap, err := ctx.GetStub().GetTransient() if err != nil { - return false, fmt.Errorf("Error getting transient: " + err.Error()) + return false, fmt.Errorf("error getting transient: %v", err) } - // Asset properties are private, therefore they get passed in transient field + /// 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") @@ -221,26 +200,29 @@ func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContext asset, err := s.ReadAsset(ctx, assetID) if err != nil { - return false, fmt.Errorf("failed to get asset: %s", err.Error()) + return false, fmt.Errorf("failed to get asset: %v", err) } collectionOwner := buildCollectionName(asset.OwnerOrg) immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionOwner, assetID) if err != nil { - return false, fmt.Errorf("failed to read asset private properties hash from seller's collection: %s", err.Error()) + return false, fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err) } if immutablePropertiesOnChainHash == nil { return false, fmt.Errorf("asset private properties hash does not exist: %s", assetID) } - // get sha256 hash of passed immutable properties 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 false, fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x", calculatedPropertiesHash, immutablePropertiesJSON, immutablePropertiesOnChainHash) + return false, fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x", + calculatedPropertiesHash, + immutablePropertiesJSON, + immutablePropertiesOnChainHash, + ) } return true, nil @@ -249,17 +231,14 @@ func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContext // 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 { - - // Get client org id and verify it matches peer org id. - // For a transfer, selling client must get endorsement from their own peer and from buyer peer, therefore don't verify client org id matches peer org id clientOrgID, err := getClientOrgID(ctx, false) if err != nil { - return fmt.Errorf("failed to get verified OrgID: %s", err.Error()) + return fmt.Errorf("failed to get verified OrgID: %v", err) } transMap, err := ctx.GetStub().GetTransient() if err != nil { - return fmt.Errorf("Error getting transient: " + err.Error()) + return fmt.Errorf("error getting transient data: %v", err) } immutablePropertiesJSON, ok := transMap["asset_properties"] @@ -273,24 +252,24 @@ func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterfac } var agreement Agreement - err = json.Unmarshal([]byte(priceJSON), &agreement) + err = json.Unmarshal(priceJSON, &agreement) if err != nil { - return fmt.Errorf("failed to unmarshal price JSON: %s", err.Error()) + return fmt.Errorf("failed to unmarshal price JSON: %v", err) } asset, err := s.ReadAsset(ctx, assetID) if err != nil { - return fmt.Errorf("failed to get asset: %s", err.Error()) + return fmt.Errorf("failed to get asset: %v", err) } err = verifyTransferConditions(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, priceJSON) if err != nil { - return fmt.Errorf("failed transfer verification: %s", err.Error()) + return fmt.Errorf("failed transfer verification: %v", err) } err = transferAssetState(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, agreement.Price) if err != nil { - return fmt.Errorf("failed asset transfer: %s", err.Error()) + return fmt.Errorf("failed asset transfer: %v", err) } return nil @@ -298,186 +277,203 @@ 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 { +func verifyTransferConditions(ctx contractapi.TransactionContextInterface, + asset *Asset, + immutablePropertiesJSON []byte, + clientOrgID string, + buyerOrgID string, + priceJSON []byte) error { - // CHECK1: auth check to ensure that client's org actually owns the asset + // CHECK1: Auth check to ensure that client's org actually owns the asset if clientOrgID != asset.OwnerOrg { 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 the hash of the passed immutable properties matches the on-chain hash - // get on chain hash collectionSeller := buildCollectionName(clientOrgID) immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID) if err != nil { - return fmt.Errorf("failed to read asset private properties hash from seller's collection: %s", err.Error()) + return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err) } if immutablePropertiesOnChainHash == nil { return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID) } - // get sha256 hash of passed immutable properties 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) + return fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x", + calculatedPropertiesHash, + immutablePropertiesJSON, + immutablePropertiesOnChainHash, + ) } - // CHECK3: verify that seller and buyer agreed on the same price + // CHECK3: Verify that seller and buyer agreed on the same price - // get seller (current owner) asking price + // Get sellers asking price assetForSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID}) if err != nil { - return fmt.Errorf("failed to create composite key: %s", err.Error()) + return fmt.Errorf("failed to create composite key: %v", err) } sellerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, assetForSaleKey) if err != nil { - return fmt.Errorf("failed to get seller price hash: %s", err.Error()) + return fmt.Errorf("failed to get seller price hash: %v", err) } if sellerPriceHash == nil { return fmt.Errorf("seller price for %s does not exist", asset.ID) } - // get buyer bid price + // 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: %s", err.Error()) + return fmt.Errorf("failed to create composite key: %v", err) } buyerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, assetBidKey) if err != nil { - return fmt.Errorf("failed to get buyer price hash: %s", err.Error()) + return fmt.Errorf("failed to get buyer price hash: %v", err) } if buyerPriceHash == nil { return fmt.Errorf("buyer price for %s does not exist", asset.ID) } - // get sha256 hash of passed price hash = sha256.New() hash.Write(priceJSON) calculatedPriceHash := hash.Sum(nil) - // verify that the hash of the passed price matches the on-chain seller price hash + // Verify that the hash of the passed price matches the on-chain sellers price hash if !bytes.Equal(calculatedPriceHash, sellerPriceHash) { - return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, seller hasn't agreed to the passed trade id and price", calculatedPriceHash, priceJSON, sellerPriceHash) + return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, seller hasn't agreed to the passed trade id and price", + calculatedPriceHash, + priceJSON, + sellerPriceHash, + ) } - // verify that the hash of the passed price matches the on-chain buyer price hash + // Verify that the hash of the passed price matches the on-chain buyer price hash if !bytes.Equal(calculatedPriceHash, buyerPriceHash) { - return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, buyer hasn't agreed to the passed trade id and price", calculatedPriceHash, priceJSON, buyerPriceHash) + return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, buyer hasn't agreed to the passed trade id and price", + calculatedPriceHash, + priceJSON, + buyerPriceHash, + ) } - // since all checks passed, return without an error return nil } -// transferAssetState makes the public and private state updates for the transferred asset +// 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 { - - // save the asset with the new owner asset.OwnerOrg = buyerOrgID - - updatedAssetJSON, _ := json.Marshal(asset) - - err := ctx.GetStub().PutState(asset.ID, updatedAssetJSON) + updatedAsset, err := json.Marshal(asset) if err != nil { - return fmt.Errorf("failed to write asset for buyer: %s", err.Error()) + return err + } + + err = ctx.GetStub().PutState(asset.ID, updatedAsset) + if err != nil { + 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) if err != nil { - return fmt.Errorf("failed setting state based endorsement for new owner: %s", err.Error()) + 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) collectionSeller := buildCollectionName(clientOrgID) err = ctx.GetStub().DelPrivateData(collectionSeller, asset.ID) if err != nil { - return fmt.Errorf("failed to delete Asset private details from seller: %s", err.Error()) + return fmt.Errorf("failed to delete Asset private details from seller: %v", err) } 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: %s", err.Error()) + 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}) if err != nil { - return fmt.Errorf("failed to create composite key for seller: %s", err.Error()) + 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: %s", err.Error()) + return fmt.Errorf("failed to delete asset price from implicit private data collection for seller: %v", err) } // Delete the price records for buyer assetPriceKey, err = ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID}) if err != nil { - return fmt.Errorf("failed to create composite key for buyer: %s", err.Error()) + 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: %s", err.Error()) + return fmt.Errorf("failed to delete asset price from implicit private data collection for buyer: %v", err) } - // Keep record for a 'receipt' in both buyer and seller private data collection to record the sales price and date - // Persist the agreed to price in a collection sub-namespace based on receipt key prefix + // Keep record for a 'receipt' in both buyers and sellers private data collection to record the sale price and date. + // Persist the agreed to price in a collection sub-namespace based on receipt key prefix. receiptBuyKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBuyReceipt, []string{asset.ID, ctx.GetStub().GetTxID()}) if err != nil { - return fmt.Errorf("failed to create composite key for receipt: %s", err.Error()) + return fmt.Errorf("failed to create composite key for receipt: %v", err) } - timestmp, err := ctx.GetStub().GetTxTimestamp() + txTimestamp, err := ctx.GetStub().GetTxTimestamp() if err != nil { - return fmt.Errorf("failed to create timestamp for receipt: %s", err.Error()) + return fmt.Errorf("failed to create timestamp for receipt: %v", err) } + timestamp, err := ptypes.Timestamp(txTimestamp) + if err != nil { + return err + } assetReceipt := receipt{ price: price, - timestamp: time.Unix(timestmp.Seconds, int64(timestmp.Nanos)), + timestamp: timestamp, + } + receipt, err := json.Marshal(assetReceipt) + if err != nil { + return fmt.Errorf("failed to marshal receipt: %v", err) } - receiptJSON, err := json.Marshal(assetReceipt) + err = ctx.GetStub().PutPrivateData(collectionBuyer, receiptBuyKey, receipt) if err != nil { - return fmt.Errorf("failed to marshal receipt: %s", err.Error()) - } - - err = ctx.GetStub().PutPrivateData(collectionBuyer, receiptBuyKey, receiptJSON) - if err != nil { - return fmt.Errorf("failed to put private asset receipt for buyer: %s", err.Error()) + return fmt.Errorf("failed to put private asset receipt for buyer: %v", err) } receiptSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetSaleReceipt, []string{ctx.GetStub().GetTxID(), asset.ID}) if err != nil { - return fmt.Errorf("failed to create composite key for receipt: %s", err.Error()) + return fmt.Errorf("failed to create composite key for receipt: %v", err) } - err = ctx.GetStub().PutPrivateData(collectionSeller, receiptSaleKey, receiptJSON) + err = ctx.GetStub().PutPrivateData(collectionSeller, receiptSaleKey, receipt) if err != nil { - return fmt.Errorf("failed to put private asset receipt for seller: %s", err.Error()) + return fmt.Errorf("failed to put private asset receipt for seller: %v", err) } return nil } // 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. +// 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) { - clientOrgID, err := ctx.GetClientIdentity().GetMSPID() if err != nil { - return "", fmt.Errorf("failed getting client's orgID: %s", err.Error()) + return "", fmt.Errorf("failed getting client's orgID: %v", err) } if verifyOrg { @@ -490,36 +486,41 @@ func getClientOrgID(ctx contractapi.TransactionContextInterface, verifyOrg bool) return clientOrgID, nil } -// verify client org id and matches peer org id. +// verifyClientOrgMatchesPeerOrg checks the client org id matches the peer org id. func verifyClientOrgMatchesPeerOrg(clientOrgID string) error { peerOrgID, err := shim.GetMSPID() if err != nil { - return fmt.Errorf("failed getting peer's orgID: %s", err.Error()) + return fmt.Errorf("failed getting peer's orgID: %v", err) } if clientOrgID != peerOrgID { - return fmt.Errorf("client from org %s is not authorized to read or write private data from an org %s peer", clientOrgID, peerOrgID) + return fmt.Errorf("client from org %s is not authorized to read or write private data from an org %s peer", + clientOrgID, + peerOrgID, + ) } 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. +// 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 { - endorsementPolicy, err := statebased.NewStateEP(nil) - + if err != nil { + return err + } err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgToEndorse) if err != nil { - return fmt.Errorf("failed to add org to endorsement policy: %s", err.Error()) + return fmt.Errorf("failed to add org to endorsement policy: %v", err) } - epBytes, err := endorsementPolicy.Policy() + policy, err := endorsementPolicy.Policy() if err != nil { - return fmt.Errorf("failed to create endorsement policy bytes from org: %s", err.Error()) + return fmt.Errorf("failed to create endorsement policy bytes from org: %v", err) } - err = ctx.GetStub().SetStateValidationParameter(assetID, epBytes) + err = ctx.GetStub().SetStateValidationParameter(assetID, policy) if err != nil { - return fmt.Errorf("failed to set validation parameter on asset: %s", err.Error()) + return fmt.Errorf("failed to set validation parameter on asset: %v", err) } return nil @@ -532,7 +533,7 @@ func buildCollectionName(clientOrgID string) string { func getClientImplicitCollectionName(ctx contractapi.TransactionContextInterface) (string, error) { clientOrgID, err := getClientOrgID(ctx, true) if err != nil { - return "", fmt.Errorf("failed to get verified OrgID: %s", err.Error()) + return "", fmt.Errorf("failed to get verified OrgID: %v", err) } err = verifyClientOrgMatchesPeerOrg(clientOrgID) @@ -544,15 +545,12 @@ func getClientImplicitCollectionName(ctx contractapi.TransactionContextInterface } func main() { - chaincode, err := contractapi.NewChaincode(new(SmartContract)) - if err != nil { - fmt.Printf("Error create transfer asset chaincode: %s", err.Error()) - return + log.Panicf("Error create transfer asset chaincode: %v", err) } if err := chaincode.Start(); err != nil { - fmt.Printf("Error starting asset chaincode: %s", err.Error()) + log.Panicf("Error starting asset chaincode: %v", err) } } 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 a52486fd..0c01c058 100644 --- a/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go +++ b/asset-transfer-secured-agreement/chaincode-go/asset_transfer_queries.go @@ -1,21 +1,6 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ + SPDX-License-Identifier: Apache-2.0 +*/ package main @@ -24,6 +9,7 @@ import ( "fmt" "time" + "github.com/golang/protobuf/ptypes" "github.com/hyperledger/fabric-contract-api-go/contractapi" ) @@ -42,27 +28,25 @@ type Agreement struct { // ReadAsset returns the public asset data func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) { - - // since only public data is accessed in this function, no access control is required - + // Since only public data is accessed in this function, no access control is required assetJSON, err := ctx.GetStub().GetState(assetID) if err != nil { - return nil, fmt.Errorf("failed to read from world state: %s", err.Error()) + return nil, fmt.Errorf("failed to read from world state: %v", err) } if assetJSON == nil { return nil, fmt.Errorf("%s does not exist", assetID) } - asset := new(Asset) - _ = json.Unmarshal(assetJSON, asset) - + var asset *Asset + err = json.Unmarshal(assetJSON, asset) + if err != nil { + return nil, err + } return asset, nil } // GetAssetPrivateProperties returns the immutable asset properties from owner's private data collection func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { - - // 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. collection, err := getClientImplicitCollectionName(ctx) if err != nil { @@ -71,7 +55,7 @@ func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionCon immutableProperties, err := ctx.GetStub().GetPrivateData(collection, assetID) if err != nil { - return "", fmt.Errorf("failed to read asset private properties from client org's collection: %s", err.Error()) + return "", fmt.Errorf("failed to read asset private properties from client org's collection: %v", err) } if immutableProperties == nil { return "", fmt.Errorf("asset private details does not exist in client org's collection: %s", assetID) @@ -80,19 +64,18 @@ func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionCon return string(immutableProperties), nil } -// GetAssetSalesPrice returns the sales price as an integer +// GetAssetSalesPrice returns the sales price func (s *SmartContract) GetAssetSalesPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { return getAssetPrice(ctx, assetID, typeAssetForSale) } -// GetAssetBidPrice returns the bid price as an integer +// GetAssetBidPrice returns the bid price func (s *SmartContract) GetAssetBidPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { return getAssetPrice(ctx, assetID, typeAssetBid) } // 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) if err != nil { return "", err @@ -100,18 +83,18 @@ func getAssetPrice(ctx contractapi.TransactionContextInterface, assetID string, assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID}) if err != nil { - return "", fmt.Errorf("failed to create composite key: %s", err.Error()) + return "", fmt.Errorf("failed to create composite key: %v", err) } - assetPriceJSON, err := ctx.GetStub().GetPrivateData(collection, assetPriceKey) + price, err := ctx.GetStub().GetPrivateData(collection, assetPriceKey) if err != nil { - return "", fmt.Errorf("failed to read asset price from implicit private data collection: %s", err.Error()) + return "", fmt.Errorf("failed to read asset price from implicit private data collection: %v", err) } - if assetPriceJSON == nil { + if price == nil { return "", fmt.Errorf("asset price does not exist: %s", assetID) } - return string(assetPriceJSON), nil + return string(price), nil } // QueryAssetSaleAgreements returns all of an organization's proposed sales @@ -119,7 +102,7 @@ func (s *SmartContract) QueryAssetSaleAgreements(ctx contractapi.TransactionCont return queryAgreementsByType(ctx, typeAssetForSale) } -// QueryAssetBuyAgreements returns all of an organization's proposed buys +// QueryAssetBuyAgreements returns all of an organization's proposed bids func (s *SmartContract) QueryAssetBuyAgreements(ctx contractapi.TransactionContextInterface) ([]Agreement, error) { return queryAgreementsByType(ctx, typeAssetBid) } @@ -133,25 +116,24 @@ func queryAgreementsByType(ctx contractapi.TransactionContextInterface, agreeTyp // Query for any object type starting with `agreeType` agreementsIterator, err := ctx.GetStub().GetPrivateDataByPartialCompositeKey(collection, agreeType, []string{}) if err != nil { - return nil, fmt.Errorf("failed to read from private data collection: %s", err.Error()) + return nil, fmt.Errorf("failed to read from private data collection: %v", err) } defer agreementsIterator.Close() - agreements := []Agreement{} - + var agreements []Agreement for agreementsIterator.HasNext() { resp, err := agreementsIterator.Next() if err != nil { return nil, err } - newAgree := new(Agreement) - err = json.Unmarshal(resp.Value, newAgree) + var agreement Agreement + err = json.Unmarshal(resp.Value, &agreement) if err != nil { return nil, err } - agreements = append(agreements, *newAgree) + agreements = append(agreements, agreement) } return agreements, nil @@ -165,27 +147,30 @@ func (s *SmartContract) QueryAssetHistory(ctx contractapi.TransactionContextInte } defer resultsIterator.Close() - records := []QueryResult{} - + var results []QueryResult for resultsIterator.HasNext() { response, err := resultsIterator.Next() if err != nil { return nil, err } - asset := new(Asset) - err = json.Unmarshal(response.Value, asset) + var asset *Asset + err = json.Unmarshal(response.Value, &asset) if err != nil { return nil, err } + timestamp, err := ptypes.Timestamp(response.Timestamp) + if err != nil { + return nil, err + } record := QueryResult{ TxId: response.TxId, - Timestamp: time.Unix(response.Timestamp.Seconds, int64(response.Timestamp.Nanos)), + Timestamp: timestamp, Record: asset, } - records = append(records, record) + results = append(results, record) } - return records, nil + return results, nil } From 09ebf1c1ceb76f152f959cf4bc3a89a4b1afd3b6 Mon Sep 17 00:00:00 2001 From: NIKHIL E GUPTA Date: Wed, 10 Jun 2020 12:35:47 -0400 Subject: [PATCH 2/3] Initialize new private data asset transfer CC Signed-off-by: NIKHIL E GUPTA --- .../assetCollection/indexes/indexOwner.json | 1 + .../chaincode-go/README.md | 306 +++++++++++ .../chaincode-go/collections_config.json | 35 ++ .../chaincode-go/go.mod | 8 + .../chaincode-go/go.sum | 138 +++++ .../chaincode-go/private_asset_queries.go | 187 +++++++ .../chaincode-go/private_asset_transfer.go | 504 ++++++++++++++++++ 7 files changed, 1179 insertions(+) create mode 100644 asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json create mode 100644 asset-transfer-private-data/chaincode-go/README.md create mode 100644 asset-transfer-private-data/chaincode-go/collections_config.json create mode 100644 asset-transfer-private-data/chaincode-go/go.mod create mode 100644 asset-transfer-private-data/chaincode-go/go.sum create mode 100644 asset-transfer-private-data/chaincode-go/private_asset_queries.go create mode 100644 asset-transfer-private-data/chaincode-go/private_asset_transfer.go diff --git a/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json b/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json new file mode 100644 index 00000000..cef82360 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json @@ -0,0 +1 @@ +{"index":{"fields":["type","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"} diff --git a/asset-transfer-private-data/chaincode-go/README.md b/asset-transfer-private-data/chaincode-go/README.md new file mode 100644 index 00000000..af2912b0 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/README.md @@ -0,0 +1,306 @@ +# Private data asset transfer scenario + +The private data asset transfer smart contract uses a simple asset transfer to demonstrate the use of private data collections. All the data that is created by the smart contract is stored in the private data that are collections specified in the `collections_config.json` file: + +- The `assetCollection` is deployed on the peers of Org1 and Org2. This collection is used to store the main asset details, such as the size, color, and owner. The `"memberOnlyRead"` and `"memberOnlyWrite"` parameters are used to specify that only Org1 and Org2 can read and write to this collection. +- The `Org1MSPPrivateCollection` is deployed only on the Org1 peer. Similarly, the `Org2MSPPrivateCollection` is only deployed on the Org2 peer. These organization specific collections are used to store the appraisal value of the asset. This allows the owner of the asset to keep the value of the asset private from other organizations on the channel. The `"endorsementPolicy"` parameter is used to create a collection specific endorsement policy. Each update to `Org1MSPPrivateCollection` or `Org2MSPPrivateCollection` needs to be endorsed by the organization that stores the collection on their peers. + +These three collections are used to transfer the asset between Org1 and Org2. In the tutorial, you will use the private data smart contract to complete the following transfer scenario: + +- A member of Org1 uses the `CreateAsset` function to create a new asset. The `CreateAsset` function reads the certificate information of the client identity that submitted the transaction using the `GetClientIdentity.GetID()` API and creates a new asset with the client identity as the asset owner. The main details of the asset, including the owner, are stored in the `assetCollection` collection. The asset is also created with an appraised value. The appraised value is used by each participant to agree to the transfer of the asset, and is only stored in each organization specific collection. Because the asset is created by a member of Org1, the initial appraisal value agreed by the asset owner is stored in the `Org1MSPPrivateCollection`. +- A member of Org2 creates an agreement to trade using the `AgreeToTransfer` function. The potential buyer uses this function to agree to an appraisal value. The value is stored in `Org2MSPPrivateCollection`, and can only read by a member of Org2. The `AgreeToTransfer` function also uses the `GetClientIdentity().GetID()` API to read the client identity that is agreeing to the Transfer. The **TransferAgreement** is stored in the `assetCollection` as a key with the name "transferAgreement{assetID}" +- After the member of Org2 has agreed to the transfer, the asset owner can transfer the asset to the buyer using the `TransferAsset` function. The smart contract completes a couple of checks before the asset is transferred: + - The transfer request is submitted by the owner of the asset. + - The smart contract uses the `GetPrivateDataHash()` function to check that the hash of the asset appraisal value in `Org1MSPPrivateCollection` matches the hash of the appraisal value in the `Org2MSPPrivateCollection`. If the hashes are the same, it confirms that the owner and the interested buyer have agreed to the same asset value. + If both conditions are met, the transfer function will get the client ID of the buyer from the transfer agreement and make the buyer the new owner of the asset. The transfer function will also delete the asset appraisal value from the collection of the former owner, as well as remove the transfer agreement from the `assetCollection`. + +The private data asset transfer enabled by this smart contract is meant to demonstrate the use private data collections. For an example of a more realistic transfer scenario, see the [secure asset transfer smart contract](../../asset-transfer-secured-agreement/chaincode-go). + +## Download the smart contract dependencies + +Before you install the smart contract on the network, you should download the smart contract dependencies. Run the following command from the `fabric-samples/asset-transfer-private-data/chaincode-go` directory. +``` +GO111MODULE=on go mod vendor +``` + +## Deploy the smart contract to the test network + +You can run the private data transfer scenario using the Fabric test network. Open a command terminal and navigate to test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial. +``` +cd fabric-samples/test-network +``` + +Run the following command to deploy the test network: + +``` +./network.sh up createChannel -ca -s couchdb +``` + +The test network is deployed with two peer organizations. The `createChannel` flag deploys the network with a single channel named `mychannel` with Org1 and Org2 as channel members. The `-ca` flag is used to deploy the network using certificate authorities. This allows you to use each organization's CA to register and enroll new users for this tutorial. + +## Deploy the smart contract to the channel + +You can use the following steps to deploy the smart contract to the channel. + +### Install and approve the chaincode as Org1 + +Set the following environment variables to operate the `peer` CLI as the Org1 admin: +``` +export PATH=${PWD}/../bin:${PWD}:$PATH +export FABRIC_CFG_PATH=$PWD/../config/ +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:7051 +``` + +Run the following command to package the private asset transfer chaincode: +``` +peer lifecycle chaincode package private_transfer.tar.gz --path ../asset-transfer-private-data/chaincode-go --lang golang --label private_transfer_1 +``` + +The command will create a chaincode package named `private_transfer.tar.gz`. We can now install this package on the Org1 peer: +``` +peer lifecycle chaincode install private_transfer.tar.gz +``` + +You will need the chaincode package ID in order to approve the chaincode definition. You can find the package ID by querying your peer: +``` +peer lifecycle chaincode queryinstalled +``` +Save the package ID as an environment variable. The package ID will not be the same for all users, so need to use the result that was returned by the previous command: +``` +export PACKAGE_ID=private_transfer_1:d195682c777705f76a4cdce903a1365dc93a9b5b6fb363eeac292e616cfb64d2 +``` +You can now approve the chaincode as Org1. This command includes a path to the collection definition file. +``` +peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name private_transfer --version 1 --package-id $PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --collections-config ../asset-transfer-private-data/chaincode-go/collections_config.json --signature-policy "OR('Org1MSP.peer','Org2MSP.peer')" +``` + +Note we are approving a chaincode endorsement policy of `"OR('Org1MSP.peer','Org2MSP.peer')"`. This allows Org1 and Org2 to create an asset without receiving an endorsement from the other organization. + + +### Install and approve the chaincode as Org2 + +We can now install and approve the chaincode as Org2. Set the following environment variables to operate as the Org2 admin: +``` +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_LOCALMSPID="Org2MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:9051 +``` + +Because the chaincode is already packaged on our local machine, we can go ahead and install the chaincode on the Org2 peer:` +``` +peer lifecycle chaincode install private_transfer.tar.gz +``` + +We can now approve the chaincode as the Org2 admin: +``` +peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name private_transfer --version 1 --package-id $PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --collections-config ../asset-transfer-private-data/chaincode-go/collections_config.json --signature-policy "OR('Org1MSP.peer','Org2MSP.peer')" +``` + +### Commit the chaincode definition the channel + +Now that a majority (2 out of 2) of channel members have approved the chaincode definition, Org2 can commit the chaincode definition and deploy the chaincode to the channel: +``` +peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name private_transfer --version 1 --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --collections-config ../asset-transfer-private-data/chaincode-go/collections_config.json --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt --signature-policy "OR('Org1MSP.peer','Org2MSP.peer')" +``` +We are now ready use the private asset transfer smart contract. + +## Register identities + +The private data transfer smart contract supports ownership by individual identities that belong to the network. In our scenario, the owner of the asset will be a member of Org1, while the buyer will belong to Org2. To highlight the connection between the `GetClientIdentity().GetID()` API and the information within a users certificate, we will register new two new identities using the Org1 and Org2 CA, and then use the CA's to generate each identities certificate and private key. + +First, we will use the Org1 CA to create the identity asset owner. Set the Fabric CA client home to the MSP of the Org1 CA admin (this identity was generated by the test network script): +``` +export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/ +``` + +You can register a new owner client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org1 --id.name owner --id.secret ownerpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +You can now generate the identity certificates and MSP folder by providing the enroll name and secret to the enroll command: +``` +fabric-ca-client enroll -u https://owner:ownerpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the owner identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp/config.yaml +``` + +We can now use the Org2 CA to create the buyer identity. Set the Fabric CA client home the Org2 CA admin: +``` +export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/ +``` + +You can register a new owner client identity using the `fabric-ca-client` tool: +``` +fabric-ca-client register --caname ca-org2 --id.name buyer --id.secret buyerpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem +``` + +We can now enroll to generate the identity MSP folder: +``` +fabric-ca-client enroll -u https://buyer:buyerpw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem +``` + +Run the command below to copy the Node OU configuration file into the buyer identity MSP folder. +``` +cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp/config.yaml +``` + +## Create an asset + +Now that we have created the identity of the asset owner, we can invoke the private data smart contract to create a new asset. Use the following environment variables to operate the `peer` CLI as the owner identity from Org1. + +``` +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:7051 +``` + +Run the following command to define the asset properties: +``` +export ASSET_PROPERTIES=$(echo -n "{\"objectType\":\"asset\",\"assetID\":\"asset1\",\"color\":\"green\",\"size\":20,\"appraisedValue\":100}" | base64 | tr -d \\n) +``` + +We can the invoke the smart contract to create the new asset: +``` +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"CreateAsset","Args":[]}' --transient "{\"asset_properties\":\"$ASSET_PROPERTIES\"}" +``` + +The command above uses the transient data flag, `--transient`, to provide the asset details to the smart contract. Transient data is not part of the transaction read/write set, and as result is not stored on the channel ledger. + +Note that command above only targets the Org1 peer. The `CreateAsset` transactions writes to two collections, `assetCollection` and `Org1MSPPrivateCollection`. The `Org1MSPPrivateCollection` requires and endorsement from the Org1 peer in order to write to the collection, while the `assetCollection` inherits the endorsement policy of the chaincode, `"OR('Org1MSP.peer','Org2MSP.peer')"`. An endorsement from the Org1 peer can meet both endorsement policies and is able to create an asset without an endorsement from Org2. + +We can read the main details of the asset that was created by using the `ReadAsset` function to query the `assetCollection` collection: +``` +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"ReadAsset","Args":["asset1"]}' +``` + +When successful, the command will return the following result: +``` +{"objectType":"asset","assetID":"asset1","color":"green","size":20,"owner":"eDUwOTo6Q049b3duZXIsT1U9Y2xpZW50LE89SHlwZXJsZWRnZXIsU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUzo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw=="} +``` + +The `"owner"` of the asset is the identity that created the asset by invoking the smart contract. The `GetClientIdentity().GetID()` API reads the common name and issuer of the identity certificate. You can see that information by decoding the owner string out of base64 format: +``` +echo eDUwOTo6Q049b3duZXIsT1U9Y2xpZW50LE89SHlwZXJsZWRnZXIsU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUzo6Q049Y2Eub3JnMS5leGFtcGxlLmNvbSxPPW9yZzEuZXhhbXBsZS5jb20sTD1EdXJoYW0sU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUw | base64 --decode +``` + +The result will show the common name and issuer of the owner certificate: +``` +x509::CN=owner,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org1.example.com,O=org1.example.com,L=Durham,ST=North Carolina,C=Umacbook-air:test-network +``` + +A member of Org1 can also read the private asset appraisal value that is stored in the `Org1MSPPrivateCollection` on the Org1 peer: +``` +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"ReadAssetPrivateDetails","Args":["Org1MSPPrivateCollection","asset1"]}' +``` +The query will return the value of the asset: +``` +{"assetID":"asset1","appraisedValue":100} +``` + +### Buyer from Org2 agrees to buy the asset + +The buyer identity from Org2 is interested in buying the asset. Set the following environment variables to operate as the buyer: + +``` +export CORE_PEER_LOCALMSPID="Org2MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/buyer@org2.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:9051 +``` + +Now that we are operating as a member of Org2, we can demonstrate that the asset appraisal is not stored on the Org2 peer: +``` +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"ReadAssetPrivateDetails","Args":["Org2MSPPrivateCollection","asset1"]}' +``` +The buyer only finds that asset1 does exist in his collection: +``` +Error: endorsement failure during invoke. response: status:500 message:"appraisal value for asset1 does not exist in private data collection" +``` + +Nor is a member of Org2 able to read the Org1 private data collection: +``` +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"ReadAssetPrivateDetails","Args":["Org1MSPPrivateCollection","asset1"]}' +``` +By setting `"memberOnlyRead": true` in the collection configuration file, we specify that only members of of Org1 can read data from the collection. A member who tries to read the collection would only get the following response. +``` +Error: endorsement failure during query. response: status:500 message:"failed to read from asset details GET_STATE failed: transaction ID: 10d39a7d0b340455a19ca4198146702d68d884d41a0e60936f1599c1ddb9c99d: tx creator does not have read access permission on privatedata in chaincodeName:private_transfer collectionName: Org1MSPPrivateCollection" +``` + +To purchase the asset, the buyer needs to agree to the same value as the asset owner. The agreed value will be stored in the `Org2MSPDetailsCollection` collection on the Org2 peer. Run the following command to agree to the appraised value of 100: +``` +export ASSET_VALUE=$(echo -n "{\"assetID\":\"asset1\",\"appraisedValue\":100}" | base64 | tr -d \\n) +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"AgreeToTransfer","Args":[]}' --transient "{\"asset_value\":\"$ASSET_VALUE\"}" +``` + +The buyer can now query the value they agreed to in the Org2 private data collection: +``` +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"ReadAssetPrivateDetails","Args":["Org2MSPPrivateCollection","asset1"]}' +``` +The invoke will return the following value: +``` +{"assetID":"asset1","appraisedValue":100} +``` + +## Transfer the asset to Org2 + +Now that buyer has agreed to buy the asset for appraised value, the owner from Org1 can transfer the asset to Org2. Set the following environment variables to operate as Org1: +``` +export CORE_PEER_LOCALMSPID="Org1MSP" +export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/owner@org1.example.com/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export CORE_PEER_ADDRESS=localhost:7051 +``` + +To transfer the asset, the owner needs to pass the MSP ID of new asset owner. The transfer function will read the client ID of the interested buyer from the transfer agreement. +``` +export ASSET_OWNER=$(echo -n "{\"assetID\":\"asset1\",\"buyerMSP\":\"Org2MSP\"}" | base64 | tr -d \\n) +``` + +The owner of the asset needs to initiate the transfer. + +``` +peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"TransferAsset","Args":[]}' --transient "{\"asset_owner\":\"$ASSET_OWNER\"}" --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +``` +You can query `asset1` to see the results of the transfer. +``` +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"ReadAsset","Args":["asset1"]}' +``` + +The results will show that the buyer identity now owns the asset: + +``` +{"objectType":"asset","assetID":"asset1","color":"green","size":20,"owner":"eDUwOTo6Q049YnV5ZXIsT1U9Y2xpZW50LE89SHlwZXJsZWRnZXIsU1Q9Tm9ydGggQ2Fyb2xpbmEsQz1VUzo6Q049Y2Eub3JnMi5leGFtcGxlLmNvbSxPPW9yZzIuZXhhbXBsZS5jb20sTD1IdXJzbGV5LFNUPUhhbXBzaGlyZSxDPVVL"} +``` + +You can base64 decode the `"owner"` to see that it is the buyer identity: +``` +x509::CN=buyer,OU=client,O=Hyperledger,ST=North Carolina,C=US::CN=ca.org2.example.com,O=org2.example.com,L=Hursley,ST=Hampshire,C=UKmacbook-air +``` + +You can also confirm that transfer removed the private details from the Org1 collection: +``` +peer chaincode query -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n private_transfer -c '{"function":"ReadAssetPrivateDetails","Args":["Org1MSPPrivateCollection","asset1"]}' +``` +Your query will return the following result: +``` +Error: endorsement failure during query. response: status:500 message:"appraisal value for asset1 does not exist in private data collection" +``` + +## Clean up + +When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created: + +``` +./network.sh down +``` diff --git a/asset-transfer-private-data/chaincode-go/collections_config.json b/asset-transfer-private-data/chaincode-go/collections_config.json new file mode 100644 index 00000000..cb3729aa --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/collections_config.json @@ -0,0 +1,35 @@ +[ + { + "name": "assetCollection", + "policy": "OR('Org1MSP.member', 'Org2MSP.member')", + "requiredPeerCount": 1, + "maxPeerCount": 1, + "blockToLive":1000000, + "memberOnlyRead": true, + "memberOnlyWrite": true +}, + { + "name": "Org1MSPPrivateCollection", + "policy": "OR('Org1MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org1MSP.member')" + } + }, + { + "name": "Org2MSPPrivateCollection", + "policy": "OR('Org2MSP.member')", + "requiredPeerCount": 0, + "maxPeerCount": 1, + "blockToLive":3, + "memberOnlyRead": true, + "memberOnlyWrite": false, + "endorsementPolicy": { + "signaturePolicy": "OR('Org2MSP.member')" + } + } +] diff --git a/asset-transfer-private-data/chaincode-go/go.mod b/asset-transfer-private-data/chaincode-go/go.mod new file mode 100644 index 00000000..f895d27d --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/go.mod @@ -0,0 +1,8 @@ +module github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go/go + +go 1.14 + +require ( + github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212 + github.com/hyperledger/fabric-contract-api-go v1.1.0 +) diff --git a/asset-transfer-private-data/chaincode-go/go.sum b/asset-transfer-private-data/chaincode-go/go.sum new file mode 100644 index 00000000..94a66455 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/go.sum @@ -0,0 +1,138 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cucumber/godog v0.8.0/go.mod h1:Cp3tEV1LRAyH/RuCThcxHS/+9ORZ+FMzPva2AZ5Ki+A= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212 h1:1i4lnpV8BDgKOLi1hgElfBqdHXjXieSuj8629mwBZ8o= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212/go.mod h1:N7H3sA7Tx4k/YzFq7U0EPdqJtqvM4Kild0JoCc7C0Dc= +github.com/hyperledger/fabric-contract-api-go v1.1.0 h1:K9uucl/6eX3NF0/b+CGIiO1IPm1VYQxBkpnVGJur2S4= +github.com/hyperledger/fabric-contract-api-go v1.1.0/go.mod h1:nHWt0B45fK53owcFpLtAe8DH0Q5P068mnzkNXMPSL7E= +github.com/hyperledger/fabric-protos-go v0.0.0-20190919234611-2a87503ac7c9/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e h1:9PS5iezHk/j7XriSlNuSQILyCOfcZ9wZ3/PiucmSE8E= +github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e/go.mod h1:xVYTjK4DtZRBxZ2D9aE4y6AbLaPwue2o/criQyQbVD0= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542 h1:6ZQFf1D2YYDDI7eSwW8adlkkavTB9sw5I24FVtEvNUQ= +golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/asset-transfer-private-data/chaincode-go/private_asset_queries.go b/asset-transfer-private-data/chaincode-go/private_asset_queries.go new file mode 100644 index 00000000..cf87cb25 --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/private_asset_queries.go @@ -0,0 +1,187 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// ReadAsset reads the information from collection +func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) { + + assetJSON, err := ctx.GetStub().GetPrivateData(assetCollection, assetID) //get the asset from chaincode state + if err != nil { + return nil, fmt.Errorf("failed to read from asset %v", err) + } + if assetJSON == nil { + return nil, fmt.Errorf("%v does not exist", assetID) + } + + asset := new(Asset) + err = json.Unmarshal(assetJSON, asset) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + return asset, nil + +} + +// ReadAssetPrivateDetails reads the asset private details in organization specific collection +func (s *SmartContract) ReadAssetPrivateDetails(ctx contractapi.TransactionContextInterface, collection string, assetID string) (*AssetPrivateDetails, error) { + + assetDetailsJSON, err := ctx.GetStub().GetPrivateData(collection, assetID) // Get the asset from chaincode state + if err != nil { + return nil, fmt.Errorf("failed to read from asset details %v", err) + } + if assetDetailsJSON == nil { + return nil, fmt.Errorf("appraisal value for %v does not exist in private data collection", assetID) + } + + assetDetails := new(AssetPrivateDetails) + err = json.Unmarshal(assetDetailsJSON, assetDetails) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + return assetDetails, nil +} + +// ReadTransferAgreement gets the identity from the transfer agreement from collection +func (s *SmartContract) ReadTransferAgreement(ctx contractapi.TransactionContextInterface, assetID string) (string, error) { + + // create composite key + transferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{assetID}) + if err != nil { + return "", fmt.Errorf("failed to create composite key: %v", err) + } + + buyerIdentity, err := ctx.GetStub().GetPrivateData(assetCollection, transferAgreeKey) // Get the identity from collection + if err != nil { + return "", fmt.Errorf("failed to read from asset %v", err) + } + if buyerIdentity == nil { + return "", fmt.Errorf("transfer agreement for %v does not exist", assetID) + } + + return string(buyerIdentity), nil + +} + +// =========================================================================================== +// GetAssetByRange performs a range query based on the start and end keys provided. Range +// queries can be used to read data from private data collections, but can not be used in +// a transaction that also writes to private data. + +// =========================================================================================== +func (s *SmartContract) GetAssetByRange(ctx contractapi.TransactionContextInterface, startKey string, endKey string) ([]Asset, error) { + + resultsIterator, err := ctx.GetStub().GetPrivateDataByRange(assetCollection, startKey, endKey) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []Asset{} + + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + asset := new(Asset) + + err = json.Unmarshal(response.Value, asset) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + results = append(results, *asset) + } + + return results, nil + +} + +// =======Rich queries ========================================================================= +// Two examples of rich queries are provided below (parameterized query and ad hoc query). +// Rich queries pass a query string to the state database. +// Rich queries are only supported by state database implementations +// that support rich query (e.g. CouchDB). +// The query string is in the syntax of the underlying state database. +// With rich queries there is no guarantee that the result set hasn't changed between +// endorsement time and commit time, aka 'phantom reads'. +// Therefore, rich queries should not be used in update transactions, unless the +// application handles the possibility of result set changes between endorsement and commit time. +// Rich queries can be used for point-in-time queries against a peer. +// ============================================================================================ + +// ===== Example: Parameterized rich query ================================================= +// QueryAssetByOwner queries for assets based on a passed in owner. +// This is an example of a parameterized query where the query logic is baked into the chaincode, +// and accepting a single query parameter (owner). +// Only available on state databases that support rich query (e.g. CouchDB) +// ========================================================================================= +func (s *SmartContract) QueryAssetByOwner(ctx contractapi.TransactionContextInterface, owner string) ([]Asset, error) { + + queryString := fmt.Sprintf("{\"selector\":{\"type\":\"asset\",\"owner\":\"%s\"}}", owner) + + queryResults, err := s.getQueryResultForQueryString(ctx, queryString) + if err != nil { + return nil, err + } + return queryResults, nil +} + +// ===== Example: Ad hoc rich query ======================================================== +// QueryAssets uses a query string to perform a query for assets. +// Query string matching state database syntax is passed in and executed as is. +// Supports ad hoc queries that can be defined at runtime by the client. +// If this is not desired, follow the QueryAssetByOwner example for parameterized queries. +// Only available on state databases that support rich query (e.g. CouchDB) +// ========================================================================================= +func (s *SmartContract) QueryAssets(ctx contractapi.TransactionContextInterface, queryString string) ([]Asset, error) { + + queryResults, err := s.getQueryResultForQueryString(ctx, queryString) + if err != nil { + return nil, err + } + return queryResults, nil +} + +// getQueryResultForQueryString executes the passed in query string. +func (s *SmartContract) getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string) ([]Asset, error) { + + resultsIterator, err := ctx.GetStub().GetPrivateDataQueryResult(assetCollection, queryString) + if err != nil { + return nil, err + } + defer resultsIterator.Close() + + results := []Asset{} + + for resultsIterator.HasNext() { + response, err := resultsIterator.Next() + if err != nil { + return nil, err + } + + asset := new(Asset) + + err = json.Unmarshal(response.Value, asset) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + results = append(results, *asset) + } + return results, nil +} diff --git a/asset-transfer-private-data/chaincode-go/private_asset_transfer.go b/asset-transfer-private-data/chaincode-go/private_asset_transfer.go new file mode 100644 index 00000000..90434cdb --- /dev/null +++ b/asset-transfer-private-data/chaincode-go/private_asset_transfer.go @@ -0,0 +1,504 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +// Asset describes main asset details that are visible to all organizations +type Asset struct { + Type string `json:"objectType"` //Type is used to distinguish the various types of objects in state database + ID string `json:"assetID"` + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` +} + +// AssetPrivateDetails describes details that are private to owners +type AssetPrivateDetails struct { + ID string `json:"assetID"` + AppraisedValue int `json:"appraisedValue"` +} + +const assetCollection = "assetCollection" + +const transferAgreementObjectType = "transferAgreement" + +type SmartContract struct { + contractapi.Contract +} + +// CreateAsset creates a new asset by placing the main asset details in the assetCollection +// that can be read by both organizations. The appraisal value is stored in the owners org specific collection. +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface) error { + + // Get new asset from transient map + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Asset properties are private, therefore they get passed in transient field + transientAssetJSON, ok := transientMap["asset_properties"] + if !ok { + return fmt.Errorf("asset not found in the transient map") + } + + type assetTransientInput struct { + Type string `json:"objectType"` //Type is used to distinguish the various types of objects in state database + ID string `json:"assetID"` + Color string `json:"color"` + Size int `json:"size"` + AppraisedValue int `json:"appraisedValue"` + } + + var assetInput assetTransientInput + err = json.Unmarshal(transientAssetJSON, &assetInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetInput.Type) == 0 { + return fmt.Errorf("objectType field must be a non-empty string") + } + if len(assetInput.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + if len(assetInput.Color) == 0 { + return fmt.Errorf("color field must be a non-empty string") + } + if assetInput.Size <= 0 { + return fmt.Errorf("size field must be a positive integer") + } + if assetInput.AppraisedValue <= 0 { + return fmt.Errorf("appraisedValue field must be a positive integer") + } + + // Check if asset already exists + assetAsBytes, err := ctx.GetStub().GetPrivateData(assetCollection, assetInput.ID) + if err != nil { + return fmt.Errorf("failed to get asset: %v", err) + } else if assetAsBytes != nil { + fmt.Println("this asset already exists: " + assetInput.ID) + return fmt.Errorf("this asset already exists: " + assetInput.ID) + } + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + // Make submitting client the owner + asset := &Asset{ + Type: assetInput.Type, + ID: assetInput.ID, + Color: assetInput.Color, + Size: assetInput.Size, + Owner: clientID, + } + assetJSONasBytes, err := json.Marshal(asset) + if err != nil { + return fmt.Errorf("failed to marshal into JSON: %v", err) + } + + // Save asset to private data collection + err = ctx.GetStub().PutPrivateData(assetCollection, assetInput.ID, assetJSONasBytes) + if err != nil { + return fmt.Errorf("failed to put asset into private data collection: %v", err) + } + + // Save asset details to collection visible to owning organization + assetPrivateDetails := &AssetPrivateDetails{ + ID: assetInput.ID, + AppraisedValue: assetInput.AppraisedValue, + } + + assetPrivateDetailsAsBytes, err := json.Marshal(assetPrivateDetails) // marshal asset details to JSON + if err != nil { + return fmt.Errorf("failed to marshal into JSON: %v", err) + } + + // Get collection name for this organization. Needs to be read by a member of the organization. + orgCollection, err := getCollectionName(ctx, true) + + // Put asset appraised value into owners org specific private data collection + err = ctx.GetStub().PutPrivateData(orgCollection, assetInput.ID, assetPrivateDetailsAsBytes) + if err != nil { + return fmt.Errorf("failed to put asset private details: %v", err) + } + return nil +} + +// AgreeToTransfer is used by the potential buyer of the asset to agree to the +// asset value. The agreed to appraisal value is stored in the buying orgs +// org specifc collection, while the the buyer client ID is stored in the asset collection +// using a composite key +func (s *SmartContract) AgreeToTransfer(ctx contractapi.TransactionContextInterface) error { + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + // Value is private, therefore it gets passed in transient field + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Persist the JSON bytes as-is so that there is no risk of nondeterministic marshaling. + valueJSONasBytes, ok := transientMap["asset_value"] + if !ok { + return fmt.Errorf("asset_value key not found in the transient map") + } + + // Unmarshal the tranisent map to get the asset ID. + var valueJSON AssetPrivateDetails + err = json.Unmarshal(valueJSONasBytes, &valueJSON) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + // Do some error checking since we get the chance + if len(valueJSON.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + if valueJSON.AppraisedValue <= 0 { + return fmt.Errorf("appraisedValue field must be a positive integer") + } + + // Get collection name for this organization. Needs to be read by a member of the organization. + orgCollection, err := getCollectionName(ctx, true) + + // Put agreed value in the org specifc private data collection + err = ctx.GetStub().PutPrivateData(orgCollection, valueJSON.ID, valueJSONasBytes) + if err != nil { + return fmt.Errorf("failed to put asset bid: %v", err) + } + + // Create agreeement that indicates which identity has agreed to purchase + // In a more realistic transfer scenario, a transfer agreement would be secured to ensure that it cannot + // be overwritten by another channel member + transferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{valueJSON.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + err = ctx.GetStub().PutPrivateData(assetCollection, transferAgreeKey, []byte(clientID)) + if err != nil { + return fmt.Errorf("failed to put asset bid: %v", err) + } + + return nil +} + +// TransferAsset transfers the asset to the new owner by setting a new owner ID +func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface) error { + + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient %v", err) + } + + // Asset properties are private, therefore they get passed in transient field + transientTransferJSON, ok := transientMap["asset_owner"] + if !ok { + return fmt.Errorf("asset owner not found in the transient map") + } + + type assetTransferTransientInput struct { + ID string `json:"assetID"` + BuyerMSP string `json:"buyerMSP"` + } + + var assetTransferInput assetTransferTransientInput + err = json.Unmarshal(transientTransferJSON, &assetTransferInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetTransferInput.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + if len(assetTransferInput.BuyerMSP) == 0 { + return fmt.Errorf("buyerMSP field must be a non-empty string") + } + + // Read asset from the private data collection + asset, err := s.ReadAsset(ctx, assetTransferInput.ID) + if err != nil { + return fmt.Errorf("failed to get asset: %v", err) + } + + // Verify transfer details and transfer owner + err = s.verifyAgreement(ctx, assetTransferInput.ID, asset.Owner, assetTransferInput.BuyerMSP) + if err != nil { + return fmt.Errorf("failed transfer verification: %v", err) + } + + buyerID, err := s.ReadTransferAgreement(ctx, assetTransferInput.ID) + + // Transfer asset in private data collection to new owner + asset.Owner = buyerID + + assetJSONasBytes, _ := json.Marshal(asset) + err = ctx.GetStub().PutPrivateData(assetCollection, assetTransferInput.ID, assetJSONasBytes) //rewrite the asset + if err != nil { + return err + } + + // Get collection name for this organization + ownersCollection, err := getCollectionName(ctx, false) + + // Delete the marble appraised value from this organiztion's private data collection + err = ctx.GetStub().DelPrivateData(ownersCollection, assetTransferInput.ID) + if err != nil { + return err + } + + // Delete the transfer agreement from the asset collection + transferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{assetTransferInput.ID}) + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + err = ctx.GetStub().DelPrivateData(assetCollection, transferAgreeKey) + if err != nil { + return err + } + + return nil + +} + +// verifyAgreement is an internal helper function used by TransferAsset to verify +// that the transfer is being initiated by the owner and that the buyer has agreed +// to the same appraisal value as the owner +func (s *SmartContract) verifyAgreement(ctx contractapi.TransactionContextInterface, assetID string, owner string, buyerMSP string) error { + + // Check 1: verify that the transfer is being initiatied by the owner + + // Get ID of submitting client identity + clientID, err := ctx.GetClientIdentity().GetID() + if err != nil { + return fmt.Errorf("failed to get verified OrgID: %v", err) + } + + if clientID != owner { + return fmt.Errorf("error: submitting client identity does not own asset") + } + + // Check 2: verify that the buyer has agreed to the appraised value + + // Get collection names + + collectionOwner, err := getCollectionName(ctx, false) // get buyers collection + + collectionBuyer := buyerMSP + "PrivateCollection" // get buyers collection + + // Get hash of owners agreed to value + ownerAppraisedValueHash, err := ctx.GetStub().GetPrivateDataHash(collectionOwner, assetID) + if err != nil { + return fmt.Errorf("failed to get hash of appraised value from owners collection %v: %v", collectionOwner, err) + } + if ownerAppraisedValueHash == nil { + return fmt.Errorf("hash of appraised value for %v does not exist in collection %v", assetID, collectionOwner) + } + + // Get hash of buyers agreed to value + buyerAppraisedValueHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, assetID) + if err != nil { + return fmt.Errorf("failed to get hash of appraised value from buyer collection %v: %v", collectionBuyer, err) + } + if buyerAppraisedValueHash == nil { + return fmt.Errorf("hash of appraised value for %v does not exist in collection %v", assetID, collectionBuyer) + } + + // Verify that the two hashes match + if !bytes.Equal(ownerAppraisedValueHash, buyerAppraisedValueHash) { + return fmt.Errorf("hash for appraised value for owner %x does not value for seller %x", ownerAppraisedValueHash, buyerAppraisedValueHash) + } + + return nil +} + +// DeleteAsset can be used by the owner of the asset to delete the asset +func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface) error { + + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("Error getting transient: %v", err) + } + + // Asset properties are private, therefore they get passed in transient field + transientDeleteJSON, ok := transientMap["asset_delete"] + if !ok { + return fmt.Errorf("asset to delete not found in the transient map") + } + + type assetDelete struct { + ID string `json:"assetID"` + } + + var assetDeleteInput assetDelete + err = json.Unmarshal(transientDeleteJSON, &assetDeleteInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetDeleteInput.ID) == 0 { + return fmt.Errorf("assetID field must be a non-empty string") + } + + valAsbytes, err := ctx.GetStub().GetPrivateData(assetCollection, assetDeleteInput.ID) //get the asset from chaincode state + if err != nil { + return fmt.Errorf("failed to read asset: %v", err) + } + if valAsbytes == nil { + return fmt.Errorf("asset private details does not exist: %v", assetDeleteInput.ID) + } + + var assetToDelete Asset + err = json.Unmarshal([]byte(valAsbytes), &assetToDelete) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + // delete the asset from state + err = ctx.GetStub().DelPrivateData(assetCollection, assetDeleteInput.ID) + if err != nil { + return fmt.Errorf("failed to delete state: %v", err) + } + + // Finally, delete private details of asset + + ownerCollection, err := getCollectionName(ctx, true) // Get owners collection. Needs to be read by a member of the organization. + + err = ctx.GetStub().DelPrivateData(ownerCollection, assetDeleteInput.ID) // Delete the asset + if err != nil { + return err + } + + return nil + +} + +// DeleteTranferAgreement can be used by the buyer to withdraw a proposal from +// the asset collection and from his own collection. +func (s *SmartContract) DeleteTranferAgreement(ctx contractapi.TransactionContextInterface) error { + + transientMap, err := ctx.GetStub().GetTransient() + if err != nil { + return fmt.Errorf("error getting transient: %v", err) + } + + // Asset properties are private, therefore they get passed in transient field + transientDeleteJSON, ok := transientMap["agree_delete"] + if !ok { + return fmt.Errorf("asset to delete not found in the transient map") + } + + type assetDelete struct { + ID string `json:"assetID"` + } + + var assetDeleteInput assetDelete + err = json.Unmarshal(transientDeleteJSON, &assetDeleteInput) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + if len(assetDeleteInput.ID) == 0 { + return fmt.Errorf("ID field must be a non-empty string") + } + + // Delete private details of agreement + + orgCollection, err := getCollectionName(ctx, true) // Get proposers collection. Needs to be read by a member of the organization. + + err = ctx.GetStub().DelPrivateData(orgCollection, assetDeleteInput.ID) // Delete the asset + if err != nil { + return err + } + + // Delete transfer agreement record + + tranferAgreeKey, err := ctx.GetStub().CreateCompositeKey(transferAgreementObjectType, []string{assetDeleteInput.ID}) // Create composite key + if err != nil { + return fmt.Errorf("failed to create composite key: %v", err) + } + + err = ctx.GetStub().DelState(tranferAgreeKey) // remove agreement from state + if err != nil { + return err + } + + return nil + +} + +// getCollectionName is an internal helper function to get collection of submitting client identity. +// The collection name 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. +func getCollectionName(ctx contractapi.TransactionContextInterface, verifyOrg bool) (string, error) { + + // Get the MSP ID of submitting client identity + clientMSPID, err := ctx.GetClientIdentity().GetMSPID() + if err != nil { + return "", fmt.Errorf("failed to get verified OrgID: %v", err) + } + + // Verify that the client is submitting request to peer in their organization + if verifyOrg { + err = verifyClientOrgMatchesPeerOrg(clientMSPID) + if err != nil { + return "", err + } + } + + // Create the collection name + orgCollection := clientMSPID + "PrivateCollection" + + return orgCollection, nil +} + +// verifyClientOrgMatchesPeerOrg is an internal function used verify client org id and matches peer org id. +func verifyClientOrgMatchesPeerOrg(clientMSPID string) error { + peerMSPID, err := shim.GetMSPID() + if err != nil { + return fmt.Errorf("failed getting peer's orgID: %v", err) + } + + if clientMSPID != peerMSPID { + return fmt.Errorf("client from org %v is not authorized to read or write private data from an org %v peer", clientMSPID, peerMSPID) + } + + return nil +} + +func main() { + + chaincode, err := contractapi.NewChaincode(new(SmartContract)) + + if err != nil { + log.Panicf("error creating private mables chaincode: %v", err) + return + } + + if err := chaincode.Start(); err != nil { + log.Panicf("error starting private mables chaincode: %v", err) + } +} From aa3ae32afb041dca69a814071e40f252b4646b60 Mon Sep 17 00:00:00 2001 From: Chris Gabriel Date: Sun, 12 Jul 2020 10:35:18 -0500 Subject: [PATCH 3/3] Fix param sequence in smartcontract smartcontract_test. Signed-off-by: Chris Gabriel --- .../chaincode-go/chaincode/smartcontract.go | 4 ++-- .../chaincode-go/chaincode/smartcontract_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/asset-transfer-basic/chaincode-go/chaincode/smartcontract.go b/asset-transfer-basic/chaincode-go/chaincode/smartcontract.go index 862634e8..a2b9b169 100644 --- a/asset-transfer-basic/chaincode-go/chaincode/smartcontract.go +++ b/asset-transfer-basic/chaincode-go/chaincode/smartcontract.go @@ -54,7 +54,7 @@ func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) } // CreateAsset issues a new asset to the world state with given details. -func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id, color, owner string, size, appraisedValue int) error { +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { exists, err := s.AssetExists(ctx, id) if err != nil { return err @@ -98,7 +98,7 @@ func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, i } // UpdateAsset updates an existing asset in the world state with provided parameters. -func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id, color, owner string, size, appraisedValue int) error { +func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { exists, err := s.AssetExists(ctx, id) if err != nil { return err diff --git a/asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go b/asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go index 1127517c..ef6e2921 100644 --- a/asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go +++ b/asset-transfer-basic/chaincode-go/chaincode/smartcontract_test.go @@ -48,15 +48,15 @@ func TestCreateAsset(t *testing.T) { transactionContext.GetStubReturns(chaincodeStub) assetTransfer := chaincode.SmartContract{} - err := assetTransfer.CreateAsset(transactionContext, "", "", "", 0, 0) + err := assetTransfer.CreateAsset(transactionContext, "", "", 0, "", 0) require.NoError(t, err) chaincodeStub.GetStateReturns([]byte{}, nil) - err = assetTransfer.CreateAsset(transactionContext, "asset1", "", "", 0, 0) + err = assetTransfer.CreateAsset(transactionContext, "asset1", "", 0, "", 0) require.EqualError(t, err, "the asset asset1 already exists") chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset")) - err = assetTransfer.CreateAsset(transactionContext, "asset1", "", "", 0, 0) + err = assetTransfer.CreateAsset(transactionContext, "asset1", "", 0, "", 0) require.EqualError(t, err, "failed to read from world state: unable to retrieve asset") } @@ -96,15 +96,15 @@ func TestUpdateAsset(t *testing.T) { chaincodeStub.GetStateReturns(bytes, nil) assetTransfer := chaincode.SmartContract{} - err = assetTransfer.UpdateAsset(transactionContext, "", "", "", 0, 0) + err = assetTransfer.UpdateAsset(transactionContext, "", "", 0, "", 0) require.NoError(t, err) chaincodeStub.GetStateReturns(nil, nil) - err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", "", 0, 0) + err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", 0, "", 0) require.EqualError(t, err, "the asset asset1 does not exist") chaincodeStub.GetStateReturns(nil, fmt.Errorf("unable to retrieve asset")) - err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", "", 0, 0) + err = assetTransfer.UpdateAsset(transactionContext, "asset1", "", 0, "", 0) require.EqualError(t, err, "failed to read from world state: unable to retrieve asset") }