摘要
程序通过 LLVM 编译为 sBPF,并在沙盒虚拟机中运行,每笔交易有 1.4M CU 的预算。运行时最多缓存 512 个已编译程序,提供日志、CPI、加密和内存等系统调用,并在新部署时延迟 1 个 slot。
编译
Solana 使用 LLVM 将程序编译为 ELF 二进制文件,其中包含 Solana 字节码格式(sBPF)。ELF 二进制文件以可执行账户的形式存储在链上。
sBPF 是 Solana 针对运行时定制的 eBPF 字节码变体。它并非标准 eBPF,而是经过 Solana 特定修改的版本。
编写程序
Solana 程序主要使用 Rust 编写,常见有两种方式:
程序执行模型
当处理一笔交易时,运行时会依次执行每条指令,通过
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 的 owner 是 native loader,则该程序为内置程序,其入口函数会直接从ProgramCacheForTxBatch查找。如果 owner 是某个 BPF loader(bpf_loader_deprecated、bpf_loader、bpf_loader_upgradeable或loader_v4),则会调用加载器自身的内置入口函数。 -
执行 BPF 程序。 对于 BPF 程序,loader entrypoint 会从程序缓存中查找已编译的可执行文件。然后
execute()函数会:
- 将账户数据序列化为扁平参数缓冲区
- 创建包含栈、堆和内存区域的 sBPF 虚拟机
- 运行已编译代码,执行期间消耗计算单元。如果超出预算,则返回
ComputationalBudgetExceeded。 - 从缓冲区反序列化账户数据回账户状态
-
弹出栈帧。 调用
pop(),用于验证指令是否违反了运行时的记账规则( lamport 余额是否平衡、只读账户未被修改、账户数据大小在限制范围内)。 -
累计计算单元。 指令消耗的计算单元通过
saturating_add累加到交易总量中。
程序缓存
运行时维护一个全局的
ProgramCache,用于存储已验证和已编译的程序。该缓存支持分叉图感知,并负责处理部署可见性规则、驱逐和 epoch 边界的重新编译。
缓存条目类型
每个被缓存的程序都有一个
ProgramCacheEntryType,用于决定其运行时行为:
| 类型 | 描述 |
|---|---|
Loaded | 已验证并编译的程序,可直接执行。 |
Builtin | 编译进 validator 二进制文件的原生程序(如 System、Stake、Vote 等)。不会存储在链上。 |
Unloaded | 之前已验证的程序,其已编译可执行文件为释放内存空间被驱逐。仍然会跟踪使用统计信息,可在无需重新验证的情况下重新加载。 |
FailedVerification | 未通过当前功能集下 sBPF 验证器验证的程序的墓碑标记。如果功能激活更改了验证规则,可能会变为 Loaded。 |
Closed | 被明确关闭或从未部署的程序的墓碑标记。也用于属于 loader 但不包含可执行代码的账户(如缓冲账户)。 |
DelayVisibility | 由 ProgramCacheForTxBatch::find() 返回的合成墓碑标记,当存在 Loaded 条目但尚未生效(其 effective_slot 在未来)。不会直接存储在缓存中。 |
可见性延迟
新部署或升级的程序不会立即生效。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,所有缓存的程序都会根据新环境
重新编译。
返回数据
Is this page helpful?