There are two related concepts in Hyperledger Fabric: Private Data and Transient Data. This paper provides four example programs, which correspond to the four combinations of Private Data and Transient Data. By observing the transactions of account books and the world state database, we can understand why Transient Data should be used as input when using Private Data.
Technically speaking, private data and transient data are two different concepts. Private data is considered how to share data among some mechanisms of the channel, while transient data is an input method when using private data. Interestingly, there is no direct relationship between the two, although in reality, when we need to use private data safely, we should usually use transient data as input.
Hyperledger Fabric blockchain development tutorial: Fabric Node.js development details | Fabric Java development details | Fabric Golang development details
1. Basic concepts
Let's review some of the core concepts that will be used in the demo.
Ledger: in the Hyperledger Fabric, when a peer node is added to the channel, a copy of the ledger will be maintained. The ledger contains a blockchain data structure for holding blocks and a world state database for keeping the latest state. When the peer node receives a new block from the sorting service and the verification is successful, the peer node submits the block to the input book, and according to the RWSet to update the corresponding world state.
Based on the consensus mechanism, most of the ledgers on different peer nodes are consistent in one channel. With one exception, Fabric supports private data that exists between partial channels.
Private data: sometimes it is necessary to share data only among some organizations in one channel. The purpose of introducing private data into Hyperledger Fabric is to meet this demand. By defining the dataset, we can declare the organization subset implemented by the private data, and all nodes (including other nodes outside the organization subset) will save the records of the private data hash to be used as evidence of the existence of the data or for audit purposes.
You can use the chain code API to use private data. We use PutPrivateData and GetPrivateData APIs in the sample code. As a contrast, we use PutState and GetState to read and write public state.
In Fabric 2.0, an implicit data set is prepared for each mechanism. This tutorial will take advantage of this implied dataset, so we don't need a separate dataset definition file.
The ledger structure of Fabric and the location of private data are shown in the figure below. In the later demonstration code, we will look at the contents of the transaction and the world status database:
Transient data: many chain code functions require additional input data when called. In most cases, we will pass in a set of parameters when we call the function, and the chain code parameters, including the function name and function parameters, will be saved in the block as part of the effective transaction, so they will exist permanently in the ledger. If for some reason we don't want to keep the list of parameters permanently on the chain, we can use transient data. Transient data is an input method that can pass parameters to chain code function but does not need to be saved in transaction record. When using transient data, a special chain code API, GetRansient method, is needed to read transient data. We'll see that in the demo code next.
Therefore, the design of chain code needs to design chain code according to business requirements, and decide which data should be input as normal parameters and recorded in the transaction, and which data should be input as transient data without being recorded in the chain.
The relationship between private data and transient data: private data and transient data are not directly related. We can use only private data instead of transient data as input, or we can use transient data in non private data. So we can get the four application scenarios in the example and observe the results in each scenario.
The relationship between private data and transient data is shown in the following figure:
The choice of input method and whether the private data or not depends on the specific business requirements, because the chain code function reflects the real business transactions. We can choose normal parameter list, transient data or two ways of input at the same time, or write data to public state or private data set. What we need is to correctly select the chain code API we need to use.
2. Application scenario overview
As mentioned before, we combine private data and transient data for 2X2, as follows:
Scenario 1: do not use private data, do not input transient data
In this scenario, data is written to the public status part of the ledger, and all peer nodes will save the same ledger data. Use PutState and GetState in the chain code to access this part of data. When we call the chain code, we use the normal parameter list to specify the input data.
This is appropriate when all mechanisms in the channel need the same data.
Scenario 2: use private data, do not input transient data
In this scenario, data is written to the private data part of the ledger, and data is saved only on the peer node of the institution defined by the private data set. In the chain code, we use PutPrivateData and GetPrivateData to access the private data set. When we call the chain code, we use the normal parameter list to specify the input data.
This method can be used when there is a privacy requirement for some data in the application and it is not sensitive to the input data.
Scenario 3: input transient data using private data
Similar to scenario 2, data is written to the private data part of the ledger. Only those peer nodes in the private data set definition will save the private data. In the chain code, we use PutPrivateData and GetPrivateData to access the dataset. When we call the chain code, we use transient data as input, so we need to use GetTransient to process the input data in the chain code.
Scenario 4: input transient data without using private data
This is an imaginary scenario, just to show that when private data is not used, transient data can also be used as input. We use PutState and GetState in the chain code to save the data to the public state of the account book, and use the transient data as the input parameters of the chain code call. As before, we use the GetTransient method in the chain code to process the input data.
3. Demonstration environment construction
In these demonstrations, we use Fabric 2.0 and First Network as Fabric network. We use the CouchDB option to start the network so that we can easily view the contents of the world state database. We will focus on peer0.org1.example.com (couchdb port 5984) and peer0.org2.example.com (couchdb port 7984) to see the behavior of the nodes in both institutions.
In the private data section, we use the implicit private data set built into Org1 ('implicit' org 'org1msp). Only peer nodes in Org1 can save private data, while nodes in Org1 and Org2 can save data hash.
We modified the SACC chain code in fabric samples. SACC chain code has two functions, set and get. To show private and transient data, we create the following functions:
- setPrivate: using the same parameter list, the data is saved in the private data set implied by Org1
- setPrivateTransient: using transient data input, the data is saved in the private data set implied by Org1
- setTransient: using transient data input, data is saved in public state
- getPrivate: extract the data stored in the private dataset implied by Org1
The revised SACC chain code is as follows:
/* * Copyright IBM Corp All Rights Reserved * * SPDX-License-Identifier: Apache-2.0 */ package main import ( "encoding/json" "fmt" "github.com/hyperledger/fabric-chaincode-go/shim" "github.com/hyperledger/fabric-protos-go/peer" ) // SimpleAsset implements a simple chaincode to manage an asset type SimpleAsset struct { } // Init is called during chaincode instantiation to initialize any // data. Note that chaincode upgrade also calls this function to reset // or to migrate data. func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { // Get the args from the transaction proposal args := stub.GetStringArgs() if len(args) != 2 { return shim.Error("Incorrect arguments. Expecting a key and a value") } // Set up any variables or assets here by calling stub.PutState() // We store the key and the value on the ledger err := stub.PutState(args[0], []byte(args[1])) if err != nil { return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0])) } return shim.Success(nil) } // Invoke is called per transaction on the chaincode. Each transaction is // either a 'get' or a 'set' on the asset created by Init function. The Set // method may create a new asset by specifying a new key-value pair. func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response { // Extract the function and args from the transaction proposal fn, args := stub.GetFunctionAndParameters() var result string var err error if fn == "set" { result, err = set(stub, args) } else if fn == "setPrivate" { result, err = setPrivate(stub, args) } else if fn == "setTransient" { result, err = setTransient(stub, args) } else if fn == "setPrivateTransient" { result, err = setPrivateTransient(stub, args) } else if fn == "getPrivate" { result, err = getPrivate(stub, args) } else { // assume 'get' even if fn is nil result, err = get(stub, args) } if err != nil { return shim.Error(err.Error()) } // Return the result as success payload return shim.Success([]byte(result)) } // Set stores the asset (both key and value) on the ledger. If the key exists, // it will override the value with the new one func set(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 2 { return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value") } err := stub.PutState(args[0], []byte(args[1])) if err != nil { return "", fmt.Errorf("Failed to set asset: %s", args[0]) } return args[1], nil } func setPrivate(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 2 { return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value") } err := stub.PutPrivateData("_implicit_org_Org1MSP", args[0], []byte(args[1])) if err != nil { return "", fmt.Errorf("Failed to set asset: %s", args[0]) } return args[1], nil } func setTransient(stub shim.ChaincodeStubInterface, args []string) (string, error) { type keyValueTransientInput struct { Key string `json:"key"` Value string `json:"value"` } if len(args) != 0 { return "", fmt.Errorf("Incorrect arguments. Expecting no data when using transient") } transMap, err := stub.GetTransient() if err != nil { return "", fmt.Errorf("Failed to get transient") } // assuming only "name" is processed keyValueAsBytes, ok := transMap["keyvalue"] if !ok { return "", fmt.Errorf("key must be keyvalue") } var keyValueInput keyValueTransientInput err = json.Unmarshal(keyValueAsBytes, &keyValueInput) if err != nil { return "", fmt.Errorf("Failed to decode JSON") } err = stub.PutState(keyValueInput.Key, []byte(keyValueInput.Value)) if err != nil { return "", fmt.Errorf("Failed to set asset") } return keyValueInput.Value, nil } func setPrivateTransient(stub shim.ChaincodeStubInterface, args []string) (string, error) { type keyValueTransientInput struct { Key string `json:"key"` Value string `json:"value"` } if len(args) != 0 { return "", fmt.Errorf("Incorrect arguments. Expecting no data when using transient") } transMap, err := stub.GetTransient() if err != nil { return "", fmt.Errorf("Failed to get transient") } // assuming only "name" is processed keyValueAsBytes, ok := transMap["keyvalue"] if !ok { return "", fmt.Errorf("key must be keyvalue") } var keyValueInput keyValueTransientInput err = json.Unmarshal(keyValueAsBytes, &keyValueInput) if err != nil { return "", fmt.Errorf("Failed to decode JSON") } err = stub.PutPrivateData("_implicit_org_Org1MSP", keyValueInput.Key, []byte(keyValueInput.Value)) if err != nil { return "", fmt.Errorf("Failed to set asset") } return keyValueInput.Value, nil } // Get returns the value of the specified asset key func get(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 1 { return "", fmt.Errorf("Incorrect arguments. Expecting a key") } value, err := stub.GetState(args[0]) if err != nil { return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err) } if value == nil { return "", fmt.Errorf("Asset not found: %s", args[0]) } return string(value), nil } // Get returns the value of the specified asset key func getPrivate(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 1 { return "", fmt.Errorf("Incorrect arguments. Expecting a key") } value, err := stub.GetPrivateData("_implicit_org_Org1MSP", args[0]) if err != nil { return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err) } if value == nil { return "", fmt.Errorf("Asset not found: %s", args[0]) } return string(value), nil } // main function starts up the chaincode in the container during instantiate func main() { if err := shim.Start(new(SimpleAsset)); err != nil { fmt.Printf("Error starting SimpleAsset chaincode: %s", err) } }
4. Demonstration of Fabric private data and transient data
First start the First Network, do not deploy the default chain code, and enable the CouchDB option:
cd fabric-samples/first-network ./byfn.sh up -n -s couchdb
When all containers (5 sorting nodes, 4 peer nodes, 4 couchdb, and 1 CLI) are started:
Create a new chain code directory:
cd fabric-samples/chaincode cp -r sacc sacc_privatetransientdemo cd sacc_privatetransientdemo
Then replace sacc.go with the chain code above.
Before the first run, we need to load the dependent modules:
GO111MODULE=on go mod vendor
Finally, we use lifecycle chaincode Command to deploy the chain code.
5. Scenario 1 Demo: do not use Fabric private data and transient data input
Scenario 1 is the most commonly used one: use the common parameter list as the input of the chain code method, and then save it in the public state. All peer nodes hold the same data. We call the set and get methods of the chain code.
docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["set","name","alice"]}' docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["get","name"]}'
The results are as follows:
Let's first look at the state of the world. This data is saved in the public state of peer0.org1.example.com and peer0.org2.example.com (mychannel ﹐ MYCC).
When viewing the transaction records in the blockchain, we can see that the key / value pair in the WriteSet is: name/alice, encoded with base64.
We can also see the parameter list when calling the chain code. The three base64 encoded parameters are set, name and alice.
As expected, RWSet updates the public status and the input parameters are recorded in the transaction.
6. Scenario 2 demonstration: using private data, not suitable for transient data input
In scenario 2, the chain code calls or uses a common parameter list, and the data is saved in the private data set of Org1. We use setPrivate and getPrivate to access the chain code
docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["setPrivate","name","bob"]}' docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["getPrivate","name"]}'
Let's first look at the state of the world. In peer0.org1.example.com, we can see that the data is saved as private data, Two databases were created: one for the actual data and one for the data hash. At peer0.org2.example.com On, we see that there are only hash files.
The hash of the content is the same on the nodes of both institutions. In addition, in peer0.org1.example.com, we can See the data entered when calling the chain code.
When looking at the transactions in the blockchain, we see that there is no RWSet. Instead, we see data being applied to The implied data set of Org1 refers to the data that has been saved in the peer, and the privacy of this part of data can be protected through hash.
We can see the list of parameters when the chain code is called. The three base64 encoded parameters are: setPrivate, name, bob.
If we are concerned about data privacy, this may be a problem. On the one hand, data is saved in private data sets, so that only limited organization nodes can be saved. On the other hand, the chain code input of this part of privacy data is still publicly visible, and it is permanently stored in the blockchain of all peer nodes. If this is not what you expect, then we also need to hide the input data. This is why transient data input is used.
6. Scenario 3 demonstration: using private data and transient data input
Scenario 3 is the recommended approach if you want to make sure that data input is not saved on the chain. In this scenario, transient data input is adopted, and the data is saved in the private data set of Org1. We use setPrivateTransient and getPrivate methods to access the chain code:
In our chain code, when we implement the function, we code the transient data to a specific JSON format {"key": "some key", "value": "some value"} (chain code 134 – 137 lines). We also require that the transient data contain a keyvalue key (chain code 149 lines). To call on the command line Using transient data in, we need to first base64 code it.
export KEYVALUE=$(echo -n "{\"key\":\"name\",\"value\":\"charlie\"}" | base64 | tr -d \\n) docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["setPrivateTransient"]}' \ --transient "{\"keyvalue\":\"$KEYVALUE\"}" docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["getPrivate","name"]}'
The results are as follows:
Again, let's first look at the state of the world. This is similar to what we saw in scenario 2. The actual data is only saved in peer0.org1.example.com, while the hash is saved in both peer nodes. Note that the revision value is currently 2, while the first revision in scenario 2 is 1. It is the chain code call that contributes to the revision of the data.
Similar to scenario 2, in the transaction records on the blockchain, we can see that there is no Write Set.
We can also see a list of parameters without calling the chain code. The only parameter is the chain code function name, setPrivateTransient. The specific call data {"key": "name", "value": "charlie"} does not appear on the blockchain.
We see that the combination of private data and private data provides some degree of data privacy.
7. Scenario 4 demonstration: not applicable to private data, using transient data input
Finally, let's take a look at the demonstration of the imaginary scene. In scenario 3, transient data is used as data, It is then kept in the open state of the ledger. We use setTransient and get to access the chain code:
export KEYVALUE=$(echo -n "{\"key\":\"name\",\"value\":\"david\"}" | base64 | tr -d \\n) docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["setTransient"]}' \ --transient "{\"keyvalue\":\"$KEYVALUE\"}" docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["get","name"]}'
The results are as follows:
You can see that the public status is updated, and the two nodes currently have the same data. Note that the revision is updated to 2
We see that the key / value pairs in the Write Set are name and david, base64 encoded.
We don't see the input data in the parameter, we only see the method name of the call setTransient.
Original link: Private data and transient data of Hyperledger Fabric - Huizhi network