PutState and GetState: The API in Chaincode Dealing with the State in the Ledger (Part 1)
By: Kc Tam
Tags: Golang, Hyperledger Fabric, Ledger, Blockchain, Chaincode

Overview

Ledger in Hyperledger Fabric is composed of two parts: blockchain of transaction records and world state (link). Each channel has its own ledger, and each peer node joining the channel will keep a separate copy of this ledger. Chaincode (smart contract in Hyperledger Fabric) keeps the business logic agreed by all participants, interacting with the world state, the single source of truth.

In this article we try to explore the way chaincode writes on and reads state from the ledger. It is done through PutState and GetState API. GetState comes with some variations to make the reading with flexibility.

APIs used in Chaincode to update / obtain state info from world state database

It is always good to learn from example. Here we choose the fabcar provided in fabric samples. For sake of simplicity we install and instantiate fabcar chaincode on the Basic Network, which is composed of a CouchDB keeping the world state. We can make observation directly on the CouchDB to see how state is stored in world state database.

We divide this article into two parts. In this first part we cover PutState, GetState and GetStateByRange. They are all found in fabcar chaincode. We will introduce the pagination and modify the chaincode to use GetStateByRangeWithPagination. The next part we will see GetStateByCompositeKey, another useful way for record indexing.

Setup

Again I am using a fabric node, in which we have all the Hyperledger Fabric images and fabric-samples installed.

Here is the quick step.

Bring up the Basic Network

cd fabric-samples/basic-network

./start.sh

Install and instantiate fabcar chaincode

docker-compose up -d cli docker exec cli peer chaincode install -n mycc -p github.com/fabcar/go -v 0 docker exec cli peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n mycc github.com/fabcar/go -v 0 -c '{"Args": []}' -P "OR('Org1MSP.member')"

Invoke initLedger() to load 10 pre-defined car records

docker exec cli peer chaincode invoke -C mychannel -n mycc -c '{"Args":["initLedger"]}'

Fabcar Chaincode Review

You can find more information about fabcar in my recent article. Basically fabcar maintains a table-like database, with a carid as index (CARn) and a record about the model, manufacturer (make), colour and owner. It comes with five chaincode functions to be invoked or queried.

initLedger(): load 10 pre-defined car records in the ledger, invoked once after chaincode is instantiated

queryAllCars(): return records of all cars

queryCar(carid): return the car record per carid

createCar(carid, make, model, colour, owner): add a new card record

changeCarOwner(carid, newOwner): change the owner of a car (carid) to the newOwner

Among these we see several APIs have been used for interacting with the ledger. They are PutState, GetState and GetStateByRange. Let’s learn how they work in the example.

PutState

PutState API is straightforward. What is required is just a key (a string) and a value (a byte array).

This is how the function is structured (link)

func (stub *ChaincodeStub) PutState(key string, value []byte) error

Let’s first examine initLedger(). initLedger() loads 10 pre-defined records on the ledger. cars is the array holding all records. The key is generated in the iteration (see below the code), and the value is generated from the array content.

We see the PutState

key is the CARi, where i is a number from 1 and increased by 1 after each round. Therefore the key will be CAR0, CAR1, CAR2, …

value is the byte array processed from JSON

After initLedger() is invoked, we will see these records stored in CouchDB.

Directly observe the 10 car records after initLedger().

And each document is a JSON (showing CAR0) here.

CAR0 record.

There is no variation when using PutState. You can see PutState also in createCar() and changeCarOwner(). We skip elaboration on them.

GetState

GetState API returns the value (as byte array) by a given key (as string) from the state.

This is how the function is structured (link)

func (stub *ChaincodeStub) GetState(key string) ([]byte, error)

We first take a look on the queryCar() function and see how GetState is used.

The carAsBytes is the byte array from the key (carid as args[0]). It is also very straightforward. Here is what we see in a query with this queryCar().

The combination of GetState and PutState is well demonstrated in function changeCarOwner(). In this function we first use GetState to obtain the record of the given carid, and update the owner with the given new owner. The result is written to ledger using PutState as mentioned above.

This is how it works.

While GetState can address the basic need of reading state from the ledger, it provides limited capability. There are some variations in GetState, and in fabcar example, we see GetStateByRange is used in function getAllCars(). We first examine the GetStateByRange, and later add a new function with pagination.

GetStateByRange

As the name suggests, GetStateByRange API return records of a given range. As it is the range on key. It only makes sense if the key is arranged in a manner of range. In fabcar example, the key is in the format of CARn, which is good using this API.

This is how the function is structured (link)

func (stub *ChaincodeStub) GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)

GetStateByRange requires two strings: startKey and endKey. And the return is an iterator which we need further process to get back the presentable result.

Here is the code in queryAllCars()

In a simple GetState we will get the value directly for a given key. In GetStateByRange an iterator is returned. The following code we see how to use an iteration to break down the iterator, and build a presentable result based on the values read within the range of key.

And here is the result.

As we see in the examples, GetStateByRange comes back with all defined within the range. In case we need just a smaller range, it is handled by pagination.

GetStateByRangeWithPagination

Pagination provides a sliding window of records, by specifying the starting record up to an amount of records. This makes sense as an additional on top of GetStateByRange. Imagine a case of thousands of record. You can select the range by specifying the beginning of record and the number of records you are interested.

Here we have GetStateByRangeWithPagination. This is how the function is structured (link)

func (stub *ChaincodeStub) GetStateByRangeWithPagination(startKey, endKey string, pageSize int32, bookmark string) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error)

We see some arguments are needed: the same startKey and endKey as what we see in GetStateByRange. Besides, we have an integer pageSize and a string bookmark. The pageSize serves as how many items to be returned, while the bookmark is the key to begin with.

Now we are ready to add a new function, called queryAllCarsWithPagination(), which requires two arguments in it: pageSize and bookmark.

We first create a duplication of fabcar chaincode into our working chaincode folder. We keep it in chaincode/testrangepage/

cd fabric-samples/chaincode

cp -r fabcar/go/ testrangepage/

cd testrangepage

mv fabcar.go testrangepage.go

Let’s work on the chaincode file testrangepage.go . There are two portions update is needed.

First, update the Invoke() as we need to add the queryAllCarsWithPagination function. Add it like this (line 16–18).

Line 16–18 added for the new function.

The second part is the new function. It directly adapts the queryAllCars(), with proper modification. The modification comes from another chaincode called marbles02 .

Here are some explanation on the modification.

We need two arguments when calling invoke queryAllCarsWithPagination(). The page size and the bookmark (the beginning carid). The check is just ensure page size is an integer. (line 3–5, 10–14)

We parse these information with the startKey and endKey as required in GetStateByRangeWithPagination . (line 16)

. (line 16) The result is an iterator, a metadata of the response and an error. The metadata contains the record count (it is not the pagesize when the starting position is close to the end), bookmark, etc. We simply ignore this for demonstration. (line 16)

The result iterator goes the same process as queryAllCars() for proper formatting.

Let’s see how it looks like. For sake of cleanness, we tear down everything from our fabcar demo before, and bring up a new Basic Network with the new chaincode.

cd fabric-samples/basic-network

./teardown.sh

./start.sh docker-compose up -d cli

docker exec cli peer chaincode install -n mycc -p github.com/testrangepage -v 0 docker exec cli peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n mycc github.com/testrangepage -v 0 -c '{"Args": []}' -P "OR('Org1MSP.member')" docker exec cli peer chaincode invoke -C mychannel -n mycc -c '{"Args":["initLedger"]}'

We first check the previous queryAllCars() and see the 10 records are already there.

Now assume we wish to see 5 records beginning with CAR3. We use queryAllCarsWithPagination(). The result should be CAR3 to CAR7.

And how about 5 records beginning with CAR8? We only see CAR8 and CAR9.

This is how pagination works with range of key.

In the next part we will address the composite key, and see how we use it to enhance the capability of reading state from ledger.