Diem 交易手续费模块深度解析:TransactionFee 的初始化、累积与链上销毁机制

原创2026-09-21 11:07:221,921 阅读
文章标签:区块链金融科技

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);
}

它依次完成三项校验与动作:

  1. DiemTimestamp::assert_genesis():断言当前处于 Genesis 阶段(时间戳尚未开始计时),防止运行时被再次调用;
  2. Roles::assert_treasury_compliance(tc_account):断言调用者是 TC 账户,这是该模块所有写操作共用的权限门禁;
  3. 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)
}

三个步骤:

  1. DiemTimestamp::assert_operating():断言链已进入运行期(Genesis 之后),防止在初始化前误收费用;
  2. 断言 CoinType 已注册为手续费货币,否则以 NOT_PUBLISHED + ETRANSACTION_FEE 中止;
  3. 借用 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> 中,交易执行完毕后:

  1. 计算 gas_used = txn_max_gas_units - gas_units_remaining,进而得到 transaction_fee_amount = txn_gas_price * gas_used;
  2. 若 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");
  3. 发送者 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);
  1. 提取:Diem::withdraw_all 一次性取走桶内全部余额(language/diem-framework/modules/Diem.move),fees.balance 归零;
  2. 取回销毁权: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 的唯一调用者;
  3. 销毁: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"的中止条件;
  4. 归还销毁权: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 是最直接的参考实现。

登录后查看全文
diem