Private Ledger is a web application written in Go to demonstrate storing private data in Hyperleder fabric multi-org environment. The blockchain network consists of four organization joined with a single channel. The ledger data created in private collection, so that the data only accessible to the related organization unless the collection data is shared to other organization. And also, this repo will be demonstrate sharing the private collection data among the organizations.
However, this explanation guide does not explain how Hyperledger Fabric works, so for the information, you can follow at Hyperledger.
Medium writeup : https://medium.com/@deeptiman/confidentiality-and-private-data-in-hyperledger-fabric-1279c8e2e57f
Private Ledger requires Docker & Go to run.
$ sudo apt install docker.io
$ sudo apt install docker-compose
$ sudo apt-get update
$ sudo apt-get install golang-go
$ export GOPATH=$HOME/go
$ export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
$ source ~/.profile
$ go version
$ go version go1.11 linux/amd64
- Sometimes, in some of the local machine not able to identify or link the hyperledger endpoints with localhost or 127.0.0.1. So, it's better to add the hyperledger endpoints in /etc/hosts mapping with 127.0.0.1
127.0.0.1 orderer.private.ledger.com
127.0.0.1 ca.org1.private.ledger.com
127.0.0.1 peer0.org1.private.ledger.com
127.0.0.1 peer1.org1.private.ledger.com
127.0.0.1 peer0.org2.private.ledger.com
127.0.0.1 peer1.org2.private.ledger.com
127.0.0.1 peer0.org3.private.ledger.com
127.0.0.1 peer1.org3.private.ledger.com
127.0.0.1 peer0.org4.private.ledger.com
127.0.0.1 peer1.org4.private.ledger.com
-
In a multi-org environment, few endorsement policies need to meet between all the organization to commit a transaction otherwise anybody can submit a transaction by signing as any member of the group.
-
The policies is mentioned in configtx.yaml
- Readers, Writers & Admins all have a specific Rules with Signing type
-
Ex : "OR('Org1MSP.member')" means any memeber of Org1 can sign a transaction and commit blocks to the blockchain. There are various endorsing principals like admin, client, peer or member. The principals can be used based on the network architecture or business logic requirements.
-
configtxgen tool will create the channel artifacts and four Anchor peers of the organizations.
-
Anchor peers of all organization will be useful to communicate with each other so that any organization peer perform a transaction then other peers of the organization will get notified.
-
All the CAs, Peers and CouchDB for all the Orgs need to be mention in the docker-compose.yaml. In this repo, you can find there are four docker-compose YAML files, which are just the extended files from the base docker-compose.yaml. I have done this for code readability and changing config will be simpler.
-
Now the script to generate the artifacts mentioned below
config.sh
./bin/cryptogen generate --config=./crypto-config.yaml
./bin/configtxgen -profile FourOrgsOrdererGenesis -outputBlock ./artifacts/orderer.genesis.block
./bin/configtxgen -profile FourOrgsChannel -outputCreateChannelTx ./artifacts/privateledger.channel.tx -channelID privateledger
./bin/configtxgen -profile FourOrgsChannel -outputAnchorPeersUpdate ./artifacts/Org1MSPanchors.tx -channelID privateledger -asOrg Org1MSP
./bin/configtxgen -profile FourOrgsChannel -outputAnchorPeersUpdate ./artifacts/Org2MSPanchors.tx -channelID privateledger -asOrg Org2MSP
./bin/configtxgen -profile FourOrgsChannel -outputAnchorPeersUpdate ./artifacts/Org3MSPanchors.tx -channelID privateledger -asOrg Org3MSP
./bin/configtxgen -profile FourOrgsChannel -outputAnchorPeersUpdate ./artifacts/Org4MSPanchors.tx -channelID privateledger -asOrg Org4MSP
-
Start the network
docker-compose up --force-recreate -d
This command will create the docker images for the organizations. It's needed to stop the existing running network and start the network as new, so I have mentioned the network down/up in a Makefile. Please use the make to run all at one instance.
There are sequence steps to follow to completely setup the multi-org network.
-
Initialize SDKs for each Organziations.
This steps will be called as an initial entry point to perform other activities in the network. Every organization will have its own SDK and will be initialized with config.yaml. The initialization process will create CA clients, MSP clients, Resource Management clients, signing identity.
- The channel will create by Orderer and will use all the signing identities of the organization admins.
req := resmgmt.SaveChannelRequest{ ChannelID: "multiorgledger", ChannelConfigPath: os.os.Getenv("GOPATH")+"/multiorgledger.channel.tx", SigningIdentities: []msp.SigningIdentity{Org1SignIdentity, Org2SignIdentity, Org3SignIdentity, Org4SignIdentity}, } txID, err := s.Resmgmt.SaveChannel( req, resmgmt.WithOrdererEndpoint(Orderer.OrdererID)) if err != nil || txID.TransactionID == "" { return errors.WithMessage(err, "failed to save anchor channel for - "+s.OrgName) }
- Each organization admins will create Anchor peers for their organization using the anchor peer artifacts.
req := resmgmt.SaveChannelRequest{ ChannelID: "multiorgledger", ChannelConfigPath: os.os.Getenv("GOPATH")+"/", //Org1MSPanchors.tx or Org2MSPanchors.tx or Org3MSPanchors.tx or Org4MSPanchors.tx SigningIdentities: []msp.SigningIdentity{Org1SignIdentity or Org2SignIdentity or Org3SignIdentity or Org4SignIdentity}, } txID, err := s.Resmgmt.SaveChannel( req, resmgmt.WithOrdererEndpoint(Orderer.OrdererID)) if err != nil || txID.TransactionID == "" { return errors.WithMessage(err, "failed to save anchor channel for - "+s.OrgName) }
-
After creating the channel, each anchor peer will join the channel and remember Orderer will only create the channel and he can't join the channel.
s.Resmgmt - is the resource management client created during Org SDK initialization. So, each organization resource management client will execute the join channel action indivisually.
if err := s.Resmgmt.JoinChannel(s.ChannelID, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithOrdererEndpoint(Orderer.OrdererID)); err != nil { return errors.WithMessage(err, "failed to make admin join channel") }
-
There are specific channel queries can be use to check if the peers of an organization has already join the channel or not.
for this query specific peer details needs to be pass, so it will check for the channel join status.
resp, err := orgResmgmt.QueryChannels(resmgmt.WithTargets(peer)) if err != nil { fmt.Println("IsJoinedChannel : failed to Query >>> "+err.Error()) return false, err } for _, chInfo := range resp.Channels { fmt.Println("IsJoinedChannel : "+chInfo.ChannelId+" --- "+s.ChannelID) if chInfo.ChannelId == s.ChannelID { return true, nil } }
-
Installing chaincode for the organization will use the same chaincode id & version unless it requires to be different for an organization on specific circumstances.
-
InstallCCRequest will be the same for all organizations but the request will be used by the individual resource management client of the organizations. So, the chaincode will be installed in all the organization separately.
req := resmgmt.InstallCCRequest{ Name: s.ChaincodeId, Path: s.ChaincodePath, Version: s.ChainCodeVersion, Package: ccPkg, } _, err = s.Resmgmt.InstallCC(req, resmgmt.WithRetry(retry.DefaultResMgmtOpts)) if err != nil { fmt.Println("failed to install chaincode : "+err.Error()) return errors.WithMessage(err, " failed to install chaincode") }
-
Chaincode instantiation in a multi org environment will be done only once. It will be initiated by any organization peers and will specify certain endorsement policy considering all the organization member or peer or admin depending on the network configurations.
-
As this application specifically developed to store private data in the chaincode, so each organization will have its policy and collections.
-
For Example
Collection : collectionOrg1Policy : "OR ('Org1MSP.member')"
so this specifies that only and only "org1" can use the collectionOrg1 data. The data is completely restricted to other organizations. As this application mainly intended to store private data per organization and later share the data among organizations with certain access query. So, each organization will have there own private collection and policies and it will be added to the chaincode instantiation request.
This function will return the collection config object per organization, so later each collection config can be pass as an array to the chaincode instantiation.
func newCollectionConfig(colName, policy string, reqPeerCount, maxPeerCount int32, blockToLive uint64) (*cb.CollectionConfig, error) { p, err := cauthdsl.FromString(policy) if err != nil { fmt.Println("failed to create newCollectionConfig : "+err.Error()) return nil, err } cpc := &cb.CollectionPolicyConfig{ Payload: &cb.CollectionPolicyConfig_SignaturePolicy{ SignaturePolicy: p, }, } return &cb.CollectionConfig{ Payload: &cb.CollectionConfig_StaticCollectionConfig{ StaticCollectionConfig: &cb.StaticCollectionConfig{ Name: colName, MemberOrgsPolicy: cpc, RequiredPeerCount: reqPeerCount, MaximumPeerCount: maxPeerCount, BlockToLive: blockToLive, }, }, }, nil } // The maximum no of block allocated to keep private data alive, after that the data will purge across the network blockToLive = 1000
// Number of peer required to distribute the private data as a condition of the endorsement of the chaincode peerCount = 0 // This is the maxium number of peers allocated to a collection, if an endorsing peer goes down, then other peers are available maximumPeerCount = 3
collCfg1, _ := newCollectionConfig("collectionOrg1", "OR ('Org1MSP.member')", peerCount, maximumPeerCount, blockToLive) collCfg2, _ := newCollectionConfig("collectionOrg2", "OR ('Org2MSP.member')", peerCount, maximumPeerCount, blockToLive) collCfg3, _ := newCollectionConfig("collectionOrg3", "OR ('Org3MSP.member')", peerCount, maximumPeerCount, blockToLive) collCfg4, _ := newCollectionConfig("collectionOrg4", "OR ('Org4MSP.member')", peerCount, maximumPeerCount, blockToLive)
cfg := []*cb.CollectionConfig{ collCfg1, collCfg2, collCfg3, collCfg4}
this collection config specifies that each collection record is private to the organization and other organization will only get hash of the ledger data.
// Any one of organization resource management client can execute the instantiate query and their peers will be mention in the target. The chaincode policy will be adding all the member of the four organization
policy = "OR ('Org1MSP.member','Org2MSP.member','Org3MSP.member','Org4MSP.member')" ccPolicy, _ := cauthdsl.FromString(policy) // cauthdsl will convert the policy string to Policy object resp, err := s.Resmgmt.InstantiateCC( s.ChannelID, resmgmt.InstantiateCCRequest{ Name: s.ChaincodeId, Path: s.ChaincodePath, Version: s.ChainCodeVersion, Args: [][]byte{[]byte("init")}, Policy: ccPolicy, CollConfig: cfg, },resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithTargets(orgPeers[0], orgPeers[1]))
-
As the chaincode only be instantiated once, so if any changes made in the chaincode, then it will be upgraded with a new version code and keeping the chaincode name same (important) in the network.
// Any one of organization resource management client can execute the upgrade query and their peers will be mention in the target.
// Policy can be remain the same unless it requires modification based on your chaincode buiseness requirements.
req := resmgmt.UpgradeCCRequest{ Name: s.ChaincodeId, Version: s.ChainCodeVersion, Path: s.ChaincodePath, Args: [][]byte{[]byte("init")}, Policy: ccPolicy, CollConfig: cfg, } resp, err := s.Resmgmt.UpgradeCC(s.ChannelID, req, resmgmt.WithRetry(retry.DefaultResMgmtOpts),resmgmt.WithTargets(orgPeers[0], orgPeers[1])) if err != nil { return errors.WithMessage(err, " >>>> failed to upgrade chaincode") }
-
Chaincode installation can be checked by querying "QueryInstalledChaincodes", which requires the peer of the organization, so it will check whether the peer has chaincode installed or not.
resp, err := resMgmt.QueryInstalledChaincodes(resmgmt.WithTargets(peer)) if err != nil { return false, errors.WithMessage(err, " QueryInstalledChaincodes for peer [%s] failed : "+peer.URL()) } found := false for _, ccInfo := range resp.Chaincodes { fmt.Println(" "+orgID+" found chaincode "+ccInfo.Name+" --- "+ccName+ " with version "+ ccInfo.Version+" -- "+ccVersion) if ccInfo.Name == ccName && ccInfo.Version == ccVersion { found = true break } } if !found { fmt.Println(" "+orgID+" chaincode is not installed on peer "+ peer.URL()) installedOnAllPeers = false }
- Chaincode instantiation can be checked by querying "QueryInstantiatedChaincodes", which requires the peer of the organization, so it will check whether the peer has chaincode instantiated or not.
chaincodeQueryResponse, err := resMgmt.QueryInstantiatedChaincodes(channelID, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithTargets(peer)) if err != nil { return false, errors.WithMessage(err, " QueryInstantiatedChaincodes return error") } fmt.Println("\n Found instantiated chaincodes on peer "+peer.URL()) found := false for _, chaincode := range chaincodeQueryResponse.Chaincodes { fmt.Println(" Found instantiated chaincode Name: "+chaincode.Name+", Version: "+chaincode.Version+", Path: "+chaincode.Path+" on peer "+peer.URL()) if chaincode.Name == ccName && chaincode.Version == ccVersion { found = true break } } if !found { fmt.Println(" "+ccName+" chaincode is not instantiated on peer "+ peer.URL()) installedOnAllPeers = false }
-
This is most important in case CA registration. In Hyperledger fabric by default "org1 & org2" are affiliated as CA organization, so any client or peer wants to register or enroll into the network via CA can pass "org1 or org2" as an affiliated organization.
-
But in case of other organization like org3 & org4, they need to be affiliated using following CA Client API.
// to perform the query individual org ca client needs to be used affl := strings.ToLower(org) + ".department1" _, err = caClient.AddAffiliation(&caMsp.AffiliationRequest{ Name: affl, Force: true, CAName: caid, }) if err != nil { return fmt.Errorf("Failed to add affiliation for CA '%s' : %v ", caid, err) }
we can also check, whether the organization has the affiliation from the CA client by using the following API.
fRes, err := caClient.GetAffiliation(affl) if afRes != nil && err != nil { fmt.Println("Affiliation Exists") AfInfo := afRes.AffiliationInfo CAName := afRes.CAName fmt.Println("AfInfo : " + AfInfo.Name) fmt.Println("CAName : " + CAName) }
- Hyperledger fabric-sdk-go is still in development. If you do dep ensure for each Gopkg.toml in PrivateLedger and Chaincode, it will download the govendor folder for each module but it will have some compilation issues while building the project. I have corrected the error for both PrivateLedger and Chaincode folder.
-
Please download the vendor folder and add it in your project repo.
PrivateLedger - https://www.dropbox.com/s/ry1jmw0y9xliose/vendor.zip?dl=0
Chaincode - https://www.dropbox.com/s/31nnqflpqwaywoa/vendor.zip?dl=0
- Add vendor folders at the location where Gopkg.toml file is located.
So, this concludes the essential multi-org network setup for a 4 organizations based network.
-
The application is developed consisting of two clients. ( REST and Web App)
-
You can use the REST client at the server running at - http://localhost:4000
-
And for Web App, you can use server running at - http://localhost:6000
This project is licensed under the MIT License