# Migration - Introduce a Leaderboard After Production
Make sure you have all you need before proceeding:
- You understand the concepts of Protobuf and migrations.
- Go is installed.
- You have the checkers blockchain codebase up to the wager denomination. If not, follow the previous steps or check out the relevant version (opens new window).
In this section, you will:
- Add a leaderboard.
- Upgrade your blockchain in production.
- Deal with data migrations and logic upgrades.
If you have been running v1 of your checkers blockchain for a while, games have been created, played on, won, and lost. In this section you will introduce v2 of your blockchain with leaderboard support. A good leaderboard fulfills these conditions:
- Any player who has ever played should have a tally of games won, lost, and forfeited.
- The leaderboard should list the players with the most wins up to a pre-determined number. For example, the leaderboard could only include the top 100 scores.
- To avoid squatting and increase engagement, when equal in value the most recent score takes precedence over an older one, so the player with the recent score is listed higher on the leaderboard.
When you introduce the leaderboard, you also have to decide what to do with your existing players and their scores from your v1 checkers blockchain.
Start your v2's leaderboard as if all played past games had been counted for the leaderboard. You only need to go through all played games, update the players with their tallies, and add a leaderboard including the information. This is possible because all past games and their outcomes are kept in the chain's state. Migration is a good method to tackle the initial leaderboard.
# Introducing a leaderboard
Several things need to be addressed before you can focus all your attention on the migration:
- Save and mark as v1 the current data types about to be modified with the new version. Data types which will remain unmodified need not be identified as such.
- Prepare your v2 blockchain:
- Define your new data types.
- Add helper functions to encapsulate clearly defined actions, like leaderboard sorting.
- Adjust the existing code to make use of and update the new data types.
- Prepare for your v1-to-v2 migration:
- Add helper functions to take large amounts of data from the latest chain state under the shape of a v1 genesis.
- Add a function to migrate from v1 to v2 genesis.
- Make sure you can handle large amounts of data.
Why do you need to make sure you can handle large amounts of data? The full state at the point of migration will be passed in the form of a gigantic v1 genesis when your migration function is called. You do not want your process to grind to a halt because of a lack of memory.
# Save your v1
Your migration steps will be handled in a new folder, x/checkers/migrations/v1tov2
, which needs to be created:
The only data structure you will eventually change is the genesis structure. The other data structures are new, so you can treat them as usual. Copy and paste your v1 genesis from the current commit and save it under another name in v1tov2/types.go
:
Your current genesis definition becomes your v2 genesis. This should be the only data structure requiring a change. However, if for example you also changed the structure of StoredGame
, then you would have to save its v1 version in the same types.go
file.
# New v2 information
It is time to take a closer look at the new data structures being introduced with the version upgrade.
If you feel unsure about creating new data structures with Ignite CLI, look at the previous sections of the exercise again.
To give the new v2 information a data structure, you need the following:
Add a set of stats per player: it makes sense to save one
struct
for each player and map it by address. Remember that a game is stored atStoredGame-value-123
, whereStoredGame-value-
is a constant prefix. In a similar fashion, Ignite CLI creates a new constant to use as the prefix for players:The new
PlayerInfo-value-
prefix for players helps differentiate between the value for players and the value for games prefixed withStoredGame-value-
. Now you can safely have bothStoredGame-value-123
andPlayerInfo-value-123
side by side in storage.This creates a Protobuf file:
Remove
creator
from the Protobuf file because it serves no purpose. Remember to add the new object to the genesis, effectively your v2 genesis:Add a leaderboard rung structure to be repeated inside the leaderboard: this stores the information of a player scoring high enough to be included in the leaderboard. It is not meant to be kept directly in storage as it is only a part of the leaderboard. Instead of involving Ignite CLI, create the structure by hand in its own file:
playerAddress
indicates the player, and gives information regardingPlayerInfo.index
.wonCount
determines the ranking on the leaderboard - the higher the count, the closer to the0
index in the array. This should exactly match the value found in the corresponding player stats. This duplication of data is a lesser evil, because ifwonCount
was missing you would have to access the player stats to sort the leaderboard.dateAdded
indicates when the player'swonCount
was last updated and determines the ranking when there is a tie inwonCount
- the more recent, the closer to the0
slot in the array.
Add a structure for the leaderboard: there is a single stored leaderboard for the whole application. Let Ignite CLI help you implement a structure:
This creates a Protobuf file that you update with your preferred type and its
import
. Again, remove thecreator
:Now update the v2 genesis file by adding the leaderboard:
Remember to make sure the initial value stored for the leaderboard is not
nil
but instead is empty. Ingenesis.go
adjust:This function returns a default genesis. This step is important if you start fresh. In your case, you do not begin with an "empty" genesis but with one resulting from the upcoming genesis migration in this exercise.
With the structure set up it is time to add the code using these new elements in normal operations.
# v2 player information helpers
When a game reaches its resolution, one of the count
s needs to add +1
.
To start, add a private helper function that gets the stats from the storage, updates the numbers as instructed, and saves it back:
You can easily call this from these public one-liner functions added to the keeper:
Which player should get +1
, and on what count? You need to identify the loser and the winner of a game to determine this. Create another private helper:
You can call this from these public helper functions added to the keeper:
Be aware of the two new error types ErrThereIsNoWinner
(opens new window) and ErrWinnerNotParseable
(opens new window).
# v2 player information handling
Now call your helper functions:
On a win:
On a forfeit:
# v2 leaderboard helpers
Continue completing your v2 before tackling the migration. Your leaderboard helpers should:
- Add a new candidate to your array.
- Sort the array according to the rules.
- Clip the array to a length of 100 and save the result.
Sorting entails comparing dates in cases of a score tie. This is potentially expensive if you are deserializing the date in the comparator itself. Instead, the comparator should be presented with data already deserialized. Prepare a data structure that has already deserialized the dateAdded
, which allows you to:
- Deserialize all the elements of the whole leaderboard's array.
- Sort its elements.
- Only then reserialize its elements.
You do not need to use this deserialized element type anywhere else, therefore you should keep it private. Create a new file full_leaderboard.go
to encapsulate all your leaderboard helpers:
You can reuse the date format used for the deadline:
Add similar functions to it, as you did when adding a deadline:
Create the methods:
The functions are called repeatedly when serializing or deserializing arrays:
As you have a function to get an array of deserialized winning players, you can now add a function to sort the slice in place:
Test in descending order first for scores and then for the added dates. Note that there is no deserialization in this func(i, j int) bool
callback. It is possible to write a one-liner inside this function but at the expense of readability.
When migrating the genesis more than one candidate will be added. Therefore, first add a helper on the deserialized elements:
Note the clipping at the leaderboard's length. Similarly, you need helpers on the leaderboard.
You can get these other helpers with a deserialization:
This assumes that a candidate is added at the current block time or migration time. A candidate is not an existing winning player but a new one.
# v2 leaderboard handling
You have created the leaderboard helper functions. In a separate file, add one last function to the keeper to implement the leaderboard. This function makes it possible to add a candidate winner and save the updated leaderboard:
Be aware of the new error ErrCannotAddToLeaderboard
(opens new window).
This completes most of the leaderboard preparation. The only task left is to call your new functions at the right junctures:
On a win:
On a forfeit:
Your leaderboard will now be updated and saved on an on-going basis as part of your v2 blockchain.
# v1 to v2 player information migration helper
With your v2 blockchain now fully operational, it is time to work on the issue of data migration.
First tackle the migration of player information. You will receive a giant v1 genesis when migrating which contains all the games played so far. You must go through them all and build the PlayerInfoList
part of the v2 genesis.
You need to apply +1
to the relevant player stats as you go through games. For performance reasons pick a map[string]*types.PlayerInfo
data type so that you can call up a player's stats by its ID in O(1)
.
Create a function that gets or creates a new PlayerInfo
in a new file:
Then create a function that does the incrementing in the map
in place:
This uses a map
of PlayerIno
only for performance reasons. Because the v2 genesis takes a list and not a map, you need to do a conversion. Add a helper:
# v1 to v2 leaderboard migration helper
You could decide to build the leaderboard as the player stats list is being built, mimicking the regular operation of your v2 checkers blockchain. Unfortunately, that would entail a lot of array sorting for what are just intermediate player stats. Instead, build the v2 genesis leaderboard only after all player stats have been gathered.
In practice you add k new winningPlayerParsed
to the array, sort it, clip it to 100, and repeat. What constitutes a good k value should be dictated by testing and performance measurements. For now use 200. Prepare a new file to encapsulate these v1-to-v2-only operations:
If your leaderboard length is 100, you would add 200 candidates for a total of 300. To accommodate such intermediate additions and sorting you can also encapsulate this in:
Now write the function that adds k candidates, sorts that intermediate result, and clips it, before adding further candidates:
# A proper v1 to v2 migration
Now your full chain state migration comes down to a genesis conversion from GenesisStateV1
to your v2 GenesisState
. You can write it in its own new file:
# Next up
Your checkers blockchain is done! It has a leaderboard, which was introduced later in production thanks to migrations.
You no doubt have many ideas about how to improve it. In particular, you could implement the missing draw mechanism, which in effect has to be accepted by both players.
It is time to move away from the checkers blockchain learning exercise, and explore another helpful tool for working with the Cosmos SDK: CosmWasm.