How Akash Network Upgrades Handle State Migration Between Versions
Akash Network upgrades use a Cosmos SDK-based migration pipeline that combines store modifications, registered module migrations, and custom upgrade handlers to atomically transform chain state at predetermined block heights.
The akash-network/node repository implements a robust state migration system built atop the Cosmos SDK's x/upgrade module. When the network transitions between versions, it must preserve existing data while restructuring the underlying key-value stores and module schemas. This article examines the exact mechanisms—from software upgrade proposals to migration execution—that ensure seamless state transitions across protocol versions.
The Cosmos SDK Foundation
Akash Network's upgrade architecture extends the Cosmos SDK's upgrade module to manage consensus-breaking changes. Every version requiring state changes introduces a software upgrade proposal that triggers a coordinated migration pipeline when the chain reaches a specific block height.
The system guarantees that binaries running before the upgrade can continue reading the old store format, while new binaries create updated stores and execute registered migrations atomically. This dual-version compatibility prevents chain splits during coordinated upgrades.
The Upgrade Pipeline: From Proposal to Execution
Upgrade Definition and Registration
Each release requiring state changes implements the IUpgrade interface within a dedicated Go package under upgrades/software/<version>/. The upgrade registration process begins at node startup when upgrades/types builds a mapping of upgrade names to initializer functions via RegisterUpgrade.
For example, in the v1.0.0 release, the initializer returns an object satisfying IUpgrade that encapsulates both store changes and migration logic:
// Located in upgrades/software/v1.0.0/upgrade.go
type upgrade struct {
*upgrades.Upgrade
}
func (up *upgrade) UpgradeHandler() upgradetypes.UpgradeHandler {
return func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
toVM, err := up.MM.RunMigrations(ctx, up.Configurator, fromVM)
// Post-migration logic executes here
return toVM, err
}
}
Store Modifications with StoreLoader
The StoreLoader() method defines structural changes to the multistore, returning a storetypes.StoreUpgrades that specifies which module stores to add, delete, or rename. This runs before any data migrations, ensuring the physical storage layout matches the new binary's expectations.
In upgrades/software/v1.0.0/upgrade.go, the v1.0.0 upgrade adds the consensus store while removing legacy modules:
func (up *upgrade) StoreLoader() *storetypes.StoreUpgrades {
return &storetypes.StoreUpgrades{
Added: []string{consensustypes.ModuleName},
Deleted: []string{
"agov", "astaking", crisistypes.ModuleName,
},
}
}
Module Migrations and Registration
Individual modules register migration functions that transform their specific KV-store data from old schemas to new ones. These registrations occur in the upgrade's init.go file using utypes.RegisterMigration, which associates a module name, target version, and handler function.
From upgrades/software/v1.0.0/init.go:
func init() {
utypes.RegisterUpgrade(v1_0_0.UpgradeName, v1_0_0.initUpgrade)
// Register market module migration to version 6
utypes.RegisterMigration(mv1.ModuleName, 6, newMarketMigration)
}
The app/upgrades.go file then binds these registrations to the Cosmos SDK's configurator:
if err := app.Configurator.RegisterMigration(module, version, migrator.GetHandler()); err != nil {
panic(err)
}
Execution at Upgrade Height
When the chain reaches the designated upgrade height, the UpgradeHandler invokes the module migrator (app.MM.RunMigrations), which iterates over all registered migrations and executes them sequentially within a single atomic transaction:
// From upgrades/software/v1.2.0/upgrade.go
toVM, err := up.MM.RunMigrations(ctx, up.Configurator, fromVM)
if err != nil {
return nil, err
}
After generic migrations complete, upgrades may execute custom logic. The v1.1.0 upgrade demonstrates this pattern by closing overdrawn escrow accounts after the standard migrations finish:
// Post-migration logic in v1.1.0
accounts := up.Keepers.Akash.Escrow().AccountsWithBalance(ctx)
for _, account := range accounts {
if account.Balance.IsNegative() {
up.Keepers.Akash.Escrow().CloseAccount(ctx, account.ID)
}
}
Height-Based Patches for Hotfixes
For critical fixes that do not require full software upgrades, the system supports RegisterHeightPatch. This mechanism injects one-time logic that executes at a specific block height without requiring a coordinated binary upgrade, providing a surgical intervention capability for urgent consensus issues.
This functionality is defined in upgrades/types/types.go and allows validators to deploy patches that modify state at predetermined heights while remaining compatible with the existing software version.
Code Example: Implementing a New Migration
To add a migration for a hypothetical v1.3.0 release that introduces a new staking module, developers create two files:
// upgrades/software/v1.3.0/init.go
func init() {
utypes.RegisterUpgrade(v1_3_0.UpgradeName, v1_3_0.initUpgrade)
// Register migration for staking module, version 8
utypes.RegisterMigration(stakingtypes.ModuleName, 8, newStakingMigration)
}
// upgrades/software/v1.3.0/upgrade.go
func (up *upgrade) StoreLoader() *storetypes.StoreUpgrades {
return &storetypes.StoreUpgrades{
Added: []string{stakingtypes.ModuleName},
Deleted: []string{},
}
}
func newStakingMigration(ctx sdk.Context, akashKeepers akashkeepers.AkashKeepers) error {
// Transform legacy validator data into new format
// Migrate delegation records
return nil
}
When the upgrade plan "v1.3.0" reaches its target height, the SDK automatically creates the new staking store, executes newStakingMigration to transform legacy data, and commits all changes atomically.
Summary
- StoreLoader (
StoreLoader()) defines physical store additions and deletions before data migration begins. - Migration Registration (
utypes.RegisterMigration) pairs module-specific transformation functions with target versions. - Atomic Execution (
app.MM.RunMigrations) processes all registered migrations within a single block transaction. - Custom Handlers allow post-migration logic for complex state transitions beyond schema changes.
- Height Patches provide emergency state intervention without full software upgrades.
Frequently Asked Questions
What triggers an Akash Network state migration?
A SoftwareUpgradeProposal submitted through governance triggers the migration when the chain reaches the specified block height. The proposal contains the upgrade name (matching the directory in upgrades/software/), target height, and optional binary download metadata parsed by util/cli/upgrade_info.go.
How does the network ensure old binaries can read state during an upgrade?
The upgrade architecture maintains backward compatibility by ensuring old binaries continue operating on the existing store format until the upgrade height. The new binary, deployed before the target height, contains both the old and new store definitions, allowing it to read legacy data and write migrated data atomically when RunMigrations executes.
Can migrations be executed without adding or removing stores?
Yes. Not all upgrades require store changes. If an upgrade only modifies data schemas within existing module stores, the StoreLoader() can return an empty storetypes.StoreUpgrades or nil, while still registering migration functions that transform the existing KV data through utypes.RegisterMigration.
What happens if a migration fails during execution?
If any registered migration returns an error during app.MM.RunMigrations, the entire upgrade handler fails, causing the block to be rejected. This atomic guarantee ensures the chain never persists partially migrated state, preventing data corruption and consensus failures.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →