# Put Your Games in Order
Make sure you have everything you need before proceeding:
- You understand the concepts of ABCI, Protobuf, and of a doubly-linked list (opens new window).
- Go is installed.
- You have the checkers blockchain codebase with
Winner
,GameDeadline
, andMoveCount
. If not, follow the previous steps or check out the relevant version (opens new window).
In this section, you will deal with:
- A FIFO data structure
- FIFO unit tests
You will learn:
- Modularity and data organization styles
In previous steps, you added the game winner, the game deadline, and the move count. These are fields that can help you determine whether a given game is stale or not.
Specifically:
- If the game has a winner, then it is not considered stale.
- If the game's deadline is in the future, then it is not stale.
- If the game's count is 0 or 1 then it should be deleted when stale.
- If the game's count is 2 or more, then it should be forfeited when stale.
# Some initial thoughts
The key difficulty is to get the list of games that satisfy the staleness criteria without grinding your blockchain to a halt with an O(n) call, where n is the total number of games in storage.
A solution is to introduce a new data structure that will let you find one stale game in O(1), and access all k stale games in O(k).
There are some initial thoughts and code needs to keep in mind during the next sections to be able to implement forfeits in the end.
Before you begin touching your code, ask:
- What conditions have to be satisfied for a game to be considered stale and the blockchain to act? There are some pointers in the first paragraph.
- How would you get rid of stale games as part of the protocol, that is without user inputs?
- How do you optimize performance and data structures so that a few stale games do not cause your blockchain to grind to a halt?
- How can you be sure that your blockchain is safe from attacks?
- How do you make your changes compatible with future plans for wagers?
- Are there errors to report back?
- What event should you emit?
# Code needs
Now, think about what possible code changes and additions you should consider:
- What Ignite CLI commands, if any, will assist you?
- How do you adjust what Ignite CLI created for you?
- How would you unit-test these new elements?
- How would you use Ignite CLI to locally run a one-node blockchain and interact with it via the CLI to see what you get?
For now, do not bother yet with future ideas like wager handling.
# Why would you get rid of stale games?
There is only one way for a game to advance through its lifecycle until resolution, win or draw: to play it.
Game inactivity could become a factor. What if a player never shows up again? Should a game remain in limbo forever?
Eventually you want to let players wager on the outcome of games, so you do not want games remaining in limbo if they have value assigned. For this reason, you need a way for games to be forcibly resolved if one player stops responding.
The simplest mechanism to expire a game is to use a deadline. If the deadline is reached, then the game is forcibly terminated and expires. The deadline is pushed back every time a move is played.
To enforce the termination, it is a good idea to use the EndBlock
part of the ABCI protocol. The call EndBlock
is triggered when all transactions of the block are delivered, and allows you to tidy up before the block is sealed. In your case, all games that have reached their deadline will be terminated.
How do you find all the games that have reached their deadline? You could use a pseudo-code like:
This approach is expensive in terms of computation. The EndBlock
code should not have to pull up all games out of the storage just to find a few that are relevant. Doing a findAll
costs O(n)
(opens new window), where n
is the total number of games.
# How can you get rid of expired games?
You need another data structure. The simplest option is a First-In-First-Out (FIFO) that is constantly updated, so that:
- When games are played, they are taken out of where they are and sent to the tail.
- It follows that games that have not been played for the longest time eventually rise to the head.
Therefore, when terminating expired games in EndBlock
, you deal with the expired games that are at the head of the FIFO. You do not stop until the head includes an ongoing game. The cost is:
O(1)
on each game creation and gameplay.O(k)
wherek
is the number of expired games on each block.k <= n
wheren
is the number of games that exist.
k
is still an unbounded number of operations. However, if you use the same expiration duration on each game, for k
games to expire together in a block they would all have to have had a move in the same previous block (give or take the block before or after). In the worst case, the largest EndBlock
computation will be proportional to the largest regular block in the past. This is a reasonable risk to take.
This only works if the expiration duration is the same for all games, instead of being a parameter left to a potentially malicious game creator.
# New information
How do you implement a FIFO from which you extract elements at random positions? Choose a doubly-linked list:
You must remember the game ID at the head to pick expired games, and at the tail to send back fresh games. The existing
SystemInfo
object is useful, as it is already expandable. Add to its Protobuf declaration:To make extraction possible, each game must know which other game takes place before it in the FIFO, and which after. Store this double-link information in
StoredGame
. Add them to the game's Protobuf declaration:There must be an "ID" that indicates no game. Use
"-1"
, which you save as a constant:Instruct Ignite CLI and Protobuf to regenerate the Protobuf files:
Adjust the default genesis values, so that it has a proper head and tail:
# FIFO management
Now that the new fields are created, you need to update them to keep your FIFO up-to-date. It's better to create a separate file that encapsulates this knowledge. Create x/checkers/keeper/stored_game_in_fifo.go
with the following:
A function to remove from the FIFO:
The game passed as an argument is not saved in storage here, even if it was updated. Only its fields in memory are adjusted. The before and after games are saved in storage. Do a
SetStoredGame
after calling this function to avoid having a mix of saves and memory states. The same applies toSetSystemInfo
.A function to send to the tail:
Again, it is advisable to do
SetStoredGame
andSetSystemInfo
after calling this function.
# FIFO Integration
With these functions ready, it is time to use them in the message handlers.
In the handler when creating a new game, set default values for
BeforeIndex
andAfterIndex
:Send the new game to the tail because it is freshly created:
In the handler, when playing a move send the game back to the tail because it was freshly updated (unless it was won, in which case it has to be removed):
Note that you also need to call
SetSystemInfo
.
You have implemented a FIFO that is updated but never really used. It will be used in the next section.
# Unit tests
At this point, your previous unit tests are failing, so they must be fixed. Add FifoHeadIndex
and FifoTailIndex
in your value requirements on SystemInfo
as you create games (opens new window) and play moves (opens new window). Also add BeforeIndex
and AfterIndex
in your value requirements on StoredGame
as you create games (opens new window) and play moves (opens new window).
Edit types/genesis_test.go
, so that it incorporates the newly added fields FifoHeadIndex
and FifoTailIndex
with the correct initialization values:
Next, you should add more specific FIFO tests. For instance, testing what happens to SystemInfo
and StoredGame
as you create up to three new games, instead of only checking at the end (opens new window):
What happens when you have two games and play once on the older one (opens new window)? Or have two games and play them twice in turn:
Run the tests again with the same command as before:
# Interact via the CLI
Time to explore the commands. You need to start afresh because you made numerous additions to the blockchain state:
Do not forget to export alice
and bob
again, as explained in an earlier section under "Interact via the CLI".
Is the genesis FIFO information correctly saved?
This should print:
If you create a game, is the game as expected?
This should print:
What about the information saved in the game?
Because it is the only game, this should print:
And if you create another game?
This should print:
Did the games also store the correct values?
For the first game:
This should print:
For the second game, run:
This should print:
Your FIFO in effect has the game IDs [1, 2]
.
Add a third game, and confirm that your FIFO is [1, 2, 3]
.
What happens if Alice plays a move in game 2
, the game in the middle?
This should print:
Is game 3
in the middle now?
This should print:
Your FIFO now has the game IDs [1, 3, 2]
. You see that game 2
, which was played on, has been sent to the tail of the FIFO.
Which is what you were expecting.
To summarize, this section has explored:
- The use of a First-In-First-Out (FIFO) data structure to sort games from the least recently played at the top of the list to the most recently played at the bottom, in order to help identify inactive games which may become candidates for forced termination, which reduces undesirable and wasteful data stored on the blockchain.
- How forced termination of games is beneficial should you implement a wager system, as it prevents any assigned value from becoming locked into inactive games by causing the inactive player to forfeit the game and lose their wager.
- How any code solution which searches the entire data store for inactive games is computationally expensive, needlessly accessing many active games to identify any inactive minority (which may not even exist).
- How a FIFO data structure definitionally orders games such that inactive games rise to the top of the list, meaning code solutions can simply run until encountering the first active game and then stop, conserving gas fees.
- What new information and functions need to be added to your code; how to integrate them into the message handlers; how to update your unit tests to prevent them from failing due to these changes; and what tests to run to test the code.
- How to interact with the CLI to check the effectiveness of your new commands.