摘要

在 SDK 中,我们经常希望按固定时间间隔运行某些代码。epochs 模块的目的,是让其他模块可以声明自己希望在每个周期收到一次信号。这样,另一个模块就可以指定它希望从 UTC 时间 x 开始,每周执行一次代码。epochs 为其他模块提供了一个通用的 epoch 接口,使它们能够在这类事件发生时轻松收到通知。

目录

  1. 概念
  2. 状态
  3. 事件
  4. Keeper
  5. Hooks
  6. 查询

概念

epochs 模块定义了按固定时间间隔执行的链上计时器。 其他 SDK 模块随后可以注册逻辑,在计时器触发时执行。 我们将两次计时器触发之间的这段时间称为一个“epoch”。 每个计时器都有唯一标识符。 每个 epoch 都会有开始时间和结束时间,其中 结束时间 = 开始时间 + 计时器间隔。 在主网上,我们目前只使用一个标识符,时间间隔为 一天。 计时器会在区块时间首次大于计时器结束时间的那个区块触发, 并将开始时间设置为上一个计时器的结束时间。(特别注意,不会设置为当前区块时间!) 这意味着,如果链停机了一段时间,你会在每个区块获得一次计时器触发, 直到计时器追赶上当前时间为止。

状态

Epochs 模块会为每个标识符保存一个 EpochInfo。 其中包含对应标识符下计时器的当前状态。 它的字段会在每次计时器触发时被修改。 EpochInfo 会在创世初始化或升级逻辑中完成初始化, 并且只会在 begin blocker 中被修改。

事件

epochs 模块会发出以下事件:

BeginBlocker

类型属性键属性值
epoch_startepoch_number{epoch\_number}
epoch_startstart_time{start\_time}

EndBlocker

类型属性键属性值
epoch_endepoch_number{epoch\_number}

Keepers

Keeper 函数

Epochs keeper 模块提供了一些用于管理 epoch 的工具函数。

Hooks

// the first block whose timestamp is after the duration is counted as the end of the epoch
  AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64)
  // new epoch is next block of epoch end block
  BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64)

模块如何接收 hooks

在其他模块的 hook 接收函数中,它们需要对 epochIdentifier 进行过滤,只对特定的 epochIdentifier 执行逻辑。对 epochIdentifier 的过滤可以放在其他 模块的 Params 中,以便通过治理进行修改。 这是标准的开发体验示例:
func (k MyModuleKeeper)

AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) {
    params := k.GetParams(ctx)
    if epochIdentifier == params.DistrEpochIdentifier {
    // my logic
}
}

Panic 隔离

如果某个 epoch hook 发生 panic,它的状态更新会被回滚,但我们仍然会继续执行剩余的 hooks。这使得可以使用更复杂的 epoch 逻辑,而不必担心状态机停止,或者导致后续模块停止执行。 这也意味着,如果你期望某个先前的 epoch hook 产生某种行为,而该 epoch hook 已经回滚,那么你的 hook 也可能出现问题。因此,在为新的 epoch hook 设计安全检查时,请务必考虑“如果前一个 hook 没有执行会怎样”。

查询

Epochs 模块提供以下查询,用于检查模块状态。
service Query {
  // EpochInfos provide running epochInfos
  rpc EpochInfos(QueryEpochsInfoRequest) returns (QueryEpochsInfoResponse) {}
  // CurrentEpoch provide current epoch of specified identifier
  rpc CurrentEpoch(QueryCurrentEpochRequest) returns (QueryCurrentEpochResponse) {}
}

Epoch Infos

查询当前正在运行的 epochInfos
<appd> query epochs epoch-infos
示例示例输出:
epochs:
- current_epoch: "183"
  current_epoch_start_height: "2438409"
  current_epoch_start_time: "2021-12-18T17:16:09.898160996Z"
  duration: 86400s
  epoch_counting_started: true
  identifier: day
  start_time: "2021-06-18T17:00:00Z"
- current_epoch: "26"
  current_epoch_start_height: "2424854"
  current_epoch_start_time: "2021-12-17T17:02:07.229632445Z"
  duration: 604800s
  epoch_counting_started: true
  identifier: week
  start_time: "2021-06-18T17:00:00Z"

当前 Epoch

按指定标识符查询当前 epoch
<appd> query epochs current-epoch [identifier]
示例查询当前的 day epoch:
<appd> query epochs current-epoch day
在此示例中,输出为:
current_epoch: "183"

Abstract

Often in the SDK, we would like to run certain code every-so often. The purpose of epochs module is to allow other modules to set that they would like to be signaled once every period. So another module can specify it wants to execute code once a week, starting at UTC-time = x. epochs creates a generalized epoch interface to other modules so that they can easily be signaled upon such events.

Contents

  1. Concept
  2. State
  3. Events
  4. Keeper
  5. Hooks
  6. Queries

Concepts

The epochs module defines on-chain timers that execute at fixed time intervals. Other SDK modules can then register logic to be executed at the timer ticks. We refer to the period in between two timer ticks as an “epoch”. Every timer has a unique identifier. Every epoch will have a start time, and an end time, where end time = start time + timer interval. On mainnet, we only utilize one identifier, with a time interval of one day. The timer will tick at the first block whose block time is greater than the timer end time, and set the start as the prior timer end time. (Notably, it’s not set to the block time!) This means that if the chain has been down for a while, you will get one timer tick per block, until the timer has caught up.

State

The Epochs module keeps a single EpochInfo per identifier. This contains the current state of the timer with the corresponding identifier. Its fields are modified at every timer tick. EpochInfos are initialized as part of genesis initialization or upgrade logic, and are only modified on begin blockers.

Events

The epochs module emits the following events:

BeginBlocker

TypeAttribute KeyAttribute Value
epoch_startepoch_number{epoch\_number}
epoch_startstart_time{start\_time}

EndBlocker

TypeAttribute KeyAttribute Value
epoch_endepoch_number{epoch\_number}

Keepers

Keeper functions

Epochs keeper module provides utility functions to manage epochs.

Hooks

// the first block whose timestamp is after the duration is counted as the end of the epoch
  AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64)
  // new epoch is next block of epoch end block
  BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64)

How modules receive hooks

On hook receiver function of other modules, they need to filter epochIdentifier and only do executions for only specific epochIdentifier. Filtering epochIdentifier could be in Params of other modules so that they can be modified by governance. This is the standard dev UX of this:
func (k MyModuleKeeper)

AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) {
    params := k.GetParams(ctx)
    if epochIdentifier == params.DistrEpochIdentifier {
    // my logic
}
}

Panic isolation

If a given epoch hook panics, its state update is reverted, but we keep proceeding through the remaining hooks. This allows more advanced epoch logic to be used, without concern over state machine halting, or halting subsequent modules. This does mean that if there is behavior you expect from a prior epoch hook, and that epoch hook reverted, your hook may also have an issue. So do keep in mind “what if a prior hook didn’t get executed” in the safety checks you consider for a new epoch hook.

Queries

The Epochs module provides the following queries to check the module’s state.
service Query {
  // EpochInfos provide running epochInfos
  rpc EpochInfos(QueryEpochsInfoRequest) returns (QueryEpochsInfoResponse) {}
  // CurrentEpoch provide current epoch of specified identifier
  rpc CurrentEpoch(QueryCurrentEpochRequest) returns (QueryCurrentEpochResponse) {}
}

Epoch Infos

Query the currently running epochInfos
<appd> query epochs epoch-infos
ExampleAn example output:
epochs:
- current_epoch: "183"
  current_epoch_start_height: "2438409"
  current_epoch_start_time: "2021-12-18T17:16:09.898160996Z"
  duration: 86400s
  epoch_counting_started: true
  identifier: day
  start_time: "2021-06-18T17:00:00Z"
- current_epoch: "26"
  current_epoch_start_height: "2424854"
  current_epoch_start_time: "2021-12-17T17:02:07.229632445Z"
  duration: 604800s
  epoch_counting_started: true
  identifier: week
  start_time: "2021-06-18T17:00:00Z"

Current Epoch

Query the current epoch by the specified identifier
<appd> query epochs current-epoch [identifier]
ExampleQuery the current day epoch:
<appd> query epochs current-epoch day
Which in this example outputs:
current_epoch: "183"