变更记录

  • 2023-06-12: 初始草案
  • 2024-06-06: 状态变更为已弃用

状态

已弃用

背景

创建 globalfee 模块是为了管理一个名为 MinimumGasPricesParam 的参数,该参数用于设置整个网络范围内的最低手续费要求。其目标是防止随机面额进入手续费归集,并减少验证者检查冗长交易手续费列表所需的时间。为了解决某些无需支付手续费、但仍需限制自愿支付手续费所用面额的场景,引入了 zero coins 作为限制面额的一种方式。然而,globalfee 模块的初始版本存在一些问题:
  • 在 globalfee 模块中,由于 MinimumGasPricesParam 允许出现零值 coin,重新定义了多个 Cosmos SDK 的 coin 方法。MinimumGasPricesParam 的类型是 sdk.DecCoins。在 Cosmos SDK 中,sdk.DecCoins 会被净化,以移除零值 coin。因此,Gaia fee antehandler 中重新定义了多个来自 sdk.Coins 的方法。
  • BypassMinFeeMsgTypes 存在于 app.toml 中,这意味着每个节点都可以定义自己的取值。因此,包含 bypass message 的交易是否会被豁免手续费并不明确。
  • 手续费检查逻辑只在 CheckTx 中执行。这会使恶意验证者有机会修改手续费检查代码,并提议不满足手续费要求的交易。

决策

为了解决这些问题,向 globalfee 模块中加入了以下改动:
  • MinimumGasPricesParam 中的 ZeroCoins:
    重构手续费检查逻辑,以便使用 Cosmos SDK 的 coin 方法,而不是重新定义的方法。
  • Bypass Message Types:
    将 BypassMinFeeMsgTypes 重构为 globalfee 模块的一个参数,从而使 bypass message 具备确定性。
  • 在 DeliverTx 中检查手续费:
    将手续费检查抽取为同时在 DeliverTx 和 CheckTx 中执行。这是为了防止恶意验证者修改手续费检查逻辑并允许任意交易通过手续费检查。因此,引入了 MinimumGasPricesParam 作为 globalfee 参数。

MinimumGasPricesParam 中的 ZeroCoins

Coins 拆分

CombinedFeeRequirement 表示同时考虑 globalFees(globalfee 模块中的 MinimumGasPricesParam)和 localFees(app.toml 中的 minimum-gas-prices)后的手续费要求。该要求的计算方式是:对于 globalFees 中存在的面额,取 globalFees 与 localFees 中的较大值。 globalfee 模块中的 MinimumGasPricesParam 允许包含 zero coin,这意味着 CombinedFeeRequirement(globalFees, localFees) 也会允许 zero coin。因此,CombinedFeeRequirement 不满足某些 sdk.Coins 方法的要求。例如,DenomsSubsetOf 方法要求 coin 集合中不能包含 zero coin。 为了解决这个问题,CombinedFeeRequirement 和 feeCoins 按照下图所示进行了拆分。 CombinedFeeRequirement 会被拆分为 zero coin 和非 zero coin,形成 nonZeroCoinFeesReq 与 zeroCoinFeesDenomReq。类似地,已支付的手续费(feeCoins)也会按照 nonZeroCoinFeesReq 和 zeroCoinFeesDenomReq 的面额拆分为 feeCoinsNonZeroDenom 和 feeCoinsZeroDenom,如下方代码片段所示。
nonZeroCoinFeesReq, zeroCoinFeesDenomReq := getNonZeroFees(feeRequired)

	// feeCoinsNonZeroDenom contains non-zero denominations from the feeRequired
	// feeCoinsNonZeroDenom is used to check if the fees meets the requirement imposed by nonZeroCoinFeesReq
	// when feeCoins does not contain zero coins' denoms in feeRequired
	feeCoinsNonZeroDenom, feeCoinsZeroDenom := splitCoinsByDenoms(feeCoins, zeroCoinFeesDenomReq)

手续费检查

