From 8a458b5cdc3db33b133477ae55df6f0464d0f927 Mon Sep 17 00:00:00 2001 From: David Enyeart Date: Sun, 6 Jan 2019 16:40:15 -0500 Subject: [PATCH] [FAB-12056] Private marbles cc use transient data The marbles02_private example needs to be updated to pass all attributes as transient data instead of arguments. This will prevent identifiable information being sent to the orderer. Also removed transferMarblesBasedOnColor since range queries and update is not supported in a transaction when using private data, as different peers may have different data in their local private state databases. Change-Id: I7c3cebcb49b6c59e4e8ea35f46a799ffdd1999e9 Signed-off-by: David Enyeart --- .../go/marbles_chaincode_private.go | 300 +++++++++--------- 1 file changed, 158 insertions(+), 142 deletions(-) diff --git a/chaincode/marbles02_private/go/marbles_chaincode_private.go b/chaincode/marbles02_private/go/marbles_chaincode_private.go index 385687c087..055d6f36de 100644 --- a/chaincode/marbles02_private/go/marbles_chaincode_private.go +++ b/chaincode/marbles02_private/go/marbles_chaincode_private.go @@ -6,18 +6,28 @@ SPDX-License-Identifier: Apache-2.0 // ====CHAINCODE EXECUTION SAMPLES (CLI) ================== -// ==== Invoke marbles ==== -// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["initMarble","marble1","blue","35","tom","99"]}' -// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["initMarble","marble2","red","50","tom","102"]}' -// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["initMarble","marble3","blue","70","tom","103"]}' -// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["transferMarble","marble2","jerry"]}' -// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["delete","marble1"]}' - -// ==== Query marbles ==== +// ==== Invoke marbles, pass private data as base64 encoded bytes in transient map ==== +// +// export MARBLE=$(echo -n "{\"name\":\"marble1\",\"color\":\"blue\",\"size\":35,\"owner\":\"tom\",\"price\":99}" | base64) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["initMarble"]}' --transient "{\"marble\":\"$MARBLE\"}" +// +// export MARBLE=$(echo -n "{\"name\":\"marble2\",\"color\":\"red\",\"size\":50,\"owner\":\"tom\",\"price\":102}" | base64) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["initMarble"]}' --transient "{\"marble\":\"$MARBLE\"}" +// +// export MARBLE=$(echo -n "{\"name\":\"marble3\",\"color\":\"blue\",\"size\":70,\"owner\":\"tom\",\"price\":103}" | base64) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["initMarble"]}' --transient "{\"marble\":\"$MARBLE\"}" +// +// export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"jerry\"}" | base64) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["transferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}" +// +// export MARBLE_DELETE=$(echo -n "{\"name\":\"marble1\"}" | base64) +// peer chaincode invoke -C mychannel -n marblesp -c '{"Args":["delete"]}' --transient "{\"marble_delete\":\"$MARBLE_DELETE\"}" + +// ==== Query marbles, since queries are not recorded on chain we don't need to hide private data in transient map ==== // peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarble","marble1"]}' // peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}' -// peer chaincode query -C mychannel -n marblesp -c '{"Args":["getMarblesByRange","marble1","marble3"]}' - +// peer chaincode query -C mychannel -n marblesp -c '{"Args":["getMarblesByRange","marble1","marble4"]}' +// // Rich Query (Only supported if CouchDB is used as state database): // peer chaincode query -C mychannel -n marblesp -c '{"Args":["queryMarblesByOwner","tom"]}' // peer chaincode query -C mychannel -n marblesp -c '{"Args":["queryMarbles","{\"selector\":{\"owner\":\"tom\"}}"]}' @@ -84,7 +94,6 @@ import ( "bytes" "encoding/json" "fmt" - "strconv" "strings" "github.com/hyperledger/fabric/core/chaincode/shim" @@ -145,9 +154,6 @@ func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { case "transferMarble": //change owner of a specific marble return t.transferMarble(stub, args) - case "transferMarblesBasedOnColor": - //transfer all marbles of a certain color - return t.transferMarblesBasedOnColor(stub, args) case "delete": //delete a marble return t.delete(stub, args) @@ -173,75 +179,95 @@ func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { func (t *SimpleChaincode) initMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response { var err error - // 0-name 1-color 2-size 3-owner 4-price - // "asdf", "blue", "35", "bob", "99" - if len(args) != 5 { - return shim.Error("Incorrect number of arguments. Expecting 5") + type marbleTransientInput struct { + Name string `json:"name"` //the fieldtags are needed to keep case from bouncing around + Color string `json:"color"` + Size int `json:"size"` + Owner string `json:"owner"` + Price int `json:"price"` } // ==== Input sanitation ==== fmt.Println("- start init marble") - if len(args[0]) == 0 { - return shim.Error("1st argument must be a non-empty string") - } - if len(args[1]) == 0 { - return shim.Error("2nd argument must be a non-empty string") + + if len(args) != 0 { + return shim.Error("Incorrect number of arguments. Private marble data must be passed in transient map.") } - if len(args[2]) == 0 { - return shim.Error("3rd argument must be a non-empty string") + + transMap, err := stub.GetTransient() + if err != nil { + return shim.Error("Error getting transient: " + err.Error()) } - if len(args[3]) == 0 { - return shim.Error("4th argument must be a non-empty string") + + if _, ok := transMap["marble"]; !ok { + return shim.Error("marble must be a key in the transient map") } - if len(args[4]) == 0 { - return shim.Error("5th argument must be a non-empty string") + + if len(transMap["marble"]) == 0 { + return shim.Error("marble value in the transient map must be a non-empty JSON string") } - marbleName := args[0] - color := strings.ToLower(args[1]) - owner := strings.ToLower(args[3]) - size, err := strconv.Atoi(args[2]) + + var marbleInput marbleTransientInput + err = json.Unmarshal(transMap["marble"], &marbleInput) if err != nil { - return shim.Error("3rd argument must be a numeric string") + return shim.Error("Failed to decode JSON of: " + string(transMap["marble"])) } - price, err := strconv.Atoi(args[4]) - if err != nil { - return shim.Error("5th argument must be a numeric string") + + if len(marbleInput.Name) == 0 { + return shim.Error("name field must be a non-empty string") + } + if len(marbleInput.Color) == 0 { + return shim.Error("color field must be a non-empty string") + } + if marbleInput.Size <= 0 { + return shim.Error("size field must be a positive integer") + } + if len(marbleInput.Owner) == 0 { + return shim.Error("owner field must be a non-empty string") + } + if marbleInput.Price <= 0 { + return shim.Error("price field must be a positive integer") } // ==== Check if marble already exists ==== - marbleAsBytes, err := stub.GetPrivateData("collectionMarbles", marbleName) + marbleAsBytes, err := stub.GetPrivateData("collectionMarbles", marbleInput.Name) if err != nil { return shim.Error("Failed to get marble: " + err.Error()) } else if marbleAsBytes != nil { - fmt.Println("This marble already exists: " + marbleName) - return shim.Error("This marble already exists: " + marbleName) + fmt.Println("This marble already exists: " + marbleInput.Name) + return shim.Error("This marble already exists: " + marbleInput.Name) } - // ==== Create marble object and marshal to JSON ==== - objectType := "marble" - marble := &marble{objectType, marbleName, color, size, owner} + // ==== Create marble object, marshal to JSON, and save to state ==== + marble := &marble{ + ObjectType: "marble", + Name: marbleInput.Name, + Color: marbleInput.Color, + Size: marbleInput.Size, + Owner: marbleInput.Owner, + } marbleJSONasBytes, err := json.Marshal(marble) if err != nil { return shim.Error(err.Error()) } - //Alternatively, build the marble json string manually if you don't want to use struct marshalling - //marbleJSONasString := `{"docType":"Marble", "name": "` + marbleName + `", "color": "` + color + `", "size": ` + strconv.Itoa(size) + `, "owner": "` + owner + `"}` - //marbleJSONasBytes := []byte(str) // === Save marble to state === - err = stub.PutPrivateData("collectionMarbles", marbleName, marbleJSONasBytes) + err = stub.PutPrivateData("collectionMarbles", marbleInput.Name, marbleJSONasBytes) if err != nil { return shim.Error(err.Error()) } - // ==== Save marble private details ==== - objectType = "marblePrivateDetails" - marblePrivateDetails := &marblePrivateDetails{objectType, marbleName, price} + // ==== Create marble private details object with price, marshal to JSON, and save to state ==== + marblePrivateDetails := &marblePrivateDetails{ + ObjectType: "marblePrivateDetails", + Name: marbleInput.Name, + Price: marbleInput.Price, + } marblePrivateDetailsBytes, err := json.Marshal(marblePrivateDetails) if err != nil { return shim.Error(err.Error()) } - err = stub.PutPrivateData("collectionMarblePrivateDetails", marbleName, marblePrivateDetailsBytes) + err = stub.PutPrivateData("collectionMarblePrivateDetails", marbleInput.Name, marblePrivateDetailsBytes) if err != nil { return shim.Error(err.Error()) } @@ -318,49 +344,72 @@ func (t *SimpleChaincode) readMarblePrivateDetails(stub shim.ChaincodeStubInterf // delete - remove a marble key/value pair from state // ================================================== func (t *SimpleChaincode) delete(stub shim.ChaincodeStubInterface, args []string) pb.Response { - var jsonResp string - var marbleJSON marble - if len(args) != 1 { - return shim.Error("Incorrect number of arguments. Expecting 1") + fmt.Println("- start delete marble") + + type marbleDeleteTransientInput struct { + Name string `json:"name"` + } + + if len(args) != 0 { + return shim.Error("Incorrect number of arguments. Private marble name must be passed in transient map.") + } + + transMap, err := stub.GetTransient() + if err != nil { + return shim.Error("Error getting transient: " + err.Error()) + } + + if _, ok := transMap["marble_delete"]; !ok { + return shim.Error("marble_delete must be a key in the transient map") + } + + if len(transMap["marble_delete"]) == 0 { + return shim.Error("marble_delete value in the transient map must be a non-empty JSON string") + } + + var marbleDeleteInput marbleDeleteTransientInput + err = json.Unmarshal(transMap["marble_delete"], &marbleDeleteInput) + if err != nil { + return shim.Error("Failed to decode JSON of: " + string(transMap["marble_delete"])) + } + + if len(marbleDeleteInput.Name) == 0 { + return shim.Error("name field must be a non-empty string") } - marbleName := args[0] // to maintain the color~name index, we need to read the marble first and get its color - valAsbytes, err := stub.GetPrivateData("collectionMarbles", marbleName) //get the marble from chaincode state + valAsbytes, err := stub.GetPrivateData("collectionMarbles", marbleDeleteInput.Name) //get the marble from chaincode state if err != nil { - jsonResp = "{\"Error\":\"Failed to get state for " + marbleName + "\"}" - return shim.Error(jsonResp) + return shim.Error("Failed to get state for " + marbleDeleteInput.Name) } else if valAsbytes == nil { - jsonResp = "{\"Error\":\"Marble does not exist: " + marbleName + "\"}" - return shim.Error(jsonResp) + return shim.Error("Marble does not exist: " + marbleDeleteInput.Name) } - err = json.Unmarshal([]byte(valAsbytes), &marbleJSON) + var marbleToDelete marble + err = json.Unmarshal([]byte(valAsbytes), &marbleToDelete) if err != nil { - jsonResp = "{\"Error\":\"Failed to decode JSON of: " + marbleName + "\"}" - return shim.Error(jsonResp) + return shim.Error("Failed to decode JSON of: " + string(valAsbytes)) } - err = stub.DelPrivateData("collectionMarbles", marbleName) //remove the marble from chaincode state + // delete the marble from state + err = stub.DelPrivateData("collectionMarbles", marbleDeleteInput.Name) if err != nil { return shim.Error("Failed to delete state:" + err.Error()) } - // maintain the index + // Also delete the marble from the color~name index indexName := "color~name" - colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marbleJSON.Color, marbleJSON.Name}) + colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marbleToDelete.Color, marbleToDelete.Name}) if err != nil { return shim.Error(err.Error()) } - - // Delete index entry to state. err = stub.DelPrivateData("collectionMarbles", colorNameIndexKey) if err != nil { return shim.Error("Failed to delete state:" + err.Error()) } - // Delete private details of marble - err = stub.DelPrivateData("collectionMarblePrivateDetails", marbleName) + // Finally, delete private details of marble + err = stub.DelPrivateData("collectionMarblePrivateDetails", marbleDeleteInput.Name) if err != nil { return shim.Error(err.Error()) } @@ -373,21 +422,48 @@ func (t *SimpleChaincode) delete(stub shim.ChaincodeStubInterface, args []string // =========================================================== func (t *SimpleChaincode) transferMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response { - // 0 1 - // "name", "bob" - if len(args) < 2 { - return shim.Error("Incorrect number of arguments. Expecting 2") + fmt.Println("- start transfer marble") + + type marbleTransferTransientInput struct { + Name string `json:"name"` + Owner string `json:"owner"` } - marbleName := args[0] - newOwner := strings.ToLower(args[1]) - fmt.Println("- start transferMarble ", marbleName, newOwner) + if len(args) != 0 { + return shim.Error("Incorrect number of arguments. Private marble data must be passed in transient map.") + } - marbleAsBytes, err := stub.GetPrivateData("collectionMarbles", marbleName) + transMap, err := stub.GetTransient() + if err != nil { + return shim.Error("Error getting transient: " + err.Error()) + } + + if _, ok := transMap["marble_owner"]; !ok { + return shim.Error("marble_owner must be a key in the transient map") + } + + if len(transMap["marble_owner"]) == 0 { + return shim.Error("marble_owner value in the transient map must be a non-empty JSON string") + } + + var marbleTransferInput marbleTransferTransientInput + err = json.Unmarshal(transMap["marble_owner"], &marbleTransferInput) + if err != nil { + return shim.Error("Failed to decode JSON of: " + string(transMap["marble_owner"])) + } + + if len(marbleTransferInput.Name) == 0 { + return shim.Error("name field must be a non-empty string") + } + if len(marbleTransferInput.Owner) == 0 { + return shim.Error("owner field must be a non-empty string") + } + + marbleAsBytes, err := stub.GetPrivateData("collectionMarbles", marbleTransferInput.Name) if err != nil { return shim.Error("Failed to get marble:" + err.Error()) } else if marbleAsBytes == nil { - return shim.Error("Marble does not exist") + return shim.Error("Marble does not exist: " + marbleTransferInput.Name) } marbleToTransfer := marble{} @@ -395,10 +471,10 @@ func (t *SimpleChaincode) transferMarble(stub shim.ChaincodeStubInterface, args if err != nil { return shim.Error(err.Error()) } - marbleToTransfer.Owner = newOwner //change the owner + marbleToTransfer.Owner = marbleTransferInput.Owner //change the owner marbleJSONasBytes, _ := json.Marshal(marbleToTransfer) - err = stub.PutPrivateData("collectionMarbles", marbleName, marbleJSONasBytes) //rewrite the marble + err = stub.PutPrivateData("collectionMarbles", marbleToTransfer.Name, marbleJSONasBytes) //rewrite the marble if err != nil { return shim.Error(err.Error()) } @@ -465,66 +541,6 @@ func (t *SimpleChaincode) getMarblesByRange(stub shim.ChaincodeStubInterface, ar return shim.Success(buffer.Bytes()) } -// ==== Example: GetStateByPartialCompositeKey/RangeQuery ========================================= -// transferMarblesBasedOnColor will transfer marbles of a given color to a certain new owner. -// Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'. -// Committing peers will re-execute range queries to guarantee that result sets are stable -// between endorsement time and commit time. The transaction is invalidated by the -// committing peers if the result set has changed between endorsement time and commit time. -// Therefore, range queries are a safe option for performing update transactions based on query results. -// =========================================================================================== -func (t *SimpleChaincode) transferMarblesBasedOnColor(stub shim.ChaincodeStubInterface, args []string) pb.Response { - - // 0 1 - // "color", "bob" - if len(args) < 2 { - return shim.Error("Incorrect number of arguments. Expecting 2") - } - - color := args[0] - newOwner := strings.ToLower(args[1]) - fmt.Println("- start transferMarblesBasedOnColor ", color, newOwner) - - // Query the color~name index by color - // This will execute a key range query on all keys starting with 'color' - coloredMarbleResultsIterator, err := stub.GetPrivateDataByPartialCompositeKey("collectionMarbles", "color~name", []string{color}) - if err != nil { - return shim.Error(err.Error()) - } - defer coloredMarbleResultsIterator.Close() - - // Iterate through result set and for each marble found, transfer to newOwner - var i int - for i = 0; coloredMarbleResultsIterator.HasNext(); i++ { - // Note that we don't get the value (2nd return variable), we'll just get the marble name from the composite key - responseRange, err := coloredMarbleResultsIterator.Next() - if err != nil { - return shim.Error(err.Error()) - } - - // get the color and name from color~name composite key - objectType, compositeKeyParts, err := stub.SplitCompositeKey(responseRange.Key) - if err != nil { - return shim.Error(err.Error()) - } - returnedColor := compositeKeyParts[0] - returnedMarbleName := compositeKeyParts[1] - fmt.Printf("- found a marble from index:%s color:%s name:%s\n", objectType, returnedColor, returnedMarbleName) - - // Now call the transfer function for the found marble. - // Re-use the same function that is used to transfer individual marbles - response := t.transferMarble(stub, []string{returnedMarbleName, newOwner}) - // if the transfer failed break out of loop and return error - if response.Status != shim.OK { - return shim.Error("Transfer failed: " + response.Message) - } - } - - responsePayload := fmt.Sprintf("Transferred %d %s marbles to %s", i, color, newOwner) - fmt.Println("- end transferMarblesBasedOnColor: " + responsePayload) - return shim.Success([]byte(responsePayload)) -} - // =======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.