Filters

# Keep an Up-To-Date Game Deadline

Make sure you have everything you need before proceeding:

In this section, you will:

  • Implement a deadline.
  • Work with dates.
  • Extend your unit tests.

In a previous step, you made it possible for players to play, and you recorded the eventual winner. Presumably most players will play their games until they reach a resolution... but not 100% of them. Some players will forget about their games, no longer care, or simply stop playing when it is obvious they are losing.

Therefore, your blockchain is at risk of accumulating stale games in its storage. 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. This is one more reason why you need a way for games to be forcibly resolved if one player stops participating.

To take care of this, you could imagine creating new messages. For instance, a player whose opponent has disappeared could raise a flag in order to seek a resolution. That would most likely require the introduction of a deadline to prevent malicious flag raising, and for the deadline to be pushed back every time a move is played.

Another way would be to have the blockchain system resolve by forfeit the stale games on its own. This is the path that this exercise takes. To achieve that, it needs a deadline. This deadline, and its testing, is the object of this section.

# Some initial thoughts

Before you begin touching your code, ask:

  • What conditions have to be satisfied for a game to be considered stale and for the blockchain to act?
  • How do you sanitize the new information inputs?
  • 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?

These are important questions, but not all are answered in this section. For instance, the question about performance and data structures is solved in the following sections.

# New information

To prepare the field, add in the StoredGame's Protobuf definition:

Copy message StoredGame { ... + string deadline = 7; } proto checkers stored_game.proto View source

To have Ignite CLI and Protobuf recompile this file, use:

On each update the deadline will always be now plus a fixed duration. In this context, now refers to the block's time. If you tried to use the node's time at the time of execution you would break the consensus, as no two nodes would have the same execution time.

Declare this duration as a new constant, plus how the date is to be represented – encoded in the saved game as a string:

Copy const ( MaxTurnDuration = time.Duration(24 * 3_600 * 1000_000_000) // 1 day DeadlineLayout = "2006-01-02 15:04:05.999999999 +0000 UTC" ) x checkers types keys.go View source

# Date manipulation

Helper functions can encode and decode the deadline in the storage.

  1. Define a new error:

    Copy var ( ... + ErrInvalidDeadline = sdkerrors.Register(ModuleName, 1108, "deadline cannot be parsed: %s") ) x checkers types errors.go View source
  2. Add your date helpers. A reasonable location to pick is full_game.go:

    Copy func (storedGame *StoredGame) GetDeadlineAsTime() (deadline time.Time, err error) { deadline, errDeadline := time.Parse(DeadlineLayout, storedGame.Deadline) return deadline, sdkerrors.Wrapf(errDeadline, ErrInvalidDeadline.Error(), storedGame.Deadline) } func FormatDeadline(deadline time.Time) string { return deadline.UTC().Format(DeadlineLayout) } x checkers types full_game.go View source

    Note that sdkerrors.Wrapf(err, ...) conveniently returns nil if err is nil.

  3. At the same time, add this to the Validate function:

    Copy ... _, err = storedGame.ParseGame() + if err != nil { + return err + } + _, err = storedGame.GetDeadlineAsTime() return err x checkers types full_game.go View source
  4. Add a function that encapsulates how the next deadline is calculated in the same file:

    Copy func GetNextDeadline(ctx sdk.Context) time.Time { return ctx.BlockTime().Add(MaxTurnDuration) } x checkers types full_game.go View source

# Updated deadline

Next, you need to update this new field with its appropriate value:

  1. At creation, in the message handler for game creation:

    Copy ... storedGame := types.StoredGame{ ... + Deadline: types.FormatDeadline(types.GetNextDeadline(ctx)), } x checkers keeper msg_server_create_game.go View source
  2. After a move, in the message handler:

    Copy ... + storedGame.Deadline = types.FormatDeadline(types.GetNextDeadline(ctx)) storedGame.Turn = rules.PieceStrings[game.Turn] ... x checkers keeper msg_server_play_move.go View source

Confirm that your project still compiles:

# Unit tests

After these changes, your previous unit tests fail. Fix them by adding Deadline wherever it should be. Do not forget that the time is taken from the block's timestamp. In the case of tests, it is stored in the context's ctx.BlockTime(). In effect, you need to add this single line:

Copy ctx := sdk.UnwrapSDKContext(context) ... require.EqualValues(t, types.StoredGame{ ... + Deadline: types.FormatDeadline(ctx.BlockTime().Add(types.MaxTurnDuration)), }, game1) x checkers keeper msg_server_play_move_test.go View source

Also add a couple of unit tests that confirm the GetDeadlineAsTime function works as intended (opens new window) and that the dates saved on create (opens new window) and on play (opens new window) are parseable.

# Interact via the CLI

There is not much to test here. Remember that you added a new field, but if your blockchain state already contains games then they are missing the new field:

This demonstrates some missing information:

Copy ... + deadline: "" ...

In effect, your blockchain state is broken. Eventually examine the section on migrations to see how to update your blockchain state to avoid such a breaking change. This broken state still lets you test the update of the deadline on play:

This contains:

Copy ... + deadline: 2023-02-05 15:26:26.832533 +0000 UTC ...

In the same vein, you can create a new game and confirm it contains the deadline.

Do you believe that all the elements are in place for you to start the forfeit mechanism? Are you thinking about doing something like this pseudo-code:

Copy allGames = keeper.GetAllStoredGames() expiredGames = allGames.filterWhere(game => now < game.Deadline) ...

If you do so, your blockchain is in extreme danger. The .GetAllStoredGames call is O(n). Its execution time is proportional to the total number of games you have in storage. It is not proportional to the number of games that have expired.

If your project is successful, this may mean pulling a million games just to forfeit a handful... on every block. At this rate, each block would come out after 30 minutes, not 6 seconds.

You need better data structures, as you will see in the next sections.

synopsis

To summarize, this section has explored:

  • How to implement a new deadline field and work with dates to enable the application to check whether games which have not been recently updated have expired or not.
  • How the deadline must use the block's time as its reference point, since a non-deterministic Date.now() would change with each execution.
  • How to test your code to ensure that it functions as desired.
  • How to interact with the CLI to create a new game with the deadline field in place
  • How, if your blockchain contains preexisting games, that the blockchain state is now effectively broken, since the deadline field of those games demonstrates missing information (which can be corrected through migration).
  • How, if you are not careful enough, you can quickly stall a successful blockchain.