费用分配

概述

PoA 模块实现了一种基于验证者权重的自定义费用分配机制。与标准的 Cosmos SDK x/distribution 模块不同,PoA 使用基于检查点的系统按比例向验证者分配费用,但不会自动发放。

费用如何累积

费用在 PoA 系统中的流转方式与标准 Cosmos SDK 不同:
  1. 区块费用:每个区块收集的交易手续费默认进入 fee_collector 模块账户;如果已配置,也可以进入 PoA 模块账户(参见费用路由设置)
  2. 检查点系统:在以下情况下,系统会为验证者更新已分配费用:
    • 任意验证者权重发生变化
    • 任意验证者提取费用
为什么要使用检查点?:这样可以在权重变化时保证分配公平。如果权重在某个周期中途发生变化,那么在新权重生效之前,费用会先按照旧的权重分布完成分配。 位置:x/poa/keeper/distribution.go:18

分配算法

基于检查点的分配

PoA 模块使用检查点系统,在验证者权重变化时公平地分配费用。系统不会在每个区块主动执行分配,而是在离散的检查点高效完成费用分配。 检查点触发条件:
  • 任意验证者权重变化(通过 MsgUpdateValidators)
  • 任意费用提取(通过 MsgWithdrawFees)
未分配费用计算: 在检查点时间 tt,计算未分配费用: Ut=Bcollector(t)−Atotal(t)U_t = B_{collector}(t) - A_{total}(t) 其中:
  • UtU_t = 检查点 tt 时的未分配费用
  • Bcollector(t)B_{collector}(t) = PoA 模块账户在时间 tt 的当前余额
  • Atotal(t)=∑i=1nFi(t)A_{total}(t) = \sum_{i=1}^{n} F_i(t) = 所有验证者此前已分配费用之和(如果尚未执行过检查点,则为 0)
按比例分配份额: 对于每个活跃验证者 ii(其中 Pi(t)>0P_i(t) > 0),按其权重占比分配份额: Si(t)=Ut×Pi(t)Ptotal(t)S_i(t) = U_t \times \frac{P_i(t)}{P_{total}(t)} 其中:
  • Si(t)S_i(t) = 在检查点 tt 分配给验证者 ii 的份额
  • Pi(t)P_i(t) = 验证者 ii 在检查点 tt 的投票权重
  • Ptotal(t)=∑j=1nPj(t)P_{total}(t) = \sum_{j=1}^{n} P_j(t) = 所有验证者权重之和
累计费用更新: 分配完成后,更新每个验证者的累计费用: Fi(t+1)=Fi(t)+Si(t)F_i(t+1) = F_i(t) + S_i(t) 其中:
  • Fi(t)F_i(t) = 检查点前验证者 ii 的累计费用
  • Fi(t+1)F_i(t+1) = 检查点后验证者 ii 的累计费用
  • Si(t)S_i(t) = 本次检查点分配的份额
总已分配额跟踪: 更新全局已分配跟踪值: Atotal(t+1)=Atotal(t)+UtA_{total}(t+1) = A_{total}(t) + U_t 在该检查点之后,Atotal(t+1)=Bcollector(t)A_{total}(t+1) = B_{collector}(t)(即所有费用都已完成分配)。

检查点序列示例

初始状态(检查点前):
  • PoA 模块账户余额:Bcollector=1000B_{collector} = 1000 代币
  • 总已分配额:Atotal=400A_{total} = 400 代币(来自之前的检查点)
  • 验证者 A:PA=50P_A = 50,已分配 FA=200F_A = 200 代币
  • 验证者 B:PB=50P_B = 50,已分配 FB=200F_B = 200 代币
  • 总权重:Ptotal=100P_{total} = 100
管理员操作:管理员提交 MsgUpdateValidators,将权重分布调整为 30/70 触发检查点(在权重变更生效前):
  1. 计算未分配额:U=1000−400=600U = 1000 - 400 = 600 代币
  2. 按当前权重(50/50)分配份额:
    • 验证者 A:SA=600×50100=300S_A = 600 \times \frac{50}{100} = 300 代币
    • 验证者 B:SB=600×50100=300S_B = 600 \times \frac{50}{100} = 300 代币
  3. 更新累计费用:
    • 验证者 A:FA=200+300=500F_A = 200 + 300 = 500 代币
    • 验证者 B:FB=200+300=500F_B = 200 + 300 = 500 代币
  4. 更新总已分配额:Atotal=400+600=1000A_{total} = 400 + 600 = 1000 代币
