Solana 交易费用优先级提案深度解析:从 fee-per-compute-unit 定价到调度器锁冲突消解
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
本文以仓库提案文档 fee_transaction_priority.md 为核心骨架,结合 Solana 源码库中的 compute-budget 指令实现(compute_budget.rs)、交易调度器实现(core/src/banking_stage/transaction_scheduler/)与优先费缓存(prioritization_fee_cache.rs),系统讲解"按费用竞拍执行优先级"的设计动机、公平性保证、调度器算法与账户锁冲突消解机制。读完本文,你将掌握
F(T) = (additional_fee + base_fee) / requested_compute_units的定价模型、Sigverify → Scheduler → BankingStage三级流水线,以及"高费用交易不被低费用交易饿死"的核心调度保证,并能从源码层面理解该提案在当今代码库中的演进形态。
一、提案背景:为什么需要费用优先级
在 Solana 这样的高吞吐区块链中,单个 leader slot 内可以容纳大量交易,而验证节点(validator)的 CPU 计算资源是有限的。当交易排队等待执行时,谁先被执行直接决定了用户体验:抢在别人前面执行的交易能更早确认、更早触发后续动作(如抢购、套利、清算)。
为此,提案引入了**附加费用(additional fee)**机制:用户可以自愿在基础费用之外额外支付一笔费用,用来"竞拍"自己的交易在 leader 交易队列中的优先级。
核心定义:fee-per-compute-unit
提案给出了交易T的优先级度量函数F(T):
F(T) = (additional_fee + base_fee) / requested_compute_units即每计算单元(compute unit)愿意支付的费用。这个定义有两个关键点:
- 分子是总费用(附加费用 + 基础费用),体现用户为这笔交易付出的总成本;
- 分母是请求的计算单元数,将费用"归一化"到单位计算资源上,防止一笔消耗海量计算单元的大交易仅凭总费用高就长期霸占 CPU,而让单位成本更高的小交易饿死。
在当今源码中,这两个概念分别对应 compute-budget 原生程序的两条指令(见 sdk/src/compute_budget.rs):
SetComputeUnitLimit(u32):设置交易允许消耗的计算单元上限,对应分母requested_compute_units;SetComputeUnitPrice(u64):以"微 lamports(micro-lamports)"为单位设置计算单元单价,用于支付更高的交易费用以获得更高优先级,对应分子中的附加费用部分。
从源码结构看,SetComputeUnitPrice的注释明确写道 "Set a compute unit price in 'micro-lamports' to pay a higher transaction fee for higher transaction prioritization",与提案中"用户竞拍优先级"的动机完全一致。
二、公平性保证:调度器必须遵守的两条规则
提案明确指出:仅仅有优先级度量还不够,调度器必须保证"公平"。给定待处理队列中的两笔交易T1与T2,且F(T1) > F(T2),调度器必须满足:
T1应优先于T2被考虑处理——高优先级交易拥有排队顺序上的优势;- 锁冲突时不得抢占:如果
T1暂时无法处理(因为有一笔正在执行的交易持有了T1所需的账户A的锁),那么T2不能被调度,即使T2可以拿到T1需要的锁也不行。
第二条规则是整套设计的灵魂:它防止低费用交易T2通过"抢先锁定账户"来**饿死(starve)**高费用交易T1。如果没有这条规则,T2可以在T1等待锁释放的空隙反复抢锁、反复插队,导致T1永远无法执行——这正是许多无优先级区块链的已知痛点。
三、交易流水线:Sigverify → Scheduler → BankingStage
提案将 leader 处理交易的流水线划分为三级:
1. Sigverify(签名验证) 2. Scheduler(调度器) 3. BankingStage threads(银行阶段线程)通道拓扑
- Sigverify 阶段产出的交易通过一个channel送入调度器;
- 调度器维护与
N个 BankingStage 线程之间的N条双向通道(实现上由两对单向通道构成); - 调度器决定把哪笔交易发给哪个 BankingStage 线程:通过该线程关联的通道把交易发送过去;
- BankingStage 线程处理完交易
T后,通过同一条通道把T回传给调度器,作为"处理完成"的信号。
这条"处理完成回传"的设计非常关键:它让调度器能够精确感知每一笔交易何时释放了它所持有的账户锁,从而决定何时唤醒被阻塞的排队交易。
四、调度器实现:五个核心数据结构
提案明确指出调度器是整条流水线中最复杂的部件。其实现由五个部分构成,当前设计下全部由单一调度器线程维护,以避免加锁带来的复杂度。
1.default_transaction_queue:默认交易队列
一个最大堆BinaryHeap<Transaction>,跟踪所有待处理交易,堆内优先级依据交易的附加费用。leader slot 开始前,从 sigverify 收到的交易会被加入此队列。
2.all_transaction_queues:多级队列
一个VecDeque<BinaryHeap<Transaction>>,管理所有待处理工作队列。不同队列的优先级不同(在"处理完成信号"一节解释),整个列表按优先级从高到低排序。初始化时all_transaction_queues[0] = default_transaction_queue。
3.locked_accounts:已锁账户表
一个HashMap<LockedPubkey, usize>,跟踪当前已调度/已发送给银行线程的交易所需执行的账户集合。账户在发送给 BankingStage 线程之前就会被加入该集合。usize是引用计数——因为同一账户可能被多笔读交易共享。
LockedPubkey的定义:
enum LockedPubkey { Read(Pubkey), Write(Pubkey), }将账户按读锁/写锁区分,是判断"冲突"的基础:读-读不冲突,读-写、写-写冲突。
4.blocked_transactions:被阻塞交易表
一个HashMap<Signature, Rc<BlockedTransactionsQueue>>,以交易签名为键,映射到BlockedTransactionsQueue:
/// Represents a heap of transactions that cannot be scheduled because they /// would take locks on accounts needed by a higher paying transaction struct BlockedTransactionsQueue { // The higher priority transaction blocking all the other transactions in // `blocked_transactions` below highest_priority_blocked_transaction: Transaction, other_blocked_transactions: BinaryHeap<Transaction> }注意结构体注释:这是一个**"因为会抢占高费用交易所需账户锁而无法被调度"的交易堆。其中highest_priority_blocked_transaction是阻塞整条队列的最高优先级交易**,其余被连带阻塞的交易放入other_blocked_transactions堆。
5.blocked_transaction_queues_by_accounts:按账户索引的阻塞队列表
一个HashMap<Pubkey, Rc<BlockedTransactionsQueue>>,以账户公钥为键。它提供了"某账户被哪条阻塞队列占用"的 O(1) 查询能力,是第 2 节公平性规则中"检测账户是否已被更高费用交易预留"的关键索引。
五、主循环算法:find_next_highest_transaction()
假设有N个 BankingStage 线程。调度器为每个银行线程运行函数find_next_highest_transaction(),核心流程如下。
第 1 步:弹出最高优先级交易
从all_transaction_queues[0]弹出最高优先级交易next_highest_transaction;如果该队列为空,则弹出 deque 中的下一个队列继续。
第 2 步:冲突检测
设transaction_accounts为next_highest_transaction所需的LockedPubkey集合,逐账户检查:
for account_key in transaction_accounts { // 检查该 LockedPubkey 是否与 locked_accounts 中任一 key 冲突, // 若冲突,说明有一笔持冲突锁的交易正在执行 if self.locked_accounts.is_conflicting(account_key) { return Conflict; } // 检查是否有更高费用的交易已经预留了该账户, // 防止低费用交易饿死高费用交易 if self.blocked_transaction_queues_by_accounts.contains_key(account_key) { return Conflict; } return NoConflict; }两处Conflict判定分别对应第 2 节的两条公平性规则:前者是物理锁冲突(账户正被占用),后者是预留锁冲突(账户已被更高费用交易"预订",即使当前空闲也不能抢)。
第 3 步:无冲突 → 加锁并派发
for account_key in transaction_accounts { self.locked_accounts.insert_reference(account_key.key()); } banking_thread_channel.send(next_highest_transaction);先将所需账户全部登记进locked_accounts(注意此时才真正"占锁"),再把交易发送到对应 BankingStage 线程的通道。
第 4 步:有冲突 → 登记为阻塞交易
for locked_account_key in transaction_accounts { let account_key = locked_account_key.key() let blocked_transaction_entry = self.blocked_transaction_queues_by_accounts.entry(account_key); match blocked_transaction_entry { Occupied(existing_blocked_transaction) => { // 该账户下已存在被阻塞交易集合,把当前交易按优先级插入堆 existing_blocked_transaction.insert_transaction(next_highest_transaction); } Vacant(vacant_entry) => { // 新建一条以当前交易为首位的阻塞队列 let new_blocked_transaction_queue = Rc::new(BlockedTransactionsQueue { highest_priority_blocked_transaction: next_highest_transaction, other_blocked_transactions: BinaryHeap::new(), }); // 以 account_key 为键插入阻塞队列索引表 vacant_entry.insert(new_blocked_transaction_queue.clone()); // 在 blocked_transactions 中登记:本组交易被 next_highest_transaction 阻塞 self.blocked_transactions.insert( next_highest_transaction.signature(), new_blocked_transaction_queue ); } } }这里的关键设计是:第一笔因某账户而阻塞的交易会"升格"为该账户的阻塞队列头,后续再遇到同一账户冲突的交易则进入该队列的堆中排队。而blocked_transactions以"阻塞者的签名"为键,为第 7 节"完成信号处理"中的队列解锁提供了直接索引。
第 5 步:循环直至满载
重复上述步骤,直到全部N个 BankingStage 线程都被发送了processing_batch批次(即命中第 3 步)的交易。
Banking 线程侧行为
提案还规定了 Banking 线程自身的两点职责:
- 维护一个按优先级排序的、由调度器发送而来的交易队列;
- 由于调度器已保证不存在锁冲突,Banking 线程可以一次性取出
M笔交易打包进 entry(区块条目)执行,无需再做锁检查。
这体现了一个重要的解耦思想:所有锁与优先级的复杂性都被收拢到调度器单线程内,银行线程只需"拿到无冲突的批量交易,直接执行并打包"。
六、处理完成信号:解锁与唤醒
主循环之外,调度器依赖 BankingStage 线程的"完成信号"来调度下一批交易。
1. 完成批次回传
BankingStage 线程处理完一批交易completed_transactions_batch后,通过同一通道将其回传给调度器。
2. 释放锁并唤醒阻塞队列
调度器收到信号后,对completed_transactions_batch中每笔已完成交易的账户集合transaction_accounts处理如下:
let mut unlocked_accounts = vec![]; // 先从跟踪表中移除所有锁 for locked_account in transaction_accounts { if self.locked_accounts.remove_reference(locked_account) { unlocked_accounts.push(locked_account.key()); } } // 检查释放这些账户后,是否有新的被阻塞交易可以运行 for account_key in unlocked_accounts { if let Some(blocked_transaction_queue) = self.blocked_transaction_queues_by_accounts.get(account_key) { // 检查阻塞本队列的交易现在能否拿到锁,若能,则解锁本队列 if blocked_transaction_queue.highest_priority_blocked_transaction.can_get_locks() { // 把交易调度给银行线程 banking_thread_channel.send(blocked_transaction_queue.highest_priority_blocked_transaction); return; } } // 若没有更高优先级交易被解锁,则继续按主循环逻辑调度 find_next_highest_transaction(); }注意remove_reference的返回值:只有引用计数归零(true)时,账户才算真正解锁,才会进入unlocked_accounts。解锁后优先检查该账户对应的阻塞队列头——这正是第 2 节公平性保证的落地:锁一释放,先唤醒被阻塞的高费用交易,而不是放任低费用交易插队。
3. 整队列解锁:阻塞者完成时的级联处理
最后还要检查:完成的交易是否恰好是某条阻塞队列的"阻塞者":
if let Some(blocked_transaction_queue) = self.blocked_transactions.get(completed_transaction.signature) { // 把队列中其余交易推到 all_transaction_queues 队首。 // 这些交易必然具有更高优先级:它们更早从主队列弹出, // 因此必然先于当前已完成的交易,优先级更高。 self.all_transaction_queues.push_front(blocked_transaction_queue.other_blocked_transactions); self.blocked_transactions.remove(completed_transaction.signature); }这一步的设计非常精妙:被阻塞队列中的交易必然比阻塞者更早从主队列弹出过(否则它们不会排在阻塞者后面进入该队列),因此它们的优先级必然更高。所以当阻塞者完成时,整个队列可以直接推入all_transaction_queues队首,在下一轮调度中优先处理,而无需逐笔重新比较优先级。
七、从提案到实现:当前源码中的演进
提案描述的是一套"单调度器线程 + 最大堆 + 锁冲突消解"的早期设计。需要说明的是,该提案属于设计文档(位于 docs/src/proposals/),当前仓库中的调度器实现已在此思路上大幅演进,但核心思想一脉相承:
1. 优先级量化:TransactionPriorityId
在 transaction_priority_id.rs 中,定义了TransactionPriorityId,结构包含priority: u64与id: TransactionId,并实现了Ord——直接按priority排序。其文档注释 "A unique identifier tied with priority ordering for a transaction/packet" 表明:每笔交易/数据包都带有一个明确的优先级数值,这与提案中"堆内按费用优先级排序"的模型完全对应。
2. 现代调度器:prio_graph_scheduler
在 core/src/banking_stage/transaction_scheduler/ 目录下,可以看到现代实现演化出了更精细的模块化结构:
- prio_graph_scheduler.rs:采用**优先级图(priority graph)**进行调度,在"按费用优先级排序"与"账户锁不冲突"两个约束之间做全局权衡;
- scheduler_controller.rs:调度器控制器,负责与 BankingStage 线程的交互与信号处理;
- thread_aware_account_locks.rs:账户锁管理——从提案中单线程维护的
HashMap<LockedPubkey, usize>演进为"线程感知"的锁分配; - transaction_state_container.rs 与 transaction_state.rs:维护每笔交易在调度周期内的状态流转。
从源码结构看,提案中的locked_accounts(引用计数锁表)、blocked_transactions/blocked_transaction_queues_by_accounts(阻塞交易管理)等概念,在现代实现中分别对应了thread_aware_account_locks、transaction_state等模块,但"先保证高优先级,再保证锁不冲突"的调度语义被完整保留。
3. 链上可观测:PrioritizationFeeCache
优先级费用不止用于调度,还会被记录并暴露给上层查询。在 prioritization_fee_cache.rs 中,PrioritizationFeeCache使用LruCache<Slot, Arc<SlotPrioritizationFee>>缓存最近若干个区块的优先费信息,并通过独立服务线程(solPrFeeCachSvc)在银行冻结时完成统计与指标上报。其get_prioritization_fees(&self, account_keys: &[Pubkey])接口(第 402 行)支持按账户查询历史区块的优先费,为 RPC 层和上层应用感知链上优先费水位提供了数据基础——这也正是用户可以通过查询历史SetComputeUnitPrice行情来校准自身出价的依据。
八、实战要点:如何利用费用优先级
结合提案模型与源码中的 compute-budget 指令(sdk/src/compute_budget.rs),用户端可以这样参与优先级竞拍:
构造优先费交易
- 设置计算单元上限:调用
set_compute_unit_limit(units),构造SetComputeUnitLimit指令,明确requested_compute_units(分母); - 设置计算单元单价:调用
set_compute_unit_price(micro_lamports),构造SetComputeUnitPrice指令,以微 lamports为单位设定单价(分子),金额越高优先级越高; - 将上述指令作为交易的第一条指令随交易一起发送。
参与竞拍的策略要点
- 盯住
F(T)而非总费用:提案的公平性规则决定了调度器比较的是"每计算单元费用"。同样的预算下,精简程序的 CU 消耗(降低分母)比单纯加钱(提高分子)更划算; - 关注历史优先费水位:可通过
PrioritizationFeeCache提供的按账户历史优先费查询能力,估算当前网络的出价区间,避免出价过低导致长时间排队; - 理解锁竞争场景:当你的交易与其他高优先级交易竞争同一热门账户(如热门 DEX 的流动性池)时,调度器会严格执行"高费用优先"——因此面向竞争场景(抢购、套利、清算)时,优先费出价要覆盖对手盘的最高价。
适用前提与限制
- 上述指令与调度逻辑以当前仓库源码为准,不同版本间指令序号与语义可能调整(
ComputeBudgetInstruction的变体顺序即经历过演进,见 sdk/src/compute_budget.rs); - 优先费只影响leader 内部的调度顺序,不改变交易的合法性校验、基础费用或其他约束;
- 附加费用在交易确认后由验证节点收取,具体分配细节属于费用分发逻辑(见 runtime/src/bank/fee_distribution.rs),不在本文提案范围内。
九、总结
fee_transaction_priority提案为 Solana 定义了一套完整、自洽的交易优先级机制:
- 定价模型:
F(T) = (additional_fee + base_fee) / requested_compute_units,用"每计算单元费用"统一度量优先级; - 公平性承诺:高费用交易优先;低费用交易不得通过抢先占锁饿死高费用交易;
- 流水线架构:
Sigverify → Scheduler → BankingStage,调度器独占全部优先级与锁管理复杂度,银行线程无锁打包执行; - 数据结构与算法:以最大堆 + 引用计数锁表 + 双向阻塞索引(
blocked_transactions/blocked_transaction_queues_by_accounts)实现"先检查物理锁、再检查预留锁"的双重冲突判定,并通过"完成信号 → 释放锁 → 唤醒阻塞队列头 → 级联解锁整队列"的闭环完成调度循环。
该提案虽为早期设计文档,但其核心语义——优先级量化、锁冲突消解、防饿死保证——至今仍是 Solana 交易调度(prio_graph_scheduler)与优先费基础设施(PrioritizationFeeCache)的理论基石。理解这份提案,是深入 Solana 交易生命周期与调度器源码的最佳起点。
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考