Skip to content

Commit

Permalink
Update secured asset transfer sample (hyperledger#781)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
denyeart authored Jul 13, 2022
1 parent 9f844e5 commit f32f77b
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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}`);
}

Expand Down Expand Up @@ -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}`);

Expand All @@ -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}`);
}
}
}
10 changes: 5 additions & 5 deletions asset-transfer-secured-agreement/application-javascript/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}`);
Expand Down
102 changes: 51 additions & 51 deletions asset-transfer-secured-agreement/chaincode-go/asset_transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
}

Expand All @@ -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 {
Expand Down Expand Up @@ -232,16 +233,16 @@ 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()
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
// 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")
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit f32f77b

Please sign in to comment.