Skip to content

Latest commit

 

History

History
695 lines (502 loc) · 22.9 KB

README.md

File metadata and controls

695 lines (502 loc) · 22.9 KB

x/accounts

The x/accounts module enhances the Cosmos SDK by providing tools and infrastructure for creating advanced smart accounts.

Basics

An account can be thought of as a simplified cosmos-sdk module that supports multiple deployments. This means:

  1. A single account implementation can be deployed to multiple addresses, similar to how CosmWasm allows multiple contract instances from one WASM upload.

  2. Each account address is mapped to its corresponding account code.

  3. Accounts maintain their own state partition, similar to modules.

  4. Accounts can define both message and query handlers.

This design allows for flexible and reusable account structures within the ecosystem.

Example account creation

Basic

Defining an account begins with creating a struct that encapsulates the account's state. If the account has no state, the struct is empty type Account struct{}.

By default, accounts utilize collections to manage their state.

State Isolation

It's crucial to understand that an account's state is isolated. This means:

  1. States are not shared between accounts of different types.
  2. States are not shared even between accounts of the same type.

For example, consider two accounts of type Counter:

  • One located at address "cosmos123"
  • Another at address "cosmos456"

These accounts do not share the same collections.Item instance. Instead, each maintains its own separate state.

type Account struct {
 // We will define that the account contains in its state a counter, it's an item.
 // It could also be a map or whatever!
 Counter collections.Item[uint64]
}

Init

Creating an account begins with defining its init message. This message is processed when an account is created, similar to:

  • The instantiate method in a CosmWasm contract
  • The constructor in an EVM contract

For an account to be a valid x/accounts implementer, it must define both:

  1. An Init method
  2. An init message

We start by defining the MsgInit and its corresponding MsgInitResponse as protobuf messages:

message MsgInit {
   uint64 counter = 1;
}

message MsgInitResponse {}

Next, we implement the Init method, which sets the initial counter. We also implement a method of the Account interface. This method:

Signals to the x/accounts runtime what the Init entrypoint is Performs some generic operations to maintain type safety in the system

Here's the Go implementation:

package counter

import (
    "context"
    "cosmossdk.io/x/accounts/accountstd"
)

type Account struct {
    Counter collections.Item[uint64]
}

func (a Account) Init(ctx context.Context, msg *MsgInit) (*MsgInitResponse, error) {
    err := a.Counter.Set(ctx, msg.Counter)
    if err != nil {
        return nil, err
    }
    
    return &MsgInitResponse{}, nil
}

func (a Account) RegisterInitHandler(builder *accountstd.InitBuilder) {
    accountstd.RegisterInitHandler(builder, a.Init)
}

Execute Handlers

Execute handlers are methods that an account can execute, defined as messages. These executions can be triggered:

  • During block execution (not queries) through transactions
  • During begin or end block

To define an execute handler, we start by creating its proto message:

message MsgIncreaseCounter {
   uint64 amount = 1;
}

message MsgIncreaseCounterResponse {
   uint64 new_value = 1;
}

Next, we implement the handling code for this message and register it using the RegisterExecuteHandlers method:

package counter

import (
    "context"
    "cosmossdk.io/x/accounts/accountstd"
)

type Account struct {
    Counter collections.Item[uint64]
}

func (a Account) Init(ctx context.Context, msg *MsgInit) (*MsgInitResponse, error) {
    err := a.Counter.Set(ctx, msg.Counter)
    if err != nil {
        return nil, err
    }
    return &MsgInitResponse{}, nil
}

// Handler for MsgIncreaseCounter
func (a Account) IncreaseCounter(ctx context.Context, msg *MsgIncreaseCounter) (*MsgIncreaseCounterResponse, error) {
    counter, err := a.Counter.Get(ctx)
    if err != nil {
        return nil, err
    }
    
    newValue := counter + msg.Amount
    err = a.Counter.Set(ctx, newValue)
    if err != nil {
        return nil, err
    }
    
    return &MsgIncreaseCounterResponse{NewValue: newValue}, nil
}

// Registration of the handler in the runtime
func (a Account) RegisterExecuteHandlers(builder *accountstd.ExecuteBuilder) {
    accountstd.RegisterExecuteHandler(builder, a.IncreaseCounter)
}

func (a Account) RegisterInitHandler(builder *accountstd.InitBuilder) {
    accountstd.RegisterInitHandler(builder, a.Init)
}

This implementation defines an IncreaseCounter method that handles the MsgIncreaseCounter message, updating the counter value and returning the new value in the response.

Query Handlers

Query Handlers are read-only methods implemented by an account to expose information about itself. This information can be accessed by:

  • External clients (e.g., CLI, wallets)
  • Other modules and accounts within the system

Query handlers can be invoked:

  1. By external clients
  2. During block execution

To define a query handler, we follow a similar process to execute handlers:

  1. Define the request and response proto messages:
message QueryCounter {}

message QueryCounterResponse {
   uint64 value = 1;
}
  1. Implement and register the query handler:
package counter

import (
    "context"
    "cosmossdk.io/x/accounts/accountstd"
)

func (a Account) QueryCounter(ctx context.Context, _ *QueryCounterRequest) (*QueryCounterResponse, error) {
    counter, err := a.Counter.Get(ctx)
    if err != nil {
        return nil, err
    }
    return &QueryCounterResponse{
        Value: counter,
    }, nil
}

func (a Account) RegisterQueryHandlers(builder *accountstd.QueryBuilder) {
    accountstd.RegisterQueryHandler(builder, a.QueryCounter)
}

This implementation defines a QueryCounter method that retrieves the current counter value and returns it in the response. The RegisterQueryHandlers method registers this query handler with the system.

The Account Constructor

After creating our basic counter account, we implement the account constructor function:

package counter

import (
   "cosmossdk.io/collections"
   "cosmossdk.io/x/accounts/accountstd"
)

func NewAccount(deps accountstd.Dependencies) (Account, error) {
   return Account{
      Counter: collections.NewItem(deps.SchemaBuilder, CounterPrefix, "counter", collections.Uint64Value),
   }, nil
}

type Account struct {
   Counter collections.Item[uint64]
}

// Rest of the Account implementation...

The accountstd.Dependencies type provides an environment with essential components:

  1. AddressCodec: For encoding and decoding addresses
  2. SchemaBuilder: For schema construction (handled by the accounts module)
  3. HeaderService: For accessing block header information
  4. Other useful services and utilities

These dependencies allow the account to interact with the blockchain system.

App Wiring

Note: This assumes you've already wired the x/accounts module in your application. If not, refer to the Simapp example.

After creating our basic account, we wire it to the x/accounts module.

Depinject Method

Define the depinject constructor:

package counterdepinject

func ProvideAccount() accountstd.DepinjectAccount {
    return accountstd.DIAccount("counter", counter.NewAccount)
}

Add this to the application:

package app

func NewApp() *App {
    // ...
    appConfig = depinject.Configs(
        AppConfig(),
        depinject.Supply(
            appOpts,
            logger,
        ),
        depinject.Provide(
            counterdepinject.ProvideAccount,
        ),
    )
    // ...
}

Manual Method

Add the account to the x/accounts Keeper:

accountsKeeper, err := accounts.NewKeeper(
    appCodec,
    runtime.NewEnvironment(/* ... */),
    signingCtx.AddressCodec(),
    appCodec.InterfaceRegistry(),
    
    accountstd.AddAccount("counter", counter.NewAccount), // Add account here
    // Add more accounts if needed
)

Choose the method that best fits your application structure.

The accountstd Package

The accountstd package provides utility functions for use within account init, execution, or query handlers. Key functions include:

  1. Whoami(): Retrieves the address of the current account.
  2. Sender(): Gets the address of the transaction sender (not available in queries).
  3. Funds(): Retrieves funds provided by the sender during Init or Execution.
  4. ExecModule(): Allows the account to execute a module message. Note: Impersonation is prevented. An account can't send messages on behalf of others.
  5. QueryModule(): Enables querying a module.

These functions, along with others, facilitate account operations and interactions within the system. For a comprehensive list of available utilities, refer to the Go documentation.

Interfaces via Messages and Queries

Accounts can handle various messages and queries, allowing for flexible interface definitions:

  1. Multiple account types can handle the same message or query.
  2. Different accounts (even with the same type but different addresses) can process identical messages or queries.

This flexibility enables defining interfaces as common sets of messages and/or queries that accounts can handle.

Example: Transaction Authentication

  • We define a MsgAuthenticate message.
  • Any account capable of handling MsgAuthenticate is considered to implement the Authentication interface.
  • This approach allows for standardized interaction patterns across different account types.

(Note: More details on the Authentication interface will be provided later.)

Full Examples

Some examples can be found in the defaults package.

The Authentication Interface

x/accounts introduces the Authentication interface, allowing for flexible transaction (TX) authentication beyond traditional public key cryptography.

Chain developers can implement tailored authentication methods for their accounts. Any account that implements the Authentication interface can be authenticated within a transaction.

To implement the Authentication interface in x/accounts, an account must expose an execution handler capable of processing a specific message type.

The key message type for authentication is MsgAuthenticate, which is defined in the module's protocol buffer files:

https://github.com/cosmos/cosmos-sdk/blob/main/x/accounts/proto/cosmos/accounts/interfaces/account_abstraction/v1/interface.proto#L9-L24

Authentication Mechanism

AnteHandler in the SDK

The Cosmos SDK utilizes an AnteHandler to verify transaction (TX) integrity. Its primary function is to ensure that the messages within a transaction are correctly signed by the purported sender.

Authentication Flow for x/accounts Module

When the AnteHandler identifies that a message sender (and transaction signer) belongs to the x/accounts module, it delegates the authentication process to that module.

Authentication Interface Requirement

For successful authentication, the account must implement the Authentication interface. If an account fails to implement this interface, it's considered non-externally owned, resulting in transaction rejection.

Sequence Diagram
graph TD
    A[Tx Is Received] --> B[Execute Signature Verification Ante Handler]
    B --> D{Is signer an x/accounts account?}
    D -->|No| E[Continue with signature verification ante handler]
    D -->|Yes| F{Does account handle MsgAuthenticate?}
    F -->|No| G[Fail TX: Non-externally owned account]
    F -->|Yes| H[Invoke signer account MsgAuthenticate]
    E --> I[End]
    G --> I
    H --> I
Loading

Implementing the Authentication Interface

To implement the Authentication interface, an account must handle the execution of MsgAuthenticate. Here's an example of how to do this:

package base

import (
   "context"
   "errors"
   aa_interface_v1 "github.com/cosmos/cosmos-sdk/x/accounts/interfaces/account_abstraction/v1"
   "github.com/cosmos/cosmos-sdk/x/accounts/std"
)

// Account represents a base account structure
type Account struct {
   // Account fields...
}

// Authenticate implements the authentication flow for an abstracted base account.
func (a Account) Authenticate(ctx context.Context, msg *aa_interface_v1.MsgAuthenticate) (*aa_interface_v1.MsgAuthenticateResponse, error) {
   if !accountstd.SenderIsAccountsModule(ctx) {
      return nil, errors.New("unauthorized: only accounts module is allowed to call this")
   }
   // Implement your authentication logic here
   // ...
   return &aa_interface_v1.MsgAuthenticateResponse{}, nil
}

// RegisterExecuteHandlers registers the execution handlers for the account.
func (a Account) RegisterExecuteHandlers(builder *accountstd.ExecuteBuilder) {
   accountstd.RegisterExecuteHandler(builder, a.SwapPubKey) // Other handlers
   accountstd.RegisterExecuteHandler(builder, a.Authenticate) // Implements the Authentication interface
}

Key Implementation Points

  1. Sender Verification: Always verify that the sender is the x/accounts module. This prevents unauthorized accounts from triggering authentication.
  2. Authentication Safety: Ensure your authentication mechanism is secure:
    • Prevent replay attacks by making it impossible to reuse the same action with the same signature.
Implementation example

Please find an example here.

Supporting Custom Accounts in the x/auth gRPC Server

Overview

The x/auth module provides a mechanism for custom account types to be exposed via its Account and AccountInfo gRPC queries. This feature is particularly useful for ensuring compatibility with existing wallets that have not yet integrated with x/accounts but still need to parse account information post-migration.

Implementation

To support this feature, your custom account type needs to implement the auth.QueryLegacyAccount handler. Here are some important points to consider:

  1. Selective Implementation: This implementation is not required for every account type. It's only necessary for accounts you want to expose through the x/auth gRPC Account and AccountInfo methods.
  2. Flexible Response: The info field in the QueryLegacyAccountResponse is optional. If your custom account cannot be represented as a BaseAccount, you can leave this field empty.

Example Implementation

A concrete example of implementation can be found in defaults/base/account.go. Here's a simplified version:

func (a Account) AuthRetroCompatibility(ctx context.Context, _ *authtypes.QueryLegacyAccount) (*authtypes.QueryLegacyAccountResponse, error) {
    seq := a.GetSequence()
    num := a.GetNumber()
    address := a.GetAddress()
    pubKey := a.GetPubKey()

    baseAccount := &authtypes.BaseAccount{
        AccountNumber: num,
        Sequence:      seq,
        Address:       address,
    }

    // Convert pubKey to Any type
    pubKeyAny, err := gogotypes.NewAnyWithValue(pubKey)
    if err != nil {
        return nil, err
    }
    baseAccount.PubKey = pubKeyAny

    // Convert the entire baseAccount to Any type
    accountAny, err := gogotypes.NewAnyWithValue(baseAccount)
    if err != nil {
        return nil, err
    }

    return &authtypes.QueryLegacyAccountResponse{
        Account: accountAny,
        Info:    baseAccount,
    }, nil
}

Usage Notes

  • Implement this handler only for account types you want to expose via x/auth gRPC methods.
  • The info field in the response can be nil if your account doesn't fit the BaseAccount structure.

Address Derivation

The x/accounts module offers two methods for deriving addresses, both ensuring non-squattability. This means each address is uniquely tied to its creator, preventing address collisions between different creators (e.g., Alice cannot create addresses that would conflict with Bob's addresses).

Method 1: Using Address Seeds

When creating an account via MsgInit, you can provide an address_seed. The address is derived using:

address = sha256(ModuleName || address_seed || creator_address)

Method 2: Using Account Numbers

If no address seed is provided, the address is derived using:

address = sha256(ModuleName || creator_address || next_account_number)

Address Seed Best Practices

  1. Address seeds must be unique per creator (not globally unique)
  2. Reusing an address seed will cause account creation to fail
  3. For programmatic account creation, use an incrementing sequence number as the address seed
  4. This is particularly useful for contracts or modules that need deterministic address generation

Genesis

Creating accounts on genesis

In order to create accounts at genesis, the x/accounts module allows developers to provide a list of genesis MsgInit messages that will be executed in the x/accounts genesis flow.

The init messages are generated offline. You can also use the following CLI command to generate the json messages: simd tx accounts init [account type] [msg] --from me --genesis. This will generate a jsonified init message wrapped in an x/accounts MsgInit.

This follows the same initialization flow and rules that would happen if the chain is running. The only concrete difference is that this is happening at the genesis block.

For example, given the following genesis.json file:

{
  "app_state": {
    "accounts": {
      "init_account_msgs": [
        {
          "sender": "account_creator_address",
          "account_type": "lockup",
          "message": {
            "@type": "cosmos.accounts.defaults.lockup.MsgInitLockupAccount",
            "owner": "some_owner",
            "end_time": "..",
            "start_time": ".."
          },
          "funds": [
            {
              "denom": "stake",
              "amount": "1000"
            }
          ]
        }
      ]
    }
  }
}

The accounts module will run the lockup account initialization message.

Bundling

Transaction bundling enables a designated account (the bundler) to submit transactions on behalf of multiple users. This approach offers several advantages:

  1. Fee Abstraction: The bundler assumes responsibility for transaction fees, simplifying the process for end-users.
  2. Flexible Fee Arrangements: Users and bundlers can negotiate fee structures off-chain, allowing for customized payment models.
  3. Improved User Experience: By abstracting away fee complexities, bundling can make blockchain interactions more accessible to a wider audience.
  4. Potential for Optimization: Bundlers can potentially optimize gas usage and reduce overall transaction costs.
graph TD
   A1[User 1] -->|Send Tx| B[Bundler]
   A2[User 2] -->|Send Tx| B
   A3[User 3] -->|Send Tx| B
   B -->|Package Txs into MsgExecuteBundle| C[MsgExecuteBundle]
   C -->|Submit| D[x/accounts module]
   D -->|Execute Tx 1| E1[Execute independently]
   D -->|Execute Tx 2| E2[Execute independently]
   D -->|Execute Tx 3| E3[Execute independently]
Loading

Tx Extension

For a transaction to be processed by a bundler, it must include a TxExtension. This extension is defined in the interface.proto file.

// TxExtension is the extension option that AA's add to txs when they're bundled.
message TxExtension {
   // authentication_gas_limit expresses the gas limit to be used for the authentication part of the
   // bundled tx.
   uint64 authentication_gas_limit = 1;
   // bundler_payment_messages expresses a list of messages that the account
   // executes to pay the bundler for submitting the bundled tx.
   // It can be empty if the bundler does not need any form of payment,
   // the handshake for submitting the UserOperation might have happened off-chain.
   // Bundlers and accounts are free to use any form of payment, in fact the payment can
   // either be empty or be expressed as:
   // - NFT payment
   // - IBC Token payment.
   // - Payment through delegations.
   repeated google.protobuf.Any bundler_payment_messages = 2;
   // bundler_payment_gas_limit defines the gas limit to be used for the bundler payment.
   // This ensures that, since the bundler executes a list of bundled tx and there needs to
   // be minimal trust between bundler and the tx sender, the sender cannot consume
   // the whole bundle gas.
   uint64 bundler_payment_gas_limit = 3;
   // execution_gas_limit defines the gas limit to be used for the execution of the UserOperation's
   // execution messages.
   uint64 execution_gas_limit = 4;
}

The purpose of the TxExtension is to provide crucial information for the bundler to process and execute the transaction efficiently and securely. It allows for fine-grained control over gas limits for different parts of the transaction execution and facilitates flexible payment arrangements between the user and the bundler. Field explanations:

  1. authentication_gas_limit (uint64):

Specifies the maximum amount of gas that can be used for authenticating the bundled transaction. Ensures that the authentication process doesn't consume excessive resources.

  1. bundler_payment_messages (repeated google.protobuf.Any):

Contains a list of messages defining how the account will pay the bundler for submitting the transaction. Offers flexibility in payment methods, including NFTs, IBC tokens, or delegations. Can be empty if payment arrangements are made off-chain or if the bundler doesn't require payment.

  1. bundler_payment_gas_limit (uint64):

Sets the maximum gas that can be used for processing the bundler payment. Prevents a malicious sender from consuming all the gas allocated for the entire bundle, enhancing security in the bundling process.

  1. execution_gas_limit (uint64):

Defines the maximum gas allowed for executing the actual transaction messages (UserOperation). Helps in accurately estimating and controlling the resources needed for the main transaction execution.

Compatibility of Your Chain with Bundling

Important Considerations

Bundling introduces a bypass mechanism for ante handler checks. This has significant implications for chains that rely on ante handlers for:

  • Message validation
  • Admission control logic

If your chain heavily depends on these ante handler functionalities, enabling bundling may compromise your chain's security or operational logic.

Disabling Bundling

For chains where bundling is incompatible with existing security measures or operational requirements, you can disable this feature. To do so:

  1. Locate your app.go file
  2. Add the following method call:

Non depinject:

// add keepers
func NewApp(...) {
    ...
    accountsKeeper, err := accounts.NewKeeper(...)
    if err != nil {
        panic(err)
    }
    accountsKeeper.DisableTxBundling() <-- // add this line
    app.AccountsKeeper = accountsKeeper
	...
}

Depinject:

	var appModules map[string]appmodule.AppModule
	if err := depinject.Inject(appConfig,
		&appBuilder,
		...
		&app.AuthKeeper,
		&app.AccountsKeeper,
		...
	); err != nil {
		panic(err)
	}
	
	app.AccountsKeeper.DisableBundling() // <- add this line