变更记录

  • 2019-08-30:初始草案

背景

目前,Cosmos SDK 允许使用自定义账户类型;auth keeper 可以存储任何满足其 Account 接口的类型。然而,auth 并不负责将账户导出到 genesis 文件或从 genesis 文件加载账户,这部分工作由 genaccounts 完成,而它只处理 4 种具体账户类型中的一种(BaseAccount、ContinuousVestingAccount、DelayedVestingAccount 和 ModuleAccount)。 希望使用自定义账户的项目(例如自定义 vesting 账户)需要 fork 并修改 genaccounts。

决策

总结来说,我们将直接使用 amino 对所有账户(接口类型)进行编组和解组,而不是转换为 genaccounts 的 GenesisAccount 类型。由于这样做会移除 genaccounts 中大部分代码,我们将把 genaccounts 合并到 auth 中。编组后的账户将存储在 auth 的 genesis 状态里。 详细变更如下:

1) 直接使用 amino 对账户进行编组和解组

auth 模块的 GenesisState 新增一个 Accounts 字段。注意,出于第 3 节所述原因,这些账户不是 exported.Account 类型。
// GenesisState - all auth state that must be provided at genesis
type GenesisState struct {
    Params   Params           `json:"params" yaml:"params"`
    Accounts []GenesisAccount `json:"accounts" yaml:"accounts"`
}
现在,auth 的 InitGenesis 和 ExportGenesis 除了处理已定义的参数外,也会对账户进行编组和解组。
// InitGenesis - Init store state from genesis data
func InitGenesis(ctx sdk.Context, ak AccountKeeper, data GenesisState) {
    ak.SetParams(ctx, data.Params)
    // load the accounts
    for _, a := range data.Accounts {
    acc := ak.NewAccount(ctx, a) // set account number
        ak.SetAccount(ctx, acc)
}
}

// ExportGenesis returns a GenesisState for a given context and keeper
func ExportGenesis(ctx sdk.Context, ak AccountKeeper)

GenesisState {
    params := ak.GetParams(ctx)

var genAccounts []exported.GenesisAccount
    ak.IterateAccounts(ctx, func(account exported.Account)

bool {
    genAccount := account.(exported.GenesisAccount)

genAccounts = append(genAccounts, genAccount)

return false
})

return NewGenesisState(params, genAccounts)
}

2) 在 auth codec 上注册自定义账户类型

auth codec 必须注册所有自定义账户类型,才能对它们进行编组。我们将遵循 gov 中处理 proposal 所建立的模式。 一个自定义账户定义示例如下:
import authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

// Register the module account type with the auth module codec so it can decode module accounts stored in a genesis file
func init() {
    authtypes.RegisterAccountTypeCodec(ModuleAccount{
}, "cosmos-sdk/ModuleAccount")
}

type ModuleAccount struct {
    ...
auth codec 的定义如下:
var ModuleCdc *codec.LegacyAmino

func init() {
    ModuleCdc = codec.NewLegacyAmino()
    // register module msg's and Account interface
    ...
    // leave the codec unsealed
}

// RegisterAccountTypeCodec registers an external account type defined in another module for the internal ModuleCdc.
func RegisterAccountTypeCodec(o interface{
}, name string) {
    ModuleCdc.RegisterConcrete(o, name, nil)
}

3) 自定义账户类型的 Genesis 校验

各模块会实现一个 ValidateGenesis 方法。由于 auth 并不了解账户的具体实现,因此账户需要自行完成校验。 我们会将账户解组到一个包含 Validate 方法的 GenesisAccount 接口中。
type GenesisAccount interface {
    exported.Account
    Validate()

error
}
随后,auth 的 ValidateGenesis 函数将变为:
// ValidateGenesis performs basic validation of auth genesis data returning an
// error for any failed validation criteria.
func ValidateGenesis(data GenesisState)

error {
    // Validate params
    ...

    // Validate accounts
    addrMap := make(map[string]bool, len(data.Accounts))
    for _, acc := range data.Accounts {

        // check for duplicated accounts
    addrStr := acc.GetAddress().String()
    if _, ok := addrMap[addrStr]; ok {
    return fmt.Errorf("duplicate account found in genesis state; address: %s", addrStr)
}

addrMap[addrStr] = true

        // check account specific validation
    if err := acc.Validate(); err != nil {
    return fmt.Errorf("invalid account found in genesis state; address: %s, error: %s", addrStr, err.Error())
}

 
}

return nil
}

4) 将 add-genesis-account cli 移动到 auth

genaccounts 模块包含一个 cli 命令,用于向 genesis 文件中添加基础账户或 vesting 账户。 这部分将迁移到 auth。至于添加自定义账户,仍由各项目自行编写命令。也可以像 gov 一样创建一个可扩展的 cli 处理器,但对这个较小的使用场景来说,这种复杂度并不值得。

5) 更新模块账户和 vesting 账户

在新方案下,模块账户类型和 vesting 账户类型需要做一些小幅更新:
  • 在 auth 的 codec 上进行类型注册(如上所示)
  • 为每个具体的 Account 类型实现一个 Validate 方法

状态

提议中

影响

