Skip to main content
Version: v0.52

Keepers

Synopsis

Keepers refer to a Cosmos SDK abstraction whose role is to manage access to the subset of the state defined by various modules. Keepers are module-specific, i.e. the subset of state defined by a module can only be accessed by a keeper defined in said module. If a module needs to access the subset of state defined by another module, a reference to the second module's internal keeper needs to be passed to the first one. This is done by specifying the module inputs in the depinject config; runtime will ensure that the correct keeper is passed to the module.

Motivation

The Cosmos SDK is a framework that makes it easy for developers to build complex decentralized applications from scratch, mainly by composing modules together. As the ecosystem of open-source modules for the Cosmos SDK expands, it will become increasingly likely that some of these modules contain vulnerabilities, as a result of the negligence or malice of their developer.

The Cosmos SDK adopts an object-capabilities-based approach to help developers better protect their application from unwanted inter-module interactions, and keepers are at the core of this approach. A keeper can be considered quite literally to be the gatekeeper of a module's store(s). Each store (typically an IAVL Store) defined within a module comes with a storeKey, which grants unlimited access to it. The module's keeper holds this storeKey (which should otherwise remain unexposed), and defines methods for reading and writing to the store(s).

The core idea behind the object-capabilities approach is to only reveal what is necessary to get the work done. In practice, this means that instead of handling permissions of modules through access-control lists, module keepers are passed a reference to the specific instance of the other modules' keepers that they need to access (this is done in the application's constructor function). As a consequence, a module can only interact with the subset of state defined in another module via the methods exposed by the instance of the other module's keeper. This is a great way for developers to control the interactions that their own module can have with modules developed by external developers.

Type Definition

keepers are generally implemented in a /keeper/keeper.go file located in the module's folder. By convention, the type keeper of a module is simply named Keeper and usually follows the following structure:

type Keeper struct {
appmodule.Environment

// External keepers, if any

// codec

// authority
}

For example, here is the type definition of the keeper from the staking module:

x/staking/keeper/keeper.go
loading...

Let us go through the different parameters:

  • Environment is a struct that holds the necessary references to services available to the modules. This includes the store services, the event manager and more.
  • An expected keeper is a keeper external to a module that is required by the internal keeper of said module. External keepers are listed in the internal keeper's type definition as interfaces. These interfaces are themselves defined in an expected_keepers.go file in the root of the module's folder. In this context, interfaces are used to reduce the number of dependencies, as well as to facilitate the maintenance of the module itself.
  • cdc is the codec used to marshal and unmarshal structs to/from []byte. The cdc can be any of codec.BinaryCodec, codec.JSONCodec or codec.Codec based on your requirements. It can be either a proto or amino codec as long as they implement these interfaces.
  • The authority listed is a module account or user account that has the right to change module level parameters. Previously this was handled by the param module, which has been deprecated.

Of course, it is possible to define different types of internal keepers for the same module (e.g. a read-only keeper). Each type of keeper comes with its own constructor function, which is called from the application's constructor function. This is where keepers are instantiated, and where developers make sure to pass correct instances of modules' keepers to other modules that require them.

Environment

Environment is a struct, part of the module Core APIs (cosmossdk.io/core/appmodule). A keeper should embed the environment struct to get access to the necessary references to services available to the modules. Runtime ensures that the Environment struct is scoped to the module.

v1.0.0-alpha.6/core/appmodule/v2/environment.go
loading...

All services are then easily available to the module wherever the keeper is used:

x/gov/keeper/proposal.go
loading...

Learn more about those services in the core api documentation.

Implementing Methods

Keepers primarily expose methods for business logic, as validity checks should have already been performed by the Msg server when keepers' methods are called.

State management is recommended to be done via Collections

State Management

In the Cosmos SDK, it is crucial to be methodical and selective when managing state within a module, as improper state management can lead to inefficiency, security risks, and scalability issues. Not all data belongs in the on-chain state; it's important to store only essential blockchain data that needs to be verified by consensus. Storing unnecessary information, especially client-side data, can bloat the state and slow down performance. Instead, developers should focus on using an off-chain database to handle supplementary data, extending the API as needed. This approach minimizes on-chain complexity, optimizes resource usage, and keeps the blockchain state lean and efficient, ensuring scalability and smooth operations.

The Cosmos SDK leverages Protocol Buffers (protobuf) for efficient state management, providing a well-structured, binary encoding format that ensures compatibility and performance across different modules. The SDK’s recommended approach for managing state is through the collections package, which simplifies state handling by offering predefined data structures like maps and indexed sets, reducing the complexity of managing raw state data. While users can opt for custom encoding schemes if they need more flexibility or have specialized requirements, they should be aware that such custom implementations may not integrate seamlessly with indexers that decode state data on the fly. This could lead to challenges in data retrieval, querying, and interoperability, making protobuf a safer and more future-proof choice for most use cases.