Private & Public Data at Hyperledger Fabric

Ta-seen Junaid
Oct 5, 2019 · 11 min read

Overview

This article represents both conceptual theories and implementation processes of public and private data at Hyperledger Fabric v1.4 blockchain platform and

We assume that you know all the key concepts of Hyperledger Fabric v1.4 and have sound knowledge on Building Your First Network.

Why we need private data?

Private data is used for data confidentiality and to hide sensitive information from other parties. Like two parties do not want to share their pricing policies among other members of Blockchain network or customers do not want to share all their information among others.

Though Hyperledger Fabric is a private permissioned Blockchain network, creating more channels for private data will cause maintenance nightmare and performance degradation. Encrypting the key-value pairs will create overheads and everyone with the shared key can access the encrypted data. So the best solution is to use private data to keep your necessary information confidential from others.

An Example

Suppose Org A, Org B, Org C and Org D are four business organizations in our blockchain network and those organizations are in Channel x for their business purpose.

Channel x with both public data and private data

Each of them has a same Ledger that holds facts about the current and historical state of a set of business objects whereas Chaincode defines the executable logic that generates new facts to add to each Ledger. So data in the Ledger is among all organizations of that channel. But Org C and Org D do not want to share their confidential data with other organizations. So they store those private data in a which is between Org C & Org D where Org A & Org B will never know the private data but a transaction hash will be added at each Ledger.

Flow of Transactions for a Channel’s Public Data

Signing transaction proposal and sending that to endorsers

Gathering signed responses from endorsers

Flow of transactions for a channel’s public data

Broadcasting request to orders

Creation of blocks by orders

Delivering blocks to peers by orders

Validating by peers

Updating Ledgers

Flow of Transactions for a Channel’s Private Data

Signing transaction proposal and sending that to endorsers

Keeping private data in transient field to hide it from endorsers

Flow of transactions for a channel’s private Data

Checking of private data collection definition by endorsers

Sending of private data to some of authorized organizations peers

Use of among authorized organizations peers to receive private data

responding with the hash of private data by endorsers

Broadcasting request to orders

Creation of blocks by orders

Delivering blocks to peers by orders

Validating by peers

Updating Ledgers with transaction hash (both authorized and unauthorized peers)

Checking by authorized peers

Moving of private data into by authorized peers

Deletion of private data from transient DB by authorized peers

Hands-on tutorial

Private data collection definition needs to write in a JSON format and needs to introduce it’s path during Chaincode instantiate process(like $GOPATH/src/github.com/chaincode/collections_config.json). We name the JSON file as collections_config.json and you can read official documents about how to write this file.

//collections_config.json
[
{
"name": "collectionMaterialOrderPrivateDetails",
"policy": "OR('CMSP.member','DMSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive": 0,
"memberOnlyRead": true
}
]

We name this as “collectionMaterialOrderPrivateDetails” and according to the policy only C and D organizations can have access into the private data.

In our Chaincode, we keep the pricing policy private so only authorized organizations can view it. The Chaincode is written by using GO language.

Chaincode
//Chaincode
package main

import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"time"

"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)

type SimpleChaincode struct {
}

type order struct {
ObjectType string `json:"docType"`
Order_raw_material_id string `json:"order_raw_material_id"`
Manufacturer_id string `json:"manufacturer_id"`
Provider_id string `json:"provider_id"`
Material_name string `json:"material_name"`
Order_medicine_id string `json:"order_medicine_id"`
Quantity string `json:"quantity"`
Status string `json:"status"`
}

type orderPrivateDetails struct {
Price string `json:"price"`
}

func main() {
err := shim.Start(new(SimpleChaincode))
if err != nil {
fmt.Printf("Error starting Simple chaincode: %s", err)
}
}

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
return shim.Success(nil)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
function, args := stub.GetFunctionAndParameters()
fmt.Println("invoke is running " + function)

