零知识证明的先进形式化验证:如何证明零知识内存

欧意交易所

欧意交易所

全球前三大交易所之一,新用户注册可拆数字盲盒,100%可以获得数字货币

点击注册 进入官网

更多交易所入口

一站式注册各大交易所、点击进入加密世界、永不失联,币安Binance/欧易OKX/GATE.IO芝麻开门/Bitget/抹茶MEXC/火币Huobi

点击进入 永不失联

在关于零知识证明的先进形式化验证的系列博客中,我们已经讨论了如何验证 ZK 指令以及对两个 ZK 漏洞的深度剖析。正如在公开报告和代码库中所显示的,通过形式化验证每一条 zkWasm 指令,我们找到并修复了每一个漏洞,从而能够完全验证整个 zkWasm 电路的技术安全性和正确性。

尽管我们已展示了验证一条 zkWasm 指令的过程,并介绍了相关的项目初步概念,但熟悉形式化验证读者可能更想了解 zkVM 与其他较小的 ZK 系统、或其他类型的字节码 VM 在验证上的独特之处。在本文中,我们将深入讨论在验证 zkWasm 内存子系统时所遇到的一些技术要点。内存是 zkVM 最为独特的部分,处理好这一点对所有其他 zkVM 的验证都至关重要。

形式化验证:虚拟机(VM)对 ZK 虚拟机(zkVM)

我们的最终目标是验证 zkWasm 的正确性,其与普通的字节码解释器(VM,例如以太坊节点所使用的 EVM 解释器)的正确性定理相似。亦即,解释器的每一执行步骤都与基于该语言操作语义的合法步骤相对应。如下图所示,如果字节码解释器的数据结构当前状态为 SL,且该状态在 Wasm 机器的高级规范中被标记为状态 SH,那么当解释器步进到状态 SL'时,必须存在一个对应的合法高级状态 SH',且 Wasm 规范中规定了 SH 必须步进到 SH'。

同样地,zkVM 也有一个类似的正确性定理:zkWasm 执行表中新的每一行都与一个基于该语言操作语义的合法步骤相对应。如下图所示,如果执行表中某行数据结构的当前状态是 SR,且该状态在 Wasm 机器的高级规范中表示为状态 SH,那么执行表的下一行状态 SR'必须对应一个高级状态 SH',且 Wasm 规范中规定了 SH 必须步进到 SH'。

由此可见,无论是在 VM 还是 zkVM 中,高级状态和 Wasm 步骤的规范是一致的,因此可以借鉴先前对编程语言解释器或编译器的验证经验。而 zkVM 验证的特殊之处在于其构成系统低级状态的数据结构类型。

首先,如我们在之前的博客文章中所述,zk 证明器在本质上是对大素数取模的整数运算,而 Wasm 规范和普通解释器处理的是 32 位或 64 位整数。zkVM 实现的大部分内容都涉及到此,因此,在验证中也需要做相应的处理。然而,这是一个“本地局部”问题:因为需要处理算术运算,每行代码变得更复杂,但代码和证明的整体结构并没有改变。

另一个主要的区别是如何处理动态大小的数据结构。在常规的字节码解释器中,内存、数据栈和调用栈都被实现为可变数据结构,同样的,Wasm 规范将内存表示为具有 get/set 方法的数据类型。例如,Geth 的 EVM 解释器有一个`Memory`数据类型,它被实现为表示物理内存的字节数组,并通过`Set 32 `和`GetPtr`方法写入和读取。为了实现一条内存存储指令,Geth 调用`Set 32 `来修改物理内存。