正面

  • 可以在无需 fork genaccounts 的情况下使用自定义账户
  • 代码行数减少

负面

中性

  • genaccounts 模块将不再存在
  • genesis 文件中的账户将存储在 auth 下的 accounts 中,而不是 genaccounts 模块中。
    • add-genesis-account cli 命令现在位于 auth 中

参考


Changelog

  • 2019-08-30: initial draft

Context

Currently, the Cosmos SDK allows for custom account types; the auth keeper stores any type fulfilling its Account interface. However auth does not handle exporting or loading accounts to/from a genesis file, this is done by genaccounts, which only handles one of 4 concrete account types (BaseAccount, ContinuousVestingAccount, DelayedVestingAccount and ModuleAccount). Projects desiring to use custom accounts (say custom vesting accounts) need to fork and modify genaccounts.

Decision

In summary, we will (un)marshal all accounts (interface types) directly using amino, rather than converting to genaccounts’s GenesisAccount type. Since doing this removes the majority of genaccounts’s code, we will merge genaccounts into auth. Marshalled accounts will be stored in auth’s genesis state. Detailed changes:

1) (Un)Marshal accounts directly using amino

The auth module’s GenesisState gains a new field Accounts. Note these aren’t of type exported.Account for reasons outlined in section 3.
// GenesisState - all auth state that must be provided at genesis
type GenesisState struct {
    Params   Params           `json:"params" yaml:"params"`
    Accounts []GenesisAccount `json:"accounts" yaml:"accounts"`
}
Now auth’s InitGenesis and ExportGenesis (un)marshal accounts as well as the defined params.
// InitGenesis - Init store state from genesis data
func InitGenesis(ctx sdk.Context, ak AccountKeeper, data GenesisState) {
    ak.SetParams(ctx, data.Params)
    // load the accounts
    for _, a := range data.Accounts {
    acc := ak.NewAccount(ctx, a) // set account number
        ak.SetAccount(ctx, acc)
}
}

// ExportGenesis returns a GenesisState for a given context and keeper
func ExportGenesis(ctx sdk.Context, ak AccountKeeper)

GenesisState {
    params := ak.GetParams(ctx)

var genAccounts []exported.GenesisAccount
    ak.IterateAccounts(ctx, func(account exported.Account)

bool {
    genAccount := account.(exported.GenesisAccount)

genAccounts = append(genAccounts, genAccount)

return false
})

return NewGenesisState(params, genAccounts)
}

2) Register custom account types on the auth codec

The auth codec must have all custom account types registered to marshal them. We will follow the pattern established in gov for proposals. An example custom account definition:
import authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

// Register the module account type with the auth module codec so it can decode module accounts stored in a genesis file
func init() {
    authtypes.RegisterAccountTypeCodec(ModuleAccount{
}, "cosmos-sdk/ModuleAccount")
}

type ModuleAccount struct {
    ...
The auth codec definition:
var ModuleCdc *codec.LegacyAmino

func init() {
    ModuleCdc = codec.NewLegacyAmino()
    // register module msg's and Account interface
    ...
    // leave the codec unsealed
}

// RegisterAccountTypeCodec registers an external account type defined in another module for the internal ModuleCdc.
func RegisterAccountTypeCodec(o interface{
}, name string) {
    ModuleCdc.RegisterConcrete(o, name, nil)
}

3) Genesis validation for custom account types

Modules implement a ValidateGenesis method. As auth does not know of account implementations, accounts will need to validate themselves. We will unmarshal accounts into a GenesisAccount interface that includes a Validate method.
type GenesisAccount interface {
    exported.Account
    Validate()

error
}
Then the auth ValidateGenesis function becomes:
// ValidateGenesis performs basic validation of auth genesis data returning an
// error for any failed validation criteria.
func ValidateGenesis(data GenesisState)

error {
    // Validate params
    ...

    // Validate accounts
    addrMap := make(map[string]bool, len(data.Accounts))
    for _, acc := range data.Accounts {

        // check for duplicated accounts
    addrStr := acc.GetAddress().String()
    if _, ok := addrMap[addrStr]; ok {
    return fmt.Errorf("duplicate account found in genesis state; address: %s", addrStr)
}

addrMap[addrStr] = true

        // check account specific validation
    if err := acc.Validate(); err != nil {
    return fmt.Errorf("invalid account found in genesis state; address: %s, error: %s", addrStr, err.Error())
}

 
}

return nil
}

4) Move add-genesis-account cli to auth

The genaccounts module contains a cli command to add base or vesting accounts to a genesis file. This will be moved to auth. We will leave it to projects to write their own commands to add custom accounts. An extensible cli handler, similar to gov, could be created but it is not worth the complexity for this minor use case.

5) Update module and vesting accounts

Under the new scheme, module and vesting account types need some minor updates:
  • Type registration on auth’s codec (shown above)
  • A Validate method for each Account concrete type

Status

Proposed

Consequences

Positive

  • custom accounts can be used without needing to fork genaccounts
  • reduction in lines of code

Negative

Neutral

  • genaccounts module no longer exists
  • accounts in genesis files are stored under accounts in auth rather than in the genaccounts module. -add-genesis-account cli command now in auth

References