if function == "initOrder" {
return t.initOrder(stub, args)
} else if function == "changeStatus" {
return t.changeStatus(stub, args)
} else if function == "changeProvider_id" {
return t.changeProvider_id(stub, args)
} else if function == "changeQuantity" {
return t.changeQuantity(stub, args)
} else if function == "readOrder" {
return t.readOrder(stub, args)
} else if function == "getHistoryForOrder" {
return t.getHistoryForOrder(stub, args)
} else if function == "getOrdersByRange" {
return t.getOrdersByRange(stub, args)
} else if function == "queryOrderByOrder_medicine_id" {
return t.queryOrderByOrder_medicine_id(stub, args)
} else if function == "readOrderPrivateDetails" {
return t.readOrderPrivateDetails(stub, args)
} else if function == "changeOrderPrivateDetails" {
return t.changeOrderPrivateDetails(stub, args)
}

fmt.Println("invoke did not find func: " + function)
return shim.Error("Received unknown function invocation")
}

func (t *SimpleChaincode) initOrder(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var err error

if len(args) != 7 {
return shim.Error("Incorrect number of arguments. Expecting 7")
}

fmt.Println("- start init order")
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[2]) <= 0 {
return shim.Error("3rd argument must be a non-empty string")
}
if len(args[3]) <= 0 {
return shim.Error("4th argument must be a non-empty string")
}
if len(args[4]) <= 0 {
return shim.Error("5th argument must be a non-empty string")
}
if len(args[5]) <= 0 {
return shim.Error("6th argument must be a non-empty string")
}
if len(args[6]) <= 0 {
return shim.Error("7th argument must be a non-empty string")
}

orderName := args[0]
manufacturer_id := args[1]
provider_id := args[2]
material_name := args[3]
order_medicine_id := args[4]
quantity := args[5]
status := args[6]

orderAsBytes, err := stub.GetState(orderName)
if err != nil {
return shim.Error("Failed to get order: " + err.Error())
} else if orderAsBytes != nil {
fmt.Println("This orderName already exists: " + orderName)
return shim.Error("This orderName already exists: " + orderName)
}

objectType := "order"
order := &order{objectType, orderName, manufacturer_id, provider_id, material_name, order_medicine_id, quantity, status}
orderJSONasBytes, err := json.Marshal(order)
if err != nil {
return shim.Error(err.Error())
}

err = stub.PutState(orderName, orderJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}

// ==== Create order private details object with price, marshal to JSON, and save to state ====
transMap, err := stub.GetTransient()
if err != nil {
return shim.Error("Error getting transient: " + err.Error())
}

if _, ok := transMap["order"]; !ok {
return shim.Error("order must be a key in the transient map")
}

if len(transMap["order"]) == 0 {
return shim.Error("order value in the transient map must be a non-empty JSON string")
}

type orderTransientInput struct {
Price string `json:"price"`
}

var orderInput orderTransientInput
err = json.Unmarshal(transMap["order"], &orderInput)
if err != nil {
return shim.Error("Failed to decode JSON of: " + string(transMap["order"]))
}

orderPrivateDetails := &orderPrivateDetails{
Price: orderInput.Price,
}
orderPrivateDetailsBytes, err := json.Marshal(orderPrivateDetails)
if err != nil {
return shim.Error(err.Error())
}
err = stub.PutPrivateData("collectionMaterialOrderPrivateDetails", orderName, orderPrivateDetailsBytes)
if err != nil {
return shim.Error(err.Error())
}

indexName := "order_medicine_id"
order_medicine_idIndexKey, err := stub.CreateCompositeKey(indexName, []string{order.Order_medicine_id, order.Order_raw_material_id})
if err != nil {
return shim.Error(err.Error())
}

value := []byte{0x00}
stub.PutState(order_medicine_idIndexKey, value)

fmt.Println("- end init order")
return shim.Success(nil)
}

