变更记录

  • 2021年9月22日:初始草案

状态

提议中

摘要

本 ADR 描述了一种关于 Cosmos SDK 模块如何使用、交互以及存储各自参数的替代方案。

背景

目前,在 Cosmos SDK 中,需要使用参数的模块会使用 x/params 模块。x/params 的工作方式是由模块定义参数,通常通过一个简单的 Params 结构体完成,并通过一个归属于相应注册模块的唯一 Subspace 将该结构体注册到 x/params 模块中。注册模块随后即可独占访问其对应的 Subspace。模块可以通过这个 Subspace 获取和设置其 Params 结构体。 此外,Cosmos SDK 的 x/gov 模块原生支持通过链上参数变更来修改参数,具体方式是使用 ParamChangeProposal 治理提案类型,由利益相关方对建议的参数变更进行投票。 使用 x/params 模块管理各个模块参数存在多种权衡。也就是说,参数管理在某种程度上几乎是“免费”的,因为开发者只需要定义 Params 结构体、Subspace 以及各种辅助函数,例如 Params 类型上的 ParamSetPairs。不过,这种方式也有一些明显缺点。这些缺点包括:参数通过 JSON 序列化后存储到状态中,而这会非常慢。此外,通过 ParamChangeProposal 治理提案进行参数变更时,无法读取或写入状态。换句话说,目前在尝试修改参数时,应用程序无法执行任何状态转换。

决策

我们将在 #9810 中 x/gov 与 x/authz 工作对齐的基础上继续推进。具体来说,模块开发者将创建一个或多个唯一的参数数据结构,并将其序列化存储到状态中。参数数据结构必须实现 sdk.Msg 接口,并提供相应的 Protobuf Msg service 方法,用于校验和更新参数以及执行所有必要变更。x/gov 模块将通过 #9810 中完成的工作分发参数消息,这些消息将由 Protobuf Msg service 处理。 需要注意的是,参数以及对应 sdk.Msg 消息应如何组织,由开发者自行决定。以下以当前 x/auth 中使用 x/params 模块进行参数管理的参数定义为例:
message Params {
  uint64 max_memo_characters       = 1;
  uint64 tx_sig_limit              = 2;
  uint64 tx_size_cost_per_byte     = 3;
  uint64 sig_verify_cost_ed25519   = 4;
  uint64 sig_verify_cost_secp256k1 = 5;
}
开发者可以选择为 Params 中的每个字段分别创建唯一的数据结构,也可以像上面 x/auth 的示例那样,只创建一个 Params 结构体。 在前一种即 x/params 风格的做法中,需要为每一个字段创建一个 sdk.Msg 以及对应的处理器。如果参数字段很多,这会变得很繁琐。在后一种做法中,只有一个数据结构,因此也只需要一个消息处理器;但相应地,该消息处理器可能需要更复杂一些,因为它可能需要区分哪些参数被修改了,哪些参数未被触及。 参数变更提案通过 x/gov 模块发起。执行则通过对根 x/gov 模块账户的 x/authz 授权完成。 继续以 x/auth 为例,下面给出一个更完整的示例:
type Params struct {
    MaxMemoCharacters      uint64
	TxSigLimit             uint64
	TxSizeCostPerByte      uint64
	SigVerifyCostED25519   uint64
	SigVerifyCostSecp256k1 uint64
}

type MsgUpdateParams struct {
    MaxMemoCharacters      uint64
	TxSigLimit             uint64
	TxSizeCostPerByte      uint64
	SigVerifyCostED25519   uint64
	SigVerifyCostSecp256k1 uint64
}

type MsgUpdateParamsResponse struct {
}

func (ms msgServer)

UpdateParams(goCtx context.Context, msg *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

  // verification logic...

  // persist params
    params := ParamsFromMsg(msg)

ms.SaveParams(ctx, params)

return &types.MsgUpdateParamsResponse{
}, nil
}

func ParamsFromMsg(msg *types.MsgUpdateParams)

Params {
  // ...
}
还应提供一个 gRPC Service 查询,例如:
service Query {
  // ...
  
  rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
    option (google.api.http).get = "/cosmos/<module>/v1beta1/params";
  }
}

message QueryParamsResponse {
  Params params = 1 [(gogoproto.nullable) = false];
}

影响

采用这种模块参数方法后,我们将获得让模块参数变更具备状态性并可扩展到几乎所有应用场景的能力。我们将能够发出事件(并通过 event hooks 中提出的工作触发注册到这些事件上的钩子)、调用其他 Msg service 方法,或执行迁移。 此外,在从状态读取参数以及向状态写入参数时,性能也将显著提升,尤其是在某一组特定参数被持续频繁读取的情况下。 不过,这种方法要求开发者实现更多类型和 Msg service 方法;如果参数很多,这可能会带来负担。此外,开发者还需要自行实现模块参数的持久化逻辑。 不过,这应当是比较简单的。

向后兼容性

这种新的模块参数处理方法天然不兼容现有的 x/params 模块。不过,x/params 仍会保留在 Cosmos SDK 中,并被标记为已弃用;除潜在的 bug 修复外,不会再增加新的功能。需要注意的是,x/params 模块可能会在未来的版本中被彻底移除。

正面影响

  • 模块参数的序列化效率更高
  • 模块能够对参数变更作出响应并执行额外操作。
  • 可以发出特殊事件,从而触发钩子。

