# Migration for Leaderboard
Make sure you have all you need before proceeding:
- You understand the concepts of Protobuf, and migrations.
- Have Go installed.
- The checkers blockchain codebase up to the wager denomination. You can get there by following the previous steps or checking out the relevant version (opens new window).
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.
When you introduce the leaderboard, you also have to decide what to do with your loyal 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. A 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. All data types, which will remain unmodified, need not be identified as such.
- Prepare your v2 blockchain by:
- Defining your new data types.
- Adding 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 your v1 to v2 migration by:
- Adding helper functions to take large amounts of data from the latest chain state under the shape of a v1 genesis.
- Adding a function to migrate from v1 to v2 genesis.
- Making 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 don't 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
, that needs to be created:
As for the data structure, the only one you will eventually change is the genesis structure. The other data structures are brand 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 all when it comes to data structures requiring a change. Unless you happened to also change the structure of StoredGame
for instance, 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 are feeling unsure about creating new data structures with Starport, take another look at the previous sections of the exercise.
To give the new v2 information a data structure you need:
A set of stats per player: it makes sense to save one
struct
for each player and map it by address. Recall that a game is stored atStoredGame-value-123
, whereStoredGame-value-
is a constant prefix. In a similar fashion, Starport is going to create 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 one for games prefixed withStoredGame-value-
. This way, you can safely have bothStoredGame-value-123
andPlayerInfo-value-123
side by side in storage.This creates a Protobuf file:
From which you remove
creator
because it serves no purposes. Do not forget to add the new object to the genesis, effectively your v2 genesis:A leaderboard rung structure to be repeated inside the leaderboard: it 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. So instead of involving Starport create the structure by hand into its own file:
Where:
playerAddress
indicates the player, so to say gives information regardingPlayerInfo.index
.wonCount
determines the ranking on the leaderboard - the higher the count, the closer to the0
index in the array. Of course, it 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.
A structure for the leaderboard: there is a single stored leaderboard for the whole application. Let Starport help you implement a structure:
Which creates a Protobuf file that you update with your preferred type and its
import
. Also, remove thecreator
:And update the v2 genesis file by adding the leaderboard:
Don't forget to make sure the initial value stored for the leaderboard is not
nil
but instead an empty one. 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
You need to do a +1
on one of the count
s when a game reaches its resolution.
Start by adding a helper private function that gets the stats from the storage, updates the numbers as instructed, and saves it back:
Which you can easily call from these public one-liner functions added to the keeper:
Which player should get +1
on what count? You need to identify the loser and the winner of a game to determine this. Create this other private helper for this:
Which you can call from these public helper functions added to the keeper:
Note 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:
And 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.
The 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
for you to:
- Deserialize all the elements of the whole leaderboard's array.
- Sort it.
- And 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 brand 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 (de-)serializing 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:
You test in descending order for scores first and then for the added dates. As you can see, there is no deserialization in this func(i, j int) bool
callback. It should be 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. With this in mind, add a first helper on the deserialized elements:
Notice the clipping at the leaderboard's length. Similarly, You need helpers on the leaderboard.
You can get these other helpers with a de-serialization:
Here we assume 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. You want this function to make it possible to add a candidate winner and save the updated leaderboard:
Note the new error ErrCannotAddToLeaderboard
(opens new window).
You are now through most of the leaderboard preparation and the only thing left is to call your new functions at the right junctures:
On a win:
And on a forfeit:
Your leaderboard is now 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 the data migration.
First tackle the migration of the player information. You will be handed a giant v1 genesis when migrating, which contains all the games played so far. You have to go through them all and build the PlayerInfoList
part of the v2 genesis.
You need to do +1
on the relevant player stats as you go through games. For performance it makes sense to pick a map[string]*types.PlayerInfo
data type so that you can call up a player's stats by its ID in O(1)
.
Begin by creating a function that gets or creates a new PlayerInfo
in a new file:
And then a function that does the incrementing in the map
in place:
You are using a map
of PlayerIno
only for performance reasons. In the end, 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 you can use 200. So 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:
Time to 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
. It in its own new file, you can write it:
# Next up
Your checkers blockchain is done. It has a leaderboard, which was introduced later in production thanks to migrations.
Now it is time to explore two other helpful tools for working with the Cosmos SDK: CosmJS and CosmWasm. Begin with CosmJS.