PutState and GetState: The API in Chaincode Dealing with the State in the Ledger (Part 2)

KC Tam
7 min readOct 13, 2019

Overview

This is the second part of the article. In the last one we have examined the fabcar chaincode and see how PutState, GetState and GetStateByRange APIs are used in chaincode. We also add a new function with pagination using GetStateByRangeWithPagination to return a portion of records based on a bookmark and a page size.

In this part we will look into the composite key, and see how composite key helps to locate records more efficiently. To show how things work, we need more modification on existing fabcar chaincode.

Composite Key

The state inside the ledger in Hyperledger Fabric is recorded as key-value pairs. If we revisit the fabcar chaincode, the key is carid, and the value is the record of that car (model, make, colour and owner).

We see it in last part. After initLedger() is invoked, here is the record kept in the world state.

The key-value pairs recorded in the ledger make carid as the identifier of car. All functions like queryCar(), changeCarOwner() all uses carid as the index to locate the car.

How can we look for a car by owner? For example, if I wish to query a car with owner “Brad”, this key-value pair storage seems not efficient. I may need to first read all the records and search it off-chain. It is still do-able, but composite key may help here.

Hyperledger Fabric comes with composite key to address this challenge. Effectively it is a new record in the ledger which helps to locate a key with an item inside the value. In this example, if we wish to find the carid from an owner “Brad”, we need a way to make reference owner “Brad” pointing to carid CAR1, and possibly more car records in case Brad has more than one cars in the ledger. This can be done with composite key.

We will take a look how Composite Key is created and used in the upcoming example.

APIs related to Composite Key

We first take a look on the several APIs related to Composite Key.

Create Composite Key (link)

func (stub *ChaincodeStub) CreateCompositeKey(objectType string, attributes []string) (string, error)

Split Composite Key (link)

func (stub *ChaincodeStub) SplitCompositeKey(compositeKey string) (string, []string, error)

GetState by Partial Composite Key (link)

func (stub *ChaincodeStub) GetStateByPartialCompositeKey(objectType string, attributes []string) (StateQueryIteratorInterface, error)

GetState by Partial Composite Key with Pagination (link)

func (stub *ChaincodeStub) GetStateByPartialCompositeKeyWithPagination(objectType string, keys []string, pageSize int32, bookmark string) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error)

We will use the first three in our coming demonstration. The pagination is almost similar to what we see in last part of article on GetStateByRangeWithPagination.

Setup

As done before, we will create a new directory inside chaincode directory, with source from fabcar/go.

cd fabric-samples/chaincode
cp -r fabcar/go/ testcompositekey/
cd testcompositekey
mv fabcar.go testcompositekey.go

Design Principle

The design (modification of chaincode) is largely based on another chaincode marbles02.

When a record is written in PutState, we create another record, with the following key/value.

  • key: “owner~carid” + Owner + CarID
  • value: empty

The key is in fact a composite key. It contains the object type (“owner~carid”), and the real Owner and CarID for this record. We don’t need to put any value, as the composite key already gives us all the information needed.

When needed, we can retrieve the CarID from the object type and a given owner. And with the CarID(s) we can retrieve back the whole record of the car.

The two APIs to handles the composite key are CreateCompositeKey and SplitCompositeKey. Here is the logic of the composite key in our setup, and how to use the API to convert them.

We will take a look how this is implemented in various functions.

Modify the Chaincode

initLedger()

Since initLedger() contains the PutState, we need to add back the composite key portion.

We first define indexName (line 2) which corresponding the object type we mentioned. It is meaningful as it is from owner to carid. After that we use CreateCompositeKey with this indexName, the owner (from cars[i].Owner) and carid (CAR+i). This composite key ownerCaridIndexKey is then written into ledger with PutState again, with an empty value (line 7–12).

Here is what happens after initLedger() is invoked. Besides the 10 records we have seen before, there are 10 other records written in the ledger.

We can tell the meaning of this composite key. Take owner~caridBradCAR1 as example.

  • owner~carid is the object type
  • Brad is the owner of CAR1
  • the final portion CAR1 is the carid

Now we have this new set of composite keys, and we can use it later in query.

changeCarOwner()

For demonstration we also modify the code in changeCarOwner() as it also has PutState. And we use changeCarOwner() later to create a situation that an owner owns multiple cars.

Here is the update.

In the original fabcar chaincode, the change owner of car only involves an update on the owner value. With composite key we need to perform two steps.

First, we build the composite key based on the owner of a given carid. This is the original composite key already written in the ledger. We call DelState API to delete this composite key record (line 17).

Then we build a new composite key with the new owner given. We call PutState API to write this new composite key (line 29–34).

createCar()

For sake of completeness, we also modify the code of createCar() as it uses PutState to write new record. We are not going to show this in the demonstration.

queryCarByOwner()

Now we are ready to create a new function, called queryCarByOwner(), by an owner given, it returns one or more car records with this given owner.

First we need to update the Invoke() portion to include this new function (line 18–20).

And here is the function.

This function is adapted from queryCar(). One argument is needed. It is stored as owner. indexName keeps the object type. Make sure we are using the same with other functions.

It calls GetStateByPartialCompositeKey API with the object type and the partial key, which is owner. (line 10)

The result is an iterator (resultsIterator).

The iterator is processed item by item. For each item (responseRange), it calls SplitCompositeKey to break down the composite key (line 28). The result of split is the object type (first item), an array of parts (second item), and error. In our case the key parts are

  • compositeKeyParts[0]: owner
  • compositeKeyParts[1]: carid

Obviously the compositeKeyParts[1] (returnedCarId) is what we need. We use the simple GetState to obtain the record of this carid.

The remain part is just formatting the result, similar to what we see in getAllCars().

Demonstration

As before, we first tear down everything from our fabcar demo before, and bring up the new chaincode.

cd fabric-samples/basic-network
./teardown.sh
./start.sh
docker-compose up -d clidocker exec cli peer chaincode install -n mycc -p github.com/testcompositekey -v 0docker exec cli peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n mycc github.com/testcompositekey -v 0 -c '{"Args": []}' -P "OR('Org1MSP.member')"docker exec cli peer chaincode invoke -C mychannel -n mycc -c '{"Args":["initLedger"]}'

First we take a normal queryAllCar() to see the 10 records already there.

According to the record, we see owner of CAR1 is “Brad”. We use queryCarByOwner() with argument Brad.

Now we change car owner of CAR5 to “Brad” using changeCarOwner(). Then we use queryCarByOwner() again.

We see two car records found with owner “Brad”.

If we take a look on the couchDB, we see the original composite key owner~caridMichelCAR5 is gone, and a new composite key owner~caridBradCAR5 is there. It is how we code in changeCarOwner().

This is how we can use composite key to create a type of indexing on a specific item (owner).

Closing

In this two-part work, we first examine the fabcar chaincode from fabric samples and understand how PutState, GetState, GetStateByRange APIs are used for working on the state in ledger. On top of it we have built two functions. In the first part we add the pagination on current queryAllCars() to make it more readable. In this part we introduce the idea of composite key in chaincode, and use GetStateByCompositeKey to build a function that can query car records based on owner. This makes our chaincode more useful and make client application development easier.

--

--