Building on Cosmos (Part 3/3): Transactions and Storing State

Stoyan Dimitrov
Eco Engineering
Published in
11 min readJan 7, 2022
Photo by NASA on Unsplash

In part two of this tutorial we added the basics of making queries to our chain. In this last section we will add on-chain storage and game logic to our chain, and then play a full game. If you haven’t gone through the prior sections of this tutorial, you can start here from part one.

Note: There is quite a bit of code here, so if you would rather check it out from its branch rather than copy it by hand, feel free to do so. You can find the code at our branch, 4_game.

Now that we have our messages defined, and have included a codec to decode them, we can move onto storing them on chain. . The component that acts as the data store for our application is called the “keeper”. In Cosmos all data is persisted in the app through a key-value store, KVStore. These KVStores abstract away the raw storage of data and messaging between the tendermint node and our app logic, instead giving us a convenient map-like interface to interact with. Each KVStore is referenced by a unique key. Whenever we need to interact with a store, we just need the app context and the store key in order to get the KVStore.

First delete the ‘Keeper’ struct in the querier.go file, then go ahead and create a file named “keeper.go” in the button directory. Let’s start with defining our keeper struct inside the “keeper.go” file.

Imports, keeper struct and constants

Our keeper has three fields:

  1. We have a reference to the bank keeper, which we will use to record the balances of the players of our game, as well as the reward pool.
  2. We have our codec that serves to decode and encode messages, as we saw earlier.
  3. Finally, we have a unique key for our KVStore for the game named “gameStoreKey”. A KVStore has both keys and values represented as byte arrays, and allows us to use just about anything as a key, and store any type as long as we can encode and decode it properly as needed.

Let’s start by defining some of the keys that we need for our game.

variable keys

We will define 3 keys for accessing our data items. The “lastPlayer” will store the byte array address of the last person to press the button, the “lastBlock” will store the block number of when the last button press happened as an int64, and the “prizeBalance” will store the game prize pool as an encoded struct. Note how convenient it is that we can store any value into the same store just by providing a method for converting it to a byte array.

Now that we have our game and our keeper fields defined, we can add a builder for our keeper and the method for getting the KVStore. The KVStore is never handled directly in our keeper, instead we keep a pointer to its key and then retrieve the store from the context of the handler that receives the message from the node.

Now that we have a way of getting our persistence KVStore, we can implement our first method of the game logic: “buttonPressed”. This method will update who is the last player to press the button, and also update the last block that the button was pressed. We record the block of the button press so that we know when 3 blocks have passed for the claim treasure event. We get the block number from the context, and will show exactly how this is done later in the tutorial. In order to set the fields in the store, we just need to use our codec to marshal them into a byte array.

Next, since we already have the setter for the last player and last block, we will write the getters for them. This is as easy as getting the KVStore and then using our codec to unmarshal them into our fields. One thing to note for a KVStore is that if the value has not been set for a particular key, then it will return an empty array of length zero. In that special case, we just return the default for that method.

Now that we have the player and block getters, let’s add the methods for getting and setting the prize pool as users play the game. Copy the two methods below:

  1. The “getPrizeBalance” and “setPrizeBalance” are basic getters and setters.
  2. The “increasePrizeBalance” increments the prize pool as the game is played, and has a single check to initialize the prize pool on the first button press when the prize balance is not defined.

We are almost finished with the keeper logic for our game. All we have left to do is to check that enough blocks have passed for a player to claim the treasure, and a method for resetting the game after someone has won the round.

All that is left now is for the game to send the prize pool to the winner. We get the prize balance from storage and then use the bank keeper to transfer it to the winner. Finally, we reset the game state.

That completes all the code we need for persisting our game state on our app blockchain! One file and a single KVStore. We created 3 keys for saving game state, and then we created setters and getters for their values. We also included the keeper of the bank module so that we could move coins around between the players and the prize pool as they played. Finally, we made some use of the context in order to figure out the current block and how long the last player action was. Next, we will code the handler that is responsible for taking a message and processing it.

The handler is responsible for processing the transaction messages that are routed to it. It’s the companion to our querier, but for transaction messages instead of queries. What differentiates the transaction messages is that they can change the state of our chain, instead of just making queries to it. In our case, we will route both of our messages to the same handler, although you can use as many handlers per module as makes sense. We will show later how to declare what handler to use for a particular route.

To start, create a file called “handler.go” in the button directory.

The constructor needs to return a function of type “Handler”. Below is what that type looks like as defined by the cosmos-sdk(you don’t need to copy this, I’ve just included it here for you to see).

First we will create a constructor for our handler, and add the imports to our handler.go file.

The sdk takes this type for registering routes with our handlers. Our constructor will return a function of type “Handler”, which will take the context and the message and return a result.

Our handler function takes the message and gets its type. We switch on the message type and pass it to a method along with the calling context and the keeper. When creating the handler we give it a reference to the keeper so that it can read/write to the app state. Let’s write “handleMsgPressButton” which will record who just pressed the button and take the fee from the player and place it in the prize pool.

