本文档详细描述了Cannon故障证明虚拟机(FPVM)的规范,该虚拟机模拟了一个运行在big-endian 32位MIPS32架构上的最小Linux系统,重点介绍了FPVM的状态(包括内存、寄存器等)、内存管理(包括堆)、延迟槽、系统调用、I/O操作(包括标准流、提示通信和预图像通信)以及异常处理,为理解和实现FPVM提供技术指导。
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> 目录
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
这是对 Cannon 故障证明虚拟机 (FPVM) 的描述。Cannon FPVM 模拟了一个运行在 big-endian 32 位 MIPS32 架构上的最小化 Linux 系统。它的许多行为都复制自 Linux/MIPS,并针对故障证明进行了一些调整。在本文档的其余部分,我们将 Cannon FPVM 简称为 FPVM。
在操作上,FPVM 是一个状态转换函数。此状态转换被称为 Step,它执行单个指令。我们说 VM 是一个函数 $f$,给定一个输入状态 $S{pre}$,在状态中编码的单个指令上执行步骤,以产生新的状态 $S{post}$。 $$f(S{pre}) \rightarrow S{post}$$
因此,FPVM 执行的程序的轨迹是 VM 状态的有序集合。
虚拟机状态突出了在 VM 上运行故障证明程序的效果。 它由以下字段组成:
memRoot
- 表示 VM 内存的 merkle root 的 bytes32
值。preimageKey
- 上次请求的 pre-image key 的 bytes32
值。preimageOffset
- 上次请求的 pre-image offset 的 32 位值。pc
- 32 位程序计数器。nextPC
- 32 位 next program counter。请注意,在执行分支/跳转延迟槽时,此值可能不总是 $pc+4$。lo
- 32 位 MIPS LO 特殊寄存器。hi
- 32 位 MIPS HI 特殊寄存器。heap
- 通过 mmap 最近一次内存分配的 32 位基地址。exitCode
- 8 位退出代码。exited
- VM 已退出的 1 位指示器。registers
- 通用 MIPS32 寄存器。每个寄存器都是一个 32 位值。状态表示为按顺序将上述字段打包到 226 字节的缓冲区中。
状态哈希是通过使用 Keccak256 哈希函数哈希 226 字节的状态缓冲区,然后将高位字节设置为相应的 VM 状态来计算的。
VM 状态可以从状态的 exited
和 exitCode
字段派生。
enum VmStatus {
Valid = 0,
Invalid = 1,
Panic = 2,
Unfinished = 3,
}
fn vm_status(exit_code: u8, exited: bool) -> u8 {
if exited {
match exit_code {
0 => VmStatus::Valid,
1 => VmStatus::Invalid,
_ => VmStatus::Panic,
}
} else {
VmStatus::Unfinished
}
}
内存表示为二进制 merkle tree。
该树具有固定的 27 层深度,叶子值为 32 字节。
这跨越了完整的 32 位地址空间,其中每个叶子包含树的那部分的内存。
状态 memRoot
表示树的 merkle root,反映了内存写入的效果。
由于这种内存表示形式,所有内存操作都是 4 字节对齐的。
内存访问不需要任何权限。指令步骤可以访问任何内存位置,因为整个地址空间都不受保护。
FPVM 状态包含一个 heap
,用于跟踪最近一次内存分配的基地址。
堆页面按照每个 mmap
系统调用,在页面边界以 bump allocated 方式分配。
mmaping 纯粹是为了满足需要系统调用的内存指针结果来定位空闲内存的程序运行时。页面大小为 4096。
FPVM 在 0x40000000
处具有固定的程序中断。但是,允许 FPVM 通过 mmap 系统调用将堆扩展到此限制之外。
为简单起见,没有针对其他内存段的“堆溢出”的内存保护。
此类 VM 步骤仍被视为有效的状态转换。
内存映射的规范超出了本文档的范围,因为它与 VM 状态无关。FPVM 实现者可以参考 Linux/MIPS 内核以获取灵感。
步骤的后状态更新 nextPC
,指示 pc
之后的指令。
但是,在正在执行分支指令的情况下,nextPC
后状态设置为分支目标。并且 pc
后状态像往常一样设置为分支延迟槽。
如果当前指令是一个填充有跳转或分支类型指令的延迟槽,则 VM 状态转换无效。 也就是说,当执行跳转/分支指令时,$nextPC \neq pc + 4$。 否则,将有两个连续的延迟槽。虽然这在典型的 MIPS 实现中被认为是“未定义”的行为,但 FPVM 必须在执行此类状态时引发异常。
系统调用的工作方式类似于 Linux/MIPS,包括系统调用约定和常规系统调用处理行为。 但是,FPVM 支持 Linux/MIPS 系统调用的子集,其行为略有不同。 下表总结了支持的系统调用及其行为。
$v0 | 系统调用 (system call) | $a0 | $a1 | $a2 | 效果 (Effect) |
---|---|---|---|---|---|
4090 | mmap | uint32 addr | uint32 len | 从堆中分配一个页面。有关详细信息,请参见 堆 (heap)。 | |
4045 | brk | 返回 0x40000000 处程序中断的固定地址 |
|||
4120 | clone | 返回 1 | |||
4246 | exit_group | uint8 exit_code | 将 Exited 和 ExitCode 状态分别设置为 true 和 $a0 。 |
||
4003 | read | uint32 fd | char *buf | uint32 count | 与 Linux/MIPS 的行为类似,支持非對齊读取。有关更多详细信息,请参见 I/O。 |
4004 | write | uint32 fd | char *buf | uint32 count | 与 Linux/MIPS 的行为类似,支持非對齊写入。有关更多详细信息,请参见 I/O。 |
4055 | fcntl | uint32 fd | int32 cmd | 与 Linux/MIPS 的行为类似。仅支持 F_GETFL (3) cmd。将所有其他命令的 errno 设置为 0x16 |
对于上述所有系统调用,通过将返回寄存器 ($v0
) 设置为 0xFFFFFFFF
(-1) 来指示错误,并相应地设置 errno ($a3
)。
VM 在系统调用处理期间不得修改 $v0
和 $a3
以外的任何寄存器。
对于不支持的系统调用,VM 除了清零系统调用返回 ($v0
) 和 errno ($a3
) 寄存器外,不得执行任何操作。
请注意,上述系统调用具有与 Linux/MIPS 相同的系统调用号和 ABI。
VM 不支持 Linux open(2)。但是,VM 可以从预定义的文件描述符集中读取和写入。 | 名称 | 文件描述符 | 描述 |
---|---|---|---|
stdin | 0 | 只读标准输入流。 | |
stdout | 1 | 只写标准输出流。 | |
stderr | 2 | 只写标准错误流。 | |
提示响应 (hint response) | 3 | 只读。用于读取预映像提示的状态。 | |
提示请求 (hint request) | 4 | 只写。用于提供 预映像提示。 | |
预映像响应 (pre-image response) | 5 | 只读。用于 读取预映像。 | |
预映像请求 (pre-image request) | 6 | 只写。用于 请求预映像。 |
引用未知文件描述符的系统调用将失败,并显示与 Linux 上相同的 EBADF
errno。
写入和读取标准输出、输入和错误流对 FPVM 状态没有影响。 只要 I/O 是 stateless 的,FPVM 实现就可以将它们用于调试目的。
所有 I/O 操作都限制为每次操作最多 4 个字节。 任何超过此限制的读取或写入系统调用请求都将被截断为 4 个字节。 因此,读取/写入系统调用的返回值最多为 4,表示实际读取/写入的字节数。
写入 stderr/stdout 标准流总是会成功,并返回写入计数输入,从而有效地继续执行而不执行写入工作。 从 stdin 读取除了返回零且 errno 设置为 0 之外没有任何作用,表示没有输入。
提示请求和响应对 VM 状态没有影响,除了将 $v0
返回寄存器设置为请求的读取/写入计数。
VM 实现可以利用提示来设置后续的 pre-image 请求。
preimageKey
和 preimageOffset
状态通过读取/写入系统调用更新为 pre-image 读取和写入文件描述符 (请参见 I/O)。
preimageKey
缓冲写入 pre-image 写入 fd 的字节流。
preimageKey
缓冲区被移动以适应写入到其末尾的新字节。
写入还会将 preimageOffset
重置为 0,表示读取新 pre-image 的意图。
在处理 pre-image 读取时,preimageKey
用于从 Oracle 中查找 pre-image 数据。
将 pre-image 在 preimageOffset
处最大 4 字节的块读取到指定的地址。
每个读取操作都会将 preimageOffset
增加请求的字节数(截断为 4 个字节并受对齐限制约束)。
如 内存 中所述,所有内存操作都是 4 字节对齐的。
由于 pre-image I/O 发生在内存上,因此所有 pre-image I/O 操作必须严格遵守对齐边界。
这意味着读取/写入操作的开始和结束必须落在同一对齐边界内。
如果操作违反了这一点,则必须截断读取/写入系统调用的输入 count
,以使读取/写入的最后一个字节的有效地址与输入有效地址匹配。
VM 必须读取/写入尽可能多的字节,而不会超过输入地址对齐边界。 例如,对 3 字节对齐的缓冲区发出的写入请求的效果必须正好是 3 个字节。 如果缓冲区未对齐,则 VM 可能会写入少于 3 个字节,具体取决于未对齐的大小。
FPVM 可能会引发异常,而不是输出 post-state 来指示无效状态转换。 通常,FPVM 必须至少在以下情况下引发异常:
VM 实现可能会在特定于该实现的其他情况下引发异常。
例如,依赖于预先提供的 merkle proof 进行内存访问的链上 FPVM 可能会在提供的 merkle proof 与 pre-state memRoot
不匹配时引发异常。
- 原文链接: github.com/ethereum-opti...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!