func (t *SimpleChaincode) readOrder(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var name, jsonResp string
var err error

if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting name of the order to query")
}

name = args[0]
valAsbytes, err := stub.GetState(name)
if err != nil {
jsonResp = "{\"Error\":\"Failed to get state for " + name + "\"}"
return shim.Error(jsonResp)
} else if valAsbytes == nil {
jsonResp = "{\"Error\":\"Order does not exist: " + name + "\"}"
return shim.Error(jsonResp)
}

return shim.Success(valAsbytes)
}

func (t *SimpleChaincode) changeStatus(stub shim.ChaincodeStubInterface, args []string) pb.Response {

if len(args) < 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}

orderName := args[0]
newStatus := args[1]
fmt.Println("- start changeStatus ", orderName, newStatus)

orderAsBytes, err := stub.GetState(orderName)
if err != nil {
return shim.Error("Failed to get order:" + err.Error())
} else if orderAsBytes == nil {
return shim.Error("Order does not exist")
}

statusToChange := order{}
err = json.Unmarshal(orderAsBytes, &statusToChange)
if err != nil {
return shim.Error(err.Error())
}
statusToChange.Status = newStatus

orderJSONasBytes, _ := json.Marshal(statusToChange)
err = stub.PutState(orderName, orderJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}

fmt.Println("- end changeStatus (success)")
return shim.Success(nil)
}

func (t *SimpleChaincode) changeProvider_id(stub shim.ChaincodeStubInterface, args []string) pb.Response {

if len(args) < 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}

orderName := args[0]
newProvider_id := args[1]
fmt.Println("- start changeStatus ", orderName, newProvider_id)

orderAsBytes, err := stub.GetState(orderName)
if err != nil {
return shim.Error("Failed to get order:" + err.Error())
} else if orderAsBytes == nil {
return shim.Error("Order does not exist")
}

provider_idToChange := order{}
err = json.Unmarshal(orderAsBytes, &provider_idToChange)
if err != nil {
return shim.Error(err.Error())
}

orderJSONasBytes, _ := json.Marshal(provider_idToChange)
err = stub.PutState(orderName, orderJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}

fmt.Println("- end changeStatus (success)")
return shim.Success(nil)
}

func (t *SimpleChaincode) changeQuantity(stub shim.ChaincodeStubInterface, args []string) pb.Response {

if len(args) < 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}

orderName := args[0]
newQuantity := args[1]
fmt.Println("- start changeStatus ", orderName, newQuantity)

orderAsBytes, err := stub.GetState(orderName)
if err != nil {
return shim.Error("Failed to get order:" + err.Error())
} else if orderAsBytes == nil {
return shim.Error("Order does not exist")
}

quantityToChange := order{}
err = json.Unmarshal(orderAsBytes, &quantityToChange)
if err != nil {
return shim.Error(err.Error())
}
quantityToChange.Quantity = newQuantity

orderJSONasBytes, _ := json.Marshal(quantityToChange)
err = stub.PutState(orderName, orderJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}

fmt.Println("- end changeStatus (success)")
return shim.Success(nil)
}

func (t *SimpleChaincode) queryOrderByOrder_medicine_id(stub shim.ChaincodeStubInterface, args []string) pb.Response {

if len(args) < 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}

order_medicine_id := args[0]

queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"order\",\"order_medicine_id\":\"%s\"}}", order_medicine_id)

queryResults, err := getQueryResultForQueryString(stub, queryString)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(queryResults)
}

func getQueryResultForQueryString(stub shim.ChaincodeStubInterface, queryString string) ([]byte, error) {

fmt.Printf("- getQueryResultForQueryString queryString:\n%s\n", queryString)

resultsIterator, err := stub.GetQueryResult(queryString)
if err != nil {
return nil, err
}
defer resultsIterator.Close()

buffer, err := constructQueryResponseFromIterator(resultsIterator)
if err != nil {
return nil, err
}

fmt.Printf("- getQueryResultForQueryString queryResult:\n%s\n", buffer.String())

return buffer.Bytes(), nil
}