检查点之后 - 权重变更已生效:
  • 验证者 A:PA=30P_A = 30(未来区块使用的新权重)
  • 验证者 B:PB=70P_B = 70(未来区块使用的新权重)
  • 现在 1000 代币都已完成分配(Atotal=BcollectorA_{total} = B_{collector})
  • 每个验证者都拥有更新后的 FiF_i 可供提取
这为什么重要?:验证者 A 在这些费用被收集的期间内拥有 50% 的权重,因此应获得 300 代币。检查点之后,它的权重降至 30%,因此未来费用将按 30/70 分配。检查点机制确保验证者按照其实际完成的工作获得奖励。 精度:使用 DecCoins(十进制币种)来防止舍入残余累积。每个验证者都会跟踪那些小到暂时无法提取的小数金额。

提取费用

MsgWithdrawFees (x/poa/keeper/msg_server.go:91) 任何验证者操作员都可以提取累计费用:
  1. 提交提取请求:由操作员地址签名
  2. 执行检查点:系统会先对所有验证者执行检查点(分配所有待处理费用)
  3. 截断:将十进制币种截断为整数币种
  4. 转账:从 PoA 模块账户向操作员地址转账
  5. 更新跟踪:总已分配额按提取数量减少
  6. 余数:小数余量保留在验证者的已分配余额中
示例:
Validator has: 100.7543 utokens allocated
Withdrawal:    100 utokens transferred to operator
Remainder:     0.7543 utokens remain allocated (less than least significant utoken digit)
位置:x/poa/keeper/distribution.go:106

提取公式

当验证者 ii 提取费用时: Wi=⌊Fi⌋W_i = \lfloor F_i \rfloor Fi′=Fi−WiF_i' = F_i - W_i Atotal′=Atotal−WiA_{total}' = A_{total} - W_i 其中:
  • WiW_i = 提取数量(截断为整数币种)
  • FiF_i = 提取前验证者的已分配费用
  • Fi′F_i' = 提取后验证者的已分配费用(十进制余量)
  • ⌊Fi⌋\lfloor F_i \rfloor = 向下取整函数(截断小数)
  • Atotal′A_{total}' = 所有验证者更新后的总已分配额

费用路由设置

PoA 拥有自己的模块账户用于收集费用。建议启用 PoA 模块账户,以便让费用记账保持隔离且准确。如果未启用,费用默认会进入标准的 fee_collector 账户。 要启用 PoA 模块账户,需要完成两处接线变更:

1. 注册 PoA 模块账户

在传给 authkeeper.NewAccountKeeper 的 maccPerms 映射中注册 poatypes.ModuleName:
app.AccountKeeper = authkeeper.NewAccountKeeper(
    appCodec,
    runtime.NewKVStoreService(storeKeys[authtypes.StoreKey]),
    authtypes.ProtoBaseAccount,
    map[string][]string{
        authtypes.FeeCollectorName: nil,
        govtypes.ModuleName:        {authtypes.Burner, authtypes.Staking},
        poatypes.ModuleName:        nil, // register PoA module account
    },
    // ...
)
来源:simapp/app.go

2. 配置 Ante Handler

在 NewDeductFeeDecorator 上使用 WithFeeRecipientModule,将费用路由到 PoA 模块账户:
anteDecorators := []sdk.AnteDecorator{
    ante.NewSetUpContextDecorator(),
    ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker),
    ante.NewValidateBasicDecorator(),
    ante.NewTxTimeoutHeightDecorator(),
    ante.NewValidateMemoDecorator(options.AccountKeeper),
    ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
    ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker).
        WithFeeRecipientModule(poatypes.ModuleName), // redirect fees to PoA module account
    ante.NewSetPubKeyDecorator(options.AccountKeeper),
    ante.NewValidateSigCountDecorator(options.AccountKeeper),
    ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer),
    ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler, options.SigVerifyOptions...),
    ante.NewIncrementSequenceDecorator(options.AccountKeeper),
}
来源:simapp/ante.go WithFeeRecipientModule 向后兼容;如果省略它,默认仍会使用标准的 fee_collector 行为。

