概述

升级处理器允许通过治理提案,在特定区块高度对所有验证者执行协调一致的链上升级。它为链以确定性方式执行数据迁移、参数更新和模块升级提供了机制。
升级处理器对于在链升级期间保持共识至关重要。所有验证者都必须在同一高度运行完全相同的升级逻辑。

何时使用升级处理器

在以下情况下必须使用升级处理器:
  • 破坏性状态变更:修改存储格式或数据结构
  • 模块迁移:更新模块版本或参数
  • 协议升级:实现需要状态转换的新功能
  • 数据迁移:在不同存储位置之间迁移数据

基本结构

注册升级处理器

升级处理器在应用的 RegisterUpgradeHandlers() 方法中注册:

模块版本管理

升级处理器会接收并返回一个 module.VersionMap,用于跟踪模块版本:
// fromVM contains the module versions before the upgrade
// The returned VersionMap contains the new versions after migration
migrations, err := app.ModuleManager.RunMigrations(ctx, app.configurator, fromVM)

组织升级代码

为了提高可维护性,建议将升级代码组织到独立的包中:

目录结构

app/
├── upgrades/
│   ├── v1_0_0/
│   │   ├── constants.go    # Upgrade name and configuration
│   │   ├── handler.go      # Main upgrade handler
│   │   └── migrations.go   # Migration logic
│   ├── v1_1_0/
│   │   ├── constants.go
│   │   ├── handler.go
│   │   └── migrations.go
│   └── types.go            # Shared types
└── upgrades.go             # RegisterUpgradeHandlers

constants.go

handler.go

迁移模式

参数迁移

将模块参数迁移到新格式:
func migrateParams(ctx sdk.Context, keeper paramskeeper.Keeper) error {
    // Get old params
    var oldParams v1.Params
    keeper.GetParamSet(ctx, &oldParams)

    // Convert to new format
    newParams := v2.Params{
        Field1: oldParams.Field1,
        Field2: convertField(oldParams.Field2),
        // New field with default value
        Field3: "default",
    }

    // Set new params
    keeper.SetParams(ctx, newParams)
    return nil
}

状态迁移

在不同存储位置之间迁移数据:
func migrateState(ctx sdk.Context, storeKey storetypes.StoreKey) error {
    store := ctx.KVStore(storeKey)

    // Iterate over old storage
    iterator := storetypes.KVStorePrefixIterator(store, oldPrefix)
    defer iterator.Close()

    for ; iterator.Valid(); iterator.Next() {
        oldKey := iterator.Key()
        value := iterator.Value()

        // Transform key/value if needed
        newKey := transformKey(oldKey)
        newValue := transformValue(value)

        // Write to new location
        store.Set(newKey, newValue)

        // Delete old entry
        store.Delete(oldKey)
    }

    return nil
}

添加或移除模块

在升级过程中添加或移除模块:
// In constants.go
var Upgrade = upgrades.Upgrade{
    UpgradeName: "v2.0.0",
    CreateUpgradeHandler: CreateUpgradeHandler,
    StoreUpgrades: storetypes.StoreUpgrades{
        Added:   []string{"newmodule"},
        Deleted: []string{"oldmodule"},
    },
}

// In handler.go
func CreateUpgradeHandler(...) upgradetypes.UpgradeHandler {
    return func(c context.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) {
        // Delete old module version
        delete(vm, "oldmodule")

        // Initialize new module
        if err := newModuleKeeper.InitGenesis(ctx, defaultGenesis); err != nil {
            return nil, err
        }

        // Run migrations
        return mm.RunMigrations(c, configurator, vm)
    }
}

最佳实践

在部署到主网之前,务必先在测试网上充分测试升级处理器。

幂等性

在可能的情况下,使迁移具备幂等性:
func migrateSomething(ctx sdk.Context, store sdk.KVStore) error {
    // Check if migration already done
    if store.Has(migrationCompleteKey) {
        ctx.Logger().Info("Migration already completed, skipping")
        return nil
    }

    // Perform migration
    // ...

    // Mark as complete
    store.Set(migrationCompleteKey, []byte{1})
    return nil
}

错误处理

使用完善的错误处理和日志记录:
func migrate(ctx sdk.Context, keeper Keeper) error {
    ctx.Logger().Info("Starting migration", "module", "mymodule")

    count := 0
    iterator := keeper.IterateAllRecords(ctx)
    defer iterator.Close()

    for ; iterator.Valid(); iterator.Next() {
        if err := processRecord(iterator.Key(), iterator.Value()); err != nil {
            ctx.Logger().Error("Failed to migrate record",
                "key", iterator.Key(),
                "error", err,
            )
            return fmt.Errorf("migration failed at record %d: %w", count, err)
        }
        count++

        // Log progress for long migrations
        if count%1000 == 0 {
            ctx.Logger().Info("Migration progress", "processed", count)
        }
    }

    ctx.Logger().Info("Migration complete", "total_migrated", count)
    return nil
}

测试

为升级处理器编写完整的测试:
func TestUpgradeHandler(t *testing.T) {
    app := setupApp(t)
    ctx := app.NewContext(false, tmproto.Header{Height: 1})

    // Setup pre-upgrade state
    setupOldState(t, ctx, app)

    // Run upgrade handler
    _, err := v1_0_0.CreateUpgradeHandler(
        app.ModuleManager,
        app.configurator,
        &upgrades.UpgradeKeepers{
            // ... keepers
        },
        app.keys,
    )(ctx, upgradetypes.Plan{Name: "v1.0.0"}, app.ModuleManager.GetVersionMap())

    require.NoError(t, err)

    // Verify post-upgrade state
    verifyNewState(t, ctx, app)
}

升级流程

创建升级提案

提交包含升级详情的治理提案:
mantrachaind tx gov submit-proposal software-upgrade v1.0.0 \
    --title "Upgrade to v1.0.0" \
    --description "Upgrade description" \
    --upgrade-height 1000000 \
    --from validator \
    --deposit 10000000stake

对提案投票

验证者和委托人对升级进行投票:
mantrachaind tx gov vote 1 yes --from validator

准备二进制文件

构建并分发包含升级处理器的新二进制文件:
# Build new binary
make build

# Test upgrade on local network
./scripts/test-upgrade.sh

# Distribute to validators
# Use Cosmovisor for automated upgrades

监控升级

在升级高度期间观察日志:
# Monitor upgrade logs
tail -f ~/.mantrachaind/logs/upgrade.log

# Verify upgrade success
mantrachaind query upgrade applied v1.0.0

Cosmos EVM 特定迁移

对于 Cosmos EVM 链,常见的特定迁移包括:
  • ERC20 预编译迁移:v0.3.x 升级到 v0.4.0 时必须执行
  • Fee Market 参数:更新 EIP-1559 参数
  • 自定义预编译:注册新的预编译合约
  • EVM 状态:迁移账户余额或合约存储

故障排查

共识失败

现象: 链在升级高度发生共识失败并停止出块 原因:
  • 验证者运行了不同版本的二进制文件
  • 未注册升级处理器
  • 迁移逻辑存在非确定性
解决方案:
  • 确保所有验证者使用相同的二进制文件
  • 验证升级处理器已正确注册
  • 检查迁移逻辑中是否存在非确定性行为

升级 Panic

现象: 节点在升级过程中发生 panic 原因:
  • 迁移中存在未处理的错误
  • 缺少必需的状态
  • 类型断言无效
解决方案:
  • 添加完善的错误处理
  • 在迁移前校验状态
  • 使用安全的类型转换

状态损坏

现象: 升级后状态无效 原因:
  • 迁移仅部分完成
  • 数据转换不正确
  • 未清理旧数据
解决方案:
  • 使迁移具备原子性
  • 充分测试转换逻辑
  • 确保旧数据被正确清理

