摘要
程序通过 LLVM 编译为 sBPF,并在沙盒虚拟机中运行,每笔交易的计算单元预算为 140 万。运行时最多缓存 512 个已编译程序,提供用于日志记录、CPI、加密和内存的系统调用,并将新部署延迟 1 个 slot。
编译
Solana 使用 LLVM 将程序编译为包含 Solana 字节码格式 (sBPF) 的 ELF 二进制文件。ELF 二进制文件以可执行账户的形式存储在链上。
sBPF 是 Solana 针对 eBPF 字节码的自定义变体,专为 Solana 运行时量身定制。它并非标准 eBPF,包含 Solana 特有的修改。
编写程序
Solana 程序主要使用 Rust 编写,可采用以下三种方式之一:
Anchor
一个使用 Rust 宏来减少样板代码的框架。推荐大多数开发者使用。
Pinocchio
一个轻量级的零拷贝 Rust 库,针对计算消耗和二进制大小进行了优化。包含用于常见 CPI 的程序专用 crate。
原生 Rust
不使用框架的纯 Rust 开发。提供完全控制权,但需要更多手动实现。
请参阅 Cross-Program Invocations,了解适用于上述各种方式的 CPI 概念和示例。
程序执行模型
处理交易时,运行时通过
process_message()
按顺序执行每条指令。对于每条指令,运行时将:
-
准备指令上下文。 调用
prepare_next_top_level_instruction()来映射指令的账户索引,设置签名者和可写标志,并配置TransactionContext。 -
检查预编译。 如果程序是 预编译,运行时将调用
process_precompile(), 该函数仍会通过push()和pop()压入和弹出栈帧, 但会绕过 sBPF 虚拟机和程序缓存查找,直接执行原生代码。 -
压入栈帧。(步骤 3-6 发生在
InvokeContext::process_instruction()和process_executable_chain()内部,由process_message()调用。)调用push()作用于InvokeContext, 该操作会递增指令栈高度并强制执行重入规则:仅当直接调用者(指令栈当前栈顶的程序)与被调用程序相同时,程序才能重入自身。深层自递归(A -> A -> A)是允许的,但受栈深度限制约束。其他重入模式(例如 A 调用 B 再调用 A)将返回InstructionError::ReentrancyNotAllowed。 -
解析程序。 运行时调用
process_executable_chain()来确定加载器。如果 program account 的所有者是 原生加载器, 则该程序是内置程序,其入口函数将直接从ProgramCacheForTxBatch中查找。如果所有者是某个 BPF 加载器(bpf_loader_deprecated、bpf_loader、bpf_loader_upgradeable或loader_v4),则改为调用该加载器自身的内置入口点。 -
执行 BPF 程序。 对于 BPF 程序, 加载器入口点 会从程序缓存中查找已编译的可执行文件。
execute()函数随后将:- 将账户数据序列化到平坦参数缓冲区中
- 创建包含栈、堆和内存区域的 sBPF 虚拟机
- 运行已编译的代码,执行期间消耗计算单元。若超出预算,则返回
ComputationalBudgetExceeded。 - 将账户数据从缓冲区反序列化回账户状态
-
弹出栈帧。 调用
pop(), 验证指令未违反运行时的记账规则(lamport 余额已平衡、只读账户未被修改、账户数据大小在限制范围内)。 -
累计计算单元。 指令消耗的计算单元通过
saturating_add加入到交易总量中。
程序缓存
运行时维护一个全局
ProgramCache,
用于存储已验证和已编译的程序。它能感知分叉图,并处理部署可见性规则、驱逐策略以及 epoch 边界重新编译。
缓存条目类型
每个缓存程序都有一个
ProgramCacheEntryType,
用于决定其运行时行为:
| 类型 | 描述 |
|---|---|
Loaded | 已验证并编译的程序,可随时执行。 |
Builtin | 编译到 validator 二进制文件中的原生程序(System、Stake、Vote 等)。不存储在链上。 |
Unloaded | 已通过验证但编译后的可执行文件因释放内存空间而被从内存中驱逐的程序。仍会跟踪使用统计信息。无需重新验证即可重新加载。 |
FailedVerification | 未通过当前功能集下 sBPF 验证器的程序的墓碑标记。若功能激活改变了验证规则,可能转变为 Loaded 状态。 |
Closed | 已显式关闭或从未部署的程序的墓碑标记。也用于属于某个加载器但不包含可执行代码的账户(如缓冲账户)。 |
DelayVisibility | 当 Loaded 条目存在但尚未生效(其 effective_slot 在未来)时,由 ProgramCacheForTxBatch::find() 返回的合成墓碑标记。从不直接存储在缓存中。 |
可见性延迟
新部署或升级的程序不会立即生效。
DELAY_VISIBILITY_SLOT_OFFSET
常量为 1,这意味着在 slot N 中部署的程序将在 slot N+1 时生效。在部署 slot 期间,任何调用新版本的尝试都将返回
DelayVisibility,导致运行时报告"程序未部署。"
驱逐策略
缓存最多持有
MAX_LOADED_ENTRY_COUNT
(512) 个已编译程序条目。当达到上限时,使用最少的程序将被驱逐至 Unloaded 状态。使用情况通过
tx_usage_counter
(每次交易引用该程序时递增)和
latest_access_slot
进行跟踪。
Epoch 边界重新编译
如果某项功能激活在 epoch 边界处更改了
ProgramRuntimeEnvironments,
所有缓存的程序都将针对新环境进行
重新编译。
返回数据
程序可以通过 sol_set_return_data 系统调用设置返回数据。该数据存储在交易级别的
TransactionReturnData
结构体中,该结构体保存数据字节以及调用该系统调用的指令所属程序的 program_id。最大大小为 1,024 字节
(MAX_RETURN_DATA)。
Is this page helpful?