How the Akash Deployment Module Manages State Transitions: A Deep Dive into the Cosmos SDK Keeper

The Akash deployment module manages state transitions through a prefix-based KV-store key rotation system that enforces a two-level state machine: deployments transition only between Active and Closed, while groups handle Open, Paused, InsufficientFunds, and Closed states via atomic delete-and-set operations in the keeper layer.

The Akash Network's deployment module implements a deterministic state machine for container orchestration contracts using the Cosmos SDK. According to the akash-network/node source code, the module separates lifecycle concerns between deployments (top-level contracts) and groups (resource sets), encoding state directly into KV-store prefixes to guarantee valid transitions and ensure every mutation emits a typed event for external observability.

Understanding the Two-Level State Machine

The module maintains distinct state machines for deployments and their constituent groups. This separation allows granular control over resource allocation while preserving top-level contract integrity.

Deployment-Level States: Active and Closed

Deployments support only two terminal states encoded as byte prefixes:

  • Active (0x01): The initial state set during creation in handler/server.go (lines 49–52) via Keeper.Create. The deployment is stored with DeploymentStateActivePrefix.
  • Closed (0x02): The terminal state reached exclusively through Keeper.CloseDeployment, which deletes the Active key and rewrites the deployment with DeploymentStateClosedPrefix.

There is no paused state at the deployment level—pausing is strictly a group-level operation. The module explicitly forbids operations on closed deployments; calls to UpdateDeployment, CloseDeployment, or group hooks on a non-Active deployment return v1.ErrDeploymentClosed (see handler/server.go lines 103–106).

Group-Level States: Open, Paused, InsufficientFunds, and Closed

Groups represent resource allocations within a deployment and support a richer state machine:

  • Open (0x01): Active resource allocation state.
  • Paused (0x02): Temporarily suspended state reached via OnPauseGroup.
  • InsufficientFunds (0x03): Entered automatically when escrowed funds deplete (set by market/escrow hooks).
  • Closed (0x04): Terminal state reached via OnCloseGroup from any non-Closed state.

How State Transitions Work in the Keeper Layer

All state mutations occur through the keeper's public methods in x/deployment/keeper/keeper.go, which implement an atomic key rotation pattern to ensure consistency.

The Prefix-Based Key Rotation Pattern

Every state transition follows a deterministic four-step process:

  1. Lookup: findGroup or findDeployment locates the current key by iterating possible prefixes until finding a match.
  2. Deletion: store.Delete(key) removes the old state entry.
  3. Mutation: The struct's State field updates to the target enum value.
  4. Insertion: A new key is encoded using GroupStateToPrefix or DeploymentStateToPrefix, and the marshaled value is stored via store.Set.

This pattern ensures that a group's state exists in exactly one prefix namespace at any time, making prefix scans efficient for queries.

Deployment Transitions: Active to Closed

When closing a deployment, the keeper performs an atomic swap between prefixes:

// In x/deployment/keeper/keeper.go → CloseDeployment
store.Delete(key)                         // Remove Active (0x01) key
deployment.State = v1.DeploymentClosed    // Flip state enum
key = MustDeploymentKey(
    DeploymentStateToPrefix(deployment.State), // Convert to Closed prefix (0x02)
    deployment.ID
)
store.Set(key, k.cdc.MustMarshal(&deployment))
ctx.EventManager().EmitTypedEvent(&v1.EventDeploymentClosed{ID: deployment.ID})

The message server in handler/server.go validates the transition before invocation, checking that deployment.State == v1.DeploymentActive and returning v1.ErrDeploymentClosed if the deployment is already terminal.

Group Transitions: Open, Paused, and Closed

Group state changes follow the same key rotation logic with specific entry points:

Open → Paused (OnPauseGroup):

store.Delete(key)               // Delete Open key (0x01)
group.State = types.GroupPaused // Set to Paused
key, err = GroupKey(GroupStateToPrefix(group.State), group.ID) // New key with 0x02 prefix
store.Set(key, k.cdc.MustMarshal(&group))
ctx.EventManager().EmitTypedEvent(&v1.EventGroupPaused{ID: group.ID})