参考资料


Overview

Upgrade handlers enable coordinated on-chain upgrades across all validators at specific block heights via governance proposals. They provide a mechanism for chains to perform data migrations, parameter updates, and module upgrades in a deterministic way.
Upgrade handlers are critical for maintaining consensus during chain upgrades. All validators must run the same upgrade logic at the same height.

When to Use Upgrade Handlers

Upgrade handlers are required when:
  • Breaking state changes: Modifying storage formats or data structures
  • Module migrations: Updating module versions or parameters
  • Protocol upgrades: Implementing new features that require state transitions
  • Data migrations: Moving data between different storage locations

Basic Structure

Registering Upgrade Handlers

Upgrade handlers are registered in your app’s RegisterUpgradeHandlers() method:

Module Version Management

The upgrade handler receives and returns a module.VersionMap that tracks module versions:
// fromVM contains the module versions before the upgrade
// The returned VersionMap contains the new versions after migration
migrations, err := app.ModuleManager.RunMigrations(ctx, app.configurator, fromVM)

Organizing Upgrade Code

For better maintainability, organize upgrades in separate packages:

Directory Structure

app/
├── upgrades/
│   ├── v1_0_0/
│   │   ├── constants.go    # Upgrade name and configuration
│   │   ├── handler.go      # Main upgrade handler
│   │   └── migrations.go   # Migration logic
│   ├── v1_1_0/
│   │   ├── constants.go
│   │   ├── handler.go
│   │   └── migrations.go
│   └── types.go            # Shared types
└── upgrades.go             # RegisterUpgradeHandlers

constants.go

handler.go

Migration Patterns

Parameter Migrations

Migrating module parameters to new formats:
func migrateParams(ctx sdk.Context, keeper paramskeeper.Keeper) error {
    // Get old params
    var oldParams v1.Params
    keeper.GetParamSet(ctx, &oldParams)

    // Convert to new format
    newParams := v2.Params{
        Field1: oldParams.Field1,
        Field2: convertField(oldParams.Field2),
        // New field with default value
        Field3: "default",
    }

    // Set new params
    keeper.SetParams(ctx, newParams)
    return nil
}

State Migrations

Moving data between different storage locations:
func migrateState(ctx sdk.Context, storeKey storetypes.StoreKey) error {
    store := ctx.KVStore(storeKey)

    // Iterate over old storage
    iterator := storetypes.KVStorePrefixIterator(store, oldPrefix)
    defer iterator.Close()

    for ; iterator.Valid(); iterator.Next() {
        oldKey := iterator.Key()
        value := iterator.Value()

        // Transform key/value if needed
        newKey := transformKey(oldKey)
        newValue := transformValue(value)

        // Write to new location
        store.Set(newKey, newValue)

        // Delete old entry
        store.Delete(oldKey)
    }

    return nil
}

Module Addition/Removal

Adding or removing modules during upgrade:
// In constants.go
var Upgrade = upgrades.Upgrade{
    UpgradeName: "v2.0.0",
    CreateUpgradeHandler: CreateUpgradeHandler,
    StoreUpgrades: storetypes.StoreUpgrades{
        Added:   []string{"newmodule"},
        Deleted: []string{"oldmodule"},
    },
}

// In handler.go
func CreateUpgradeHandler(...) upgradetypes.UpgradeHandler {
    return func(c context.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) {
        // Delete old module version
        delete(vm, "oldmodule")

        // Initialize new module
        if err := newModuleKeeper.InitGenesis(ctx, defaultGenesis); err != nil {
            return nil, err
        }

        // Run migrations
        return mm.RunMigrations(c, configurator, vm)
    }
}

Best Practices

Always test upgrade handlers thoroughly on testnets before mainnet deployment.

Idempotency

Make migrations idempotent when possible:
func migrateSomething(ctx sdk.Context, store sdk.KVStore) error {
    // Check if migration already done
    if store.Has(migrationCompleteKey) {
        ctx.Logger().Info("Migration already completed, skipping")
        return nil
    }

    // Perform migration
    // ...

    // Mark as complete
    store.Set(migrationCompleteKey, []byte{1})
    return nil
}

Error Handling

Use comprehensive error handling and logging:
func migrate(ctx sdk.Context, keeper Keeper) error {
    ctx.Logger().Info("Starting migration", "module", "mymodule")

    count := 0
    iterator := keeper.IterateAllRecords(ctx)
    defer iterator.Close()

    for ; iterator.Valid(); iterator.Next() {
        if err := processRecord(iterator.Key(), iterator.Value()); err != nil {
            ctx.Logger().Error("Failed to migrate record",
                "key", iterator.Key(),
                "error", err,
            )
            return fmt.Errorf("migration failed at record %d: %w", count, err)
        }
        count++

        // Log progress for long migrations
        if count%1000 == 0 {
            ctx.Logger().Info("Migration progress", "processed", count)
        }
    }

    ctx.Logger().Info("Migration complete", "total_migrated", count)
    return nil
}

Testing

Create comprehensive tests for upgrade handlers:
func TestUpgradeHandler(t *testing.T) {
    app := setupApp(t)
    ctx := app.NewContext(false, tmproto.Header{Height: 1})

    // Setup pre-upgrade state
    setupOldState(t, ctx, app)

    // Run upgrade handler
    _, err := v1_0_0.CreateUpgradeHandler(
        app.ModuleManager,
        app.configurator,
        &upgrades.UpgradeKeepers{
            // ... keepers
        },
        app.keys,
    )(ctx, upgradetypes.Plan{Name: "v1.0.0"}, app.ModuleManager.GetVersionMap())

    require.NoError(t, err)

    // Verify post-upgrade state
    verifyNewState(t, ctx, app)
}

Upgrade Process

Create Upgrade Proposal

Submit a governance proposal with the upgrade details:
mantrachaind tx gov submit-proposal software-upgrade v1.0.0 \
    --title "Upgrade to v1.0.0" \
    --description "Upgrade description" \
    --upgrade-height 1000000 \
    --from validator \
    --deposit 10000000stake

Vote on Proposal

Validators and delegators vote on the upgrade:
mantrachaind tx gov vote 1 yes --from validator

Prepare Binary

Build and distribute the new binary with the upgrade handler:
# Build new binary
make build

# Test upgrade on local network
./scripts/test-upgrade.sh

# Distribute to validators
# Use Cosmovisor for automated upgrades

Monitor Upgrade

Watch logs during the upgrade height:
# Monitor upgrade logs
tail -f ~/.mantrachaind/logs/upgrade.log

# Verify upgrade success
mantrachaind query upgrade applied v1.0.0

Cosmos EVM Specific Migrations

For Cosmos EVM chains, specific migrations include:
  • ERC20 Precompiles Migration: Required for v0.3.x to v0.4.0
  • Fee Market Parameters: Updating EIP-1559 parameters
  • Custom Precompiles: Registering new precompiled contracts
  • EVM State: Migrating account balances or contract storage

Troubleshooting

Consensus Failure

Symptom: Chain halts with consensus failure at upgrade height Causes:
  • Validators running different binary versions
  • Upgrade handler not registered
  • Non-deterministic migration logic
Solution:
  • Ensure all validators have the same binary
  • Verify upgrade handler is registered
  • Review migration logic for non-determinism

Upgrade Panic

Symptom: Node panics during upgrade Causes:
  • Unhandled error in migration
  • Missing required state
  • Invalid type assertions
Solution:
  • Add comprehensive error handling
  • Validate state before migration
  • Use safe type conversions

State Corruption

Symptom: Invalid state after upgrade Causes:
  • Partial migration completion
  • Incorrect data transformation
  • Missing cleanup of old data
Solution:
  • Make migrations atomic
  • Thoroughly test transformations
  • Ensure old data is properly cleaned up

References