Update secured asset transfer sample (#781)

A recent commit added the potential buyer to an asset's state based endorsement policy.
That change was problematic because if the transfer fell through, the buyer lost control of the asset,
in that they could no longer update the asset or change the sell price or sell to somebody else.

The asset state based endorsement policy is now based on the seller only, and we document
that additional parties could be added such as a trusted third party (although no
such party exists in test network at this time).

This commit also re-adds some necessary verifications, and make other minor edits and
comments to help users understand the sample.

Signed-off-by: David Enyeart <enyeart@us.ibm.com>
This commit is contained in:
Dave Enyeart 2022-07-13 14:09:14 -04:00 committed by GitHub
parent 9f844e5de3
commit f32f77b129
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 75 deletions

View file

@ -108,7 +108,7 @@ async function main(): Promise<void> {
assetId: assetKey,
price: 110,
tradeId: now,
}, mspIdOrg2);
});
// Check the private information about the asset from Org2. Org1 would have to send Org2 asset details,
// so the hash of the details may be checked by the chaincode.
@ -141,12 +141,12 @@ async function main(): Promise<void> {
// Org1 will try to transfer the asset to Org2
// This will fail due to the sell price and the bid price are not the same.
try{
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, mspIdOrg1, mspIdOrg2);
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
}
// Agree to a sell by Org1, the seller will agree to the bid price of Org2.
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now}, mspIdOrg2);
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now});
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
@ -166,14 +166,14 @@ async function main(): Promise<void> {
// Org2 user will try to transfer the asset to Org1.
// This will fail as the owner is Org1.
try{
await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2);
await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
}
// Org1 will transfer the asset to Org2.
// This will now complete as the sell price and the bid price are the same.
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2);
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);

View file

@ -136,7 +136,7 @@ export class ContractWrapper {
console.log(`*** Result: committed, Desc: ${asset.publicDescription}`);
}
public async agreeToSell(assetPrice: AssetPrice, buyerOrgID: string): Promise<void> {
public async agreeToSell(assetPrice: AssetPrice): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
const assetPriceJSON: AssetPriceJSON = {
@ -146,16 +146,11 @@ export class ContractWrapper {
};
await this.#contract.submit('AgreeToSell', {
arguments:[assetPrice.assetId, buyerOrgID],
arguments:[assetPrice.assetId],
transientData: {asset_price: JSON.stringify(assetPriceJSON)},
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
});
//update local record of sbe to inlcude buyer org if not already
if (this.#endorsingOrgs[assetPrice.assetId].indexOf('buyerOrgID') == -1){
this.#endorsingOrgs[assetPrice.assetId].push(buyerOrgID);
}
console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`);
}
@ -258,7 +253,7 @@ export class ContractWrapper {
console.log('*** Result: GetAssetBidPrice', result);
}
public async transferAsset(assetPrice: AssetPrice, ownerOrgID: string, buyerOrgID: string): Promise<void> {
public async transferAsset(assetPrice: AssetPrice, endorsingOrganizations: string[], ownerOrgID: string, buyerOrgID: string): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org} and ${buyerOrgID}.${RESET}`);
@ -273,9 +268,9 @@ export class ContractWrapper {
await this.#contract.submit('TransferAsset', {
arguments:[assetPrice.assetId, buyerOrgID],
transientData: { asset_price: JSON.stringify(assetPriceJSON) },
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
endorsingOrganizations: endorsingOrganizations
});
console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`);
}
}
}

View file

@ -312,8 +312,8 @@ async function main() {
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
//call agree to sell with desired price and target buyer organization
await transaction.submit(assetKey, org2);
//call agree to sell with desired price
await transaction.submit(assetKey);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 110`);
} catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);
@ -368,7 +368,7 @@ async function main() {
const asset_properties_string = JSON.stringify(asset_properties);
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetKey} as Org2 - endorsed by Org2${RESET}`);
transaction = contractOrg2.createTransaction('AgreeToBuy');
transaction.setEndorsingOrganizations(org1, org2);
transaction.setEndorsingOrganizations(org2);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string),
asset_properties: Buffer.from(asset_properties_string)
@ -431,11 +431,11 @@ async function main() {
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
transaction = contractOrg1.createTransaction('AgreeToSell');
transaction.setEndorsingOrganizations(org1, org2);
transaction.setEndorsingOrganizations(org1);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey, org2);
await transaction.submit(assetKey);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 100`);
} catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);

