PaimaStudios / paima-engine

Novel trustless web3/blockchain gaming engine.
MIT License
55 stars 18 forks source link

Scheduling events based on time instead of blocks #413

Open SebastienGllmt opened 1 month ago

SebastienGllmt commented 1 month ago

Currently, when calling createScheduledData, you need to pass a blockHeight: number of when to trigger the event. Of course, if you're using emulated blocks or a chain that has a deterministic block time, this works fine. However, not all networks have this and not all use-cases benefit from emulated blocks.

It is actually possible right now to do this even without emulated blocks. You can do this using the following trick:

  1. Schedule an event for next block
  2. Once the event from (1) triggers, check the timestamp in the header
  3. If the timestamp is less than your target, just schedule an event again

Of course this works, but it's ugly to do this from a user perspective. It feels like something that is an engine-level feature

There are two ways we could handle this at the engine level:

  1. Reschedule every block: just like mentioned above, we could reschedule the event every block on their behalf until the right time has come. All this requires is tracking in the database some new target timestamp
    1. Advantage: requires minimal code change
    2. Disadvantage: constantly recreating events like this messes with statistics and explorers for the game (ex: #411) because it's constantly spamming transactions (although at least they all get deleted)
  2. Create a new event type: instead of using the scheduled data which requires a block, we should have some new event system that instead uses a timestamp (or make the block field optional in the existing system, along with a new timestamp field). Then, when it's time for an STF call, we check if there any events with a timestamp ready to use
    1. Advantage: we don't need to constantly re-create scheduled data. It can just be a single event that gets triggered when necessary
    2. Disadvantage: requires handling of this new block-less schedule data type

Note: it's not really possible to have timers trigger the STF without a block being made, as this functionally ends up being equivalent to the emulated block feature

Additional consideration: timezones

If you want to schedule events at a specific time instead of a block, it's reasonable you may want to schedule for a specific timezone (ex: midnight PST). However, this has two unfortunate problems:

  1. Timezones change: some areas have daylight savings so the time mapping changes during the year. Additionally, if some US states decide to abolish daylight savings, the mapping of Unix timestamp to midnight changes as well. In practice, as long as all nodes apply this change at the same time in the same way, it won't cause any issues.
  2. Tricky support: to solve the problem mentioned in (1), we need a library to maps timezones in a way that is not only smart enough to handle things like daylight savings, but to also keep that library up-to-date to take into account any laws pass that affect timezones of importance

Implementing timezones: Date api

One option for implementing this is using nodejs's built-in support for the IANA timezone database. Although this is great as it means no external dependencies are required, it does come with some issues:

  1. It means that to update your timezone database, you also need to update your nodejs version
  2. The nodejs version of the database is not guaranteed to match the version used by your browser

However, if we did us the nodejs route, the code to start a timer based on midnight Japan starting from the first block would look like the following unfortunately long code block.

const startBlockDate = new Date(blockHeader.timestamp);

const [hours, minutes, seconds] = startBlockDate
  .toLocaleString('en-US', {
    timeZone: 'Asia/Singapore', // insert the desired timezone here
    hour: 'numeric',
    hour12: false,
    minute: 'numeric',
    second: 'numeric',
  })
  .split(':')
  .map(Number);

// since we can't know when a day ends (it ends at a different time during leap seconds for example)
// we instead subtract time back to 00:00
const midnightDate = new Date(
  startBlockDate.getTime() - (hours * 60 * 60 + minutes * 60 + seconds) * 1000
);
midnightDate.setUTCMilliseconds(0); // we want to ignore ms, which are timezone independent anyway
// add a day to get the next day
// DANGER: this can be subtly wrong if "next day" on your machine is different than "next day" in the timezone you're interested in. To fix this, you would have to do another round of `toLocaleString` and adjust any offset
//         but I think at this point this code block has conveyed the built-in Date API is definitely not the way to go due to its complexity
midnightDate.setDate(midnightDate.getDate() + 1);

// this is our final timestamp we want!
midnightDate.getTime()

Implementing timezones: Temporal api

There is an upcoming Temporal API coming to browsers and nodejs. It's currently supported on zero plaforms, but it is at stage 3 and the API is nice

// you need this import until Temporal is standardized
import { Temporal } from 'temporal-polyfill';

const midnightTimestamp = Temporal.Instant
  .fromEpochMilliseconds(blockHeader.timestamp)
  .toZonedDateTimeISO('Asia/Singapore') // insert your timezone here
  .add({ days: 1 })
  .startOfDay()
  .epochMilliseconds

Implementing timezones: dayjs package

This is a popular data management library in JS. It's not built-in, but it gets us a similar API as what Temporal would give us

const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');

dayjs.extend(utc);
dayjs.extend(timezone);

let specificTimestamp = new Date();

const nextDay = dayjs(blockHeader.timestamp)
    .tz('Asia/Singapore') // insert your timezone here
    .add(1, 'day')
    .startOf('day')
    .valueOf();