Private & Public Data at Hyperledger Fabric v1.4

ta-seen Junaid
Oct 5 · 10 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 it is the first article that covers implementation processes of public and private data into a same channel.

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 public 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 SideDB which is private 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 gossip protocol 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 SideDB 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 processes of both private and public data on same channel and so we hope it will help you to develop your blockchain network. My colleagues at BJIT Group helped me to write this article.

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

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade