在真实运行中的链上执行迁移之前,请先完整阅读并理解本页全部内容。
概要 原地存储迁移允许模块升级到包含破坏性变更的新版本。本文档同时介绍模块侧(编写迁移)和应用侧(在升级期间运行迁移)的做法。
Cosmos SDK 支持两种链升级方式:一种是将整个应用状态导出为 JSON,并使用修改后的 genesis 文件重新启动;另一种是执行原地存储迁移,直接更新状态。对于状态规模较大的链,原地迁移速度明显更快,也是生产网络的标准做法。 本页介绍如何编写模块迁移,以及如何在应用的升级处理器中运行这些迁移。

共识版本

要成功升级现有模块,每个 AppModule 都需要实现 ConsensusVersion() uint64 函数。
  • 版本号必须由模块开发者硬编码。
  • 初始版本必须设置为 1。
共识版本用于表示应用模块中会破坏状态兼容性的版本;当模块引入破坏性变更时,必须递增该版本号。

注册迁移

要注册模块升级期间执行的功能,你必须注册需要执行哪些迁移。 迁移注册发生在 Configurator 中,通过 RegisterMigration 方法完成。AppModule 对 Configurator 的引用位于 RegisterServices 方法中。 你可以注册一个或多个迁移。如果注册多个迁移脚本,需要按版本递增顺序列出这些迁移,并确保迁移链条足以达到目标共识版本。例如,要将某个模块迁移到版本 3,应分别为版本 1 和版本 2 注册迁移,如下例所示:
func (am AppModule) RegisterServices(cfg module.Configurator) {
    // --snip--
    if err := cfg.RegisterMigration(types.ModuleName, 1, func(ctx sdk.Context) error {
        // Perform in-place store migrations from ConsensusVersion 1 to 2.
        return nil
    }); err != nil {
        panic(fmt.Sprintf("failed to migrate %s from version 1 to 2: %v", types.ModuleName, err))
    }

    if err := cfg.RegisterMigration(types.ModuleName, 2, func(ctx sdk.Context) error {
        // Perform in-place store migrations from ConsensusVersion 2 to 3.
        return nil
    }); err != nil {
        panic(fmt.Sprintf("failed to migrate %s from version 2 to 3: %v", types.ModuleName, err))
    }
}
由于这些迁移本质上是需要访问 Keeper 存储的函数,因此应像下面示例那样,使用一个围绕 keeper 封装的 Migrator 包装器:
package keeper

import (
    sdk "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/x/bank/exported"
    v2 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v2"
    v3 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v3"
    v4 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v4"
)

// Migrator is a struct for handling in-place store migrations.
type Migrator struct {
    keeper         BaseKeeper
    legacySubspace exported.Subspace
}

// NewMigrator returns a new Migrator.
func NewMigrator(keeper BaseKeeper, legacySubspace exported.Subspace) Migrator {
    return Migrator{keeper: keeper, legacySubspace: legacySubspace}
}

// Migrate1to2 migrates from version 1 to 2.
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
    return v2.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
}

// Migrate2to3 migrates x/bank storage from version 2 to 3.
func (m Migrator) Migrate2to3(ctx sdk.Context) error {
    return v3.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
}

// Migrate3to4 migrates x/bank storage from version 3 to 4.
func (m Migrator) Migrate3to4(ctx sdk.Context) error {
    m.MigrateSendEnabledParams(ctx)
    return v4.MigrateStore(ctx, m.keeper.storeService, m.legacySubspace, m.keeper.cdc)
}

编写迁移脚本

要定义升级期间执行的功能,请编写迁移脚本,并将相关函数放在 migrations/ 目录下。例如,要为 bank 模块编写迁移脚本,应将这些函数放在 x/bank/migrations/ 中。导入各个版本包,并在对应的 Migrator 方法中调用其 MigrateStore 函数:
// Migrating bank module from version 1 to 2
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
    return v2.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc) // v2 is package `x/bank/migrations/v2`.
}
如果你想查看一个关于余额键迁移实现变更的示例代码,可以参考 migrateBalanceKeys。作为背景,这段代码为 bank 存储引入了迁移逻辑,将地址更新为以其字节长度作为前缀,具体方案见 ADR-028。

在应用中运行迁移

模块完成迁移注册后,应用会在 UpgradeHandler 中运行这些迁移。升级处理器的类型如下:
type UpgradeHandler func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error)
处理器会接收由 x/upgrade 存储的 VersionMap(反映前一个二进制版本中的共识版本),执行任何额外的升级逻辑,并且必须返回 RunMigrations 产生的更新后 VersionMap。在 app.go 中注册该处理器:
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
    // optional: additional upgrade logic here
    return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)
})
RunMigrations 会按顺序遍历所有已注册模块,检查每个模块在 VersionMap 中的版本,并为那些共识版本已提升的模块运行所有已注册迁移脚本。更新后的 VersionMap 会返回给 upgrade keeper,并持久化到 x/upgrade 存储中。

迁移顺序

默认情况下,迁移会按模块名的字母顺序执行,但有一个例外:由于与其他模块存在状态依赖关系,x/auth 总是最后执行(见 cosmos/cosmos-sdk#10591)。如果要调整顺序,请在 app.go 中调用 app.ModuleManager.SetOrderMigrations(module1, module2, ...)。如果遗漏任何已注册模块,该函数会触发 panic。

在升级期间添加新模块

新模块之所以会被识别出来,是因为它们在 x/upgrade 的 VersionMap 存储中没有对应条目。RunMigrations 会自动为它们调用 InitGenesis。 如果你需要为新模块添加存储,请在升级执行之前配置 store loader:
upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk()
if err != nil {
    panic(err)
}
if upgradeInfo.Name == "my-plan" && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) {
    storeUpgrades := storetypes.StoreUpgrades{
        Added: []string{"newmodule"},
    }
    app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades))
}
如果你需要跳过新模块的 InitGenesis(例如,你会在处理器中手动初始化状态),那么在调用 RunMigrations 之前,请先在 fromVM 中设置该模块的版本:
fromVM["newmodule"] = newmodule.AppModule{}.ConsensusVersion()
return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)

创世状态

在启动一条新链时,必须在 genesis 期间将每个模块的共识版本保存到状态中。在 app.go 的 InitChainer 中加入以下代码:
func (app *MyApp) InitChainer(ctx sdk.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) {
    // ...
    app.UpgradeKeeper.SetModuleVersionMap(ctx, app.ModuleManager.GetVersionMap())
    // ...
}
这样,Cosmos SDK 就能在未来升级中检测到是否引入了具有更新共识版本的模块。

覆盖 Genesis 函数

SDK 提供了一些可供应用开发者导入的模块,而这些模块通常已经自带 InitGenesis 函数。如果你希望在升级期间为其中某个模块运行自定义的 genesis 函数,而不是默认实现,那么你必须同时做到两件事:在处理器中调用你的自定义函数,并且在 fromVM 中手动设置该模块的共识版本。如果缺少第二步,RunMigrations 会在你已经完成初始化后,仍然执行该模块现有的 InitGenesis。
对于任何你要覆盖其 InitGenesis 的模块,都必须在 fromVM 中手动设置其共识版本。否则,SDK 会在执行你的自定义 InitGenesis 之外,再额外调用一次模块默认的 InitGenesis。
import foo "github.com/my/module/foo"

app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
    // Prevent RunMigrations from calling foo's default InitGenesis.
    fromVM["foo"] = foo.AppModule{}.ConsensusVersion()

    // Run your custom genesis initialization for foo.
    app.ModuleManager.Modules["foo"].(module.HasGenesis).InitGenesis(ctx, app.appCodec, myCustomGenesisState)

    return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)
})

将全节点同步到已升级的区块链

加入一条已经完成升级的链的全节点,必须从该链在 genesis 时使用的初始二进制版本启动,并重放所有历史升级。如果所有升级计划都包含二进制下载说明,那么 Cosmovisor 的自动下载模式可以自动处理这件事。否则,你必须手动提供每一个历史二进制版本。 有关安装和配置,请参阅 Cosmovisor 指南。
Read and understand all of this page before running a migration on a live chain.
Synopsis In-place store migrations allow modules to upgrade to new versions that include breaking changes. This document covers both the module-side (writing migrations) and the app-side (running migrations during an upgrade).
The Cosmos SDK supports two approaches to chain upgrades: exporting the entire application state to JSON and starting fresh with a modified genesis file, or performing in-place store migrations that update state directly. In-place migrations are significantly faster for chains with large state and are the standard approach for live networks. This page covers how to write module migrations and how to run them inside an upgrade handler in your app.

Consensus Version

Successful upgrades of existing modules require each AppModule to implement the function ConsensusVersion() uint64.
  • The versions must be hard-coded by the module developer.
  • The initial version must be set to 1.
Consensus versions serve as state-breaking versions of app modules and must be incremented when the module introduces breaking changes.

Registering Migrations

To register the functionality that takes place during a module upgrade, you must register which migrations you want to take place. Migration registration takes place in the Configurator using the RegisterMigration method. The AppModule reference to the configurator is in the RegisterServices method. You can register one or more migrations. If you register more than one migration script, list the migrations in increasing order and ensure there are enough migrations that lead to the desired consensus version. For example, to migrate to version 3 of a module, register separate migrations for version 1 and version 2 as shown in the following example:
func (am AppModule) RegisterServices(cfg module.Configurator) {
    // --snip--
    if err := cfg.RegisterMigration(types.ModuleName, 1, func(ctx sdk.Context) error {
        // Perform in-place store migrations from ConsensusVersion 1 to 2.
        return nil
    }); err != nil {
        panic(fmt.Sprintf("failed to migrate %s from version 1 to 2: %v", types.ModuleName, err))
    }

    if err := cfg.RegisterMigration(types.ModuleName, 2, func(ctx sdk.Context) error {
        // Perform in-place store migrations from ConsensusVersion 2 to 3.
        return nil
    }); err != nil {
        panic(fmt.Sprintf("failed to migrate %s from version 2 to 3: %v", types.ModuleName, err))
    }
}
Since these migrations are functions that need access to a Keeper’s store, use a wrapper around the keepers called Migrator as shown in this example:
package keeper

import (
    sdk "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/x/bank/exported"
    v2 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v2"
    v3 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v3"
    v4 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v4"
)

// Migrator is a struct for handling in-place store migrations.
type Migrator struct {
    keeper         BaseKeeper
    legacySubspace exported.Subspace
}

// NewMigrator returns a new Migrator.
func NewMigrator(keeper BaseKeeper, legacySubspace exported.Subspace) Migrator {
    return Migrator{keeper: keeper, legacySubspace: legacySubspace}
}

// Migrate1to2 migrates from version 1 to 2.
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
    return v2.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
}

// Migrate2to3 migrates x/bank storage from version 2 to 3.
func (m Migrator) Migrate2to3(ctx sdk.Context) error {
    return v3.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
}

// Migrate3to4 migrates x/bank storage from version 3 to 4.
func (m Migrator) Migrate3to4(ctx sdk.Context) error {
    m.MigrateSendEnabledParams(ctx)
    return v4.MigrateStore(ctx, m.keeper.storeService, m.legacySubspace, m.keeper.cdc)
}

Writing Migration Scripts

To define the functionality that takes place during an upgrade, write a migration script and place the functions in a migrations/ directory. For example, to write migration scripts for the bank module, place the functions in x/bank/migrations/. Import each version package and call its MigrateStore function from the corresponding Migrator method:
// Migrating bank module from version 1 to 2
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
    return v2.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc) // v2 is package `x/bank/migrations/v2`.
}
To see example code of changes that were implemented in a migration of balance keys, check out migrateBalanceKeys. For context, this code introduced migrations of the bank store that updated addresses to be prefixed by their length in bytes as outlined in ADR-028.

Running Migrations in the App

Once modules have registered their migrations, the app runs them inside an UpgradeHandler. The upgrade handler type is:
type UpgradeHandler func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error)
The handler receives the VersionMap stored by x/upgrade (reflecting the consensus versions from the previous binary), performs any additional upgrade logic, and must return the updated VersionMap from RunMigrations. Register the handler in app.go:
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
    // optional: additional upgrade logic here
    return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)
})
RunMigrations iterates over all registered modules in order, checks each module’s version in the VersionMap, and runs all registered migration scripts for modules whose consensus version has increased. The updated VersionMap is returned to the upgrade keeper, which persists it in the x/upgrade store.

Order of migrations

By default, migrations run in alphabetical order by module name, with one exception: x/auth runs last due to state dependencies with other modules (see cosmos/cosmos-sdk#10591). To change the order, call app.ModuleManager.SetOrderMigrations(module1, module2, ...) in app.go. The function panics if any registered module is omitted.

Adding new modules during an upgrade

New modules are recognized because they have no entry in the x/upgrade VersionMap store. RunMigrations calls InitGenesis for them automatically. If you need to add stores for a new module, configure the store loader before the upgrade runs:
upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk()
if err != nil {
    panic(err)
}
if upgradeInfo.Name == "my-plan" && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) {
    storeUpgrades := storetypes.StoreUpgrades{
        Added: []string{"newmodule"},
    }
    app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades))
}
To skip InitGenesis for a new module (for example, if you are manually initializing state in the handler), set its version in fromVM before calling RunMigrations:
fromVM["newmodule"] = newmodule.AppModule{}.ConsensusVersion()
return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)

Genesis state

When starting a new chain, the consensus version of each module must be saved to state during genesis. Add this to InitChainer in app.go:
func (app *MyApp) InitChainer(ctx sdk.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) {
    // ...
    app.UpgradeKeeper.SetModuleVersionMap(ctx, app.ModuleManager.GetVersionMap())
    // ...
}
This lets the Cosmos SDK detect when modules with newer consensus versions are introduced in a future upgrade.

Overwriting genesis functions

The SDK provides modules that app developers can import, and those modules often already have an InitGenesis function. If you want to run a custom genesis function for one of those modules during an upgrade instead of the default one, you must both call your custom function in the handler AND manually set that module’s consensus version in fromVM. Without the second step, RunMigrations will run the module’s existing InitGenesis even though you already initialized it.
You must manually set the consensus version in fromVM for any module whose InitGenesis you are overriding. If you don’t, the SDK will call the module’s default InitGenesis in addition to your custom one.
import foo "github.com/my/module/foo"

app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
    // Prevent RunMigrations from calling foo's default InitGenesis.
    fromVM["foo"] = foo.AppModule{}.ConsensusVersion()

    // Run your custom genesis initialization for foo.
    app.ModuleManager.Modules["foo"].(module.HasGenesis).InitGenesis(ctx, app.appCodec, myCustomGenesisState)

    return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)
})

Syncing a Full Node to an Upgraded Blockchain

A full node joining an already-upgraded chain must start from the initial binary that the chain used at genesis and replay all historical upgrades. If all upgrade plans include binary download instructions, Cosmovisor’s auto-download mode handles this automatically. Otherwise, you must provide each historical binary manually. See the Cosmovisor guide for setup and configuration.