Cosmos SDK x/epochs 模块完全指南:链上定时器、Epoch 钩子与跨模块周期调度

发布时间:2026/10/12 1:25:20
Cosmos SDK x/epochs 模块完全指南:链上定时器、Epoch 钩子与跨模块周期调度 区块链【免费下载链接】cosmos-sdkFramework for building performant, customizable blockchains with native interoperability项目地址https://gitcode.com/gh_mirrors/co/cosmos-sdk点击查看免费下载导读x/epochs是 Cosmos SDK 中负责提供**链上定时器on-chain timer**的模块它允许任意模块声明希望每经过一个固定时间周期收到一次信号例如从某个 UTC 时间起每周执行一次代码。通过统一的 Epoch 接口Identifier、开始时间、间隔时长、当前期号其他模块只需注册钩子Hooks即可在定时器滴答tick的边界上被可靠地通知。读完本文你将掌握 Epoch 的滴答机制与状态模型、四个核心 Keeper 函数、两种钩子回调AfterEpochEnd/BeforeEpochStart、手动与 depinject 两种接线方式、Panic 隔离语义以及两条查询命令的实际用法。模块定位解决什么问题在 SDK 中经常需要每隔一段时间运行某些代码。x/epochs的设计目标见 x/epochs/README.md是让其他模块声明自己希望在某个周期边界被信号触发。另一个模块可以指定从 UTC 时间 x 开始每周执行一次代码。epochs为其他模块提供了一层通用的 Epoch 接口使它们能轻松地在这些事件上被唤起而无需各自维护一套定时逻辑。核心概念Epoch 如何运转epochs模块定义了以固定时间间隔执行的链上定时器其他 SDK 模块可以注册逻辑在定时器滴答时执行。两个滴答之间的时间段称为一个epoch纪元。其关键语义如下唯一标识每个定时器都有一个唯一的identifier如day、week、hourly。起止时间每个 epoch 都有start_time与end_time其中end time start time timer interval。主网实践主网上通常只使用一个标识符时间间隔为一天。滴答判定定时器会在第一个区块时间大于定时器结束时间的区块处滴答并把新的开始时间设置为上一个定时器的结束时间注意不是当前区块时间。停机追赶这意味着如果链宕机了一段时间恢复后你会看到每个区块触发一次滴答直到定时器追赶完毕。源码印证BeginBlocker 中的滴答逻辑滴答逻辑实现在 x/epochs/keeper/abci.go 的BeginBlocker中逐条遍历所有EpochInfo// 如果区块时间 初始 start_time直接跳过 if blockTime.Before(epochInfo.StartTime) { return false, nil } // 若计数尚未开始则标记为需要启动首个 epoch shouldInitialEpochStart : !epochInfo.EpochCountingStarted epochEndTime : epochInfo.CurrentEpochStartTime.Add(epochInfo.Duration) shouldEpochStart : (blockTime.After(epochEndTime)) || shouldInitialEpochStart也就是说滴答条件为区块时间严格晚于当前 epoch 结束时间blockTime.After(epochEndTime)或者该定时器尚未启动shouldInitialEpochStart。这正是 README 所述第一个区块时间大于 timer end time 才滴答的实现。关于边界条件的精确语义可参考 x/epochs/keeper/abci_test.go 中的TestEpochInfoBeginBlockChanges测试它用定时器间隔 1 纳秒eps验证了滴答边界并覆盖了停机追赶Downtime recovery场景。状态模型EpochInfoepochs模块为每个标识符保存一条EpochInfo它描述了对应定时器的当前状态其字段在每次滴答时被更新。EpochInfo 在创世初始化或升级逻辑中创建并且只会在 BeginBlocker 中被修改。EpochInfo 字段详解依据 proto 定义 proto/cosmos/epochs/v1beta1/genesis.proto各字段含义如下字段类型说明identifierstring该定时器的唯一引用start_timeTimestamp定时器首次滴答的时间若在未来则 epoch 直到该时间才开始durationDuration两次滴答之间的间隔必须非零且应大于链的预期出块时间current_epochint64当前 epoch 编号即定时器已滴答的次数首次滴答current_epoch1定义为第一个区块时间大于start_time的区块current_epoch_start_timeTimestamp当前定时器区间的开始时间区间为(start, start duration]滴答时置为last_epoch_start_time duration同一标识符每块最多滴答一次。注意该值可能与 epoch 实际开始的墙钟时间相差很大停机追赶场景下尤其明显epoch_counting_startedbool该定时器是否已开始计数current_epoch_start_heightint64当前 epoch 起始的区块高度即定时器上次滴答的区块高度proto 注释中还给出了一个停机追赶的推演示例假设current_epoch_start_time 10、duration 5链在t14下线、t30恢复则t30块启动区间(10, 15]、t31启动(15, 20]……直到t36才启动(35, 40]每个块恰好追赶一个区间。存储与创世状态在存储层面Keeper 使用cosmossdk.io/collections维护一张以字符串identifier为键的EpochInfo映射前缀KeyPrefixEpoch定义于 x/epochs/types/keys.go。默认创世状态见 x/epochs/types/genesis.go按字母序包含四个定时器epochs : []EpochInfo{ NewGenesisEpochInfo(day, time.Hour*24), // alphabetical order NewGenesisEpochInfo(hour, time.Hour), NewGenesisEpochInfo(minute, time.Minute), NewGenesisEpochInfo(week, time.Hour*24*7), }校验规则Validate()包括identifier不能为空、duration不能为 0、CurrentEpoch与CurrentEpochStartHeight必须非负创世状态还要求所有 epoch 标识符唯一。创世时InitGenesis会逐个调用AddEpochInfo见 x/epochs/keeper/genesis.go导出则通过AllEpochInfos回读全部状态。事件Eventsepochs模块在 BeginBlocker 中通过EmitTypedEvent发出两类事件消息体定义于 proto/cosmos/epochs/v1beta1/events.protoBeginBlocker 阶段TypeAttribute KeyAttribute Valueepoch_startepoch_number{epoch_number}epoch_startstart_time{start_time}EndBlocker 阶段TypeAttribute KeyAttribute Valueepoch_endepoch_number{epoch_number}从源码看EventEpochEnd在旧 epoch 结束时钩子执行前发出EventEpochStart在新 epoch 启动时发出且其EpochStartTime取CurrentEpochStartTime.Unix()均为 TypedEvent便于索引器与链下服务消费。Keeper 函数epochskeeper 提供以下函数来管理 epoch完整实现见 x/epochs/keeper/epoch.go// GetEpochInfo returns epoch info by identifier. func (k *Keeper) GetEpochInfo(ctx sdk.Context, identifier string) (types.EpochInfo, error) // AddEpochInfo adds a new epoch info. Will return an error if the epoch fails validation, // or re-uses an existing identifier. This method also sets the start time if left unset, // and sets the epoch start height. func (k *Keeper) AddEpochInfo(ctx sdk.Context, epoch types.EpochInfo) error // AllEpochInfos iterate through epochs to return all epochs info. func (k *Keeper) AllEpochInfos(ctx sdk.Context) ([]types.EpochInfo, error) // NumBlocksSinceEpochStart returns the number of blocks since the epoch started. // If the epoch started on block N, then calling this during block N (after BeforeEpochStart) // would return 0. Calling it any point in block N1 (assuming the epoch doesnt increment) // would return 1. func (k *Keeper) NumBlocksSinceEpochStart(ctx sdk.Context, identifier string) (int64, error)实现要点AddEpochInfo会先调用epoch.Validate()并检查标识符是否已存在已存在则返回错误若StartTime为零值则取当前区块时间若CurrentEpochStartHeight 0且StartTime不晚于当前区块时间则将其设为当前区块高度。NumBlocksSinceEpochStart返回ctx.BlockHeight() - epoch.CurrentEpochStartHeight若区块时间早于StartTime会返回尚未开始的错误。它可用于实现距 epoch 开始已过多少块这类治理/激励逻辑。NewKeeper只接受store.KVStoreService与codec.BinaryCodec两个依赖见 x/epochs/keeper/keeper.go并通过collections.NewSchemaBuilder构建 schemaSetHooks只允许调用一次重复调用会 panic。Hooks模块如何接收周期信号x/epochs通过钩子接口向其他模块广播 epoch 边界事件接口定义于 x/epochs/types/hooks.go// the first block whose timestamp is after the duration is counted as the end of the epoch AfterEpochEnd(ctx context.Context, epochIdentifier string, epochNumber int64) error // new epoch is next block of epoch end block BeforeEpochStart(ctx context.Context, epochIdentifier string, epochNumber int64) errorAfterEpochEnd在区块时间超过 duration 后的第一个区块处表示该 epoch 已结束。BeforeEpochStart在 epoch 结束块的下一块调用表示新 epoch 开始。钩子接收方必须过滤 identifier其他模块的钩子接收函数需要过滤epochIdentifier只对特定标识符执行逻辑。过滤所用的标识符可以放在该模块的Params中以便通过治理修改。标准开发范式如下func (k MyModuleKeeper) AfterEpochEnd(ctx context.Context, epochIdentifier string, epochNumber int64) error { params : k.GetParams(ctx) if epochIdentifier params.DistrEpochIdentifier { // my logic } return nil }手动接线Manual Wiring在应用app.go中手动接入epochs模块的完整步骤与 simapp/app.go 的真实写法一致导入相关包import ( // ... github.com/cosmos/cosmos-sdk/x/epochs epochskeeper github.com/cosmos/cosmos-sdk/x/epochs/keeper epochstypes github.com/cosmos/cosmos-sdk/x/epochs/types )在应用结构体中添加 epochs keeperEpochsKeeper *epochskeeper.Keeper添加存储键keys : storetypes.NewKVStoreKeys( // ... epochstypes.StoreKey, )实例化 keeperepochsKeeper : epochskeeper.NewKeeper( runtime.NewKVStoreService(keys[epochstypes.StoreKey]), appCodec, ) app.EpochsKeeper epochsKeeper为 epochs keeper 设置钩子在 simapp 中此处为待插入的空MultiEpochHooksapp.EpochsKeeper.SetHooks( epochstypes.NewMultiEpochHooks( // insert epoch hooks receivers here app.SomeOtherModule ), )将 epochs 模块加入模块管理器app.ModuleManager module.NewManager( // ... epochs.NewAppModule(appCodec, app.EpochsKeeper), )配置SetOrderBeginBlockers与SetOrderInitGenesissimapp 中 epochs 均位于列表内见 simapp/app.go 与 simapp/app.goapp.ModuleManager.SetOrderBeginBlockers( // ... epochstypes.ModuleName, )app.ModuleManager.SetOrderInitGenesis( // ... epochstypes.ModuleName, )DI 接线depinject 自动注入第一步设置 keeper。导入 keeper 并添加到应用结构体与 depinject 系统epochskeeper github.com/cosmos/cosmos-sdk/x/epochs/keeperEpochsKeeper *epochskeeper.Keeperdepinject.Inject( appConfig, appBuilder, app.appCodec, app.legacyAmino, app.txConfig, app.interfaceRegistry, // ... other modules app.EpochsKeeper, // NEW MODULE! )第二步设置模块配置。导入相关包注意_副作用导入会触发 x/epochs/depinject.go 中的appconfig.RegisterModuleimport ( epochsmodulev1 cosmossdk.io/api/cosmos/epochs/module/v1 _ github.com/cosmos/cosmos-sdk/x/epochs // import for side-effects epochstypes github.com/cosmos/cosmos-sdk/x/epochs/types )在 app config 中为 BeginBlockers 与 InitGenesis 添加条目BeginBlockers: []string{ // ... epochstypes.ModuleName, },InitGenesis: []string{ // ... epochstypes.ModuleName, },在 ModuleConfig 中为 epochs 添加配置项模块配置对象cosmos.epochs.module.v1.Module定义于 proto/cosmos/epochs/module/v1/module.proto{ Name: epochstypes.ModuleName, Config: appconfig.WrapAny(epochsmodulev1.Module{}), },第三步通过 EpochHooksWrapper 自动注入钩子。depinject 可以自动把你的钩子添加到 epochsKeeper前提是模块输出一个epochtypes.EpochHooksWrapper类型的依赖type TestInputs struct { depinject.In } type TestOutputs struct { depinject.Out Hooks types.EpochHooksWrapper } func DummyProvider(in TestInputs) TestOutputs { return TestOutputs{ Hooks: types.EpochHooksWrapper{ EpochHooks: testEpochHooks{}, }, } }其背后的实现是InvokeSetHooksx/epochs/depinject.go它收集所有模块提供的EpochHooksWrapper按模块名字典序排序后组装成MultiEpochHooks并调用keeper.SetHooks。完整可运行示例见 x/epochs/depinject_test.go其中TestInvokeSetHooks验证了钩子按字典序moduleA、moduleB挂载TestDepinject则验证了 depinject 注入的 keeper 与模块内部 keeper 指向同一实例。Panic 隔离如果某个 epoch 钩子发生 panic它的状态更新会被回滚但模块会继续执行剩余的钩子。这允许使用更复杂的 epoch 逻辑而不必担心状态机停机或阻塞后续模块。实现上BeginBlocker通过ctx.CacheContext()为每个钩子创建缓存上下文钩子执行成功才writeFn()提交见 x/epochs/keeper/abci.go 与 x/epochs/keeper/abci.go。设计警示这也意味着——如果你依赖前一个 epoch 钩子的行为而该钩子被回滚了你的钩子也可能出问题。所以在设计新 epoch 钩子的安全检查时务必考虑前一个钩子没执行会怎样。测试覆盖见 x/epochs/types/hooks_test.go 的TestHooksPanicRecovery它构造一个报错钩子 正常钩子的组合验证报错钩子失败后正常钩子仍被调用expectedCounterValues中正常钩子计数递增。Queries查询模块状态epochs模块提供以下 gRPC 查询proto 定义见 proto/cosmos/epochs/v1beta1/query.protoREST 路径分别为GET /cosmos/epochs/v1beta1/epochs与GET /cosmos/epochs/v1beta1/current_epoch实现见 x/epochs/keeper/grpc_query.goservice Query { // EpochInfos provide running epochInfos rpc EpochInfos(QueryEpochsInfoRequest) returns (QueryEpochsInfoResponse) {} // CurrentEpoch provide current epoch of specified identifier rpc CurrentEpoch(QueryCurrentEpochRequest) returns (QueryCurrentEpochResponse) {} }Epoch Infos查询所有运行中的 epochappd query epochs epoch-infos示例输出一个真实链上同时维护day与week两个定时器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注意duration: 86400s即一天24 小时604800s即一周7 天。若请求为空req nilgRPC 层会返回InvalidArgument该查询内部直接调用AllEpochInfos。Current Epoch按标识符查询当前 epochappd query epochs current-epoch [identifier]例如查询当前dayepochappd query epochs current-epoch day输出current_epoch: 183若传入的identifier为空会返回InvalidArgument错误若标识符不存在则返回 identifier not available。在真实应用中的位置x/epochs已在simappCosmos SDK 的参考应用中完整集成可作为手动接线的活范例keeper 在 simapp/app.go 实例化并挂载钩子AppModule 在 simapp/app.go 加入模块管理器随后被配置进 BeginBlockers 与 InitGenesis 顺序。此外该模块实现了module.AppModuleSimulation接口模拟环境下会随机生成 110 个 epoch、每个时长 1 小时到 1 周见 x/epochs/simulation/genesis.go方便在仿真测试中验证依赖 epoch 的其他模块逻辑。总结x/epochs以极小的 API 面一条EpochInfo记录、四个 Keeper 函数、两个钩子回调、两条查询提供了通用的周期调度能力确定性滴答只依赖区块时间与current_epoch_start_time duration的数学关系停机追赶时每块一个滴答状态始终收敛可组合性其他模块通过AfterEpochEnd/BeforeEpochStart 标识符过滤接入标识符可放入模块 Params 由治理调整容错性钩子 panic 只回滚自身状态不中断其余钩子也绝不使链停机两种接线方式手动接线适合传统app.go结构与 depinject 自动注入通过EpochHooksWrapper输出钩子按模块名字典序排列。如需深入源码建议按以下路径阅读概念与用法见 x/epochs/README.md滴答实现见 x/epochs/keeper/abci.go钩子机制见 x/epochs/types/hooks.go创世默认值与校验见 x/epochs/types/genesis.go边界行为测试见 x/epochs/keeper/abci_test.go 与 x/epochs/types/hooks_test.go。赞分享区块链【免费下载链接】cosmos-sdkFramework for building performant, customizable blockchains with native interoperability项目地址https://gitcode.com/gh_mirrors/co/cosmos-sdk点击查看免费下载相关推荐Brython 浏览器定时器完全指南browser.timer 模块的延时、周期与动画调度实战Brython 浏览器定时器完全指南browser.timer 模块的延时、周期与动画调度实战 browser.timer 是 Brython 提供的浏览器定编程语言语言运行时编译器前端Node.js 18.13.0 (LTS) 版本发布深度解读File 类、测试运行器 Mock 与构建可扩展性Node.js 18.13.0 LTS 版本发布深度解读File 类、测试运行器 Mock 与构建可扩展性 本篇技术指南围绕 Node.js 18.13.0区块链Roc 多行字符串表达式全流程解析从 \\ 语法到词法、解析、规范化与类型推断的快照验证Roc 多行字符串表达式全流程解析从 \\ 语法到词法、解析、规范化与类型推断的快照验证 Roc 语言提供了一种以 \\ 开头书写多行字符串的语法编译器会把区块链上一篇Umi-OCR插件TesseractOCR排版解析方案技术解析下一篇Pythonz常见问题解决安装失败、版本冲突的终极解决方案 创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询