Building a Tendermint & Cosmos SDK App (Part 4): Core Blockchain Continued
This is blog post number four in the series of blog posts showing how to build an application on top of Tendermint using Cosmos-SDK. It will continue with the implementation of the core logic — the tic tac toe module. The previous post can be found here.
The keeper contains the core logic of the module, and is by far its largest component, containing the core business logic.
For the sake of briefness, we will only show here a part of its implementation, and the rest will be available on Github: https://github.com/Sentrylink/tic_tac_toe/tree/basic/x/tic_tac_toe.
This is the logic on how to create the game, kicked off by the StartGame message in user’s transaction and invoked by the handler, after the basic validation and signature verification.
In this step, we are doing the following three things:
- Fetching the id of the next game, from the key-value storage, via call to the method getGameId
- We are creating the structure to represent the new tic-tac-toe game
- We are storing the following structure, inside the method storeGame
We are using the Cosmos-SDK’s interface to access the key-value storage. If we were to build directly on top of Tendermint, we would have to implement our own storage layer, with all of the complexities that arise from it. This is performed as following:
From the context object we are obtaining the reference to the key-value storage, using the key that we passed to the keeper during the initial instantiation. This is how the storage layer is setup in Cosmos-SDK.
After that, we are encoding the game state as JSON, and storing it with the key being the game id. Simple. Cosmos-SDK provides handy default key value storage implementation, which is perfect enough for the large majority of the applications.
The way that the game is retrieved from the storage layer as pretty straightforward as well:
After obtaining the reference to the storage the same way as before, we are trying to retrieve the encoded game state for the given id. If it does not exist, we are returning nil. If it exists, we are decoding it from its JSON representation and returning the reference to a game structure. Simple.
The next crucial part of the application, is the function Play in the Keeper component. It is called by the handler, once the Play message has been sent by the user in a transaction. It looks like this:
The first part ensures that the business rules of the application are respected:
- Game must be started before being played. Every game has its ID. If there is no such game in the state, error is returned
- Game can only be played if there is no winner yet
- Player can only play if it is his turn
- Player can only pick a field that is not already taken
- Player can only play if he is participating in a game, meaning that he is either the sender of the StartGame message or the invitee inside of it
After this, the main logic kicks off. The field is marked as taken by the given player, and we check whether it is enough to decide the winner of the game. If so, the field Winner of the game will be changed from 0 to 1 or 2, depending on which player won.
After that, the new, updated game state is encoded and stored in the key-value storage with the call to storeGame method.
The next important part is the querier. To integrate into the framework of Cosmos-SDK, we need to implement a quierier which conforms to a specific interface, that will be called by the SDK for user’s queries whose path begins with our module. All of the queries have the path in the format of
It looks like this:
In this case we only support one query — answering about the game state, based on the game id provided by the user in the query path. This query returns the game state, encoded in JSON. It is implemented as following:
After obtaining the game id from the query path, we are calling the getGame method from keeper, and returning the appropriate response to the client.
The next important part is registering our custom types with Cosmos-SDK’s codec. By doing that we benefit from the SDK’s machinery to handle many of the underlying intricacies.
It looks like this:
What we did is register the three of our types that we want to encode:
When encoding into either binary or JSON shape, Cosmos-SDK will include the registered name, e.g. tictactoe/StartGame so it knows how to perform the reverse operation.
The final part is to integrate our module into the Cosmos-SDK application. Application is the central part which does the following important parts:
- Connects all of the modules via their Keepers
- Registers queriers and handlers with appropriates routes so that the Cosmos-SDK can perform the routing appropriately
- Calls all of the codec registration functions for each module
- Mounts key-value storage
- Implements various ABCI hooks, if we want to overwrite the default ones
Since our application is not complex, we do not need to overwrite any of the default ABCI method implementations. We only need to integrate our module. This is how it looks like
First we are creating a Keeper of our own module, which will be contained inside the application.
We are creating a subdomain in our key-value storage that will contain all of the application state for our application. We choose to store it under “tictactoe” sub-key.
Then, we are giving the application routing information that will be used when new messages and queries arrive for processing. Based on that information, the Cosmos-SDK application will call appropriate handler and querier, whose implementation we have already seen.
The final part is making sure that the key-value storage key for our module is properly mounted, as an argument to a MountStores method call.
There are few conclusions that we can take after all of this.
First, without Tendermint, if we wanted to implement tic-tac-toe on blockchain, it would either have to be a set of smart contracts on Ethereum or on some other platform, or we would need to implement the whole blockchain from scratch.
In the first case we would need to conform strictly to the interfaces and restrictions on that platform, which can have its downsides.
In the latter case, the resources that we would have to spend on the core blockchain implementation would be far greater than the resources spent on developing the application logic.
Second, with using Cosmos-SDK to build applications on top of Tendermint, we further reduced that effort. By fitting into the provided framework, and using the existing utilities in the SDK, we reduced the amount of work required tremendously. We could simply focus on our business logic, without having to worry about any of the lower-level details.
Third, Tendermint & Cosmos-SDK offer a great amount of flexibility. The interfaces are fairly general and do not offer any restrictions, so any kind of logic can be built. The only limit that the platform imposes is that the validator set cannot be too large, and that all of the validators need to be known at all times, determined by some application logic. The validators cannot come and go as they like, for example like miners do in Bitcoin or Ethereum.
This concludes part 4 of this series. In the next post, we will focus on building the CLI and running the application!