免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Solana 交易费用优先级提案深度解析:从 fee-per-compute-unit 定价到调度器锁冲突消解

Solana 交易费用优先级提案深度解析:从 fee-per-compute-unit 定价到调度器锁冲突消解 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.rsSetComputeUnitLimit(u32)设置交易允许消耗的计算单元上限对应分母requested_compute_unitsSetComputeUnitPrice(u64)以微 lamportsmicro-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默认交易队列一个最大堆BinaryHeapTransaction跟踪所有待处理交易堆内优先级依据交易的附加费用。leader slot 开始前从 sigverify 收到的交易会被加入此队列。2.all_transaction_queues多级队列一个VecDequeBinaryHeapTransaction管理所有待处理工作队列。不同队列的优先级不同在处理完成信号一节解释整个列表按优先级从高到低排序。初始化时all_transaction_queues[0] default_transaction_queue。3.locked_accounts已锁账户表一个HashMapLockedPubkey, usize跟踪当前已调度/已发送给银行线程的交易所需执行的账户集合。账户在发送给 BankingStage 线程之前就会被加入该集合。usize是引用计数——因为同一账户可能被多笔读交易共享。LockedPubkey的定义enum LockedPubkey { Read(Pubkey), Write(Pubkey), }将账户按读锁/写锁区分是判断冲突的基础读-读不冲突读-写、写-写冲突。4.blocked_transactions被阻塞交易表一个HashMapSignature, RcBlockedTransactionsQueue以交易签名为键映射到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: BinaryHeapTransaction }注意结构体注释这是一个**因为会抢占高费用交易所需账户锁而无法被调度的交易堆。其中highest_priority_blocked_transaction是阻塞整条队列的最高优先级交易**其余被连带阻塞的交易放入other_blocked_transactions堆。5.blocked_transaction_queues_by_accounts按账户索引的阻塞队列表一个HashMapPubkey, RcBlockedTransactionsQueue以账户公钥为键。它提供了某账户被哪条阻塞队列占用的 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账户锁管理——从提案中单线程维护的HashMapLockedPubkey, 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使用LruCacheSlot, ArcSlotPrioritizationFee缓存最近若干个区块的优先费信息并通过独立服务线程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),仅供参考
返回列表