feeCheck 的工作流如下所示: 这种拆分使得可以分别用 nonZeroCoinFeesReq 检查 feeCoinsNonZeroDenom,以及用 zeroCoinFeesDenomReq 检查 feeCoinsZeroDenom (如下方代码片段所示)。在使用 nonZeroCoinFeesReq 检查 feeCoinsNonZeroDenom 时,由于 nonZeroCoinFeesReq 中已移除 zero coin,因此可以直接使用 Cosmos SDK 的 coin 方法;而在使用 zeroCoinFeesDenomReq 检查 feeCoinsZeroDenom 时,只需要检查面额即可。 使用 nonZeroCoinFeesReq 检查 feeCoinsNonZeroDenom:
if !feeCoinsNonZeroDenom.IsAnyGTE(nonZeroCoinFeesReq) {
    return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins.String(), feeRequired.String())
}
下面是一个在 fee antehandler 中进行 coin 拆分与检查的示例: 假设: globalfee=[1photon, 0uatom, 1stake] 且 local min-gas-prices=[0.5stake] 手续费要求: combinedFeeRequirement=[1photon, 0uatom, 1stake] 拆分手续费要求: 将 combinedFeeRequirement 拆分为 nonZeroCoinFeesReq=[0uatom],以及 nonZeroCoinFeesReq=[1photon, 1stake] 拆分已支付手续费: 如果 paidFee=[1uatom, 0.5photon], 则 splitCoinsByDenoms 会将 paidFee 拆分为 feeCoinsZeroDenom=[1uatom](其面额与 combinedFeeRequirement 中 zero coin 的面额相同),以及 feeCoinsNonZeroDenom=[0.5stake] 然后 feeCoinsZeroDenom=[1uatom] 会由 nonZeroCoinFeesReq=[1photon, 1stake] 进行检查。 请注意,feeCoins 本身不包含 zero coin。手续费 coin 的拆分依据是 zeroCoinFeesDenomReq 或 nonZeroCoinFeesDenomReq 中的面额。如果 feeCoins 中包含的 coin 不属于 zeroCoinFeesDenomReq 和 nonZeroCoinFeesDenomReq 这两者中的任意一方,则交易应被拒绝。反之,如果 feeCoins 的面额属于 zeroCoinFeesDenomReq 或 nonZeroCoinFeesDenomReq 中的任意一方,并且 len(zeroCoinFeesDenomReq)!=0,则交易可以直接通过;否则,仍需检查手续费金额。

Bypass Message Types

在重构之前,BypassMinFeeMsgTypes 是 config/app.toml 中的一项配置。为实现网络级别的一致性,BypassMinFeeMsgTypes 被重构为 globalfee 模块的一个参数。相应地,MaxTotalBypassMinFeeMsgGasUsage 也被引入为 globalfee 参数。

DeliverTx 中的手续费检查

在 DeliverTx 函数中实现手续费检查,会带来以下几个要求:
  • 确定性的最低手续费要求:对于 DeliverTx 流程,必须具备确定性的最低手续费要求。在 CheckTx 中,手续费通过 CombinedFeeRequirement(globalFees, localFees) 进行检查,该要求同时考虑了来自 config/app.toml 的 minimum-gas-prices 和来自 globalfee Params 的 MinimumGasPricesParam(更多细节见 globalfee)。CombinedFeeRequirement 包含非确定性部分,即来自 app.toml 的 minimum-gas-prices。因此,CombinedFeeRequirement 不能用于 DeliverTx。在 DeliverTx 中,手续费验证只使用 globalfee Params 中的 MinimumGasPricesParam。代码实现如下。
func (mfd FeeDecorator)

GetTxFeeRequired(ctx sdk.Context, tx sdk.FeeTx) (sdk.Coins, error) {
	// Get required global fee min gas prices
	// Note that it should never be empty since its default value is set to coin={"StakingBondDenom", 0
}

globalFees, err := mfd.GetGlobalFee(ctx, tx)
    if err != nil {
    return sdk.Coins{
}, err
}

	// In DeliverTx, the global fee min gas prices are the only tx fee requirements.
    if !ctx.IsCheckTx() {
    return globalFees, nil
}

	// In CheckTx mode, the local and global fee min gas prices are combined
	// to form the tx fee requirements

	// Get local minimum-gas-prices
    localFees := GetMinGasPrice(ctx, int64(tx.GetGas()))

	// Return combined fee requirements
	return CombinedFeeRequirement(globalFees, localFees)
}
  • 确定性的绕过参数:某条消息是否可以绕过最低手续费的判定也必须具备确定性。为此,BypassMinFeeMsgTypes 和 MaxTotalBypassMinFeeMsgGasUsage 参数被迁移到持久化存储中。
  • 模块初始化顺序:genutils 模块必须在 globalfee 模块之前初始化。这是因为 genutils 模块中的 DeliverGenTxs 会在 initGenesis 期间被调用。该函数会执行 DeliverTx,随后调用 FeeDecorator 中的 AnteHandle,从而触发 DeliverTx 中的手续费检查。 为防止 DeliverGenTxs 经过手续费检查,globalfee 模块应在 genutils 模块之后初始化。这样的顺序可以确保在手续费检查发生时,所有必要组件都已就位。更多背景可参见 Gaia Issue #2489。

影响

正面影响

这次重构使代码更易于维护。它能够防止恶意验证者绕过手续费检查,并使 bypass message 在网络层面生效。