View file

@ -57,17 +57,18 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface,
return "", fmt.Errorf("asset_properties key not found in the transient map")
}
// AssetID will be the hash of the asset's properties
hash := sha256.New()
hash.Write(immutablePropertiesJSON)
assetID := hex.EncodeToString(hash.Sum(nil))
// Get client org id and verify it matches peer org id.
// In this scenario, client is only authorized to read/write private data from its own peer.
// Get the clientOrgId from the input, will be used for implicit collection, owner, and state-based endorsement policy
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return "", err
}
// In this scenario, client is only authorized to read/write private data from its own peer, therefore verify client org id matches peer org id.
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return "", err
@ -89,7 +90,8 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface,
return "", fmt.Errorf("failed to put asset in public data: %v", err)
}
// Set the endorsement policy such that an owner org peer is required to endorse future updates
// Set the endorsement policy such that an owner org peer is required to endorse future updates.
// In practice, consider additional endorsers such as a trusted third party to further secure transfers.
endorsingOrgs := []string{clientOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
@ -108,13 +110,8 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface,
// ChangePublicDescription updates the assets public description. Only the current owner can update the public description
func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionContextInterface, assetID string, newDescription string) error {
// No need to check client org id matches peer org id, rely on the asset ownership check instead.
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return err
}
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return err
}
@ -138,9 +135,8 @@ func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionConte
return ctx.GetStub().PutState(assetID, updatedAssetJSON)
}
// AgreeToSell adds seller's asking price to seller's implicit private data collection and requires to specify the next possible buyer
// Set the endorsement policy such that seller org and passed target buyer org peers are both required to endorse the tranfer
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
// AgreeToSell adds seller's asking price to seller's implicit private data collection.
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error {
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return err
@ -151,18 +147,17 @@ func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface,
return err
}
// Verify that this client belongs to the peer's org
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return err
}
// Verify that this clientOrgId actually owns the asset.
if clientOrgID != asset.OwnerOrg {
return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg)
}
// Set the endorsement policy such that owner org and seller org peers are both required to endorse the tranfer
endorsingOrgs := []string{clientOrgID, buyerOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
return fmt.Errorf("failed setting state based endorsement for buyer and future seller: %v", err)
}
return agreeToPrice(ctx, assetID, typeAssetForSale)
}
@ -178,6 +173,12 @@ func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface,
return err
}
// Verify that this client belongs to the peer's org
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return err
}
// Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transientMap["asset_properties"]
if !ok {
@ -232,8 +233,8 @@ func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, p
return nil
}
// VerifyAssetProperties Allows a buyer to validate the properties of
// an asset against the owner's implicit private data collection
// VerifyAssetProperties allows a buyer to validate the properties of
// an asset they intend to buy against the owner's implicit private data collection
// and verifies that the asset properties never changed from the origin of the asset by checking their hash against the assetID
func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
transMap, err := ctx.GetStub().GetTransient()
@ -241,7 +242,7 @@ func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContext
return false, fmt.Errorf("error getting transient: %v", err)
}
/// Asset properties must be retrieved from the transient field as they are private
// Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transMap["asset_properties"]
if !ok {
return false, fmt.Errorf("asset_properties key not found in the transient map")
@ -342,7 +343,7 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("a client from %s cannot transfer a asset owned by %s", clientOrgID, asset.OwnerOrg)
}
// CHECK2: Verify that both buyers and seller on-chain asset defintion hash matches
// CHECK2: Verify that buyer and seller on-chain asset defintion hash matches
collectionSeller := buildCollectionName(clientOrgID)
collectionBuyer := buildCollectionName(buyerOrgID)
@ -361,7 +362,7 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
}
// verify that the hash of the passed immutable properties matches the on-chain hash
// verify that buyer and seller on-chain asset defintion hash matches
if !bytes.Equal(sellerPropertiesOnChainHash, buyerPropertiesOnChainHash) {
return fmt.Errorf("on chain hash of seller %x does not match on-chain hash of buyer %x",
sellerPropertiesOnChainHash,
@ -425,12 +426,13 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
// transferAssetState performs the public and private state updates for the transferred asset
// changes the endorsement for the transferred asset sbe to the new owner org
func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, clientOrgID string, buyerOrgID string, price int) error {
// Update ownership in public state
asset.OwnerOrg = buyerOrgID
updatedAsset, err := json.Marshal(asset)
if err != nil {
return err
}
err = ctx.GetStub().PutState(asset.ID, updatedAsset)
if err != nil {
return fmt.Errorf("failed to write asset for buyer: %v", err)
@ -443,32 +445,29 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse
return fmt.Errorf("failed setting state based endorsement for new owner: %v", err)
}
// Transfer the private properties (delete from seller collection, create in buyer collection)
// Delete asset description from seller collection
collectionSeller := buildCollectionName(clientOrgID)
err = ctx.GetStub().DelPrivateData(collectionSeller, asset.ID)
if err != nil {
return fmt.Errorf("failed to delete Asset private details from seller: %v", err)
}
collectionBuyer := buildCollectionName(buyerOrgID)
// Delete the price records for seller
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key for seller: %v", err)
}
err = ctx.GetStub().DelPrivateData(collectionSeller, assetPriceKey)
if err != nil {
return fmt.Errorf("failed to delete asset price from implicit private data collection for seller: %v", err)
}
// Delete the price records for buyer
collectionBuyer := buildCollectionName(buyerOrgID)
assetPriceKey, err = ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key for buyer: %v", err)
}
err = ctx.GetStub().DelPrivateData(collectionBuyer, assetPriceKey)
if err != nil {
return fmt.Errorf("failed to delete asset price from implicit private data collection for buyer: %v", err)
@ -527,7 +526,22 @@ func getClientOrgID(ctx contractapi.TransactionContextInterface) (string, error)
return clientOrgID, nil
}
// verifyClientOrgMatchesPeerOrg checks the client org id matches the peer org id.
// getClientImplicitCollectionNameAndVerifyClientOrg gets the implicit collection for the client and checks that the client is from the same org as the peer
func getClientImplicitCollectionNameAndVerifyClientOrg(ctx contractapi.TransactionContextInterface) (string, error) {
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return "", err
}
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return "", err
}
return buildCollectionName(clientOrgID), nil
}
// verifyClientOrgMatchesPeerOrg checks that the client is from the same org as the peer
func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
peerOrgID, err := shim.GetMSPID()
if err != nil {
@ -544,6 +558,11 @@ func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
return nil
}
// buildCollectionName returns the implicit collection name for an org
func buildCollectionName(clientOrgID string) string {
return fmt.Sprintf("_implicit_org_%s", clientOrgID)
}
// setAssetStateBasedEndorsement adds an endorsement policy to an asset so that the passed orgs need to agree upon transfer
func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgsToEndorse []string) error {
endorsementPolicy, err := statebased.NewStateEP(nil)
@ -566,8 +585,7 @@ func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface,
return nil
}
// GetAssetHash Allows a buyer to validate the properties of
// an asset against the asset Id and return the hash
// GetAssetHashId allows a potential buyer to validate the properties of an asset against the asset Id hash on chain and returns the hash
func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterface) (string, error) {
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
@ -594,24 +612,6 @@ func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterfa
return asset.ID, nil
}
func buildCollectionName(clientOrgID string) string {
return fmt.Sprintf("_implicit_org_%s", clientOrgID)
}
func getClientImplicitCollectionName(ctx contractapi.TransactionContextInterface) (string, error) {
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return "", err
}
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return "", err
}
return buildCollectionName(clientOrgID), nil
}
func main() {
chaincode, err := contractapi.NewChaincode(new(SmartContract))
if err != nil {

View file

@ -47,8 +47,8 @@ func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, a
// GetAssetPrivateProperties returns the immutable asset properties from owner's private data collection
func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionContextInterface, assetID string) (string, error) {
// In this scenario, client is only authorized to read/write private data from its own peer.
collection, err := getClientImplicitCollectionName(ctx)
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
if err != nil {
return "", err
}
@ -76,7 +76,8 @@ func (s *SmartContract) GetAssetBidPrice(ctx contractapi.TransactionContextInter
// getAssetPrice gets the bid or ask price from caller's implicit private data collection
func getAssetPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) (string, error) {
collection, err := getClientImplicitCollectionName(ctx)
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
if err != nil {
return "", err
}
@ -108,7 +109,7 @@ func (s *SmartContract) QueryAssetBuyAgreements(ctx contractapi.TransactionConte
}
func queryAgreementsByType(ctx contractapi.TransactionContextInterface, agreeType string) ([]Agreement, error) {
collection, err := getClientImplicitCollectionName(ctx)
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
if err != nil {
return nil, err
}