一条链不必整条搬家:Cosmos SDK 就地存储迁移的版本账 图 1
一条链不必整条搬家:Cosmos SDK 就地存储迁移的版本账 · 图 1

Cosmos 系应用升级时,链不一定整条停机重来。SDK 提供了一条更细粒度的路:每个模块自带一个共识版本号,升级处理器按版本差自动编排迁移脚本,只搬那些真正变了存储结构的模块。这条机制的官方名字是就地存储迁移(In-Place Store Migrations)。本文按 SDK 文档拆解它的三个核心部件:ConsensusVersion、RegisterMigration 和 VersionMap。

模块先报版本号

要让旧存储结构顺利过渡到新代码,模块必须实现 ConsensusVersion 方法,返回一个硬编码的无符号整数,初始版本必须是一。文档的措辞很明确:这个版本号为模块的状态破坏性变更服务,模块改了存储布局就得把它加一。它和软件发布版本是两套数字——软件可以打补丁重发,共识版本号只在存储语义变化时前进,因此它是链上状态”由哪套布局解释”的唯一权威标记。

迁移脚本逐级挂链

模块开发者通过 Configurator 的 RegisterMigration 注册升级逻辑,签名里带着”从第几版迁到下一版”。多个脚本必须按版本递增排列,且链条要连续:要从 1 走到 3,就得同时注册 1 到 2 和 2 到 3 两段脚本,任何一次升级都会沿链条逐级执行,不能跳档。迁移函数需要访问 Keeper 的存储,SDK 的惯常做法是在 Keeper 外再包一层 Migrator。bank 模块就有现成例子:v2 迁移把余额键里的地址改成带长度前缀的写法,对应公钥地址改造的存储搬家。文档还建议迁移脚本单独放在模块的 migrations 目录里、按目标版本命名,让”哪段代码把状态从哪一版搬到哪一版”在仓库结构上一眼可查——链上只有一个版本号,代码侧的这套对应关系是审计迁移是否可信的第一入口。

图:模块版本链条——从旧共识版本逐级迁移到新版本的阶梯示意

升级处理器怎么编排

应用在初始化时通过 SetUpgradeHandler 登记一个以升级计划名标识的处理器,SDK 把模块 VersionMap(每个模块的共识版本号集合)作为参数传进来。什么都不想写的时候,处理器直接调用默认管理器按版本差执行已注册迁移就行,返回的 VersionMap 会被持久化。新增模块是另一个故事:默认迁移器遇到 VersionMap 里版本号为零的模块会跳过,因为它假定创世已经处理过——要引入新模块,就得手动把它的版本号抬到当前 ConsensusVersion,跳过创世,或者在升级处理器里触发一个自定义创世函数。删除模块则相反,处理器里可以不执行任何动作,历史版本的状态仍留在存储里供追溯。

它解决什么、不解决什么

还要看清这些函数被谁在什么时候调用。共识节点到达升级计划的高度后会停下来等待新二进制,重启后登记在该计划名下的处理器被执行一次:传入的 fromVM 是链上记着的各模块当前版本,处理器返回的 VersionMap 会被写回并作为新的基线。也就是说,版本号既是迁移的触发条件,也是迁移完成的凭据——一旦新版本落账,同一段脚本不会再被触发。反过来说,脚本一旦跑完又发现写错了,链上并没有”退回旧版本号”的按钮,通常只能往前补一次新迁移,或者协调重来,这决定了迁移脚本应当先在测试网完整演练到版本落账为止。

这套机制把”链要停下来换软件”简化成”链在约定的高度跑一遍迁移函数”:脚本只在版本号匹配时执行一次,重跑不会重复迁移,这是文档强调用版本号防重复执行的用意。但要注意边界:就地迁移说的是状态布局的搬迁,节点换二进制、二进制的自动切换仍由升级计划与配套进程负责;迁移本身在那个高度集中发生,脚本写得重,出块就会在那个点卡顿。对观察链升级的用户来说,判断一次升级是否动了存储结构,看的就是各模块共识版本号有没有前进,这比看软件发布说明更接近链上事实。