Solana 综合计算费用提案解析:从签名计费到基于计算单元的全面费用模型
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
本篇技术指南深度解析 Solana(本仓库)官方设计提案《Comprehensive Compute Fees》的核心思路:以"计算单元(Compute Unit)"为统一度量衡,重构交易费用评估与区块调度体系。你将理解费用从仅按签名数计费演进为涵盖签名验证、写锁、指令数据、账户大小、计算预算、预编译程序六大维度的综合模型,并看到该提案在cost-model、program-runtime、accounts-db、runtime等模块中的真实落地实现,以及确定性费用(deterministic fees)机制的演进方向。
提案背景:为什么签名数不足以衡量验证者工作量
Solana 最初的费用结构只基于交易中的签名数量(number of signatures),其设计意图本是用它来近似"验证者处理这笔交易需要付出的工作量"。但 提案原文 明确指出:验证者实际承担的用户自定义工作远不止签名验证。处理一笔典型交易通常包括:
- 签名验证(signature verification)
- 账户加锁(account locking)
- 账户加载(account loading)
- 指令处理(instruction processing)
仅凭签名数量,无法反映交易在指令复杂度、写账户数量、加载数据量上的真实开销,也就无法为"每笔交易究竟消耗了区块多少处理能力"提供准确依据。
提案总体方案:把费用拆解为可度量的计算单元
提案的解决方案并不直接规定原生代币(SOL/lamports)与每种费用类别的兑换单价,而是设定评判标准并提供成本模型(cost model)可用的"旋钮"(knobs)——也就是把费用拆解为若干类"计算单元成本",相加即得到处理该笔交易的整体成本。有了这个总成本,运行时(runtime)就可以:
- 收取更有代表性的费用;
- 做出更好的交易调度决策(决定哪些交易进入同一批次/区块)。
费用的六大计算维度
提案给出了费用可基于以下要素计算的完整清单:
| # | 费用类别 | 计费方式 |
|---|---|---|
| 1 | 签名数量 | 每个签名固定费率 |
| 2 | 写锁数量 | 每个可写账户固定费率 |
| 3 | 数据字节成本 | 交易所有指令数据长度总和 × 每字节固定费率 |
| 4 | 账户大小 | 无法预先得知,先按最大账户大小(10m)预收,实际大小确定后退还差额 |
| 5 | 计算预算 | 默认 200k 单元,可通过计算预算指令请求更高上限(最高 1m 单元),先按默认/请求值预收,处理结束后按实际消耗退还差额,只用多少付多少;内置程序(builtin)固定成本,SBF 程序运行时实测 |
| 6 | 预编译程序 | 预编译程序执行计算密集型操作,其工作量可基于指令数据数组解析预测,按程序分别定价;因在 bank 外部处理,其计算成本不计入计算预算,也不参与交易调度决策 |
各组成部分固定成本的测定方法在 issue #19627 中描述(见下文"从提案到实现")。
成本模型(Cost Model):调度决策的核心输入
提案定义的成本模型用于评估交易在槽内(in-slot)处理期间会给集群带来多少负载,并据此决定如何把交易最佳地分批(batch)调度。
成本模型的评判标准与费用标准几乎完全一致,但排除签名和预编译程序。原因很关键:这两项成本发生在交易被调度之前(签名校验在入口处完成,预编译程序在 bank 之外执行),因此不会影响交易在槽内的处理时长,也就无需参与调度决策。
实现一:CostModel 的六项成本核算
提案在 cost-model/src/cost_model.rs 中完整落地,核心入口是CostModel::calculate_cost(返回TransactionCost)。它依次核算:
- 签名成本:普通签名、secp256k1 指令签名、ed25519 指令签名分别乘以固定单价(
SIGNATURE_COST、SECP256K1_VERIFY_COST、ED25519_VERIFY_COST),见 cost_model.rs; - 写锁成本:
WRITE_LOCK_UNITS × 可写账户数。有一个 feature 开关cost_model_requested_write_lock_cost:开启后按"请求的写锁数"计费而非"实际降级后的可写账户数",这解决了系统程序等不可写账户被降级(demote)导致成本低估的问题,对应测试见 cost_model.rs; - 执行成本:遍历指令,内置程序查
BUILT_IN_INSTRUCTION_COSTS表取固定值,SBF 程序按DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT估算,上限截断到MAX_COMPUTE_UNIT_LIMIT;若交易显式设置了SetComputeUnitLimit,则以其为执行成本;若计算预算指令解析失败,交易不会被执行,成本按 0 计(见 cost_model.rs); - 数据字节成本:指令数据总长度除以
INSTRUCTION_DATA_BYTES_COST; - 账户数据大小成本:由系统程序的
CreateAccount/Allocate等指令反序列化得到space字段估算,见 cost_model.rs; - 加载账户数据大小成本:受 feature
include_loaded_accounts_data_size_in_fee_calculation控制,按加载上限与堆成本调用FeeStructure::calculate_memory_usage_cost计算。
实现二:单位换算常量与区块级限额
提案中"固定费率""上限"等参数在 cost-model/src/block_cost_limits.rs 中统一定义:
COMPUTE_UNIT_TO_US_RATIO = 30:集群平均"计算单元 → 微秒"换算率;SIGNATURE_COST = 30 × 24 = 720、SECP256K1_VERIFY_COST = 30 × 223 = 6690、ED25519_VERIFY_COST = 30 × 76 = 2280;WRITE_LOCK_UNITS = 30 × 10 = 300;INSTRUCTION_DATA_BYTES_COST = 140 / 30 = 4(每 4 字节指令数据 ≈ 1 计算单元);- 内置程序固定成本表
BUILT_IN_INSTRUCTION_COSTS,覆盖 stake、config、vote、system、compute-budget、address-lookup-table、bpf-loader(upgradeable/deprecated/默认)、loader-v4,其中 secp256k1 与 ed25519 预编译程序成本为 0(其在 bank 之外执行); - 区块级限额:
MAX_BLOCK_UNITS = 48_000_000(400ms 回放 × 30 × 并发 4)、MAX_WRITABLE_ACCOUNT_UNITS = 12_000_000、MAX_VOTE_UNITS = 36_000_000(为普通交易预留 25%)、MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA = 100_000_000(每区块账户数据增长上限),均有const_assert_eq!静态断言校验。
实现三:CostTracker 的调度准入控制
cost-model/src/cost_tracker.rs 把成本模型与区块调度连接起来。CostTracker维护cost_by_writable_accounts(按可写账户记账)、block_cost、vote_cost、account_data_size等状态,提供:
would_fit(&tx_cost):判断交易是否放得下当前区块,依次检查投票限额、区块总额限、单账户成本上限、账户数据增量上限,以及每个可写账户的链式累计成本是否超限(防止过多交易写同一账户而降低并行度);try_add:通过检查后原子累加成本(test_cost_tracker_try_add_is_atomic验证失败时状态回滚);update_execution_cost:交易执行完毕后,用实际执行单元数修正估算值(多退少补);- 错误类型映射为对应
TransactionError:WouldExceedMaxBlockCostLimit、WouldExceedMaxVoteCostLimit、WouldExceedMaxAccountCostLimit、WouldExceedAccountDataBlockLimit等。
在 core/src/banking_stage 中,CostModel::calculate_cost被调度控制器(scheduler_controller.rs)、QoS 服务(qos_service.rs)以及按账户转发批次逻辑(forward_packet_batches_by_accounts.rs)大量调用——这正是提案所说的"更佳的交易调度决策"。
实现四:简单投票交易的静态成本
投票交易(vote transaction)结构固定(1~2 个签名、2 个写锁、1 条投票指令、加载账户数据不足一页),因此 transaction_cost.rs 将其成本静态定为SIMPLE_VOTE_USAGE_COST = 3428,并以内联断言约束其等于"投票指令默认单元 + 1 个签名成本 + 2 个写锁成本 + 8 单元加载成本"。测试test_vote_transaction_cost验证投票交易成本恒为 3428,而同一笔交易若按普通交易计算则为 20535。
计算预算:可请求的交易级 CU 上限与堆大小
提案明确指出:可通过预编译的ComputeBudget程序请求更高的交易级计算预算上限与程序堆大小,且请求的提升会反映在交易费用中。
计算预算指令集
在 sdk/src/compute_budget.rs 中定义了ComputeBudgetInstruction枚举(程序 ID 为ComputeBudget111111111111111111111111111111):
RequestHeapFrame(u32):请求交易级程序堆区大小(必须为 1024 的倍数,作用于交易内每个程序及所有 CPI 调用);SetComputeUnitLimit(u32):设置交易允许消耗的计算单元上限;SetComputeUnitPrice(u64):以"微 lamports"为单位设置计算单元单价,支付更高交易费以获得更高优先级;SetLoadedAccountsDataSizeLimit(u32):设置交易可加载的账户数据总大小上限。
对应便捷构造方法request_heap_frame、set_compute_unit_limit、set_compute_unit_price、set_loaded_accounts_data_size_limit均以 borsh 序列化。
预算的解析与上限
program-runtime/src/compute_budget_processor.rs 是预算处理核心:
DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT = 200_000:每条非计算预算指令的默认预算(提案中的"默认 200k 单元"即指每条指令的默认,与交易内指令数相乘得交易预算);MAX_COMPUTE_UNIT_LIMIT = 1_400_000:交易级计算单元上限(提案中的 1m 上限在实现中演进为 1.4m);MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES = 64MiB:单交易可加载账户数据上限;MAX_HEAP_FRAME_BYTES = 256KiB:堆帧最大值,sanitize_requested_heap_size校验 1024 倍数与上下界;process_compute_budget_instructions在交易清洗(sanitizing)阶段解析预算指令,重复指令返回DuplicateInstruction错误,非法数据返回InvalidInstructionData;处理失败则交易直接丢弃、不会执行。
预算如何映射为费用
预算上限最终通过ComputeBudgetLimits → FeeBudgetLimits(见 compute_budget_processor.rs)进入费用计算,其中优先级费用由 program-runtime/src/prioritization_fee.rs 计算:compute_unit_price × compute_unit_limit(微 lamports)向上取整为 lamports。
费用结构定义在 sdk/src/fee.rs 的FeeStructure:
lamports_per_signature(默认 0.000005 SOL)、lamports_per_write_lock、compute_fee_bins(按 CU 上限分档定价的"费用仓");calculate_fee_details汇总签名费、写锁费、计算费(按loaded_accounts_data_size_cost + compute_unit_limit落入对应FeeBin),再加上优先级费用得到FeeDetails;calculate_memory_usage_cost按 32KiB 一页(ACCOUNT_DATA_COST_PAGE_SIZE)× 堆成本(DEFAULT_HEAP_COST = 8)计费,测试(fee.rs)验证了"不足一页按一页计"的取整行为。
缓存账户大小:避免按最大值预收的浪费
提案提出:账户大小无法预先获知,先按最大账户大小(10m)预收、结算后退差,会带来不必要的资金占用与复杂度,因此规划了"缓存账户大小并使用缓存值替代最大值"的优化(对应 issue #20511)。其后续实现思路是让验证者复用账户数据缓存(见 accounts-db 中的缓存与哈希基础设施)来提前获知实际账户大小,从而让成本估算更贴近真实负载。
预编译程序失败的费用处理
预编译程序(secp256k1、ed25519)在 bank 外部执行,其失败是否收费、按何标准收费,属于提案规划中的独立议题(对应 issue #20481)。从当前实现看,block_cost_limits.rs 将两个预编译程序的执行成本记为 0,但其指令签名验证成本(SECP256K1_VERIFY_COST/ED25519_VERIFY_COST)仍计入签名成本——签名验证发生在调度前,无论程序执行成败都已发生,这与提案"签名成本不计入槽内调度"的设计一致。
费率治理(Rate Governing)的重新评估
提案指出当前费率治理的问题:按签名数治理会把费率压到下限,因为每个槽内的实际签名数远低于"目标签名数/槽"。
新的治理方向是:不再用签名数,而是由成本模型反馈其观察到的批次/队列负载。费用停留在目标费率,仅当负载超过某个待定的阈值时才上调;治理将作用于所有费用类别(而非仅签名维度)。
从源码看,旧机制的载体是 sdk/program/src/fee_calculator.rs 的FeeRateGovernor:
target_lamports_per_signature(默认 10_000)、target_signatures_per_slot(默认 50 × 每槽毫秒数)、min/max_lamports_per_signature(目标值的 50%~1000%)、burn_percent(默认 50% 费用销毁);new_derived依据latest_signatures_per_slot / target_signatures_per_slot的比例,以目标值的 5% 为步长平滑调整lamports_per_signature。
这正是提案所批评的"基于签名数的治理",而基于CostTracker负载反馈的新治理机制在该提案框架下被设计为替代方案。
确定性费用(Deterministic Fees):保留还是废除?
Solana 的费用目前基于给定 blockhash 是确定性的,这一特性极大简化了客户端交互:
- 清空同时作为 payer 的账户时,可预先算好费用并把剩余余额全部转出,不用担心费用变化导致账户残留极小余额;
- 离线签名时,payer 可根据 nonce 的 blockhash 保证将被收取的费用。
确定性从何而来
确定性通过两条路径实现:
- Blockhash 队列:包含最近(约 2 分钟内)的 blockhash 列表及各自对应的
lamports_per_signature值。队列是快照的序列化成员之一,因此bank hash 依赖它。实现见 accounts-db/src/blockhash_queue.rs:HashAge结构体持有fee_calculator,get_lamports_per_signature按交易 blockhash 查表,register_hash/genesis_hash在注册时写入当时的费率; - Nonce 账户:用于离线签名的 nonce 账户在其账户数据中保存
lamports_per_signature(见 sdk/src/nonce_account.rs 的lamports_per_signature_of)。
两种情况下,评估费用时都用交易的 blockhash 查出应使用的lamports_per_signature。
演进的三重挑战
FeeCalculator暴露给客户端:它持有lamports_per_signature,任何费用标准的演进都会因向后兼容(backward-compatibility)而变得困难。解法是弃用FeeCalculator对象,新 API 改为"传入 message、返回费用"(见 fee_calculator.rs 中calculate_fee已标记#[deprecated(since = "1.9.0")]);- Blockhash 队列条目内嵌费用标准:条目属于 bank hash 的一部分,费用标准演进涉及快照/银行哈希的更多工作与风险;
- Nonce 账户数据内嵌费用标准:演进需要修改 nonce 账户数据结构与大小。
两条解决路线
路线 A:废除确定性费用。客户端通过 RPC 请求当前费用估算,实际费用在交易处理时评估;费用变化受治理约束、随网络负载缓慢变化,2 分钟窗口内差异很小。Nonce 账户不再存储费用标准,改为存储费用上限(fee cap)——处理时评估的费用若超过上限则交易失败。此路线将费用标准完全移出 blockhash 队列与 nonce 账户,二者的数据结构在未来费用标准演进时无需任何变更。
路线 B:保留确定性费用。客户端通过 RPC 计算当前费用并传入一个与之关联的 blockhash。Blockhash 队列与 nonce 账户改用版本化但内部化的Fee对象(类似FeeCalculator);每次费用标准演进时新增一个版本,新产生的 blockhash 队列条目与新 nonce 账户使用新版本。
从提案到实现:一个仍在演进的架构
综合来看,这份提案并非一次性完成的重写,而是分阶段、带 feature gate 的渐进式演进。从当前仓库源码结构可以梳理出以下落地事实:
- 成本模型模块独立成 crate cost-model,以
CostModel(估成本)+CostTracker(准入控制)+block_cost_limits(常量/限额)三个文件形成完整闭环; - 调度侧接入:banking stage 的 QoS 与调度控制器在把交易放入区块前都调用
CostModel::calculate_cost,并将结果交给CostTracker::try_add做多维度限额检查; - 费用计算侧:
FeeStructure(sdk/src/fee.rs)已经包含"每签名 + 每写锁 + 计算单元分档 + 优先级费"的多元结构,已远超旧的"仅按签名 ×lamports_per_signature"模型; - 若干演进受 feature gate 控制:
include_loaded_accounts_data_size_in_fee_calculation(#30657,账户加载数据计入基础费用)、cost_model_requested_write_lock_cost(#34819,成本模型按请求写锁计费)、remove_rounding_in_fee_calculation(#34982,移除费用计算中的不必要取整),均定义在 sdk/src/feature_set.rs,测试通过开关 feature 验证新旧行为(见 cost_model.rs)。
因此,阅读本提案时应将其视为 Solana 费用体系演进的路线图:其中"六大费用维度 + 成本模型驱动调度 + 费率治理改革 + 确定性费用再设计"的主体框架已落地,而"账户大小缓存替代最大值预收""预编译失败费用""负载反馈型费率治理"等条目仍在推进中。
进一步阅读
- 提案原文:docs/src/proposals/comprehensive-compute-fees.md
- 成本模型核心实现:cost-model/src/cost_model.rs、cost-model/src/cost_tracker.rs、cost-model/src/block_cost_limits.rs、cost-model/src/transaction_cost.rs
- 计算预算指令与处理:sdk/src/compute_budget.rs、program-runtime/src/compute_budget_processor.rs
- 费用结构:sdk/src/fee.rs、sdk/program/src/fee_calculator.rs、program-runtime/src/prioritization_fee.rs
- 确定性费用的 blockhash 队列载体:accounts-db/src/blockhash_queue.rs
- 调度侧调用点示例:core/src/banking_stage/transaction_scheduler/scheduler_controller.rs
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考