We first parse the cost into a Coins type and check if the player is paying the game fee. We then use the bank keeper to subtract the fee from the player’s account and then we add the fee to the prize pool by calling our keeper. If the player sent more coins than the fee, they get the difference back because we only deduct the fee from their balance. Finally we update the last player and set the current block number as the last played block. We then return an empty “sdk.Result” which indicates that the message was processed successfully and the updated keeper state should be committed in a block. Notice that we could have told the bank keeper to change the amount of coins in the player’s balance to whatever we wanted since the bank keeper is just another manager of key-value stores. It would be easy to change this module to reward our friends with free money every time they hit the button, but we aren’t trying to implement the Federal Reserve in this tutorial so let’s move on to “handleMsgClaimTreasure”.

Here we check to see if enough blocks have passed since the last button press, and if the caller was the last to press the button. If everything checks out we transfer the prize pool to the caller and reset the game.

All we have left now is to fix the querier to look at our chain instead of just returning an error, add our transaction messages to our cli, and then plug our new keeper and handler into our app.

Let’s start with the querier. We just need to get the state from our keeper, and remove the “not implemented” error messages.

x/button/querier.go

To add the transaction messages to our CLI is a bit more involved. Unlike the query commands, we need to sign the transactions in order to post them to the chain.

Fig. 2 Path of a transaction sent to our app

From Fig. 2, we can see the path that a transaction goes through when we send it to our chain. Unlike the query, each transaction needs to be signed by an account. It first gets decoded from its byte form, then some basic checks on the message are run to validate that it is properly set. Next, the auth module takes the signed message and validates that the signer is the sender account, before sending it off to the router or rejecting the message. The router then sends the message to the correct handler where it is branched off by message type to its own logic. Finally, the handler may interact with a keeper and change the state of the chain based on the message before returning a response to the caller. The response would usually be asynchronous, but for simplicity we make the transaction blocking so that the response is returned synchronously from our CLI command.

Create a file called ‘tx.go’ in ‘/x/button/cli’ and copy the code below.

/x/button/cli/tx.go

In both transactions, we create a CLI context and a transaction builder. Then we make the message and pass it into our tx builder to get the user's password, sign the transaction and then post it to the node. Using cobra, we add some instructions on what parameters each command takes and a short description of what the command does.

Let’s group the transaction subcommands under our root command in ‘button_cli.go’.

/x/button/cli/button_cli.go

Finally, we need to plug in our keeper and its keystore, as well as our message handler into our app.go.

There we have it. All of our code for playing a game on our own blockchain. We started with defining the messages that our players would send to play the game. We then registered the messages with a type in our codec so that we knew how to encode and decode them from byte arrays. Once we had our codec, we created a querier and added it to our toolchain so that we could check the state of our game. We then implemented our persistent blockchain data storage with the keeper. We built the keeper with our codec, a KVStore specifically for our game state, and the bank module. Finally, we implemented our own handler that could take the decoded messages that our players were sending to our blockchain, and then process them and change the state of the game with the help of our game keeper. All that is left now is to rebuild our project, and play our game on our own app specific chain.

Your code should now look like our branch, 4_game. If you had trouble copying all of the code above, you can just checkout the branch and continue from there.

Next, let’s recompile our project and start the chain up.

Our script creates 2 accounts and gives them each ‘1000gamecoin’. When creating the accounts, the script gives each account a password from the password.txt file.

For both accounts the password is “jakejake”, as set in “scripts/passwords.txt”.

We will use these two accounts to play the game. For convenience, we don’t want to have to type the raw address of the accounts, so let’s save them in variables.

Now let’s check that their balances are correct with the account’s query. We use the ‘ — trust-node’ flag to tell the program to assume that our node is running the correct chain, otherwise we would have to give the tool the id of the chain to use.

If your output looks like that above, it means everything is set up correctly and we can start a game. The Pubkey field might not be empty if the account for Alice has already made an on-chain transaction, but it doesn’t really matter.

Alice has ‘1000gamecoin’, which is what we set her initial balance in the genesis file. The account number and sequence are there to prevent replay attacks. Now that we have checked that our accounts are there, let’s have Bob press the button.

We can see that we pass the play command the value, the chain id, and the sender. The btcli then checks the local list of keys, finds the Bob account, and then asks us for the password so that it can decrypt the private key and use it to sign our transaction before broadcasting the signed transaction to the node we have running. We can check the prize pool and how many blocks have passed since we pressed the button.

Great! Now let's press the button from Alice’s account.

We can go back and forth doing this until our accounts run out of funds. Let’s instead try to claim the treasure from Bob’s account. This should fail since he isn’t the last to have played the button.

Press the button several times now from both accounts.

Check how much money we have in Bob’s account:

Now let’s check how long its been since the button was pressed. If its over 3 blocks, then we know that we can try to claim it from Alice’s account.

Great, enough blocks have passed. Now we can claim the treasure and end the game.

Now all there is to do is check Alice’s account to see if the money is all there:

That’s it! We played through an entire round of our game. Let’s quickly review the steps that got us here so fast.

  1. We checked that our accounts were correctly set from the genesis file and had a balance of gamecoin by querying the chain’s accounts.
  2. We then created and signed transactions for playing the game.
  3. Finally we made sure enough blocks had passed before claiming the treasure.

We have completed this tutorial into building our own application specific blockchain. We hope you learned a lot and enjoyed it. Here at Eco, we prize new technologies in the blockchain space that enable us to build the digital currency network for payments. You can find out more about us at https://eco.com/

We are always looking for people with the right skills and drive to join us[link greenhouse board].

--

--