Diem 交易手续费模块深度解析:TransactionFee 的初始化、累积与链上销毁机制
Diem 交易手续费模块深度解析:TransactionFee 的初始化、累积与链上销毁机制
导读
本文深入剖析 Diem 区块链网络中负责收集与销毁交易手续费(gas fee)的 Move 模块 0x1::TransactionFee,围绕该模块的资源定义、Genesis 初始化流程、费用收取、以及由 Treasury Compliance 账户触发的批量销毁(burn)机制展开。文中所有实现均以仓库 language/diem-framework/modules/TransactionFee.move 为事实依据,并结合 DiemAccount、Genesis、TreasuryComplianceScripts 与 Move Prover 规范,说明交易手续费从用户扣款、链上归集到最终销毁的完整生命周期。读完本文,你将掌握该模块的四个公开入口函数的语义、权限约束与调用场景,并能在此基础上理解 Diem 经济模型中“手续费不流向矿工/验证者,而是由协会链下回收”这一设计。
一、模块定位:手续费的生命周期管理
在 Diem 链上,交易执行消耗的 gas 由发送账户以某种已注册货币(默认 XUS)支付。这笔费用不会像传统公链那样奖励给验证者,而是先汇聚到一个链上“费用桶”(fees bucket)中,再由 Treasury Compliance(TC,财务合规)账户定期执行销毁,使对应法币准备金可以在链下被协会回收。TransactionFee 模块正是这个“桶”的定义者与管理者:
- 初始化(initialize):在 Genesis 阶段为 TC 账户建立收集手续费所需的资源;
- 收取(pay_fee):在每笔交易的 epilogue 阶段,把实际 gas 费用存入桶中;
- 累积(balance 字段):桶内的
Diem<CoinType>余额随每笔交易持续累积; - 销毁(burn_fees):由 TC 账户一次性预烧(preburn)并销毁自上次销毁以来累积的全部费用,将对应价值从链上总市值(market cap)中扣除。
模块文件顶部注释即言明其职责——"Functions to initialize, accumulated, and burn transaction fees"(初始化、累积并销毁交易手续费)。该模块的 Move 源码位于 language/diem-framework/modules/TransactionFee.move,本文引用的是由源码自动生成的配套 API 文档 language/diem-framework/modules/doc/TransactionFee.md,其中同时包含实现代码与形式化规范(specification)。
二、核心资源:TransactionFee
2.1 资源结构与设计意图
模块唯一的资源 TransactionFee<CoinType> 带有一个 phantom CoinType 类型参数,意味着同一种货币只对应一份资源,其字段如下:
/// The `TransactionFee` resource holds a preburn resource for each
/// fiat `CoinType` that can be collected as a transaction fee.
struct TransactionFee<phantom CoinType> has key {
balance: Diem<CoinType>,
preburn: Preburn<CoinType>,
}
| 字段 | 类型 | 语义 |
|---|---|---|
balance |
Diem<CoinType> |
当前已收集但尚未销毁的手续费余额 |
preburn |
Diem::Preburn<CoinType> |
销毁时使用的预烧(preburn)资源 |
两个字段各司其职:balance 是累积中的“桶”;preburn 则是销毁时借用的预烧通道。整个 TransactionFee<CoinType> 资源发布在 TC 账户地址 @TreasuryCompliance(十六进制地址 0xB1E55ED)之下。由于该资源声明了 has key 能力,它可以作为全局存储项被 move_to 发布、被 borrow_global_mut 借用以修改。
2.2 状态检查与错误常量
模块只定义一个错误常量:
/// A `TransactionFee` resource is not in the required state
const ETRANSACTION_FEE: u64 = 0;
ETRANSACTION_FEE 作为错误原因码(reason code),配合 Errors::not_published、Errors::already_published、Errors::invalid_state 等错误类别使用,分别对应“该币种尚未被列为手续费货币”“该币种手续费资源已发布”“XDX 销毁路径尚未实现”三种状态异常。TreasuryComplianceScripts 的文档明确列出了 Errors::NOT_PUBLISHED + ETRANSACTION_FEE 表示"CoinType is not an accepted transaction fee currency"(该币种不是被接受的交易手续费货币),见 language/diem-framework/modules/TreasuryComplianceScripts.move。
2.3 初始化状态查询函数
模块提供两个查询函数供其他模块与脚本判断状态:
public fun is_coin_initialized<CoinType>(): bool {
exists<TransactionFee<CoinType>>(@TreasuryCompliance)
}
fun is_initialized(): bool {
is_coin_initialized<XUS>()
}
is_coin_initialized<CoinType>():检查指定CoinType是否已在 TC 账户下发布手续费资源;is_initialized():仅针对 XUS 判断模块是否整体初始化(因为 Genesis 只注册 XUS 一种手续费货币),该函数为模块私有(无public修饰)。
三、Genesis 初始化:initialize 与 add_txn_fee_currency
3.1 initialize:Genesis 唯一入口
initialize 只能在 Genesis 阶段调用一次,其实现如下:
public fun initialize(
tc_account: &signer,
) {
DiemTimestamp::assert_genesis();
Roles::assert_treasury_compliance(tc_account);
// accept fees in all the currencies
add_txn_fee_currency<XUS>(tc_account);
}
它依次完成三项校验与动作:
DiemTimestamp::assert_genesis():断言当前处于 Genesis 阶段(时间戳尚未开始计时),防止运行时被再次调用;Roles::assert_treasury_compliance(tc_account):断言调用者是 TC 账户,这是该模块所有写操作共用的权限门禁;add_txn_fee_currency<XUS>(tc_account):注册 XUS 为手续费货币。
Genesis 编排脚本 language/diem-framework/modules/Genesis.move 在完成 XUS::initialize、XDX::initialize、AccountFreezing::initialize 之后调用 TransactionFee::initialize(tc_account),随后才初始化验证者集合并启动链时间(DiemTimestamp::set_time_has_started)。这种顺序与模块级不变量"DiemTimestamp::is_operating() ==> is_initialized()"(一旦链开始运行,手续费资源必须已初始化)严格对应。
3.2 add_txn_fee_currency:注册单个币种
add_txn_fee_currency 是 initialize 的泛型扩展点,用于为任意已注册货币配置手续费收集状态:
public fun add_txn_fee_currency<CoinType>(tc_account: &signer) {
Roles::assert_treasury_compliance(tc_account);
Diem::assert_is_currency<CoinType>();
assert(
!is_coin_initialized<CoinType>(),
Errors::already_published(ETRANSACTION_FEE)
);
move_to(
tc_account,
TransactionFee<CoinType> {
balance: Diem::zero(),
preburn: Diem::create_preburn(tc_account)
}
)
}
其要点:
- 前置条件:调用者为 TC 账户、
CoinType是链上已注册货币(Diem::assert_is_currency)、且该币种尚未注册过手续费资源(否则以ALREADY_PUBLISHED中止); balance初始化为Diem::zero()(language/diem-framework/modules/Diem.move);preburn通过Diem::create_preburn(tc_account)创建——该函数同样要求 TC 角色并断言货币已注册,见 language/diem-framework/modules/Diem.move,其注释明确写道"for transaction fees, or for the XDX reserve"正是此场景。
值得注意的是:当前 Genesis 仅注册 XUS(代码注释 "accept fees in all the currencies" 表明后续可能扩展其他法币),所以 burn_fees 等函数虽然泛型化为任意 CoinType,实际链上路径以 XUS 为主。
四、费用收取:pay_fee 与交易 epilogue 的联动
4.1 pay_fee 实现
pay_fee 负责把一笔具体的 Diem<CoinType> 存入手续费桶:
public fun pay_fee<CoinType>(coin: Diem<CoinType>) acquires TransactionFee {
DiemTimestamp::assert_operating();
assert(is_coin_initialized<CoinType>(), Errors::not_published(ETRANSACTION_FEE));
let fees = borrow_global_mut<TransactionFee<CoinType>>(@TreasuryCompliance);
Diem::deposit(&mut fees.balance, coin)
}
三个步骤:
DiemTimestamp::assert_operating():断言链已进入运行期(Genesis 之后),防止在初始化前误收费用;- 断言
CoinType已注册为手续费货币,否则以NOT_PUBLISHED+ETRANSACTION_FEE中止; - 借用 TC 账户下的
TransactionFee资源,调用Diem::deposit把coin并入balance。
Move Prover 规范还精确刻画了该函数的行为(见 language/diem-framework/modules/TransactionFee.move):
ensures post_fees.value == fees.value + coin.value;
即:费用桶余额精确增加 coin.value。
4.2 谁在调用:DiemAccount 的 epilogue
pay_fee 的调用者是账户模块的 epilogue(交易收尾)逻辑。在 language/diem-framework/modules/DiemAccount.move 的 epilogue_common<Token> 中,交易执行完毕后:
- 计算
gas_used = txn_max_gas_units - gas_units_remaining,进而得到transaction_fee_amount = txn_gas_price * gas_used; - 若
transaction_fee_amount > 0,则从发送者余额中Diem::withdraw(coin, transaction_fee_amount),并立即调用TransactionFee::pay_fee(...)存入桶中(注释明确"withdraw_from_balance is not used as limits do not apply to this transaction fee"); - 发送者
sequence_number自增。
而 prologue(交易前置校验)阶段(language/diem-framework/modules/DiemAccount.move)则反向使用了模块的查询函数:
- [PCA6]
TransactionFee::is_coin_initialized<Token>():校验 gas 费用可以用该货币支付; - [PCA7]/[PCA8]:校验账户在该货币下有余额且余额足够覆盖最大手续费。
由此形成完整闭环:prologue 校验(能否支付)→ 执行交易 → epilogue 扣款并 pay_fee(实际支付)。两条规范(PrologueCommonAbortsIf 中的 aborts_if max_transaction_fee > 0 && !TransactionFee::is_coin_initialized<Token>() 与 pay_fee 规范)保证了状态一致性。
五、费用销毁:burn_fees 的完整流程
5.1 函数签名与前置条件
public fun burn_fees<CoinType>(
tc_account: &signer,
) acquires TransactionFee {
DiemTimestamp::assert_operating();
Roles::assert_treasury_compliance(tc_account);
assert(is_coin_initialized<CoinType>(), Errors::not_published(ETRANSACTION_FEE));
...
}
三个前置条件:链处于运行期、调用者具备 TC 角色(Roles::assert_treasury_compliance,规范注释引用权限模型 [H3][PERMISSION])、CoinType 已注册手续费资源。任何不满足都会中止交易。
5.2 非 XDX 分支:批量提取并销毁
对于非 XDX 货币(当前实际路径为 XUS),实现分四步:
// extract fees
let fees = borrow_global_mut<TransactionFee<CoinType>>(@TreasuryCompliance);
let coin = Diem::withdraw_all(&mut fees.balance);
let burn_cap = Diem::remove_burn_capability<CoinType>(tc_account);
// burn
Diem::burn_now(
coin,
&mut fees.preburn,
@TreasuryCompliance,
&burn_cap
);
Diem::publish_burn_capability(tc_account, burn_cap);
- 提取:
Diem::withdraw_all一次性取走桶内全部余额(language/diem-framework/modules/Diem.move),fees.balance归零; - 取回销毁权:
Diem::remove_burn_capability<CoinType>(tc_account)从 TC 账户临时取走BurnCapability(language/diem-framework/modules/Diem.move)。注意 language/diem-framework/modules/Diem.move 注释明确指出TransactionFee::burn_fees是remove_burn_capability的唯一调用者; - 销毁:
Diem::burn_now(coin, &mut fees.preburn, @TreasuryCompliance, &burn_cap)执行 preburn + burn 的快捷路径(language/diem-framework/modules/Diem.move)。burn_now内部要求coin.value > 0,否则以Diem::ECOIN(INVALID_ARGUMENT)中止——这也解释了 TreasuryComplianceScripts 文档中"collected fees are zero"的中止条件; - 归还销毁权:
Diem::publish_burn_capability(tc_account, burn_cap)将销毁权发布回 TC 账户,保证BurnCapability不可转移(规范 [J3][PERMISSION]),后续仍可继续执行销毁。
规范保证(language/diem-framework/modules/TransactionFee.move):
ensures Diem::spec_market_cap<CoinType>()
== old(Diem::spec_market_cap<CoinType>()) - old(spec_transaction_fee<CoinType>().balance.value);
ensures spec_transaction_fee<CoinType>().balance.value == 0;
即:销毁后链上总市值精确减去已收集手续费金额,且费用桶余额归零。
5.3 XDX 分支:暂未实现的 STUB
当 CoinType 为 XDX 时(XDX::is_xdx<CoinType>()),当前实现直接中止:
// TODO: Once the composition of XDX is determined fill this in to
// unpack and burn the backing coins of the XDX coin.
abort Errors::invalid_state(ETRANSACTION_FEE)
规范 BurnFeesXDX 中 aborts_if true with Errors::INVALID_STATE 显式声明该分支必然中止;注释明确其为 "STUB: To be filled in at a later date once the makeup of the XDX has been determined"(待 XDX 组成确定后,解包并销毁其底层法币储备)。因此在当前仓库代码中,XDX 手续费销毁路径是刻意未实现的占位逻辑,这与 XDX 作为一篮子合成稳定币、底层构成未定的设计状态一致。
5.4 外部触发:burn_txn_fees 脚本
链上对外暴露的触发方式是 TreasuryComplianceScripts 中的脚本函数 burn_txn_fees<CoinType>(language/diem-framework/modules/TreasuryComplianceScripts.move):
public(script) fun burn_txn_fees<CoinType>(tc_account: signer) {
TransactionFee::burn_fees<CoinType>(&tc_account);
}
该脚本的文档化行为(同文件 L272-L306)指出:
- 成功后,
0xB1E55ED下TransactionFee<CoinType>资源的balance与preburn两个字段都归零——自上次调用以来收集的全部费用一次销毁; - 销毁事件:在
Diem::CurrencyInfo<CoinType>资源(发布于0xA550C18)的burn_events事件句柄上发出Diem::BurnEvent; - 中止条件汇总表:非 TC 账户(
REQUIRES_ADDRESS+ETREASURY_COMPLIANCE)、币种未注册(NOT_PUBLISHED+ETRANSACTION_FEE)、收集费用为零(INVALID_ARGUMENT+ECOIN)。
六、形式化验证:Move Prover 规范要点
该模块附带完整的 Move Prover 规范(spec 块),是理解其安全属性的最佳文档:
(1)模块级不变量(language/diem-framework/modules/TransactionFee.move)
invariant [suspendable] DiemTimestamp::is_operating() ==> is_initialized();
一旦链时间开始(is_operating 为真),手续费资源必须已初始化。由于 Genesis 中 TransactionFee::initialize 先于 DiemTimestamp::set_time_has_started 执行,该不变量在启动序列中天然成立。
(2)pay_fee 的 deposit 语义:post_fees.value == fees.value + coin.value,精确刻画余额累加,杜绝任何手续费丢失或凭空增发的可能。
(3)burn_fees 的市值守恒:市场总市值减少量恰等于被销毁的手续费金额,杜绝“销毁后余额不清零”或“市值扣减不一致”两类缺陷。
(4)权限模型:所有写操作(initialize、add_txn_fee_currency、burn_fees)均要求 Roles::assert_treasury_compliance;BurnFeesNotXDX 规范还断言 TC 账户取回 BurnCapability 且其不可转移(ensures exists<Diem::BurnCapability<CoinType>>(Signer::spec_address_of(tc_account)))。这些规范中引用的 [ACCESS_CONTROL]/[ROLE]/[PERMISSION] 标记对应 Diem 改进提案 DIP-2 的访问控制模型。
(5)辅助规范函数:spec_transaction_fee<CoinType>() 以只读方式 borrow_global<TransactionFee<CoinType>>(@TreasuryCompliance) 暴露规范视图,供 pay_fee、burn_fees 等规范引用。
七、源码对照与扩展阅读
| 关注点 | 仓库位置 |
|---|---|
| 模块源码(含规范) | language/diem-framework/modules/TransactionFee.move |
| 模块 API 文档(本文主参考) | language/diem-framework/modules/doc/TransactionFee.md |
| Genesis 初始化编排 | language/diem-framework/modules/Genesis.move |
| prologue 费用可支付性校验 | language/diem-framework/modules/DiemAccount.move |
| epilogue 扣款并 pay_fee | language/diem-framework/modules/DiemAccount.move |
| 对外销毁脚本 burn_txn_fees | language/diem-framework/modules/TreasuryComplianceScripts.move |
| Diem::burn_now / create_preburn / withdraw_all | language/diem-framework/modules/Diem.move、language/diem-framework/modules/Diem.move、language/diem-framework/modules/Diem.move |
结语
TransactionFee 模块以极简的资源结构与四个函数,完整支撑了 Diem 交易手续费的"收集—累积—批量销毁"生命周期,并通过 Genesis 时序约束(初始化先于链启动)、角色权限门禁(仅 TC 账户)与 Move Prover 规范(余额守恒、市值扣减精确)三重保障安全。其核心设计取向——手续费由协议统一归集而非奖励给验证者、销毁后由协会链下回收法币准备金——体现了 Diem 治理模型中"链上价值与链下法币一一对应"的整体思路。对于希望在 Diem 框架上实现自定义费用货币或理解 gas 经济模型的开发者,TransactionFee 是最直接的参考实现。