Paused → Open (OnStartGroup):

store.Delete(key)               // Delete Paused key (0x02)
group.State = types.GroupOpen   // Revert to Open
key, err = GroupKey(GroupStateToPrefix(group.State), group.ID) // Back to 0x01 prefix
store.Set(key, k.cdc.MustMarshal(&group))
ctx.EventManager().EmitTypedEvent(&v1.EventGroupStarted{ID: group.ID})

Any → Closed (OnCloseGroup): This method accepts groups in Open, Paused, or InsufficientFunds states, deletes the current key regardless of prefix, and stores the group under GroupStateClosedPrefix (0x04), emitting EventGroupClosed.

Validation and Guardrails in the Message Server

The gRPC message server in x/deployment/handler/server.go provides the first line of defense against illegal transitions:

  • CreateDeployment: Initializes with State: v1.DeploymentActive and validates groups.
  • UpdateDeployment: Aborts if deployment.State != v1.DeploymentActive (lines 103–106).
  • CloseDeployment: Returns early if already closed (lines 30–32).
  • PauseGroup/StartGroup/CloseGroup: Each calls validation methods (ValidatePausable, ValidateStartable, ValidateClosable) on the group object before invoking the keeper.

These checks prevent unnecessary gas consumption and provide clear error messages, while the keeper's internal logic serves as the final safety net.

Event Emissions for State Changes

Every state transition emits typed events through ctx.EventManager().EmitTypedEvent, creating an immutable audit trail:

Transition Event Type Emitted In
Deployment creation EventDeploymentCreated Keeper.Create
Deployment closure EventDeploymentClosed Keeper.CloseDeployment
Group pause EventGroupPaused Keeper.OnPauseGroup
Group start EventGroupStarted Keeper.OnStartGroup
Group closure EventGroupClosed Keeper.OnCloseGroup

These events enable block explorers to index state changes and allow downstream modules (market, escrow) to react to lifecycle changes asynchronously.

Summary

  • Two-level architecture: Deployments track Active/Closed states while groups manage Open, Paused, InsufficientFunds, and Closed states independently.
  • Prefix-based storage: State is encoded in KV-store keys (0x01 for Active/Open, 0x02 for Paused/Closed, 0x03 for InsufficientFunds, 0x04 for Closed groups), enabling efficient prefix scans.
  • Atomic transitions: The keeper implements delete-and-set key rotation in x/deployment/keeper/keeper.go to ensure state consistency.
  • Double validation: The message server in handler/server.go validates business logic before the keeper enforces storage constraints.
  • Observability: Every transition emits typed Cosmos SDK events for external indexing and cross-module communication.

Frequently Asked Questions

Can a deployment be paused in the Akash deployment module?

No, pausing is not supported at the deployment level. The v1.Deployment proto only defines DeploymentActive and DeploymentClosed states. Pausing is implemented exclusively at the group level through OnPauseGroup, which transitions groups between GroupOpen and GroupPaused states while the parent deployment remains Active.

What happens if I try to update a closed deployment?

The message server rejects the transaction immediately. In handler/server.go lines 103–106, UpdateDeployment checks if deployment.State != v1.DeploymentActive and returns v1.ErrDeploymentClosed. This guardrail prevents any modifications to terminal deployments, ensuring that only the escrow module can trigger final closure through CloseDeployment.

How does the module handle groups that run out of funds?

When escrowed funds deplete, the market or escrow module hooks trigger a transition to GroupStateInsufficientFunds (prefix 0x03). This is not a direct external transaction but an internal state change initiated by economic mechanisms. The group remains in this state until either funding is restored (transitioning back to Open) or the group is Closed.

Where are the state transition prefixes defined?

The byte prefixes are defined in x/deployment/keeper/key.go through helper functions DeploymentStateToPrefix and GroupStateToPrefix. Deployments use 0x01 for Active and 0x02 for Closed, while groups use 0x01 (Open), 0x02 (Paused), 0x03 (InsufficientFunds), and 0x04 (Closed). These prefixes are prepended to owner addresses and sequence numbers to create unique KV-store keys for each state instance.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →