Key collisions — recap

Imagine we have an asset in the blockchain with symbol ABC . Besides, there are three users who can transfer asset units among them. The current balance of asset units for each user is held in the ledger state under the keys that contain user name (or ID) and the asset symbol. For example:

Alice/ABC -> 50

Bob/ABC -> 0

Claire/ABC -> 0

Now, let’s assume the scenario when two smart contracts are executed in the same time:

Alice transfers 10 ABC to Bob. Alice tranfers 15 ABC to Claire.

Since both smart contracts try to modify the value under Alice/ABC key, the Hyperledger Fabric blockchain will fail one of them because of key collision. This is the mechanism of maintaining the state consistency with the cost of availability. Some smart contracts cannot be executed in parallel.

In the previous article the performance issues that may occur were solved by batching. We did not call smart contracts in parallel, but instead call a smart contract that accepted a list of operations to perform. This is a convenient approach, because it allows to perform strict business validations — you just can check the current state. However it has three major downsides:

Limited scalability, because you still need to call smart contracts in one thread.

Leaky abstraction, because if you want to send the batches in parallel and avoid errors, you need to know the potential key collisions in the client application level.

Limited ability to call operations by different users.

In this article the another approach will be examined. We will allow the client application to create transfer requests with no strict validation and no risk of key collisions. The validation and state modification process will be executed in the separate smart contract.

Transfer requests instead of transfers

The most important benefit of batching, except throughput improvement, is the ability to perform business validations of the state. It is possible to check if Alice has enough asset units to perform the transfer.

It is not possible to check it, if you perform two transfers from Alice in the same time in separate smart contract invocations. But you can actually start two transfers in parallel and delay the validation.

In the simplest approach you request the transfer and update the balances immediately. In the running total approach you have two separate smart contracts for that operations.

The main difference from the client application perspective is that you don’t know the transfer operation result right after submission. You need to wait a while and make a second call to check if the transfer was completed or rejected. Besides, you should call the update balances smart contract periodically.

Let’s start with the code sample for starting a transfer.

In the listing you can find some concepts that are familiar from the previous post. We have an architecture with TransferService and TransferRepository (the latter one is omitted for the brevity). We have the same way of handling idempotency (the OperationService which saves the operation result). I highly recommend to read them.

The new thing, which is important for our new approach is TransferStatus . When we start the transfer, it has the STARTED status. After the running total smart contract it may become COMPLETED or REJECTED . You probably won’t avoid the statuses or some similar approach to represent the business object lifecycle.

The signatures for both smart contracts may look as follows:

The first one is responsible for starting a transfer and basically calls TransferService.start() method presented before. The second one should search for a transfers with STARTED status, perform business validation (check current balances), make the transfer COMPLETED or REJECTED and update balances if needed. It seems to be simple, but — surprise!— the devil is in the detail.

There are several issues that need to be raised:

How to find and update transfers with the given status?

How to determine the order of operations?

How to avoid key collisions (or phantom reads) with the running total smart contract?

Remember, we have a distributed system with MVCC onboard and a key-value store as a world state. The answers to that questions might not be so obvious.

Finding and updating transfers with the given status

Hyperledger Fabric may work with LevelDB or CouchDB database. If you use the first one, you can query only against the keys. If you use the latter one, you can perform rich queries against attributes as well. But let’s assume we use LevelDB only. In this case you need to make the status the part of the key. For example:

Then you can find all transfers with the STARTED status with method getStateByPartialCompositeKey() . Note the TransferIterator class as well, which will be useful in the subsequent listings.

Remember, you can query against the keys hierarchically, i.e. you need to provide given first parts of the key. In the example above you cannot query by the idempotency key.

And the second thing to remember: the number of results may be limited. For example for the CouchDB configuration there is a totalQueryLimit parameter that determines the maximal number of results for a query (it is set by default to 100 000).

The relevant part of TransferService to verify and complete transfers may look as follows:

Even if your contract will get a limited number of transfers, you can call it again to process the remaining ones. Of course this approach is correct only if the query returns transfers in the proper order.