mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-18 07:55:10 +00:00
A recent commit added the potential buyer to an asset's state based endorsement policy. That change was problematic because if the transfer fell through, the buyer lost control of the asset, in that they could no longer update the asset or change the sell price or sell to somebody else. The asset state based endorsement policy is now based on the seller only, and we document that additional parties could be added such as a trusted third party (although no such party exists in test network at this time). This commit also re-adds some necessary verifications, and make other minor edits and comments to help users understand the sample. Signed-off-by: David Enyeart <enyeart@us.ibm.com>
624 lines
21 KiB
Go
624 lines
21 KiB
Go
/*
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
typeAssetForSale = "S"
|
|
typeAssetBid = "B"
|
|
typeAssetSaleReceipt = "SR"
|
|
typeAssetBuyReceipt = "BR"
|
|
)
|
|
|
|
type SmartContract struct {
|
|
contractapi.Contract
|
|
}
|
|
|
|
// Asset struct and properties must be exported (start with capitals) to work with contract api metadata
|
|
type Asset struct {
|
|
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 {
|
|
price int
|
|
timestamp time.Time
|
|
}
|
|
|
|
// CreateAsset creates an asset, sets it as owned by the client's org and returns its id
|
|
// the id of the asset corresponds to the hash of the properties of the asset that are passed by transiet field
|
|
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, publicDescription string) (string, error) {
|
|
transientMap, err := ctx.GetStub().GetTransient()
|
|
if err != nil {
|
|
return "", fmt.Errorf("error getting transient: %v", err)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// AssetID will be the hash of the asset's properties
|
|
hash := sha256.New()
|
|
hash.Write(immutablePropertiesJSON)
|
|
assetID := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
// Get the clientOrgId from the input, will be used for implicit collection, owner, and state-based endorsement policy
|
|
clientOrgID, err := getClientOrgID(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// In this scenario, client is only authorized to read/write private data from its own peer, therefore verify client org id matches peer org id.
|
|
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
asset := Asset{
|
|
ObjectType: "asset",
|
|
ID: assetID,
|
|
OwnerOrg: clientOrgID,
|
|
PublicDescription: publicDescription,
|
|
}
|
|
assetBytes, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create asset JSON: %v", err)
|
|
}
|
|
|
|
err = ctx.GetStub().PutState(assetID, assetBytes)
|
|
if err != nil {
|
|
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.
|
|
// In practice, consider additional endorsers such as a trusted third party to further secure transfers.
|
|
endorsingOrgs := []string{clientOrgID}
|
|
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed setting state based endorsement for buyer and seller: %v", err)
|
|
}
|
|
|
|
// Persist private immutable asset properties to owner's private data collection
|
|
collection := buildCollectionName(clientOrgID)
|
|
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to put Asset private details: %v", err)
|
|
}
|
|
|
|
return assetID, nil
|
|
}
|
|
|
|
// ChangePublicDescription updates the assets public description. Only the current owner can update the public description
|
|
func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionContextInterface, assetID string, newDescription string) error {
|
|
|
|
clientOrgID, err := getClientOrgID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
asset, err := s.ReadAsset(ctx, assetID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get asset: %v", err)
|
|
}
|
|
|
|
// 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: %v", err)
|
|
}
|
|
|
|
return ctx.GetStub().PutState(assetID, updatedAssetJSON)
|
|
}
|
|
|
|
// AgreeToSell adds seller's asking price to seller's implicit private data collection.
|
|
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error {
|
|
asset, err := s.ReadAsset(ctx, assetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientOrgID, err := getClientOrgID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify that this client belongs to the peer's org
|
|
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify that this clientOrgId actually owns the asset.
|
|
if clientOrgID != asset.OwnerOrg {
|
|
return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg)
|
|
}
|
|
|
|
return agreeToPrice(ctx, assetID, typeAssetForSale)
|
|
}
|
|
|
|
// AgreeToBuy adds buyer's bid price and asset properties to buyer's implicit private data collection
|
|
func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface, assetID string) error {
|
|
transientMap, err := ctx.GetStub().GetTransient()
|
|
if err != nil {
|
|
return fmt.Errorf("error getting transient: %v", err)
|
|
}
|
|
|
|
clientOrgID, err := getClientOrgID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify that this client belongs to the peer's org
|
|
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Asset properties must be retrieved from the transient field as they are private
|
|
immutablePropertiesJSON, ok := transientMap["asset_properties"]
|
|
if !ok {
|
|
return fmt.Errorf("asset_properties key not found in the transient map")
|
|
}
|
|
|
|
// Persist private immutable asset properties to seller's private data collection
|
|
collection := buildCollectionName(clientOrgID)
|
|
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put Asset private details: %v", err)
|
|
}
|
|
|
|
return agreeToPrice(ctx, assetID, typeAssetBid)
|
|
}
|
|
|
|
// agreeToPrice adds a bid or ask price to caller's implicit private data collection
|
|
func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error {
|
|
// In this scenario, both buyer and seller are authoried to read/write private about transfer after seller agrees to sell.
|
|
clientOrgID, err := getClientOrgID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
transMap, err := ctx.GetStub().GetTransient()
|
|
if err != nil {
|
|
return fmt.Errorf("error getting transient: %v", err)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
collection := buildCollectionName(clientOrgID)
|
|
|
|
// Persist the agreed to price in a collection sub-namespace based on priceType key prefix,
|
|
// 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: %v", err)
|
|
}
|
|
|
|
// 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: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyAssetProperties allows a buyer to validate the properties of
|
|
// an asset they intend to buy against the owner's implicit private data collection
|
|
// and verifies that the asset properties never changed from the origin of the asset by checking their hash against the assetID
|
|
func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
|
|
transMap, err := ctx.GetStub().GetTransient()
|
|
if err != nil {
|
|
return false, fmt.Errorf("error getting transient: %v", err)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
asset, err := s.ReadAsset(ctx, assetID)
|
|
if err != nil {
|
|
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: %v", err)
|
|
}
|
|
if immutablePropertiesOnChainHash == nil {
|
|
return false, fmt.Errorf("asset private properties hash does not exist: %s", assetID)
|
|
}
|
|
|
|
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,
|
|
)
|
|
}
|
|
|
|
// verify that the hash of the passed immutable properties and on chain hash matches the assetID
|
|
if !(hex.EncodeToString(immutablePropertiesOnChainHash) == assetID) {
|
|
return false, fmt.Errorf("hash %x for passed immutable properties %s does match on-chain hash %x but do not match assetID %s: asset was altered from its initial form",
|
|
calculatedPropertiesHash,
|
|
immutablePropertiesJSON,
|
|
immutablePropertiesOnChainHash,
|
|
assetID)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// TransferAsset checks transfer conditions and then transfers asset state to buyer.
|
|
// TransferAsset can only be called by current owner
|
|
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
|
|
clientOrgID, err := getClientOrgID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
transMap, err := ctx.GetStub().GetTransient()
|
|
if err != nil {
|
|
return fmt.Errorf("error getting transient data: %v", err)
|
|
}
|
|
|
|
priceJSON, ok := transMap["asset_price"]
|
|
if !ok {
|
|
return fmt.Errorf("asset_price key not found in the transient map")
|
|
}
|
|
|
|
var agreement Agreement
|
|
err = json.Unmarshal(priceJSON, &agreement)
|
|
if err != nil {
|
|
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: %v", err)
|
|
}
|
|
|
|
err = verifyTransferConditions(ctx, asset, clientOrgID, buyerOrgID, priceJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed transfer verification: %v", err)
|
|
}
|
|
|
|
err = transferAssetState(ctx, asset, clientOrgID, buyerOrgID, agreement.Price)
|
|
if err != nil {
|
|
return fmt.Errorf("failed asset transfer: %v", err)
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// verifyTransferConditions checks that client org currently owns asset and that both parties have agreed on price
|
|
func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
|
|
asset *Asset,
|
|
clientOrgID string,
|
|
buyerOrgID string,
|
|
priceJSON []byte) error {
|
|
|
|
// 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 buyer and seller on-chain asset defintion hash matches
|
|
|
|
collectionSeller := buildCollectionName(clientOrgID)
|
|
collectionBuyer := buildCollectionName(buyerOrgID)
|
|
sellerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
|
|
}
|
|
if sellerPropertiesOnChainHash == nil {
|
|
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
|
|
}
|
|
buyerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, asset.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
|
|
}
|
|
if buyerPropertiesOnChainHash == nil {
|
|
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
|
|
}
|
|
|
|
// verify that buyer and seller on-chain asset defintion hash matches
|
|
if !bytes.Equal(sellerPropertiesOnChainHash, buyerPropertiesOnChainHash) {
|
|
return fmt.Errorf("on chain hash of seller %x does not match on-chain hash of buyer %x",
|
|
sellerPropertiesOnChainHash,
|
|
buyerPropertiesOnChainHash,
|
|
)
|
|
}
|
|
|
|
// CHECK3: Verify that seller and buyer agreed on the same 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: %v", err)
|
|
}
|
|
sellerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, assetForSaleKey)
|
|
if err != nil {
|
|
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 buyers bid price
|
|
assetBidKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
|
|
if err != nil {
|
|
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: %v", err)
|
|
}
|
|
if buyerPriceHash == nil {
|
|
return fmt.Errorf("buyer price for %s does not exist", asset.ID)
|
|
}
|
|
|
|
hash := sha256.New()
|
|
hash.Write(priceJSON)
|
|
calculatedPriceHash := hash.Sum(nil)
|
|
|
|
// 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,
|
|
)
|
|
}
|
|
|
|
// 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 nil
|
|
}
|
|
|
|
// transferAssetState performs the public and private state updates for the transferred asset
|
|
// changes the endorsement for the transferred asset sbe to the new owner org
|
|
func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, clientOrgID string, buyerOrgID string, price int) error {
|
|
|
|
// Update ownership in public state
|
|
asset.OwnerOrg = buyerOrgID
|
|
updatedAsset, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ctx.GetStub().PutState(asset.ID, updatedAsset)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write asset for buyer: %v", err)
|
|
}
|
|
|
|
// Changes the endorsement policy to the new owner org
|
|
endorsingOrgs := []string{buyerOrgID}
|
|
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed setting state based endorsement for new owner: %v", err)
|
|
}
|
|
|
|
// Delete asset description from seller collection
|
|
collectionSeller := buildCollectionName(clientOrgID)
|
|
err = ctx.GetStub().DelPrivateData(collectionSeller, asset.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete Asset private details from seller: %v", err)
|
|
}
|
|
|
|
// Delete the price records for seller
|
|
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create composite key for seller: %v", err)
|
|
}
|
|
err = ctx.GetStub().DelPrivateData(collectionSeller, assetPriceKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete asset price from implicit private data collection for seller: %v", err)
|
|
}
|
|
|
|
// Delete the price records for buyer
|
|
collectionBuyer := buildCollectionName(buyerOrgID)
|
|
assetPriceKey, err = ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create composite key for buyer: %v", err)
|
|
}
|
|
err = ctx.GetStub().DelPrivateData(collectionBuyer, assetPriceKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete asset price from implicit private data collection for buyer: %v", err)
|
|
}
|
|
|
|
// 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: %v", err)
|
|
}
|
|
|
|
txTimestamp, err := ctx.GetStub().GetTxTimestamp()
|
|
if err != nil {
|
|
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: timestamp,
|
|
}
|
|
receipt, err := json.Marshal(assetReceipt)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal receipt: %v", err)
|
|
}
|
|
|
|
err = ctx.GetStub().PutPrivateData(collectionBuyer, receiptBuyKey, receipt)
|
|
if err != nil {
|
|
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: %v", err)
|
|
}
|
|
|
|
err = ctx.GetStub().PutPrivateData(collectionSeller, receiptSaleKey, receipt)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put private asset receipt for seller: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getClientOrgID gets the client org ID.
|
|
func getClientOrgID(ctx contractapi.TransactionContextInterface) (string, error) {
|
|
clientOrgID, err := ctx.GetClientIdentity().GetMSPID()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed getting client's orgID: %v", err)
|
|
}
|
|
|
|
return clientOrgID, nil
|
|
}
|
|
|
|
// getClientImplicitCollectionNameAndVerifyClientOrg gets the implicit collection for the client and checks that the client is from the same org as the peer
|
|
func getClientImplicitCollectionNameAndVerifyClientOrg(ctx contractapi.TransactionContextInterface) (string, error) {
|
|
clientOrgID, err := getClientOrgID(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return buildCollectionName(clientOrgID), nil
|
|
}
|
|
|
|
// verifyClientOrgMatchesPeerOrg checks that the client is from the same org as the peer
|
|
func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
|
|
peerOrgID, err := shim.GetMSPID()
|
|
if err != nil {
|
|
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 nil
|
|
}
|
|
|
|
// buildCollectionName returns the implicit collection name for an org
|
|
func buildCollectionName(clientOrgID string) string {
|
|
return fmt.Sprintf("_implicit_org_%s", clientOrgID)
|
|
}
|
|
|
|
// setAssetStateBasedEndorsement adds an endorsement policy to an asset so that the passed orgs need to agree upon transfer
|
|
func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgsToEndorse []string) error {
|
|
endorsementPolicy, err := statebased.NewStateEP(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgsToEndorse...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add org to endorsement policy: %v", err)
|
|
}
|
|
policy, err := endorsementPolicy.Policy()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create endorsement policy bytes from org: %v", err)
|
|
}
|
|
err = ctx.GetStub().SetStateValidationParameter(assetID, policy)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set validation parameter on asset: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAssetHashId allows a potential buyer to validate the properties of an asset against the asset Id hash on chain and returns the hash
|
|
func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterface) (string, error) {
|
|
transientMap, err := ctx.GetStub().GetTransient()
|
|
if err != nil {
|
|
return "", fmt.Errorf("error getting transient: %v", err)
|
|
}
|
|
|
|
// Asset properties must be retrieved from the transient field as they are private
|
|
propertiesJSON, ok := transientMap["asset_properties"]
|
|
if !ok {
|
|
return "", fmt.Errorf("asset_properties key not found in the transient map")
|
|
}
|
|
|
|
hash := sha256.New()
|
|
hash.Write(propertiesJSON)
|
|
assetID := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
asset, err := s.ReadAsset(ctx, assetID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get asset: %v, asset properies provided do not represent any on chain asset", err)
|
|
}
|
|
if asset.ID != assetID {
|
|
return "", fmt.Errorf("Asset properies provided do not correpond to any on chain asset")
|
|
}
|
|
return asset.ID, nil
|
|
}
|
|
|
|
func main() {
|
|
chaincode, err := contractapi.NewChaincode(new(SmartContract))
|
|
if err != nil {
|
|
log.Panicf("Error create transfer asset chaincode: %v", err)
|
|
}
|
|
|
|
if err := chaincode.Start(); err != nil {
|
|
log.Panicf("Error starting asset chaincode: %v", err)
|
|
}
|
|
}
|