变更记录

  • 2020-01-08:初始版本
  • 2020-01-09:为处理 vesting 账户所做的调整
  • 2020-01-14:根据评审反馈更新
  • 2020-01-30:根据实现情况更新

术语表

  • denom / 面额键:唯一的代币标识符。

背景

在无许可 IBC 的场景下,任何人都可以向任何其他账户发送任意面额的代币。当前,所有非零余额都与账户一起存储在 sdk.Coins 结构中,这会带来潜在的拒绝服务风险,因为每次修改账户时,如果面额数量过多,加载和存储的成本都会变得很高。更多背景可参见议题 5467 和 4982。 仅仅在面额数量达到上限后拒绝新的入账并不可行,因为这会引入一种恶意骚扰路径:有人可以通过 IBC 向某个用户发送大量无意义的代币,从而阻止该用户接收真实且有价值的面额(例如质押奖励)。

决策

余额应按账户和面额分别存储在一个对面额和账户均唯一的键下,从而可以对特定账户在特定面额下的余额实现 O(1) 的读写访问。

账户接口(x/auth)

由于代币余额现在将由 bank 模块存储和管理,GetCoins() 和 SetCoins() 将从账户接口中移除。 vesting 账户接口将以 LockedCoins 替代 SpendableCoins,因为前者不再需要账户余额。此外,TrackDelegation() 现在将接收 vesting 余额中所有代币的账户余额,而不是加载整个账户余额。 vesting 账户仍将继续存储 original vesting、delegated free 和 delegated vesting 代币(这是安全的,因为这些字段不会包含任意面额)。

Bank keeper(x/bank)

以下 API 将被加入 x/bank keeper:
  • GetAllBalances(ctx Context, addr AccAddress) Coins
  • GetBalance(ctx Context, addr AccAddress, denom string) Coin
  • SetBalance(ctx Context, addr AccAddress, coin Coin)
  • LockedCoins(ctx Context, addr AccAddress) Coins
  • SpendableCoins(ctx Context, addr AccAddress) Coins
还可以补充其他 API,以支持遍历以及那些对核心功能或持久化并非必需的辅助能力。 余额将优先按地址、再按面额存储(反过来也可以,但假定获取单个账户全部余额的场景会更常见):
var BalancesPrefix = []byte("balances")

func (k Keeper)

SetBalance(ctx Context, addr AccAddress, balance Coin)

error {
    if !balance.IsValid() {
    return err
}
    store := ctx.KVStore(k.storeKey)
    balancesStore := prefix.NewStore(store, BalancesPrefix)
    accountStore := prefix.NewStore(balancesStore, addr.Bytes())
    bz := Marshal(balance)

accountStore.Set([]byte(balance.Denom), bz)

return nil
}
这将使余额按 balances/{address}/{denom} 的字节表示形式建立索引。 DelegateCoins() 和 UndelegateCoins() 将被调整为:仅按(反)委托金额中出现的面额,分别加载对应的账户余额。因此,对账户余额的任何变更都将按面额进行。 SubtractCoins() 和 AddCoins() 将被调整为直接读写余额,而不是调用 GetCoins() / SetCoins()(这两个接口将不再存在)。 trackDelegation() 和 trackUndelegation() 将被调整为不再更新账户余额。 为了保持向后兼容,外部 API 需要扫描某个账户下的全部余额。建议这些 API 在可能的情况下使用 GetBalance 和 SetBalance,而不是 GetAllBalances,以避免加载整个账户余额。

Supply 模块

为了实现总供应量不变量,supply 模块现在需要扫描所有账户,并通过 x/bank Keeper 调用 GetAllBalances,随后汇总这些余额并检查其是否与预期的总供应量一致。

状态

已接受。

影响

正面

  • 余额读写可实现 O(1) 复杂度(相对于账户具有非零余额的面额数量而言)。注意,这里并不是指实际 I/O 成本,而是所需直接读取次数的总量。

负面

  • 在一次交易中读取和写入单个账户的全部余额时,效率会略低一些。

中性

无特别影响。

参考


Changelog

  • 2020-01-08: Initial version
  • 2020-01-09: Alterations to handle vesting accounts
  • 2020-01-14: Updates from review feedback
  • 2020-01-30: Updates from implementation

Glossary

  • denom / denomination key — unique token identifier.

Context

With permissionless IBC, anyone will be able to send arbitrary denominations to any other account. Currently, all non-zero balances are stored along with the account in an sdk.Coins struct, which creates a potential denial-of-service concern, as too many denominations will become expensive to load & store each time the account is modified. See issues 5467 and 4982 for additional context. Simply rejecting incoming deposits after a denomination count limit doesn’t work, since it opens up a griefing vector: someone could send a user lots of nonsensical coins over IBC, and then prevent the user from receiving real denominations (such as staking rewards).

Decision

Balances shall be stored per-account & per-denomination under a denomination- and account-unique key, thus enabling O(1) read & write access to the balance of a particular account in a particular denomination.

Account interface (x/auth)

GetCoins() and SetCoins() will be removed from the account interface, since coin balances will now be stored in & managed by the bank module. The vesting account interface will replace SpendableCoins in favor of LockedCoins which does not require the account balance anymore. In addition, TrackDelegation() will now accept the account balance of all tokens denominated in the vesting balance instead of loading the entire account balance. Vesting accounts will continue to store original vesting, delegated free, and delegated vesting coins (which is safe since these cannot contain arbitrary denominations).

Bank keeper (x/bank)

The following APIs will be added to the x/bank keeper:
  • GetAllBalances(ctx Context, addr AccAddress) Coins
  • GetBalance(ctx Context, addr AccAddress, denom string) Coin
  • SetBalance(ctx Context, addr AccAddress, coin Coin)
  • LockedCoins(ctx Context, addr AccAddress) Coins
  • SpendableCoins(ctx Context, addr AccAddress) Coins
Additional APIs may be added to facilitate iteration and auxiliary functionality not essential to core functionality or persistence. Balances will be stored first by the address, then by the denomination (the reverse is also possible, but retrieval of all balances for a single account is presumed to be more frequent):
var BalancesPrefix = []byte("balances")

func (k Keeper)

SetBalance(ctx Context, addr AccAddress, balance Coin)

error {
    if !balance.IsValid() {
    return err
}
    store := ctx.KVStore(k.storeKey)
    balancesStore := prefix.NewStore(store, BalancesPrefix)
    accountStore := prefix.NewStore(balancesStore, addr.Bytes())
    bz := Marshal(balance)

accountStore.Set([]byte(balance.Denom), bz)

return nil
}
This will result in the balances being indexed by the byte representation of balances/{address}/{denom}. DelegateCoins() and UndelegateCoins() will be altered to only load each individual account balance by denomination found in the (un)delegation amount. As a result, any mutations to the account balance by will made by denomination. SubtractCoins() and AddCoins() will be altered to read & write the balances directly instead of calling GetCoins() / SetCoins() (which no longer exist). trackDelegation() and trackUndelegation() will be altered to no longer update account balances. External APIs will need to scan all balances under an account to retain backwards-compatibility. It is advised that these APIs use GetBalance and SetBalance instead of GetAllBalances when possible as to not load the entire account balance.

Supply module

The supply module, in order to implement the total supply invariant, will now need to scan all accounts & call GetAllBalances using the x/bank Keeper, then sum the balances and check that they match the expected total supply.

Status

Accepted.

Consequences

Positive

  • O(1) reads & writes of balances (with respect to the number of denominations for which an account has non-zero balances). Note, this does not relate to the actual I/O cost, rather the total number of direct reads needed.

Negative

  • Slightly less efficient reads/writes when reading & writing all balances of a single account in a transaction.

Neutral

None in particular.

References