Solana eBPF 虚拟机
也许您已经看到了最近关于 Solana 虚拟机 (SVM) 的所有炒作以及在其上构建的所有激动人心的项目。也许您甚至读过我们之前关于 SVM API 的博客文章。
无论哪种情况,您可能都在寻找有关 Solana 虚拟机的更多信息。本指南将带您了解 Agave 验证器对rBPF 虚拟机的使用,解释它是什么、它如何工作以及验证器如何使用它来执行 Solana 程序。
rBPF:用于 eBPF 程序的 Rust 虚拟机和 JIT 编译器
rBPF VM
rBPF虚拟机是Quentin Monnet创建的扩展伯克利包过滤器 (eBPF) 虚拟机的 Rust 实现。在 Solana 早期,rBPF 项目在 Solana Labs 下分叉并进行了轻微修改以支持自定义 Solana 特定功能,这将在后面的部分中介绍。如今,rBPF 分叉由 Anza 工程师维护。
如存储库的 README 文件中所述,rBPF VM 旨在在用户空间而不是内核中运行。这使得 rBPF 成为 Solana 验证器用于执行程序的虚拟机的理想候选者,因为验证器的运行时在节点的用户空间中运行。
流行的术语“SVM”实际上有点用词不当。在整个生态系统中,当 Solana 开发人员提到 Solana 虚拟机 (SVM) 时,他们通常指的是 Solana 运行时内的整个事务处理管道或执行层。然而,负责执行 Solana 程序的实际虚拟机是一个 eBPF VM,受到 Solana 虚拟机指令集架构 (SVM ISA) 的限制。
Solana rBPF 是实现 SVM ISA 的 Rust 虚拟机,由 Agave 验证器使用。例如,Firedancer 有一个完全重新实现的虚拟机版本,它遵循 SVM ISA。
虚拟机本身也可以访问 Solana 协议定义的一组系统调用(“sycalls”),后面的部分会介绍这些系统调用。这些系统调用也是对较低级别的 Solana 虚拟机环境施加的约束的一部分。
伯克利包过滤器
Solana 程序被编译为伯克利数据包过滤器(BPF) 格式。BPF 最初是为伯克利软件发行版(BSD) Unix 系统设计的,用于过滤操作系统内核中的网络数据包。该格式利用类似于鉴别器的限定符,允许高效过滤数据包而无需复制数据。
BPF 程序使用这些限定符来定义数据包被捕获或丢弃的条件。它们定义了一组对寄存器、内存或数据包数据进行操作的指令(操作码)。
BPF 最终演变为扩展伯克利数据包过滤器(eBPF) 格式,Solana 的 LLVM 现已将其编译为该格式。eBPF 允许程序配置专为安全内核执行而设计的受限指令集和约束。这对 Solana 程序非常有用,因为它可以防止验证器崩溃并为所有程序创建一致的环境。
Solana 程序通常用 Rust 编写,然后由Solana 平台工具编译为 eBPF 。但是,程序也可以用Zig 、C 或汇编编写。平台工具确保程序被编译为正确的 eBPF 格式,以遵守 Solana VM 施加的 eBPF 限制,下一节将对此进行描述。
Solana rBPF ISA
如上一节所述,eBPF 允许平台(例如虚拟机)为 eBPF 程序施加严格的指令集和约束。Solana rBPF 存储库的指令集架构(ISA) 正是对此进行了定义。所有 Solana 虚拟机都必须遵守此 ISA,才能符合 Solana 协议。
第一部分介绍rBPF VM 支持的注册表。它们有 64 位宽,这意味着它们可以容纳 64 位整数或地址。
name | feature set | kind | Solana ABI |
---|---|---|---|
r0 | all | GPR | Return value |
r1 | all | GPR | Argument 0 |
r2 | all | GPR | Argument 1 |
r3 | all | GPR | Argument 2 |
r4 | all | GPR | Argument 3 |
r5 | all | GPR | Argument 4 <br/>or stack spill ptr |
r6 | all | GPR | Call-preserved |
r7 | all | GPR | Call-preserved |
r8 | all | GPR | Call-preserved |
r9 | all | GPR | Call-preserved |
r10 | all | Frame pointer | System register |
r11 | from v2 | Stack pointer | System register |
pc | all | Program counter | Hidden register |
寄存器是存储当前正在操作的数据的小型内存位置。ISA 定义了十个通用寄存器 (GPR)。
r0
保存函数的返回数据。r1
通过r5
存储函数参数,并且r5
实际上可以存储“溢出”数据,这些数据由指向某些堆栈数据的指针表示。r6
到 都是r9
调用保留寄存器,这意味着它们的值在函数调用之间得以保留。
除了 GPR 之外,还有一个帧指针 ( r10
),它引用内存中的当前堆栈帧,一个堆栈指针 ( r11
),它跟踪堆栈顶部的位置,以及一个程序计数器 ( pc
),它保存正在执行的当前指令的地址。
下一节将介绍指令布局。如文档中所述,字节码以 64 位槽位编码,指令可以占用一个或两个槽位,由第一个槽位的操作码指示。
class | opcode | dst reg | src reg | offset | immediate | |
---|---|---|---|---|---|---|
0..3 | 3..8 | 8..12 | 12..16 | 16..32 | 32..64 | Bits |
low byte high byte
bit index | meaning |
---|---|
0..=2 | instruction class |
3..=7 | operation code |
8..=11 | destination register |
12..=15 | source register |
16..=31 | offset |
32..=63 | immediate |
指令布局精确涵盖了指令在虚拟机中的编码方式以及每个位的含义。
- 指令类:标识指令的类型(算术、内存访问等)。
- 操作码:具体操作本身。
- 目标寄存器:存储运算结果的寄存器。
- 源寄存器:运算输入数据的来源寄存器。
- 偏移量:用于内存访问或者跳转偏移量。
- 立即数:常量值。
下一节将介绍 rBPF VM 支持的所有操作码。文档中提供的表格详细列出了 VM 支持的每个操作码,其中行标签是操作码的高四位,列标签是操作码的低四位。
在操作码之后,ISA 定义了一个“按类别划分的指令”部分,其中定义了有关特定操作及其约束的细节。例如,它涵盖了 32 位和 64 位算术、乘法、除法、余数、内存访问和控制流。对于每个部分,都提供了有关预期恐慌的具体信息。这些是前面提到的 eBPF 约束,它们在 SVM ISA 中明确定义。
请注意,ISA 中存在 32 位和 64 位算术定义并不意味着虚拟机可以在 32 位和 64 位架构上运行。这些部分专门定义了算术运算,这些运算可能使用 32 位进行内存优化,或在必要时使用 64 位。
ISA 中定义的恐慌相当简单。对于除法,它将除以零和负溢出定义为恐慌情况。对于内存访问,它引用越界或访问违规(即写入只读部分)。最后,对于控制流,提到了越界、对未注册函数的引用和堆栈溢出。
最后,验证部分定义了验证 eBPF 程序的规则,该规则涉及程序二进制文件的静态分析。总之,这构成了 Solana 虚拟机的整个 eBPF VM ISA 定义。
Solana VM 内置程序(加载器)
当 eBPF VM 加载二进制文件时,已编译 eBPF 程序中的函数会被读入所谓的函数注册表。不过,rBPF VM 支持所谓的“内置程序”,这些程序也有自己的函数注册表。
您可能熟悉 Solana 运行时中的这个术语,它使用内置(有时称为“本机”)程序。这两个术语的设计相同,因为两者具有一些相同的行为。Solana 本机程序为运行时提供的功能与 rBPF 虚拟机内置程序为执行 BPF 程序提供的功能相同:访问执行环境中内置的函数。
当 Solana 运行时遇到内置程序的指令(例如系统传输)时,它不会加载和执行某个已编译的 BPF 程序,而是简单地调用运行时内置的函数来执行传输。这个内置函数是系统程序,其代码实际上随 Solana 运行时一起提供,是其环境不可或缺的一部分。这些程序不存在于链上,而是在其相应地址处具有链上占位符。
类似地,在 rBPF 虚拟机环境中,执行程序实际上可以定义调用内置函数的指令。这些函数(就像运行时的内置程序一样)内置在 VM 中。
// <https://github.com/solana-labs/rbpf/blob/9d1a9a0c394e65a322be2826144b64f00fbce1a4/src/vm.rs#L365>
impl<'a, C: ContextObject> EbpfVm<'a, C> {
/* ... */
pub fn execute_program(
&mut self,
executable: &Executable<C>,
interpreted: bool,
) -> (u64, ProgramResult)
}
// <https://github.com/solana-labs/rbpf/blob/7364447cba1319e8b63d54b521776439181853a7/src/elf.rs#L249>
pub struct Executable<C: ContextObject> {
/* ... */
function_registry: FunctionRegistry<usize>,
loader: Arc<BuiltinProgram<C>>,
}
// <https://github.com/solana-labs/rbpf/blob/7364447cba1319e8b63d54b521776439181853a7/src/program.rs#L214>
pub struct BuiltinProgram<C: ContextObject> {
/* ... */
functions: FunctionRegistry<BuiltinFunction<C>>,
}
VM 级内置程序(允许可执行程序访问内置函数集)称为加载器。VM 内置函数有很多种,但 VM 加载器提供的主要函数是系统调用(或“syscall”)。
Solana 系统调用允许执行 eBPF 程序来调用其编译字节码之外的函数(内置于虚拟机中),以执行许多操作,例如:
- 打印日志消息
- 调用其他 Solana 程序(CPI)
- 执行加密算术运算
与虚拟机的 ISA 类似,所有 Solana 系统调用都是 Solana 协议的一部分,并且具有明确定义的接口。这些接口的更改以及新系统调用的引入均受 Solana 改进文档 (SIMD) 流程的管理。
Agave 验证器在 BPF Loader 上实现了所有 Solana 系统调用,BPF Loader 是提供给 VM 的加载器机制。BPF Loader 已有多个版本,包括目前正在开发的Loader v4。
Solana BPF Loaders 也是运行时内置程序(类似于 System 程序),由运行时调用。实际上,当链上 eBPF 程序被指令调用时,运行时实际上会调用拥有它的 BPF Loader 程序来执行它。稍后将详细介绍这一点。
程序执行
rBPF VM 库可以通过两种方式执行 eBPF 程序:通过解释器或使用 JIT 编译为 x86_64 机器代码。
解释器只是逐条执行每条指令,在运行时解释并执行每条指令。这可能会增加一点运行时开销,因为解释器必须在运行时确定每条指令的作用,然后才能执行它,但好处是加载时间大大减少。
相反,将程序实时(JIT)编译为 x86_64 机器代码可以使程序的执行速度更快,但由于初始编译,加载时间会更长。
Agave 目前使用 JIT 编译有几个原因。首先,系统调用目前是动态注册的,这意味着它们无法在静态分析期间由验证步骤处理,而是被标记为“未知”的外部函数调用。在 JIT 编译步骤中,程序二进制文件中的系统调用函数引用会链接到其注册的内置函数。
Agave之旅
了解了 rBPF 虚拟机如何工作的一般背景后,现在是时候了解一下 Agave 验证器,并确切了解当用户发送包含链上程序指令的交易时,rBPF VM 是如何用于执行 Solana 程序的。
项目部署
在开始了解 Agave 的指令处理管道之前,重要的是了解开发人员部署 Solana 程序时发生的情况。
程序部署是通过调用 BPF Loader 程序来完成的,如前所述,这是一个内置程序。作为内置程序,它允许程序访问额外的计算资源,从而实现验证请求部署的程序所必需的几个关键步骤。
solana program deploy
例如,当您运行 CLI 命令时,CLI 将发送一组事务,这些事务将首先分配一个缓冲区帐户并将程序的 ELF 文件写入其中。ELF 非常大,因此这需要经过几个事务才能完成,其中 ELF 会被分块。缓冲区包含整个 ELF 后,即可“部署”程序(最后的 CLI 指令)。
当 BPF Loader 程序的“部署”指令被调用时,它将尝试验证存储在提供的缓冲账户中的 ELF,如果成功,则将 ELF 移入程序账户并将其标记为可执行。只有在成功验证之后,程序才能被 Solana 交易指令调用。
程序 ELF 的验证在 BPF Loader 的宏中已经做了很好的描述。步骤如下:deploy_program!
- 使用“严格”运行时环境将程序加载为 eBPF 可执行文件。此步骤中“严格”环境的目的是防止部署带有弃用 ELF 标头或系统调用的程序。这使用rBPF 中的方法,该方法验证 ELF 文件结构并执行指令重定位。
load
- 根据 ISA 验证已加载的执行程序字节。这使用rBPF 中的方法。
verify
- 使用当前运行环境重新加载程序。
ELF 验证是程序部署中非常重要的一步,因为它直接关系到虚拟机 ISA 设定的期望。BPF Loader 程序实际上将使用 rBPF 库提供的 eBPF 验证工具来验证程序二进制文件,确保它不违反任何约束。
这意味着只有有效的 Solana eBPF 程序二进制文件才能成为活动的 Solana 程序。从性能角度来看,这允许运行时通过简单检查它是否是可执行程序来快速丢弃无效的 Solana 程序二进制文件,因为它只有在通过部署验证后才能变为可执行程序。
交易管道
如前所述,运行时只有在成功部署并验证后才会遇到可执行的 BPF 程序。考虑到这一假设,可以通过 Agave 中的交易管道跟踪有效链上 BPF 程序的交易指令的生命周期。
交易由调度程序调度处理,最终通过Bank实例进行处理。Bank 使用 SVM API 的交易批处理器(具体方法是 )来处理交易。load_and_execute_sanitized_transactions
给定一批交易,处理器将首先评估是否有支付交易费用所需的账户。然后,它将过滤任何可执行程序账户,以供程序 JIT 缓存加载。
程序JIT 缓存仅仅是已经过 JIT 编译为 x86_64 机器代码(如前所述)并准备执行的程序的缓存。程序缓存的最大职责实际上是跨分支加载程序的正确版本,同时考虑到部署或关闭后可能出现的冲突版本。
不久之后,所有必要的账户都会被加载以处理交易。然后,处理交易,如果它是有效交易,则执行。执行交易涉及许多相邻的小步骤,但在本练习中,我们可以主要关注针对已加载的 BPF 程序执行指令的路径。
首先需要的是实例。这是特定于 Agave 的上下文对象,其中包含 rBPF VM 所需的许多特定于 Solana 协议的上下文配置。事实上,rBPF VM 本身是某些上下文对象的通用对象。InvokeContext
/// Main pipeline from runtime to program execution.
pub struct InvokeContext<'a> {
/// Information about the currently executing transaction.
pub transaction_context: &'a mut TransactionContext,
/// The local program cache for the transaction batch.
pub program_cache_for_tx_batch: &'a mut ProgramCacheForTxBatch,
/// Runtime configurations used to provision the invocation environment.
pub environment_config: EnvironmentConfig<'a>,
/// The compute budget for the current invocation.
compute_budget: ComputeBudget,
/// Instruction compute meter, for tracking compute units consumed against
/// the designated compute budget during program execution.
compute_meter: RefCell<u64>,
log_collector: Option<Rc<RefCell<LogCollector>>>,
/// Latest measurement not yet accumulated in [ExecuteDetailsTimings::execute_us]
pub execute_time: Option<Measure>,
pub timings: ExecuteDetailsTimings,
pub syscall_context: Vec<Option<SyscallContext>>,
traces: Vec<Vec<[u64; 12]>>,
}
在此调用上下文中,将处理事务的消息,在此期间将逐条执行每条指令。对于每条指令,将使用 eBPF VM 直接或间接调用目标程序。下一节将介绍调用样式之间的关系。
调用 BPF 程序
在运行时调用 BPF 程序的过程相当复杂。然而,本节将分解该过程,以阐明在 Agave 源代码中可以发现的各种细微差别。
首先,再次回顾一下 Solana 内置程序非常重要。正如我们提到的,这些程序内置于运行时中,因此它们不需要 eBPF 虚拟机即可执行。但是,无论如何都会使用一个。
使用 eBPF VM 执行运行时内置程序主要是为了在内置程序和 BPF 程序之间强制使用一致的接口。这个通用接口称为程序入口点。
// Psuedo-code Rust interface
fn rust(
vm: &mut ContextObject,
arg_a: u64,
arg_b: u64,
arg_c: u64,
arg_d: u64,
arg_e: u64,
memory_mapping: &mut MemoryMapping,
) -> Result
回到 Agave 事务管道之旅,我们在 处停了下来InvokeContext
。所有指令都由方法InvokeContext
内的处理。在此方法中,仅直接调用内置程序。process_executable_chain
首先,运行时确定哪个加载器拥有目标程序。如果是本机加载器,则目标程序是内置程序。如果它是 BPF 加载器之一(所有 BPF 程序都归其所有),则调用该特定的 BPF 加载器内置程序来实际调用目标 BPF 程序。此步骤只是获取要使用的正确加载器 ID。
let builtin_id = {
let borrowed_root_account = instruction_context
.try_borrow_program_account(self.transaction_context, 0)
.map_err(|_| InstructionError::UnsupportedProgramId)?;
let owner_id = borrowed_root_account.get_owner();
if native_loader::check_id(owner_id) {
*borrowed_root_account.get_key()
} else {
*owner_id
}
};
接下来,从加载器的函数注册表中获取对加载器的入口点函数(前面介绍的接口)的引用。这将用于调用内置加载器。
// The Murmur3 hash value (used by RBPF) of the string "entrypoint"
const ENTRYPOINT_KEY: u32 = 0x71E3CF81;
let entry = self
.program_cache_for_tx_batch
.find(&builtin_id)
.ok_or(InstructionError::UnsupportedProgramId)?;
let function = match &entry.program {
ProgramCacheEntryType::Builtin(program) => program
.get_function_registry()
.lookup_by_key(ENTRYPOINT_KEY)
.map(|(_name, function)| function),
_ => None,
}
.ok_or(InstructionError::UnsupportedProgramId)?;
再往下几行就是最终创建 eBPF VM 的地方。但是,这个 VM 只是一个模型。使用模拟 VM 可以强制遵守接口,还允许运行时将内置程序作为 rBPF 内置函数(或系统调用)调用。
let mock_config = Config::default();
let empty_memory_mapping =
MemoryMapping::new(Vec::new(), &mock_config, &SBPFVersion::V1).unwrap();
let mut vm = EbpfVm::new(
self.program_cache_for_tx_batch
.environments
.program_runtime_v2
.clone(),
&SBPFVersion::V1,
// Removes lifetime tracking
unsafe { std::mem::transmute::<&mut InvokeContext, &mut InvokeContext>(self) },
empty_memory_mapping,
0,
);
vm.invoke_function(function);
在 rBPF 内部,该方法仅调用 Rust 接口,而对其调用的实体一无所知。在本例中,它是一个 Solana 内置程序。invoke_functionEbpfVm
/// Invokes a built-in function
pub fn invoke_function(&mut self, function: BuiltinFunction<C>) {
function(
unsafe {
std::ptr::addr_of_mut!(*self)
.cast::<u64>()
.offset(get_runtime_environment_key() as isize)
.cast::<Self>()
},
self.registers[1],
self.registers[2],
self.registers[3],
self.registers[4],
self.registers[5],
);
}
您可能想知道:如果运行时仅使用模拟的 eBPF VM 调用内置函数,那么我的指令的实际目标 BPF 程序在哪里被调用?答案从运行时代码中无法立即看出,因为该机制实际上是 BPF Loader 程序处理器的一部分。
如上所述,当 BPF 程序被指令瞄准时,运行时将调用其所有者,即 BPF 加载程序之一。BPF 加载程序的处理器将确定它收到了哪种类型的指令。这可以是程序帐户管理指令(即升级、关闭)或 BPF 程序的调用。
如果 BPF Loader 程序的账户在指令上下文中存在,处理器就会推断该指令是针对 BPF Loader 程序的。相反,如果推断目标程序是 BPF 程序,则会调用 BPF Loader 的函数。execute
pub fn process_instruction_inner(
invoke_context: &mut InvokeContext,
) -> Result<u64, Box<dyn std::error::Error>> {
/* ... */
let program_account =
instruction_context.try_borrow_last_program_account(transaction_context)?;
// Program Management Instruction
if native_loader::check_id(program_account.get_owner()) {
/* ... */
return {
/* more logic ... */
process_loader_upgradeable_instruction(invoke_context)
}
}
// If the program account is not the BPF Loader program,
// execute the BPF program.
}
BPF Loader 的功能包含真实executeeBPF VM的所有设置步骤,它将执行目标 BPF 程序。
执行 BPF 程序
如上一节所示,当使用 eBPF VM 调用 Solana 内置程序时,invoke_function
将调用该方法,该方法会盲目调用内置函数。但是,当执行 BPF 程序时,运行时实际上会调用 VM 的方法。此时,正确设置 VM 变得至关重要。execute_program
Agave 运行时的介绍在上一节的 BPF Loader 函数中结束execute
。在此功能中,将配置适当的 eBPF VM 并用于执行 BPF 程序。设置 VM 涉及四个重要步骤。
- 参数序列化
- 堆栈和堆的配置
- 内存映射配置
- 系统调用上下文配置
参数序列化是将规范程序参数序列化到虚拟机的内存区域(程序 ID、帐户信息、指令数据)的过程。在此步骤中,所有帐户、指令数据、程序 ID 都将被序列化,最终它们将由大多数 Solana 开发人员所熟知的 SDK 宏进行反序列化。entrypoint!
let (parameter_bytes, regions, accounts_metadata) = serialization::serialize_parameters(
invoke_context.transaction_context,
instruction_context,
!direct_mapping,
)?;
接下来,配置程序内存的堆栈和堆。开发人员可以使用 Compute Budget 程序请求更多堆空间。
macro_rules! create_vm {
/* ... */
let stack_size = $program.get_config().stack_size();
let heap_size = invoke_context.get_compute_budget().heap_size;
let heap_cost_result = invoke_context.consume_checked($crate::calculate_heap_cost(
heap_size,
invoke_context.get_compute_budget().heap_cost,
));
/* ... */
}
现在参数已序列化为内存区域,并且已配置堆栈和堆,所有这些区域都可用于构建主机内存到 VM 内存的内存映射。这会将这些新配置的区域与程序 ELF 中的区域结合起来,形成一个完整的映射,供 VM 使用。
最后,设置调用上下文中的系统调用上下文,用于存储账户字段的内存地址,以便提供更好的错误堆栈跟踪。
pub struct SyscallContext {
pub allocator: BpfAllocator,
pub accounts_metadata: Vec<SerializedAccountMetadata>,
pub trace_log: Vec<[u64; 12]>,
}
pub struct SerializedAccountMetadata {
pub original_data_len: usize,
pub vm_data_addr: u64,
pub vm_key_addr: u64,
pub vm_lamports_addr: u64,
pub vm_owner_addr: u64,
}
impl<'a> ContextObject for InvokeContext<'a> {
fn trace(&mut self, state: [u64; 12]) {
self.syscall_context
.last_mut()
.unwrap()
.as_mut()
.unwrap()
.trace_log
.push(state);
}
fn consume(&mut self, amount: u64) {
// 1 to 1 instruction to compute unit mapping
// ignore overflow, Ebpf will bail if exceeded
let mut compute_meter = self.compute_meter.borrow_mut();
*compute_meter = compute_meter.saturating_sub(amount);
}
fn get_remaining(&self) -> u64 {
*self.compute_meter.borrow()
}
}
ContextObject
rBPF特征在结构上的实现InvokeContext
(前面提到过)显示了系统调用上下文用于提供跟踪的位置。InvokeContext
还负责计量整个事务的计算单元 (CU)。该ContextObject
方法consume
在每个程序执行结束时调用,从而减少事务其余部分的 CU 计量。
VM 在每条指令上都会检查 CU 计量溢出,因此一旦达到最大 CU 预算,下一条指令将以 中止。这可以防止任何长时间运行的进程或无限循环通过恶意程序进程对 Solana 验证器执行拒绝服务攻击。它还会返回错误,导致在超出 CU 时交易失败。Error::ExceededMaxInstructions
完成这四个基本步骤后,就可以正确配置 eBPF VM并执行程序了。
Ok(EbpfVm::new(
program.get_loader().clone(),
program.get_sbpf_version(),
invoke_context,
memory_mapping,
stack_size,
))
let (compute_units_consumed, result) = vm.execute_program(executable, !use_jit);
rBPF 中程序的执行只是执行程序二进制文件中每个操作码的过程,直到程序执行完毕。如前文所述,这可以通过解释或执行 JIT 编译的二进制文件来完成。完成后,VM 将返回一个代码,其中零表示程序成功。u64
VM 中的恐慌由rBPF 库中的 处理。在 Agave 运行时端,这些错误由转换为运行时类型。例如,如果系统调用抛出,则该错误通过 传递到 VM ,然后在运行时重新转换为其正确类型。对于大多数其他变体,将抛出臭名昭著的错误。EbpfError
`InstructionErrorInvokeContext
InstructionErrorEbpfError::SyscallError(..)
InstructionErrorEbfError
InstructionError::ProgramFailedToComplete`
最后,虚拟机的返回代码通过运行时传播,最终产生交易指令的程序结果。这产生了许多 Solana 开发人员共同的交易结果,至此结束了对 Agave 的运行时和虚拟机的这次游览!
随着我们继续发展和优化运行时,Anza 仍然高度关注提高性能、优化计算单元和扩展功能。在Solana 改进文档流程中发表您的意见,参与讨论!
原文:https://www.anza.xyz/blog/the-solana-ebpf-virtual-machine
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »