# Store Object - Make a Checkers Blockchain

Make sure you have all you need before proceeding with the exercise:

synopsis

In this section, you will handle:

  • The Stored Game object
  • Protobuf objects
  • Query.proto
  • Protobuf service interfaces
  • Your first unit test

In the Ignite CLI introduction section you learned how to start a completely new blockchain. Now it is time to dive deeper and explore how you can create a blockchain to play a decentralized game of checkers.

A good start to developing a checkers blockchain is to define the ruleset of the game. There are many versions of the rules. Choose a very simple set of basic rules (opens new window) to avoid getting lost in the rules of checkers or the proper implementation of the board state.

Use a ready-made implementation (opens new window) with the additional rule that the board is 8x8 and played on black cells. This code will not need adjustments. Copy this rules file into a rules folder inside your module. Change its package from checkers to rules. You can do this by command-line:

Copy $ cd checkers $ mkdir x/checkers/rules $ curl https://raw.githubusercontent.com/batkinson/checkers-go/a09daeb1548dd4cc0145d87c8da3ed2ea33a62e3/checkers/checkers.go | sed 's/package checkers/package rules/' > x/checkers/rules/checkers.go

Do not focus on the GUI, this procedure lays the foundation for an interface.

Now it is time to create the first object.

# The stored game object

Begin with the minimum game information needed to be stored:

  • Red player. A string, the serialized address.
  • Black player. A string, the serialized address.
  • Game proper. A string, the game as it is serialized by the rules file.
  • Player to play next. A string.

# How to store

After you know what to store, you have to decide how to store a game. This is important if you want your blockchain application to accommodate multiple simultaneous games. The game is identified by a unique ID.

How should you generate the ID? Players cannot choose it themselves, as this could lead to transactions failing because of an ID clash. You cannot rely on a large random number like a universally unique identifier (UUID), because transactions have to be verifiable in the future. It is better to have a counter incrementing on each new game. This is possible because the code execution happens in a single thread.

The counter must be kept in storage between transactions. Instead of a single counter, you can keep a unique object at a singular location, and easily add relevant elements to the object as needed in the future. Designate idValue to the counter.

You can rely on Ignite CLI's assistance:

  • Call the object that contains the counter NextGame and instruct Ignite CLI with scaffold single:

    Copy $ ignite scaffold single nextGame idValue:uint --module checkers --no-message

    You must add --no-message. If you omit it, Ignite CLI creates an sdk.Msg and an associated service, whose purpose is to overwrite your NextGame object. Your NextGame.IdValue must be controlled/incremented by the application and not by a player sending a value of their own choosing. Ignite CLI still creates convenient getters.

  • You need a map because you're storing games by ID. Instruct Ignite CLI with scaffold map using the StoredGame name:

    Copy $ ignite scaffold map storedGame game turn red black --module checkers --no-message

    Here --no-message prevents game objects from being created or overwritten with a simple sdk.Msg. The application instead creates and updates the objects when receiving properly crafted messages like create game or play a move.

The Ignite CLI scaffold command creates several files, as you can see here (opens new window) and here (opens new window).

The command added new constants:

Copy const ( NextGameKey = "NextGame-value-" ) const ( StoredGameKey = "StoredGame-value-" ) x checkers types keys.go View source

These constants are used as prefixes for the keys that can access the storage location of objects.

# Protobuf objects

Ignite CLI creates the Protobuf objects in the proto directory before compiling them. The NextGame object looks like this:

Copy message NextGame { string creator = 1; uint64 idValue = 2; } proto checkers next_game.proto View source

The StoredGame object looks like this:

Copy message StoredGame { string creator = 1; string index = 2; string game = 3; string turn = 4; string red = 5; string black = 6; } proto checkers stored_game.proto View source

Both objects compile to:

Copy type NextGame struct { Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` IdValue uint64 `protobuf:"varint,2,opt,name=idValue,proto3" json:"idValue,omitempty"` } x checkers types next_game.pb.go View source

They also compile to:

Copy type StoredGame struct { Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` Index string `protobuf:"bytes,2,opt,name=index,proto3" json:"index,omitempty"` Game string `protobuf:"bytes,3,opt,name=game,proto3" json:"game,omitempty"` Turn string `protobuf:"bytes,4,opt,name=turn,proto3" json:"turn,omitempty"` Red string `protobuf:"bytes,5,opt,name=red,proto3" json:"red,omitempty"` Black string `protobuf:"bytes,6,opt,name=black,proto3" json:"black,omitempty"` } x checkers types stored_game.pb.go View source

