变更记录

  • 2020/08/18:初始草案
  • 2021/05/05:移除基于区块高度的过期支持,并简化命名。

状态

已接受

背景

为了发起区块链交易,签名账户必须持有足额且币种正确的余额来支付手续费。在某些交易场景中,必须持续维护一个拥有足够手续费的钱包,会成为采用门槛。 例如,在正确配置权限后,某人可以临时将提案投票能力委托给一个“burner”账户,而该账户存储在仅具备最低限度安全性的手机上。 其他用例还包括:工人在供应链中追踪物品,或农户提交田间数据用于分析或合规目的。 对于所有这些用例,如果不再要求这些账户始终维持合适的手续费余额,用户体验将显著提升。如果我们希望在供应链追踪这类场景中实现企业级采用,这一点尤为重要。 一种方案是提供服务,自动为这些账户补充合适的手续费;但更好的用户体验,是允许这些账户在合理的消费限制下,从一个公共手续费池账户中扣取手续费。 单一资金池可以减少大量小额“充值”交易带来的周转成本,也能更高效地利用建立该资金池的组织所拥有的资源。

决策

我们提出通过模块 x/feegrant 作为解决方案,使一个账户(“granter”,授权方)能够授予另一个账户(“grantee”,被授权方)额度,使其在一组明确定义的限制内,使用授权方账户余额支付手续费。 手续费额度通过可扩展接口 FeeAllowanceI 定义:
type FeeAllowanceI {
  // Accept can use fee payment requested as well as timestamp of the current block
  // to determine whether or not to process this. This is checked in
  // Keeper.UseGrantedFees and the return values should match how it is handled there.
  //
  // If it returns an error, the fee payment is rejected, otherwise it is accepted.
  // The FeeAllowance implementation is expected to update it's internal state
  // and will be saved again after an acceptance.
  //
  // If remove is true (regardless of the error), the FeeAllowance will be deleted from storage
  // (eg. when it is used up). (See call to RevokeFeeAllowance in Keeper.UseGrantedFees)

Accept(ctx sdk.Context, fee sdk.Coins, msgs []sdk.Msg) (remove bool, err error)

  // ValidateBasic should evaluate this FeeAllowance for internal consistency.
  // Don't allow negative amounts, or negative periods for example.
  ValidateBasic()

error
}
定义了两种基础手续费额度类型 BasicAllowance 和 PeriodicAllowance,以支持已知用例:
// BasicAllowance implements FeeAllowanceI with a one-time grant of tokens
// that optionally expires. The delegatee can use up to SpendLimit to cover fees.
message BasicAllowance {
  // spend_limit specifies the maximum amount of tokens that can be spent
  // by this allowance and will be updated as tokens are spent. If it is
  // empty, there is no spend limit and any amount of coins can be spent.
  repeated cosmos_sdk.v1.Coin spend_limit = 1;

  // expiration specifies an optional time when this allowance expires
  google.protobuf.Timestamp expiration = 2;
}

// PeriodicAllowance extends FeeAllowanceI to allow for both a maximum cap,
// as well as a limit per time period.
message PeriodicAllowance {
  BasicAllowance basic = 1;

  // period specifies the time duration in which period_spend_limit coins can
  // be spent before that allowance is reset
  google.protobuf.Duration period = 2;

  // period_spend_limit specifies the maximum number of coins that can be spent
  // in the period
  repeated cosmos_sdk.v1.Coin period_spend_limit = 3;

  // period_can_spend is the number of coins left to be spent before the period_reset time
  repeated cosmos_sdk.v1.Coin period_can_spend = 4;

  // period_reset is the time at which this period resets and a new one begins,
  // it is calculated from the start time of the first transaction after the
  // last period ended
  google.protobuf.Timestamp period_reset = 5;
}

可以通过 MsgGrantAllowance 和 MsgRevokeAllowance 授予和撤销额度:
// MsgGrantAllowance adds permission for Grantee to spend up to Allowance
// of fees from the account of Granter.
message MsgGrantAllowance {
     string granter = 1;
     string grantee = 2;
     google.protobuf.Any allowance = 3;
 }

 // MsgRevokeAllowance removes any existing FeeAllowance from Granter to Grantee.
 message MsgRevokeAllowance {
     string granter = 1;
     string grantee = 2;
 }
为了在交易中使用额度,我们向交易的 Fee 类型新增字段 granter:
package cosmos.tx.v1beta1;

message Fee {
  repeated cosmos.base.v1beta1.Coin amount = 1;
  uint64 gas_limit = 2;
  string payer = 3;
  string granter = 4;
}
granter 必须为空,或者必须对应一个已经向手续费支付方授予手续费额度的账户(手续费支付方可以是第一个签名者,也可以是 payer 字段的值)。 我们还将创建一个名为 DeductGrantedFeeDecorator 的新 AnteDecorator,用于处理设置了 fee_payer 的交易,并根据手续费额度正确扣除费用。

影响

正面影响

  • 对于那些仅为支付手续费而维护账户余额较为繁琐的用例,用户体验将得到改善

负面影响

中性影响

  • 必须为交易 Fee 消息新增一个字段,并创建一个新的 AnteDecorator 以支持其使用

参考资料

  • 描述初始工作的博客文章:Link
  • 最初的公开规范:Link
  • 来自 B-harvest、影响了该设计的原始子密钥提案:Link

Changelog

  • 2020/08/18: Initial Draft
  • 2021/05/05: Removed height based expiration support and simplified naming.

Status

Accepted

Context

In order to make blockchain transactions, the signing account must possess a sufficient balance of the right denomination in order to pay fees. There are classes of transactions where needing to maintain a wallet with sufficient fees is a barrier to adoption. For instance, when proper permissions are setup, someone may temporarily delegate the ability to vote on proposals to a “burner” account that is stored on a mobile phone with only minimal security. Other use cases include workers tracking items in a supply chain or farmers submitting field data for analytics or compliance purposes. For all of these use cases, UX would be significantly enhanced by obviating the need for these accounts to always maintain the appropriate fee balance. This is especially true if we wanted to achieve enterprise adoption for something like supply chain tracking. While one solution would be to have a service that fills up these accounts automatically with the appropriate fees, a better UX would be provided by allowing these accounts to pull from a common fee pool account with proper spending limits. A single pool would reduce the churn of making lots of small “fill up” transactions and also more effectively leverages the resources of the organization setting up the pool.

Decision

As a solution we propose a module, x/feegrant which allows one account, the “granter” to grant another account, the “grantee” an allowance to spend the granter’s account balance for fees within certain well-defined limits. Fee allowances are defined by the extensible FeeAllowanceI interface:
type FeeAllowanceI {
  // Accept can use fee payment requested as well as timestamp of the current block
  // to determine whether or not to process this. This is checked in
  // Keeper.UseGrantedFees and the return values should match how it is handled there.
  //
  // If it returns an error, the fee payment is rejected, otherwise it is accepted.
  // The FeeAllowance implementation is expected to update it's internal state
  // and will be saved again after an acceptance.
  //
  // If remove is true (regardless of the error), the FeeAllowance will be deleted from storage
  // (eg. when it is used up). (See call to RevokeFeeAllowance in Keeper.UseGrantedFees)

Accept(ctx sdk.Context, fee sdk.Coins, msgs []sdk.Msg) (remove bool, err error)

  // ValidateBasic should evaluate this FeeAllowance for internal consistency.
  // Don't allow negative amounts, or negative periods for example.
  ValidateBasic()

error
}
Two basic fee allowance types, BasicAllowance and PeriodicAllowance are defined to support known use cases:
// BasicAllowance implements FeeAllowanceI with a one-time grant of tokens
// that optionally expires. The delegatee can use up to SpendLimit to cover fees.
message BasicAllowance {
  // spend_limit specifies the maximum amount of tokens that can be spent
  // by this allowance and will be updated as tokens are spent. If it is
  // empty, there is no spend limit and any amount of coins can be spent.
  repeated cosmos_sdk.v1.Coin spend_limit = 1;

  // expiration specifies an optional time when this allowance expires
  google.protobuf.Timestamp expiration = 2;
}

// PeriodicAllowance extends FeeAllowanceI to allow for both a maximum cap,
// as well as a limit per time period.
message PeriodicAllowance {
  BasicAllowance basic = 1;

  // period specifies the time duration in which period_spend_limit coins can
  // be spent before that allowance is reset
  google.protobuf.Duration period = 2;

  // period_spend_limit specifies the maximum number of coins that can be spent
  // in the period
  repeated cosmos_sdk.v1.Coin period_spend_limit = 3;

  // period_can_spend is the number of coins left to be spent before the period_reset time
  repeated cosmos_sdk.v1.Coin period_can_spend = 4;

  // period_reset is the time at which this period resets and a new one begins,
  // it is calculated from the start time of the first transaction after the
  // last period ended
  google.protobuf.Timestamp period_reset = 5;
}

Allowances can be granted and revoked using MsgGrantAllowance and MsgRevokeAllowance:
// MsgGrantAllowance adds permission for Grantee to spend up to Allowance
// of fees from the account of Granter.
message MsgGrantAllowance {
     string granter = 1;
     string grantee = 2;
     google.protobuf.Any allowance = 3;
 }

 // MsgRevokeAllowance removes any existing FeeAllowance from Granter to Grantee.
 message MsgRevokeAllowance {
     string granter = 1;
     string grantee = 2;
 }
In order to use allowances in transactions, we add a new field granter to the transaction Fee type:
package cosmos.tx.v1beta1;

message Fee {
  repeated cosmos.base.v1beta1.Coin amount = 1;
  uint64 gas_limit = 2;
  string payer = 3;
  string granter = 4;
}
granter must either be left empty or must correspond to an account which has granted a fee allowance to fee payer (either the first signer or the value of the payer field). A new AnteDecorator named DeductGrantedFeeDecorator will be created in order to process transactions with fee_payer set and correctly deduct fees based on fee allowances.

Consequences

Positive

  • improved UX for use cases where it is cumbersome to maintain an account balance just for fees

Negative

Neutral

  • a new field must be added to the transaction Fee message and a new AnteDecorator must be created to use it

References

  • Blog article describing initial work: Link
  • Initial public specification: Link
  • Original subkeys proposal from B-harvest which influenced this design: Link