负面影响

引入 FeeDecorator 后,替代了 Cosmos SDK 中 MempoolFeeDecorator 的用法。当前,如果在 AnteDecorator 链中同时加入 FeeDecorator 和 MempoolFeeDecorator,会导致重复检查。不过,未来随着 Cosmos SDK 的更新,FeeDecorator 和 MempoolFeeDecorator 之间也可能出现不兼容的情况。

Changelog

  • 2023-06-12: Initial Draft
  • 2024-06-06: Change status to deprecated

Status

Deprecated

Context

The globalfee module was created to manage a parameter called MinimumGasPricesParam, which sets a network-wide minimum fee requirement. The intention was to stop random denominations from entering fee collections and to reduce the time validators take to check a long list of transaction fees. To address scenarios where no fee payment is required but the denominations for volunteered paid fees are still restricted, the zero coins was introduced to serve as a means of limiting the denoms. Nevertheless, the initial version of the globalfee module had some issues:
  • In the globalfee module, several Cosmos SDK coins methods were redefined because of the allowance of zero-value coins in the MinimumGasPricesParam. The MinimumGasPricesParam is of sdk.DecCoins type. In the Cosmos SDK, sdk.DecCoins are sanitized to remove zero-value coins. As a result, several methods from sdk.Coins were redefined in the Gaia fee antehandler.
  • BypassMinFeeMsgTypes exists in app.toml, which means each node can define its own value. Thus, it’s not clear whether a transaction containing bypass-messages will be exempted from paying a fee.
  • The fee check logic is only executed in CheckTx. This could enable malicious validators to change the fee check code and propose transactions that do not meet the fee requirement.

Decision

To fix these problems, the following changes are added to the globalfee module:
  • ZeroCoins in MinimumGasPricesParam:
    Refactor the fee check logics, in order to use the Cosmos SDK coins’ methods instead of the redefined methods.
  • Bypass Message Types:
    BypassMinFeeMsgTypes is refactored to be a param of the globalfee module, in order to make the bypass messages deterministic.
  • Check Fees in DeliverTx:
    The fee check is factored to executed in both DeliverTx and CheckTx. This is to prevent malicious validators from changing the fee check logic and allowing any transactions to pass fee check. As a consequence, MinimumGasPricesParam is introduced as a globalfee param.

ZeroCoins in MinimumGasPricesParam

Coins Split

CombinedFeeRequirement refers to the fee requirement that takes into account both globalFees (MinimumGasPricesParam in the globalfee module) and localFees (minimum-gas-prices in app.toml). This requirement is calculated as the maximum value between globalFees and localFees for denomination exists globalFees. The allowance of zero coins in the MinimumGasPricesParam within the globalfee module implies that CombinedFeeRequirement(globalFees, localFees) also permits zero coins. Therefore, the CombinedFeeRequirement doesn’t meet the requirements of certain sdk.Coins methods. For instance, the DenomsSubsetOf method requires coins that do not contain zero coins. To address this issue, the CombinedFeeRequirement and feeCoins are split as shown in the chart below. The CombinedFeeRequirement is split into zero and non-zero coins, forming nonZeroCoinFeesReq and zeroCoinFeesDenomReq. Similarly, the paid fees (feeCoins) are split into feeCoinsNonZeroDenom and feeCoinsZeroDenom, based on the denominations of nonZeroCoinFeesReq and zeroCoinFeesDenomReq as shown in the following code snippet.
nonZeroCoinFeesReq, zeroCoinFeesDenomReq := getNonZeroFees(feeRequired)

	// feeCoinsNonZeroDenom contains non-zero denominations from the feeRequired
	// feeCoinsNonZeroDenom is used to check if the fees meets the requirement imposed by nonZeroCoinFeesReq
	// when feeCoins does not contain zero coins' denoms in feeRequired
	feeCoinsNonZeroDenom, feeCoinsZeroDenom := splitCoinsByDenoms(feeCoins, zeroCoinFeesDenomReq)

Fee Checks