func constructQueryResponseFromIterator(resultsIterator shim.StateQueryIteratorInterface) (*bytes.Buffer, error) {

var buffer bytes.Buffer
buffer.WriteString("[")

bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}

if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")

buffer.WriteString(", \"Record\":")
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

return &buffer, nil
}

func (t *SimpleChaincode) getHistoryForOrder(stub shim.ChaincodeStubInterface, args []string) pb.Response {

if len(args) < 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}

orderName := args[0]

fmt.Printf("- start getHistoryForOrder: %s\n", orderName)

resultsIterator, err := stub.GetHistoryForKey(orderName)
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()

var buffer bytes.Buffer
buffer.WriteString("[")

bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
response, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}

if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"TxId\":")
buffer.WriteString("\"")
buffer.WriteString(response.TxId)
buffer.WriteString("\"")

buffer.WriteString(", \"Value\":")

if response.IsDelete {
buffer.WriteString("null")
} else {
buffer.WriteString(string(response.Value))
}

buffer.WriteString(", \"Timestamp\":")
buffer.WriteString("\"")
buffer.WriteString(time.Unix(response.Timestamp.Seconds, int64(response.Timestamp.Nanos)).String())
buffer.WriteString("\"")

buffer.WriteString(", \"IsDelete\":")
buffer.WriteString("\"")
buffer.WriteString(strconv.FormatBool(response.IsDelete))
buffer.WriteString("\"")

buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

fmt.Printf("- getHistoryForOrder returning:\n%s\n", buffer.String())

return shim.Success(buffer.Bytes())
}

func (t *SimpleChaincode) getOrdersByRange(stub shim.ChaincodeStubInterface, args []string) pb.Response {

if len(args) < 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}

startKey := args[0]
endKey := args[1]

resultsIterator, err := stub.GetStateByRange(startKey, endKey)
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()

buffer, err := constructQueryResponseFromIterator(resultsIterator)
if err != nil {
return shim.Error(err.Error())
}

fmt.Printf("- getOrdersByRange queryResult:\n%s\n", buffer.String())

return shim.Success(buffer.Bytes())
}

func (t *SimpleChaincode) readOrderPrivateDetails(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var name, jsonResp string
var err error

if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting name of the order to query")
}

name = args[0]
valAsbytes, err := stub.GetPrivateData("collectionMaterialOrderPrivateDetails", name) //get the order private details from chaincode state
if err != nil {
jsonResp = "{\"Error\":\"Failed to get private details for " + name + ": " + err.Error() + "\"}"
return shim.Error(jsonResp)
} else if valAsbytes == nil {
jsonResp = "{\"Error\":\"order private details does not exist: " + name + "\"}"
return shim.Error(jsonResp)
}

return shim.Success(valAsbytes)
}

func (t *SimpleChaincode) changeOrderPrivateDetails(stub shim.ChaincodeStubInterface, args []string) pb.Response {

if len(args) < 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}

orderName := args[0]
newPrice := args[1]
fmt.Println("- start changeStatus ", orderName, newPrice)

orderAsBytes, err := stub.GetPrivateData("collectionMaterialOrderPrivateDetails", orderName)
if err != nil {
return shim.Error("Failed to get order:" + err.Error())
} else if orderAsBytes == nil {
return shim.Error("Order does not exist")
}

priceToChange := orderPrivateDetails{}
err = json.Unmarshal(orderAsBytes, &priceToChange)
if err != nil {
return shim.Error(err.Error())
}
priceToChange.Price = newPrice

orderJSONasBytes, _ := json.Marshal(priceToChange)
err = stub.PutPrivateData("collectionMaterialOrderPrivateDetails", orderName, orderJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}

fmt.Println("- end changeStatus (success)")
return shim.Success(nil)
}

To install Chanincode:

peer chaincode install -n ${CHAINCODE_NAME} -v 1.0 -p github.com/chaincode/ordermaterial/

To instantiate Chaincode:

peer chaincode instantiate -o orderer.example.com:7050 — tls — cafile $ORDERER_CA_FILE -C $CHANNEL_NAME -n $CHAINCODE_NAME -v 1.0 -c ‘{“Args”:[]}’ -P “OR (‘AMSP.peer’,’BMSP.peer’,’CMSP.peer’,’DMSP.peer’)” — collections-config $GOPATH/src/github.com/chaincode/collections_config.json

To invoke Chaincode with both public and private data:

export ORDER=$(echo -n “{\”price\”:\”100\”}” | base64 | tr -d \\n)
peer chaincode invoke -o orderer.example.com:7050 — tls — cafile $ORDERER_CA_FILE -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“function”:”initOrder”,”Args”:[“12”, “manu”, “pro”, “material”, “1a1”, “10”, “accept”]}’ — transient “{\”order\”:\”$ORDER\”}”

Some invoking commands for both private and public data:

export ORDER=$(echo -n “{\”price\”:\”200\”}” | base64 | tr -d \\n)
peer chaincode invoke -o orderer.example.com:7050 — tls — cafile $ORDERER_CA_FILE -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“function”:”initOrder”,”Args”:[“1”, “manu”, “pro12”, “material123”, “1a1”, “102”, “accept”]}’ — transient “{\”order\”:\”$ORDER\”}”
peer chaincode invoke -o orderer.example.com:7050 — tls — cafile $ORDERER_CA_FILE -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“function”:”changeStatus”,”Args”:[“12” , “Good”]}’peer chaincode invoke -o orderer.example.com:7050 — tls — cafile $ORDERER_CA_FILE -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“function”:”changeQuantity”,”Args”:[“12” , “100”]}’peer chaincode invoke -o orderer.example.com:7050 — tls — cafile $ORDERER_CA_FILE -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“function”:”changeProvider_id”,”Args”:[“12” , “pro10”]}’peer chaincode invoke -o orderer.example.com:7050 — tls — cafile $ORDERER_CA_FILE -C $CHANNEL_NAME -n $CHAINCODE_NAME -c’{“function”:”changeOrderPrivateDetails”,”Args”:[“12” , “300”]}’

Some query commands for both private and public data:

peer chaincode query -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“function”:”readOrder”,”Args”:[“12”]}’peer chaincode query -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“function”:”getOrdersByRange”,”Args”:[“1” , “15”]}’peer chaincode query -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“Args”:[“queryOrderByOrder_medicine_id”,”1a1"]}’peer chaincode query -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“Args”:[“readOrderPrivateDetails”,”12"]}’peer chaincode query -C $CHANNEL_NAME -n $CHAINCODE_NAME -c ‘{“Args”:[“getHistoryForOrder”,”12"]}’

Summary:

In this article we try to explain public and private data of Hyperledger Fabric v1.4 blockchain platform with a hands-on tutorial about how to implement both private and public data on same channel. We hardly find any tutorial about implementation processesofboth private and public data on same channel and so we hope it will help you to develop your blockchain network.

Get Best Software Deals Directly In Your Inbox

Coinmonks

Coinmonks is a non-profit Crypto educational publication.

Sign up for Coinmonks

By Coinmonks

A newsletter that brings you week's best crypto and blockchain stories and trending news directly in your inbox, by CoinCodeCap.com Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Ta-seen Junaid

Written by

Coinmonks

Coinmonks

Coinmonks is a non-profit Crypto educational publication. Follow us on Twitter @coinmonks Our other project — https://coincodecap.com

Ta-seen Junaid

Written by

Coinmonks

Coinmonks

Coinmonks is a non-profit Crypto educational publication. Follow us on Twitter @coinmonks Our other project — https://coincodecap.com

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store