安全性注意事项

  1. 十进制精度:
    • 使用 DecCoins 以防止残余累积
    • 验证者会跟踪小数金额
    • 余数会在多次提取之间保留
    • 防止舍入误差持续累积

Fee Distribution

Overview

The PoA module implements a custom fee distribution mechanism based on validator power. Unlike the standard Cosmos SDK x/distribution module, PoA uses a checkpoint-based system to allocate fees proportionally to validators without automatic distribution.

How Fees Accumulate

Fees flow through the PoA system differently than standard Cosmos SDK:
  1. Block Fees: Transaction fees collected in each block go to the fee_collector module account by default, or to the PoA module account if configured (see Fee Routing Setup)
  2. Checkpoint System: Allocated fees are updated for validators when:
    • Any validator power changes
    • Any validator withdraws fees
Why Checkpointing?: Ensures fair distribution when power changes. If power changes mid-period, fees are allocated based on old power distribution before the change takes effect. Location: x/poa/keeper/distribution.go:18

Distribution Algorithm

Checkpoint-Based Allocation

The PoA module uses a checkpoint system to allocate fees fairly when validator power changes. Rather than distributing fees actively at every block, allocation efficiently happens at discrete checkpoints. Checkpoint Triggers:
  • Any validator power change (via MsgUpdateValidators)
  • Any fee withdrawal (via MsgWithdrawFees)
Unallocated Fees Calculation: At checkpoint time tt, calculate unallocated fees: Ut=Bcollector(t)−Atotal(t)U_t = B_{collector}(t) - A_{total}(t) Where:
  • UtU_t = unallocated fees at checkpoint tt
  • Bcollector(t)B_{collector}(t) = current balance in the PoA module account
  • Atotal(t)=∑i=1nFi(t)A_{total}(t) = \sum_{i=1}^{n} F_i(t) = sum of all previously- allocated fees across all validators (0 if no checkpoints have been done)
Proportional Share Allocation: For each active validator ii (where Pi(t)>0P_i(t) > 0), allocate a share proportional to their power: Si(t)=Ut×Pi(t)Ptotal(t)S_i(t) = U_t \times \frac{P_i(t)}{P_{total}(t)} Where:
  • Si(t)S_i(t) = share allocated to validator ii at checkpoint tt
  • Pi(t)P_i(t) = voting power of validator ii at checkpoint tt
  • Ptotal(t)=∑j=1nPj(t)P_{total}(t) = \sum_{j=1}^{n} P_j(t) = sum of all validator powers
Accumulated Fees Update: After allocation, update each validator’s accumulated fees: Fi(t+1)=Fi(t)+Si(t)F_i(t+1) = F_i(t) + S_i(t) Where:
  • Fi(t)F_i(t) = validator ii‘s accumulated fees before checkpoint
  • Fi(t+1)F_i(t+1) = validator ii‘s accumulated fees after checkpoint
  • Si(t)S_i(t) = share allocated in this checkpoint
Total Allocated Tracking: Update the global allocated tracker: Atotal(t+1)=Atotal(t)+UtA_{total}(t+1) = A_{total}(t) + U_t After this checkpoint, Atotal(t+1)=Bcollector(t)A_{total}(t+1) = B_{collector}(t) (all fees are now allocated).

Example Checkpoint Sequence

Initial State (before checkpoint):
  • PoA module account balance: Bcollector=1000B_{collector} = 1000 tokens
  • Total allocated: Atotal=400A_{total} = 400 tokens (from previous checkpoints)
  • Validator A: PA=50P_A = 50, FA=200F_A = 200 tokens allocated
  • Validator B: PB=50P_B = 50, FB=200F_B = 200 tokens allocated
  • Total power: Ptotal=100P_{total} = 100
Admin Action: Admin submits MsgUpdateValidators to change power distribution to 30/70 Checkpoint Triggered (before power change takes effect):
  1. Calculate unallocated: U=1000−400=600U = 1000 - 400 = 600 tokens
  2. Allocate shares based on current power (50/50):
    • Validator A: SA=600×50100=300S_A = 600 \times \frac{50}{100} = 300 tokens
    • Validator B: SB=600×50100=300S_B = 600 \times \frac{50}{100} = 300 tokens
  3. Update accumulated fees:
    • Validator A: FA=200+300=500F_A = 200 + 300 = 500 tokens
    • Validator B: FB=200+300=500F_B = 200 + 300 = 500 tokens
  4. Update total allocated: Atotal=400+600=1000A_{total} = 400 + 600 = 1000 tokens