The Workflow of feeCheck is shown below: The split enable checking feeCoinsNonZeroDenom against nonZeroCoinFeesReq, and feeCoinsZeroDenom against zeroCoinFeesDenomReq (as shown in the following code snippet). In the check of feeCoinsNonZeroDenom against nonZeroCoinFeesReq, the Cosmos SDK coins’ methods can be used since zero coins are removed from the nonZeroCoinFeesReq, while in the check feeCoinsZeroDenom against zeroCoinFeesDenomReq, only denoms need to be checked. Checking feeCoinsNonZeroDenom against nonZeroCoinFeesReq:
if !feeCoinsNonZeroDenom.IsAnyGTE(nonZeroCoinFeesReq) {
    return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins.String(), feeRequired.String())
}
Here is an example of how the coins split and checked in fee antehandler: assumption: globalfee=[1photon, 0uatom, 1stake] and local min-gas-prices=[0.5stake] fee requirement: combinedFeeRequirement=[1photon, 0uatom, 1stake] split fee requirement: the combinedFeeRequirement into nonZeroCoinFeesReq=[0uatom], and nonZeroCoinFeesReq=[1photon, 1stake] split the paid fees: if paidFee=[1uatom, 0.5photon], the splitCoinsByDenoms splits the paidFee into feeCoinsZeroDenom=[1uatom] (the same denom as zero coins in combinedFeeRequirement), and feeCoinsNonZeroDenom=[0.5stake] then feeCoinsZeroDenom=[1uatom] is checked by nonZeroCoinFeesReq=[1photon, 1stake]. Please note that feeCoins does not contain zero coins. The fee coins are split according to the denoms in zeroCoinFeesDenomReq or nonZeroCoinFeesDenomReq. If feeCoins contains coins not in both zeroCoinFeesDenomReq and nonZeroCoinFeesDenomReq, the transaction should be rejected. On the contrary, if feeCoins’ denoms are in either zeroCoinFeesDenomReq or nonZeroCoinFeesDenomReq, and len(zeroCoinFeesDenomReq)!=0, the transaction can directly pass, otherwise, the fee amount need to be checked.

Bypass Message Types

BypassMinFeeMsgTypes was a setup in config/app.toml before the refactor. BypassMinFeeMsgTypes is refactored to be a param of the globalfee module to get a network level agreement. Correspondingly,MaxTotalBypassMinFeeMsgGasUsage is also introduced as a globalfee param.

Fee Checks in DeliverTx

Implementing fee checks within the DeliverTx function introduces a few requirements:
  • Deterministic Minimum Fee Requirement: For the DeliverTx process, it is essential to have a deterministic minimum fee requirement. In CheckTx, fee is checked by the CombinedFeeRequirement(globalFees, localFees), which considers both minimum-gas-prices from config/app.toml and MinimumGasPricesParam from the globalfee Params (For more details, see globalfee). CombinedFeeRequirement contains non-deterministic part: minimum-gas-prices from app.toml. Therefore, CombinedFeeRequirement cannot be used in DeliverTx. In DeliverTx, only MinimumGasPricesParam in globalfee Params is used for fee verification. The code implementation is shown below.
func (mfd FeeDecorator)

GetTxFeeRequired(ctx sdk.Context, tx sdk.FeeTx) (sdk.Coins, error) {
	// Get required global fee min gas prices
	// Note that it should never be empty since its default value is set to coin={"StakingBondDenom", 0
}

globalFees, err := mfd.GetGlobalFee(ctx, tx)
    if err != nil {
    return sdk.Coins{
}, err
}

	// In DeliverTx, the global fee min gas prices are the only tx fee requirements.
    if !ctx.IsCheckTx() {
    return globalFees, nil
}

	// In CheckTx mode, the local and global fee min gas prices are combined
	// to form the tx fee requirements

	// Get local minimum-gas-prices
    localFees := GetMinGasPrice(ctx, int64(tx.GetGas()))

	// Return combined fee requirements
	return CombinedFeeRequirement(globalFees, localFees)
}
  • Deterministic Bypass Parameters: The decision of whether a message can bypass the minimum fee has to be deterministic as well. To ensure this, BypassMinFeeMsgTypes and MaxTotalBypassMinFeeMsgGasUsage parameters are moved to a persistent store.
  • Module Initialization Order: The genutils module must be initialized before the globalfee module. This is due to the DeliverGenTxs in the genutils module, is called during initGenesis. This function executes DeliverTx, which subsequently calls the AnteHandle in FeeDecorator, triggering the fee check in DeliverTx. To prevent the DeliverGenTxs go through a fee check, the initialization of the globalfee module should occur after the genutils module. This sequencing ensures that all necessary components are in place when the fee check occurs. See Gaia Issue #2489 for more context.

Consequences

Positive

This refactor results in code that is easier to maintain. It prevents malicious validators from escaping fee checks and make the bypass messages work at network level.

Negative

The introduction of FeeDecorator has replaced the usage of MempoolFeeDecorator in the Cosmos SDK. Currently, if both FeeDecorator and MempoolFeeDecorator are added to the AnteDecorator chain, it will result in redundant checks. However, there’s potential for FeeDecorator and MempoolFeeDecorator to become incompatible in the future, depending on updates to the Cosmos SDK.