These are not the only created Protobuf objects. The genesis state is also defined in Protobuf:

Copy import "checkers/stored_game.proto"; import "checkers/next_game.proto"; message GenesisState { repeated StoredGame storedGameList = 2; NextGame nextGame = 1; } proto checkers genesis.proto View source

This is compiled to:

Copy type GenesisState struct { StoredGameList []*StoredGame `protobuf:"bytes,2,rep,name=storedGameList,proto3" json:"storedGameList,omitempty"` NextGame *NextGame `protobuf:"bytes,1,opt,name=nextGame,proto3" json:"nextGame,omitempty"` } x checkers types genesis.pb.go View source

You can find query objects as part of the boilerplate objects created by Ignite CLI. Ignite CLI creates the objects according to a model, but this does not prevent you from making changes later if you decide these queries are not needed:

Copy message QueryGetNextGameRequest {} message QueryGetNextGameResponse { NextGame NextGame = 1; } proto checkers query.proto View source

The query objects for StoredGame are more useful for your checkers game, and look like this:

Copy message QueryGetStoredGameRequest { string index = 1; } message QueryGetStoredGameResponse { StoredGame StoredGame = 1; } message QueryAllStoredGameRequest { cosmos.base.query.v1beta1.PageRequest pagination = 1; } message QueryAllStoredGameResponse { repeated StoredGame StoredGame = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } proto checkers query.proto View source

# How Ignite CLI works

Ignite CLI puts the different Protobuf messages into different files depending on their use:

  • query.proto - for objects related to reading the state. Ignite CLI modifies this file as you add queries. This includes objects to query your stored elements (opens new window).
  • tx.proto - for objects that relate to updating the state. As you have only defined storage elements with --no-message, it is empty for now. The file will be modified as you add transaction-related elements like the message to create a game.
  • genesis.proto - for the genesis. Ignite CLI modifies this file according to how your new storage elements evolve.
  • next_game.proto and stored_game.proto - separate files created once, that remain untouched by Ignite CLI. You are free to modify them but be careful with numbering (opens new window).

Files updated by Ignite CLI include comments like:

Copy // this line is used by starport scaffolding # 2 proto checkers query.proto View source

Ignite CLI adds code right below the comments, which explains why the oldest lines appear lower than recent ones. Make sure to keep these comments where they are so that Ignite CLI knows where to inject code in the future. You could add your code above or below the comments.

Some files created by Ignite CLI can be updated, but you should not modify the Protobuf-compiled files *.pb.go (opens new window) and *.pb.gw.go (opens new window) as they are recreated on every re-run of ignite generate proto-go or equivalent.

# Files to adjust

Ignite CLI creates files that you can and should update. For example, the default genesis values:

Copy const DefaultIndex uint64 = 1 func DefaultGenesis() *GenesisState { return &GenesisState{ StoredGameList: []*StoredGame{}, NextGame: &NextGame{"", uint64(DefaultIndex)}, } } x checkers types genesis.go View source

You can choose to start with no games or insert a number of games to start with. In either case, you must choose the first ID of the first game, which here is set at 1 by reusing the DefaultIndex value.

# Protobuf service interfaces

In addition to created objects, Ignite CLI also creates services that declare and define how to access the newly-created storage objects. Ignite CLI introduces empty service interfaces that can be filled as you add objects and messages when scaffolding a brand new module.

In this case, Ignite CLI added to service Query how to query for your objects:

Copy service Query { rpc StoredGame(QueryGetStoredGameRequest) returns (QueryGetStoredGameResponse) { option (google.api.http).get = "/alice/checkers/checkers/storedGame/{index}"; } rpc StoredGameAll(QueryAllStoredGameRequest) returns (QueryAllStoredGameResponse) { option (google.api.http).get = "/alice/checkers/checkers/storedGame"; } rpc NextGame(QueryGetNextGameRequest) returns (QueryGetNextGameResponse) { option (google.api.http).get = "/alice/checkers/checkers/nextGame"; } } proto checkers query.proto View source

Ignite CLI separates concerns into different files in the compilation of a service. Some should be edited and some should not. The following were prepared by Ignite CLI for your checkers game:

# Helper functions

Your stored game stores are only strings, but they represent sdk.AccAddress or even a game from the rules file. Therefore, you add helper functions to StoredGame to do operations on them.

  1. Get the game Creator:

    Copy func (storedGame *StoredGame) GetCreatorAddress() (creator sdk.AccAddress, err error) { creator, errCreator := sdk.AccAddressFromBech32(storedGame.Creator) return creator, sdkerrors.Wrapf(errCreator, ErrInvalidCreator.Error(), storedGame.Creator) } x checkers types full_game.go View source

    Do the same for the red (opens new window) and black (opens new window) players.

  2. Parse the game so that it can be played. The Turn has to be set by hand:

    Copy func (storedGame *StoredGame) ParseGame() (game *rules.Game, err error) { game, errGame := rules.Parse(storedGame.Game) if errGame != nil { return nil, sdkerrors.Wrapf(errGame, ErrGameNotParseable.Error()) } game.Turn = rules.StringPieces[storedGame.Turn].Player if game.Turn.Color == "" { return nil, sdkerrors.Wrapf(errors.New(fmt.Sprintf("Turn: %s", storedGame.Turn)), ErrGameNotParseable.Error()) } return game, nil } x checkers types full_game.go View source
  3. Introduce your own errors:

    Copy var ( ErrInvalidCreator = sdkerrors.Register(ModuleName, 1100, "creator address is invalid: %s") ErrInvalidRed = sdkerrors.Register(ModuleName, 1101, "red address is invalid: %s") ErrInvalidBlack = sdkerrors.Register(ModuleName, 1102, "black address is invalid: %s") ErrGameNotParseable = sdkerrors.Register(ModuleName, 1103, "game cannot be parsed") ) x checkers types errors.go View source

# Unit tests

Now that you have added some code on top of what Ignite CLI created for you, you should add unit tests. You will not add code to test the code generated by Ignite CLI, as your project is not yet ready to test the framework. However, Ignite CLI added some unit tests of its own. Run those for the keeper:

Copy $ go test github.com/alice/checkers/x/checkers/keeper

# Your first unit test

A good start is to test that the default genesis is created as expected. Beside x/checkers/types/genesis.go, create a new genesis_test.go:

Copy func TestDefaultGenesisIsCorrect(t *testing.T) { require.EqualValues(t, &GenesisState{ StoredGameList: []*StoredGame{}, NextGame: &NextGame{"", uint64(1)}, }, DefaultGenesis()) } x checkers types genesis_test.go View source

To run it, use go test with the package name:

Copy $ go test github.com/alice/checkers/x/checkers/types

This should return something like:

Copy ok github.com/alice/checkers/x/checkers/types 0.814s

Alternatively, call it from the folder itself:

Copy $ cd x/checkers/types/ $ go test

You want your tests to pass when everything is okay, but you also want them to fail when something is wrong. Make sure your new test fails by changing uint64(1) to uint64(2). You should get the following:

Copy --- FAIL: TestDefaultGenesisIsCorrect (0.00s) genesis_test.go:10: Error Trace: genesis_test.go:10 Error: Not equal: expected: &types.GenesisState{StoredGameList:[]*types.StoredGame{}, NextGame:(*types.NextGame)(0xc000506cf0)} actual : &types.GenesisState{StoredGameList:[]*types.StoredGame{}, NextGame:(*types.NextGame)(0xc000506d08)} Diff: --- Expected +++ Actual @@ -5,3 +5,3 @@ Creator: (string) "", - IdValue: (uint64) 2 + IdValue: (uint64) 1 }) Test: TestDefaultGenesisIsCorrect FAIL exit status 1

This appears complex, but the significant aspect is this:

Copy Diff: --- Expected +++ Actual - IdValue: (uint64) 2 + IdValue: (uint64) 1

For expected and actual to make sense, you have to ensure that they are correctly placed in your call. When in doubt, go to the require function definition:

Copy func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {...} require require.go View source

# Debug your unit test

Your first unit test is a standard Go unit test. If you use an IDE like Visual Studio Code it is ready to assist run the test in debug mode. Next to the function name is a small green tick. If you hover below it, a faint red dot appears:

This red dot is a potential breakpoint. Add one on the DefaultGenesis() line. The dot is now bright and stays there:

Right-click on the green tick, and choose Debug Test. If it asks you to install a package, accept. Eventually it stops at the breakpoint and displays the current variables and a panel for stepping actions:

If you are struggling with a test, create separate variables in order to inspect them in debug. From there, follow your regular step-by-step debugging process. If you are not familiar with debugging, this online tutorial (opens new window) will be helpful.

# More unit tests

With a simple yet successful unit test, you can add more consequential ones to test helper methods. Since you are going to repeat some actions, it is worth adding a reusable function:

Copy const ( alice = "cosmos1jmjfq0tplp9tmx4v9uemw72y4d2wa5nr3xn9d3" bob = "cosmos1xyxs3skf3f4jfqeuv89yyaqvjc6lffavxqhc8g" carol = "cosmos1e0w5t53nrq7p66fye6c8p0ynyhf6y24l4yuxd7" ) func GetStoredGame1() *StoredGame { return &StoredGame{ Creator: alice, Black: bob, Red: carol, Index: "1", Game: rules.New().String(), Turn: "b", } } x checkers types full_game_test.go View source

Now you can test the function to get the creator's address. One test for the happy path, and another for the error:

Copy func TestCanGetAddressCreator(t *testing.T) { aliceAddress, err1 := sdk.AccAddressFromBech32(alice) creator, err2 := GetStoredGame1().GetCreatorAddress() require.Equal(t, aliceAddress, creator) require.Nil(t, err1) require.Nil(t, err2) } func TestGetAddressWrongCreator(t *testing.T) { storedGame := GetStoredGame1() storedGame.Creator = "cosmos1jmjfq0tplp9tmx4v9uemw72y4d2wa5nr3xn9d4" creator, err := storedGame.GetCreatorAddress() require.Nil(t, creator) require.EqualError(t, err, "creator address is invalid: cosmos1jmjfq0tplp9tmx4v9uemw72y4d2wa5nr3xn9d4: decoding bech32 failed: checksum failed. Expected 3xn9d3, got 3xn9d4.") require.EqualError(t, storedGame.Validate(), err.Error()) } x checkers types full_game_test.go View source

You can do the same for Black (opens new window) and Red (opens new window).

Test that you can parse a game (opens new window), even if it has been tampered with (opens new window), except if the tamper is wrong (opens new window) or if the turn is wrongly saved (opens new window).

Interested in integration tests? Skip ahead to the section where you learn about them.

# Interact via the CLI

Ignite CLI created a set of files for you. It is time to see whether you can already interact with your new checkers blockchain.

  1. Start the chain in its shell:

    Copy $ ignite chain serve --reset-once

    This ends with:

    Copy ... 🌍 Tendermint node: http://0.0.0.0:26657 🌍 Blockchain API: http://0.0.0.0:1317 🌍 Token faucet: http://0.0.0.0:4500
  2. Check the values saved in NextGame. Look at the relevant client/cli file, which Ignite CLI created to find out what command is relevant. Here it is query_next_game.go (opens new window). You can also ask the CLI:

    Copy $ checkersd query checkers --help

    And that is show-next-game (opens new window):

    Copy $ checkersd query checkers show-next-game

    This returns:

    Copy NextGame: creator: "" idValue: "0"

    This is as expected. No games have been created yet, so the game counter is still at 0.

  3. The --output flag allows you to get your results in a JSON format, which might be useful if you would like to use a script to parse the information. When you use the --help flag, you see which flags are available for a specific command:

    Copy $ checkersd query checkers show-next-game --help

    Among the output, you see:

    Copy ... -o, --output string Output format (text|json) (default "text")

    Now try again a bit differently:

    Copy $ checkersd query checkers show-next-game --output json

    This should print:

    Copy {"NextGame":{"creator":"","idValue":"0"}}
  4. You can similarly confirm there are no stored games (opens new window):

    Copy $ checkersd query checkers list-stored-game

    This should print:

    Copy StoredGame: [] pagination: next_key: null total: "0"

Remember how you wrote --no-message? That was to not create messages or transactions, which would directly update your checkers storage. Soft-confirm there are no commands available:

Copy $ checkersd tx checkers --help

# Next up

Want to continue developing your checkers blockchain? In the next section, you will learn all about introducing an sdk.Msg to create a game.