func opMstore(pc *uint 64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {

// pop value of the stack

mStart, val := scope.Stack.pop(), scope.Stack.pop()

scope.Memory.Set 32(mStart.Uint 64(), &val)

return nil, nil

}

在上述解释器的正确性证明中,我们在对解释器中的具体内存和在规范中的抽象内存进行赋值之后,证明其高级状态和低级状态相互匹配,这相对来说是比较容易的。

然而,对于 zkVM 而言,情况将变得更加复杂。

zkWasm 的内存表和内存抽象层

在 zkVM 中,执行表上有用于固定大小数据的列(类似于 CPU 中的寄存器),但它不能用来处理动态大小的数据结构,这些数据结构要通过查找辅助表来实现。zkWasm 的执行表有一个 EID 列,该列的取值为 1、 2、 3 ……,并且有内存表和跳转表两个辅助表,分别用于表示内存数据和调用栈。

以下是一个提款程序的实现示例:

int balance, amount;

void main () {

balance = 100;

  amount = 10;

balance -= amount; // withdraw

}

执行表的内容和结构相当简单。它有 6 个执行步骤(EID 1 到 6),每个步骤都有一行列出其操作码(opcode),如果该指令是内存读取或写入,则还会列出其地址和数据:

内存表中的每一行都包含地址、数据、起始 EID 和终止 EID。起始 EID 是写入该数据到该地址的执行步骤的 EID,终止 EID 是下一个将会写入该地址的执行步骤的 EID。(它还包含一个计数,我们稍后详细讨论。)对于 Wasm 内存读取指令电路,其使用查找约束来确保表中存在一个合适的表项,使得读取指令的 EID 在起始到终止的范围内。(类似地,跳转表的每一行对应于调用栈的一帧,每行均标有创建它的调用指令步骤的 EID。)

这个内存系统与常规 VM 解释器的区别很大:内存表不是逐步更新的可变内存,而是包含整个执行轨迹中所有内存访问的历史记录。为了简化程序员的工作,zkWasm 提供了一个抽象层,通过两个便捷入口函数来实现。分别是:

alloc_memory_table_lookup_write_cell

Alloc_memory_table_lookup_read_cell

其参数如下:

例如,zkWasm 中实现内存存储指令的代码包含了一次对’write alloc’函数的调用:

let memory_table_lookup_heap_write1 = allocator

    .alloc_memory_table_lookup_write_cell_with_value(

"store write res 1",

constraint_builder,

eid,

move |____| constant_from!(LocationType::Heap as u 64),

move |meta| load_block_index.expr(meta),   // address

move |____| constant_from!( 0),             // is 32-bit

move |____| constant_from!( 1),             // (always) enabled

    );

let store_value_in_heap1 = memory_table_lookup_heap_write1.value_cell;

`alloc`函数负责处理表之间的查找约束以及将当前`eid`与内存表条目相关联的算术约束。由此,程序员可以将这些表看作普通内存,并且在代码执行之后 `store_value_in_heap1`的值已被赋给了 `load_block_index` 地址。

类似地,内存读取指令使用`read_alloc`函数实现。在上面的示例执行序列中,每条加载指令有一个读取约束,每条存储指令有一个写入约束,每个约束都由内存表中的一个条目所满足。

mtable_lookup_write(row 1.eid, row 1.store_addr, row 1.store_value)

                       ⇐ (row 1.eid= 1 ∧ row 1.store_addr=balance ∧ row 1.store_value= 100 ∧ ...)

mtable_lookup_write(row 2.eid, row 2.store_addr, row 2.store_value)

                      ⇐ (row 2.eid= 2 ∧ row 2.store_addr=amount ∧ row 2.store_value= 10 ∧ ...)

mtable_lookup_read(row 3.eid, row 3.load_addr, row 3.load_value)

                      ⇐ ( 2<row 3.eid≤ 6 ∧ row 3.load_addr=amount ∧ row 3.load_value= 100 ∧ ...)

mtable_lookup_read(row 4.eid, row 4.load_addr, row 4.load_value)

                      ⇐ ( 1<row 4.eid≤ 6 ∧ row 4.load_addr=balance ∧ row 4.load_value= 10 ∧ ...)

mtable_lookup_write(row 6.eid, row 6.store_addr, row 6.store_value)

                      ⇐ (row 6.eid= 6 ∧ row 6.store_addr=balance ∧ row 6.store_value= 90 ∧ ...)

形式化验证的结构应与被验证软件中所使用的抽象相对应,使得证明可以遵循与代码相同的逻辑。对于 zkWasm,这意味着我们需要将内存表电路和“alloc read/write cell”函数作为一个模块来进行验证,其接口则像可变内存。给定这样的接口后,每条指令电路的验证可以以类似于常规解释器的方式进行,而额外的 ZK 复杂性则被封装在内存子系统模块中。

在验证中,我们具体实现了“内存表其实可以被看作是一个可变数据结构”这个想法。亦即,编写函数 `memory_at type`,其完整扫描内存表、并构建相应的地址数据映射。(这里变量 `type` 的取值范围为三种不同类型的 Wasm 内存数据:堆、数据栈和全局变量。)而后,我们证明由 alloc 函数所生成的内存约束等价于使用 set 和 get 函数对相应地址数据映射所进行的数据变更。我们可以证明:

更多交易所入口

一站式注册各大交易所、点击进入加密世界、永不失联,币安Binance/欧易OKX/GATE.IO芝麻开门/Bitget/抹茶MEXC/火币Huobi

点击进入 永不失联

目录[+]