After Checkpoint - Power Change Applied:
  • Validator A: PA=30P_A = 30 (new power for future blocks)
  • Validator B: PB=70P_B = 70 (new power for future blocks)
  • All 1000 tokens now allocated (Atotal=BcollectorA_{total} = B_{collector})
  • Each validator has updated FiF_i available for withdrawal
Why This Matters: Validator A earned 300 tokens (50% share) based on their power during the period when those fees were collected. After the checkpoint, their power drops to 30%, so future fees will be split 30/70. Checkpointing ensures validators are rewarded based on the work they actually performed. Precision: Uses DecCoins (decimal coins) to prevent rounding dust accumulation. Each validator tracks fractional amounts that are too small to withdraw.

Withdrawing Fees

MsgWithdrawFees (x/poa/keeper/msg_server.go:91) Any validator operator can withdraw accumulated fees:
  1. Submit Withdrawal: Signed by operator address
  2. Checkpoint: System checkpoints all validators first (allocates any pending fees)
  3. Truncate: Decimal coins truncated to whole coins
  4. Transfer: Coins transferred from the PoA module account to operator address
  5. Update Tracking: Total allocated decreases by withdrawn amount
  6. Remainder: Decimal remainder stays in validator’s allocated balance
Example:
Validator has: 100.7543 utokens allocated
Withdrawal:    100 utokens transferred to operator
Remainder:     0.7543 utokens remain allocated (less than least significant utoken digit)
Location: x/poa/keeper/distribution.go:106

Withdrawal Formula

When validator ii withdraws fees: Wi=⌊Fi⌋W_i = \lfloor F_i \rfloor Fi′=Fi−WiF_i' = F_i - W_i Atotal′=Atotal−WiA_{total}' = A_{total} - W_i Where:
  • WiW_i = amount withdrawn (truncated to integer coins)
  • FiF_i = validator’s allocated fees before withdrawal
  • Fi′F_i' = validator’s allocated fees after withdrawal (decimal remainder)
  • ⌊Fi⌋\lfloor F_i \rfloor = floor function (truncate decimals)
  • Atotal′A_{total}' = updated total allocated across all validators

Fee Routing Setup

PoA has its own module account for collecting fees. Enabling the PoA module account is recommended to keep fee accounting isolated and accurate. If not enabled, fees are deposited into the standard fee_collector account by default. To enable the PoA module account, two wiring changes are required:

1. Register the PoA Module Account

Register poatypes.ModuleName in the maccPerms map passed to authkeeper.NewAccountKeeper:
app.AccountKeeper = authkeeper.NewAccountKeeper(
    appCodec,
    runtime.NewKVStoreService(storeKeys[authtypes.StoreKey]),
    authtypes.ProtoBaseAccount,
    map[string][]string{
        authtypes.FeeCollectorName: nil,
        govtypes.ModuleName:        {authtypes.Burner, authtypes.Staking},
        poatypes.ModuleName:        nil, // register PoA module account
    },
    // ...
)
Source: simapp/app.go

2. Configure the Ante Handler

Use WithFeeRecipientModule on NewDeductFeeDecorator to route fees to the PoA module account:
anteDecorators := []sdk.AnteDecorator{
    ante.NewSetUpContextDecorator(),
    ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker),
    ante.NewValidateBasicDecorator(),
    ante.NewTxTimeoutHeightDecorator(),
    ante.NewValidateMemoDecorator(options.AccountKeeper),
    ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
    ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker).
        WithFeeRecipientModule(poatypes.ModuleName), // redirect fees to PoA module account
    ante.NewSetPubKeyDecorator(options.AccountKeeper),
    ante.NewValidateSigCountDecorator(options.AccountKeeper),
    ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer),
    ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler, options.SigVerifyOptions...),
    ante.NewIncrementSequenceDecorator(options.AccountKeeper),
}
Source: simapp/ante.go WithFeeRecipientModule is backwards compatible — omitting it defaults to the standard fee_collector behavior.

Security Considerations

  1. Decimal Precision:
    • Uses DecCoins to prevent dust accumulation
    • Validators track fractional amounts
    • Remainders preserved across withdrawals
    • Prevents rounding errors from accumulating