Noname 3.0:原生 Hint、标准库、编译器可视化工具及更多!- ZKSECURITY

Noname 3.0 发布,它是一种受 Rust 和 Golang 启发的 zk 编程语言,旨在为开发人员提供比 Circom 更好的体验。此版本包含原生 hint、标准库 (stdlib) 和更丰富的调试功能,并介绍了 hint 函数的使用方式、标准库的模块、迭代器、日志记录和编译器管道可视化工具,同时规划了未来的开发方向。

noname-v3

我们已经研究 noname 一段时间了,这是一种受 Rust 和 Golang 启发的 zk 编程语言,目标是为开发者提供比 Circom 更好的体验。我们很高兴地宣布,noname 现在应该可以实现与 Circom 完全相同的功能。

我们推出 noname 3.0,这是 noname 最重要的更新,包括原生 hints,一个标准库 (stdlib),更多的调试功能,以及许多为开发者提供的质量改进。在这篇文章中,我们将更详细地讨论这些更新,并概述我们 noname 下一阶段开发的重点。

Hint 函数

由于算术后端的各种限制,许多计算必须在电路之外完成。以除法运算为例:

res = dividend / divisor

在 R1CS 后端中,每个约束都以二次形式 a * b = c 表示。为了使除法在这种约束系统中工作,我们必须将原始方程转换为 res * divisor = dividend。给定变量 dividenddivisor,我们需要计算 res 的值,然后使其成为约束的一部分以进行评估。此计算称为 hint 计算。

电路外计算是 hint 计算的另一个名称。之所以将其视为“电路外”,是因为虽然 hint 值成为电路的一部分,但 hint 计算过程本身并非属于电路的一部分。换句话说,hint 计算中的步骤不受约束。

因此,在 ZK 编译器中使用 hint 功能需要权衡。一方面,它可以促进创新,因为人们可以使用电路中的 hints 做任何他们想做的事情。另一方面,如果 hints 没有得到适当的约束,它可能会引入严重的安全问题。权衡方案是引入一个类似于 Rust 中的 unsafe 关键字,以强调使用 hints 时的潜在风险。这是一个简单的例子:

hint fn div(dividend: Field; divisor: Field) -> Field {
    return dividend / divisor;
}

fn constrained_div(xx: Field, yy: Field) -> Field {
    // 如果不存在 `unsafe`,将会抛出错误
    let res = unsafe div(xx, yy);

    // 这个关系证明了 res 是除法结果
    assert_eq(res * yy, xx);

    return res;
}

调用 hint 函数时需要关键字 unsafe,目的是让用户承认变量 resdiv 函数之间的关系不受约束。这意味着可以为变量 res 分配任何值。

unsafe 关键字的必要性有助于提高人们的认识,即用户有责任确保存在必要的约束,以实现 hint 计算的意图。例如,assert_eq(res * yy, xx) 是一个手动约束,用于确保输入和输出之间的关系对于 hint 函数 div 成立。

初始标准库

这是 noname 标准库的首次更新,这对于实际项目至关重要,因为编写电路通常需要处理低级别的编程细节。创建诸如位分解和比较器之类的电路对于初学者来说可能是令人生畏的,因为它们需要深入了解算术后端和安全影响。

初始标准库具有最基本的模块:std::bitsstd::comparatorstd::mimcstd::multiplexerstd::ints

下面提供了每个模块的示例。

位分解

这是 stdlib 中最基本的模块。许多电路将依赖于它,特别是对于范围检查。

签名

fn to_bits(const LEN: Field, value: Field) -> [Bool; LEN]
fn from_bits(bits: [Bool; LEN]) -> Field

示例

use std::bits; // 导入 `bits` 模块

fn main(pub xx: Field) {
    // 将 xx 变量的值分解为 3 位
    let bits = bits::to_bits(3, xx);

    // 假设 xx = 2,则位将为 [0, 1, 0]
    assert(!bits[0]);
    assert(bits[1]);
    assert(!bits[2]);

    // 将位转换回字段值
    let val = bits::from_bits(bits);
    assert_eq(val, xx);
}

比较器

另一个基本模块是比较器,它依赖于位模块。

签名:

fn less_than(const LEN: Field, lhs: Field, rhs: Field) -> Bool

示例:

use std::comparator; // 导入比较器模块

fn main(pub lhs: Field, rhs: Field) -> Bool {
    // 当 lhs < rhs 时应返回 true,否则返回 false
    let res = comparator::less_than(3, lhs, rhs);

    return res;
}

无符号整数

在没有封装的情况下,使用像 comparator 这样的低级 API 很容易出错,这些 API 需要对函数参数进行某些假设。如果提供的参数值与假设不符,则可能导致稳健性问题。

为了缓解因误解低级 API 而产生的问题,UInts 在 noname 中由结构体表示。结构体方法封装了结构体内部值上的函数。例如,Uint8.new 函数确保表示 Uint8 的值已进行范围检查。

struct Uint8 {
    inner: Field,
}

fn Uint8.new(val: Field) -> Uint8 {
    let bit_len = 8;
    bits::check_field_size(bit_len);

    // 范围检查
    let ignore_ = bits::to_bits(bit_len, val);

    return Uint8 {
        inner: val
    };
}

有了这个基础结构体,就可以向该结构体添加方法。

less_than 方法为例:

fn Uint8.less_than(self, rhs: Uint8) -> Bool {
    return comparator::less_than(8, self.inner, rhs.inner);
}

该结构体有助于封装诸如 comparator::less_than 之类的低级 API。这样,用户无需处理低级参数,从而有助于避免安全漏洞。这种方法提供了管理低级和高级 API 的灵活性。

此外,UInts 目前支持基本运算,例如加法、减法、乘法、除法、取模等。

MiMC

MiMC 是一种简单的哈希算法。在 noname 中,主函数是 mimc7_hash,它使用低级 API mimc7_cipher 作为其核心函数。我们还硬编码了循环次数,以避免潜在的滥用。

签名:

fn mimc7_hash(values: [Field; LEN], key: Field) -> Field

示例:

let key = 321;
let val = [1, 2, 3]; // 要哈希的值数组
let res = mimc::mimc7_hash(val, xx); // 返回哈希值

多路复用器

在电路中,访问数组元素不像在传统编程语言中那样简单。在底层,它需要对输入和输出进行一定的安排才能满足必要的约束。多路复用器模块是一个高级 API,可以更轻松地访问数组元素。

签名:

// 根据 `target_idx` 从 2D 数组中选择一个元素,并返回长度为 `WIDLEN` 的向量。
fn select_element(arr: [[Field; WIDLEN]; ARRLEN], target_idx: Field) -> [Field; WIDLEN]

示例:

let xx = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
let idx = 1
// 应返回 [4, 5, 6]
let chosen_elements = multiplexer::select_element(xx, idx);

迭代器

一个新添加的语言特性是迭代器,它可以很好地与通用数组一起工作。通用特性能够处理不同长度的数组,而迭代器特性简化了循环遍历这些通用数组的过程。

示例:

fn main(pub arr: [Field; LEN]) {
    let mut sum = 0;

    // 没有迭代器
    for idx in 0..LEN {
        sum = sum + arr[idx];
    }

    // 使用迭代器
    for elm in arr {
        sum = sum + elm;
    }
}

日志记录

如果没有适当的日志记录工具,几乎不可能构建出有趣的东西。一个方便的日志记录功能可以为开发人员节省大量时间。Noname 最近添加了自己的日志记录函数来满足这一需求。

示例

fn main(pub public_input: Field, private_input: Field) {
    let xx = private_input + public_input;
    let yy = private_input * public_input;
    log(xx); // 将打印出类似 "span:147, val: 4" 的内容
    log(yy); // "span:148, val: 4"
    assert_eq(xx, yy);
}
fn main(pub xx: Field) {
    let mut thing = Thing { xx: xx, };
    log(thing.xx); // 打印出结构体的字段
    thing.xx = thing.xx + 1;
    log(thing.xx);
}

目前,它仅支持打印字段。此功能还有很大的改进空间。计划中的一项改进是允许每个日志记录自定义消息(也列在本帖的末尾)。

编译器管道可视化工具

Noname 的编译器有一系列管道步骤,用于将代码编译成电路。调查代码是如何编译的,需要深入了解编译器的工作方式,以及在 noname 代码库中进行手动日志记录。

我们构建了一个编译器可视化工具,以帮助调查编译过程。该工具还可以作为了解编译器如何工作的学习工具。例如,你可以编写自己的 noname 代码,并使用此工具来可视化代码的逐步编译过程。

要使用它,请从 noname 包的根文件夹运行以下命令:

noname build --server-mode

在下面的屏幕截图中,两个面板并排显示,显示了编译器在不同时间点的状态。

visualizer

这些面板用于比较编译过程中两个不同工件之间的差异。

例如,面板灰色区域中的每一行都是可点击的,显示其相应的详细信息。要调查 bits 模块的词法分析器如何编译为 AST,你可以单击面板 1 中的第一个元素和面板 2 中的第二个元素。这允许你检查左侧的词法分析器Token和右侧生成的 AST。

接下来是什么?

有了调试器、hint 函数和初始 stdlib,noname 现在更接近于实际 ZK 项目编写其电路的位置。为了更进一步,以下是我们将要处理的优先任务。

我们将添加更多 stdlib 以涵盖更多用例。这些包括布尔类型稀疏 Merkle 树bigint 等。

此外,还将改进语言功能: 允许在 ITE 分支中使用表达式:当前的 if else 分支仅允许使用单个变量。允许在条件分支中使用表达式将能够在必要时编写复杂的逻辑。

更好的 logger:支持为每个日志记录自定义消息。这应该会大大改善开发体验。

允许复杂的包结构:这将允许构建复杂的 noname 第三方软件包,并帮助管理代码结构。

动态模块加载:代替加载所有内置函数和所有 stdlib,由于动态模块加载将删除不必要的序言,因此这将提高编译过程的效率。

对结构体字段的可访问性控制:这基本上为结构体字段提供了一个“白名单”,以避免结构体 API 的滥用。

通用结构体:通用函数特性允许基于不同的常量值创建不同情况下的函数模板。它极大地提高了代码的可重用性,同时简化了代码库。通用结构体特性与此目的完全相同,但适用于结构体。

此外,以下是一些简单但有影响的任务:

我们感谢你的贡献。如果你想研究这些功能中的任何一个或有任何建议,请随时与我们联系。

  • 原文链接: blog.zksecurity.xyz/post...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
zksecurity
zksecurity
Security audits, development, and research for ZKP, FHE, and MPC applications, and more generally advanced cryptography.