负面影响

  • 对模块开发者来说,模块参数的处理会稍微更繁琐一些:
    • 模块现在需要自行负责参数状态的持久化和读取
    • 模块现在需要为每种唯一的参数数据结构提供唯一的消息处理器,以处理对应的参数变更。

中性影响

  • 需要先审查并合并 #9810。

参考资料


Changelog

  • Sep 22, 2021: Initial Draft

Status

Proposed

Abstract

This ADR describes an alternative approach to how Cosmos SDK modules use, interact, and store their respective parameters.

Context

Currently, in the Cosmos SDK, modules that require the use of parameters use the x/params module. The x/params works by having modules define parameters, typically via a simple Params structure, and registering that structure in the x/params module via a unique Subspace that belongs to the respective registering module. The registering module then has unique access to its respective Subspace. Through this Subspace, the module can get and set its Params structure. In addition, the Cosmos SDK’s x/gov module has direct support for changing parameters on-chain via a ParamChangeProposal governance proposal type, where stakeholders can vote on suggested parameter changes. There are various tradeoffs to using the x/params module to manage individual module parameters. Namely, managing parameters essentially comes for “free” in that developers only need to define the Params struct, the Subspace, and the various auxiliary functions, e.g. ParamSetPairs, on the Params type. However, there are some notable drawbacks. These drawbacks include the fact that parameters are serialized in state via JSON which is extremely slow. In addition, parameter changes via ParamChangeProposal governance proposals have no way of reading from or writing to state. In other words, it is currently not possible to have any state transitions in the application during an attempt to change param(s).

Decision

We will build off of the alignment of x/gov and x/authz work per #9810. Namely, module developers will create one or more unique parameter data structures that must be serialized to state. The Param data structures must implement sdk.Msg interface with respective Protobuf Msg service method which will validate and update the parameters with all necessary changes. The x/gov module via the work done in #9810, will dispatch Param messages, which will be handled by Protobuf Msg services. Note, it is up to developers to decide how to structure their parameters and the respective sdk.Msg messages. Consider the parameters currently defined in x/auth using the x/params module for parameter management:
message Params {
  uint64 max_memo_characters       = 1;
  uint64 tx_sig_limit              = 2;
  uint64 tx_size_cost_per_byte     = 3;
  uint64 sig_verify_cost_ed25519   = 4;
  uint64 sig_verify_cost_secp256k1 = 5;
}
Developers can choose to either create a unique data structure for every field in Params or they can create a single Params structure as outlined above in the case of x/auth. In the former, x/params, approach, a sdk.Msg would need to be created for every single field along with a handler. This can become burdensome if there are a lot of parameter fields. In the latter case, there is only a single data structure and thus only a single message handler, however, the message handler might have to be more sophisticated in that it might need to understand what parameters are being changed vs what parameters are untouched. Params change proposals are made using the x/gov module. Execution is done through x/authz authorization to the root x/gov module’s account. Continuing to use x/auth, we demonstrate a more complete example:
type Params struct {
    MaxMemoCharacters      uint64
	TxSigLimit             uint64
	TxSizeCostPerByte      uint64
	SigVerifyCostED25519   uint64
	SigVerifyCostSecp256k1 uint64
}

type MsgUpdateParams struct {
    MaxMemoCharacters      uint64
	TxSigLimit             uint64
	TxSizeCostPerByte      uint64
	SigVerifyCostED25519   uint64
	SigVerifyCostSecp256k1 uint64
}

type MsgUpdateParamsResponse struct {
}

func (ms msgServer)

UpdateParams(goCtx context.Context, msg *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

  // verification logic...

  // persist params
    params := ParamsFromMsg(msg)

ms.SaveParams(ctx, params)

return &types.MsgUpdateParamsResponse{
}, nil
}

func ParamsFromMsg(msg *types.MsgUpdateParams)

Params {
  // ...
}
A gRPC Service query should also be provided, for example:
service Query {
  // ...
  
  rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
    option (google.api.http).get = "/cosmos/<module>/v1beta1/params";
  }
}

message QueryParamsResponse {
  Params params = 1 [(gogoproto.nullable) = false];
}

Consequences

As a result of implementing the module parameter methodology, we gain the ability for module parameter changes to be stateful and extensible to fit nearly every application’s use case. We will be able to emit events (and trigger hooks registered to that events using the work proposed in event hooks), call other Msg service methods or perform migration. In addition, there will be significant gains in performance when it comes to reading and writing parameters from and to state, especially if a specific set of parameters are read on a consistent basis. However, this methodology will require developers to implement more types and Msg service methods which can become burdensome if many parameters exist. In addition, developers are required to implement persistence logic for module parameters. However, this should be trivial.

Backwards Compatibility

The new method for working with module parameters is naturally not backwards compatible with the existing x/params module. However, the x/params will remain in the Cosmos SDK and will be marked as deprecated with no additional functionality being added apart from potential bug fixes. Note, the x/params module may be removed entirely in a future release.

Positive

  • Module parameters are serialized more efficiently
  • Modules are able to react on parameters changes and perform additional actions.
  • Special events can be emitted, allowing hooks to be triggered.

Negative

  • Module parameters becomes slightly more burdensome for module developers:
    • Modules are now responsible for persisting and retrieving parameter state
    • Modules are now required to have unique message handlers to handle parameter changes per unique parameter data structure.

Neutral

  • Requires #9810 to be reviewed and merged.

References