cairo 1.0官方文档翻译加学习(15 拼图)

15拼图游戏

在这里插入图片描述
就这么个玩意,我没玩过,看起来类似中国的华容道
我们将以 15 拼图为例——我们将展示如何编写一个 Cairo 程序来验证 15 拼图的解决方案(初始状态将是一个输入),从而让您证明您知道初始状态的解决方案状态(不必向验证证明的人透露解决方案!)。

我们需要检查什么

我们将解决方案表示为两个列表:第一个将包含空图块的位置(行和列,两者都从零开始索引),因此在上面的示例中我们有:[(0, 2), (1 , 2), (1, 3), (2, 3), (3, 3)]。第二个列表将包含要移动的图块上的数字,因此我们有:[3, 7, 8, 12]。请注意,第一个列表总是长一个元素。我们称第二个列表的长度为 n_steps(这确实是步数),第一个列表的长度为 n_steps + 1。

我们将验证以下属性是否成立:这里类似编写zk电路添加约束的过程

  1. 第一个列表中的位置是有意义的——所有数字都在 0 到 3 之间,每对连续的数字代表相邻的位置。约束块不能越界(初始状态和边界)
  2. 根据第一个列表中的位置,第二个列表中的数字对应于图块的值。例如,3 是空白块初始状态下的位置应放的图块, 同时是 (0, 2) 图块下一个状态下位置(1,2)的图块。类似地,7 是第二状态中位置 (1, 3) 处的图块,以及下一个状态中位置 (1, 2) 处的图块。约束中间的移动过程
  3. 最终状态是“已解决”的。约束答案是正确的(结果)

定义位置结构体

让我们首先定义一个表示图块位置的结构:

struct Location {
    row: felt,
    col: felt,
}

第一行 struct Location { 开始定义结构。接下来我们定义两个成员 row 和 col,都是 felt 类型。最后我们用 } 字符关闭结构。

验证单个位置的有效性

func verify_valid_location(loc: Location*) {
    // Check that row is in the range 0-3.
    tempvar row = loc.row;
    assert row * (row - 1) * (row - 2) * (row - 3) = 0;

    // Check that col is in the range 0-3.
    tempvar col = loc.col;
    assert col * (col - 1) * (col - 2) * (col - 3) = 0;

    return ();
}

表达式 loc: Location* 指示 Cairo 将 loc 解释为 Location 实例的地址。这意味着它会期望地址 loc 处内存的值是该位置的行,地址 loc + 1 处的值是列。 Cairo 让我们使用 loc.row 和 loc.col 解决这两个值。

接下来我们看到一个临时变量的定义。我们在上面提到 Cairo 内存是不可变的,所以“变量”这个名字可能会产生误导(因为它的值不能改变)。形式为 tempvar a = expr 的语句;分配一个内存单元,将其命名为 a,并为其分配 值。

临时变量的范围是有限的。例如,临时变量可能由于跳转(例如,if 语句)或函数调用而被撤销。你可能还记得,在第一节中我们提到Cairo有一些微妙的地方,这就是其中之一。因为这个函数非常简单,没有跳转,也没有使用 tempvar 调用其他函数,所以这里是没有问题的。

Cairo没有 < 运算符。原因是在 Cairo 机器中,小于操作是一个复杂的操作,所以 Cairo 有一个名为 range-check 的内置函数,它允许比较值。讲道理,读到这,有点zk的感觉了
Cairo有调用range-check的库函数,例如 assert_nn_le(),它获取两个参数 x 和 y 并验证0<=x<=y
划重点,这里不翻译了我直接解释吧 这里这个代码是用了一个数学技巧,就是几个多项式乘积为0,那么至少存在一项为0。那么我们看row * (row - 1) * (row - 2) * (row - 3) = 0,就是row只能是0或者1或者2或者3这4个值,否则乘积就不为0了,进而约束了row的范围是0到3。

函数的最后一行是return();,不像高级语言中return语句是隐式的,即使没有返回值,你也必须在函数的末尾显式地使用return()。
到这里完成了第一步的约束

验证两个连续的位置

让我们继续验证两个连续位置是否相邻:如果我们查看两个位置之间的差异,我们希望看到 (0, 1)、(0, -1)、(1, 0)、(-1, 0).例如,上例中的前两个位置是 (0, 2) 和 (1, 2),实际上是 (0, 2) - (1, 2) = (-1, 0)。

func verify_adjacent_locations(
    loc0: Location*, loc1: Location*
) {
    alloc_locals;
    local row_diff = loc0.row - loc1.row;
    local col_diff = loc0.col - loc1.col;

    if (row_diff == 0) {
        // The row coordinate is the same. Make sure the
        // difference in col is 1 or -1.
        assert col_diff * col_diff = 1;
        return ();
    } else {
        // Verify the difference in row is 1 or -1.
        assert row_diff * row_diff = 1;
        // Verify that the col coordinate is the same.
        assert col_diff = 0;
        return ();
    }
}

该函数使用局部变量。这些类似于临时变量,除了它们可以访问的范围受到的限制要少得多——您可以从它们的定义开始到函数结束访问它们。

Alloc_locals;是Cairo机制的一部分。它分配了函数本地变量所需的内存。通常,这应该是使用局部变量的函数中的第一个语句。如果您尝试使用没有该行的本地变量,则编译将失败。

因此,如果编译器知道我何时使用局部变量,为什么它不能为我添加该行?有两个原因:

  1. Cairo 是一种显式语言——在大多数情况下,除非代码明确说明,否则它不会自动添加指令。
  2. 在某些情况下,可以避免此语句,并通过增加 ap 寄存器作为其他指令的一部分来手动分配所需的内存。在其他情况下,将它放在代码的不同部分是有意义的。

让我们回顾一下函数的流程:首先,我们计算行和列的差异(回想一下,我们期望它们为 -1、0 或 1)然后,如果行相同,则列差col_diff 必须为 -1 或 1(相当于 col_diff * col_diff = 1)。如果不为零,则列必须相同且行差必须为 -1 或 1。

引用、临时变量和局部变量

引用是使用 let 语句定义的,例如 let x = y * y * y;。您应该将 x 视为表达式 y * y * y 的别名,这意味着指令 let x = y * y * y;本身不会导致执行任何计算。另一方面,后面的指令如 assert x * x = 1;将变成 assert (y * y * y) * (y * y * y) = 1;。定义引用的范围源于定义别名表达式的范围。

注意:语法 let x = foo(…) 是上述的一个例外——它立即调用 foo() (不像其他 let 语句,它不会导致实际计算),并创建一个对返回值的引用 x的 foo()。一般来说,引用不能包含函数调用

临时变量和局部变量是引用的特例。它们指向一个特定的存储单元,存储计算结果。因此语句 tempvar x = y * y * y;将调用计算,并且 x 将是包含结果的存储单元的别名,而不是表达式 y * y * y

临时变量不需要事先分配内存,但它们的范围是有限的。局部变量位于函数堆栈的开头,因此需要使用指令 alloc_locals 预先分配,但在函数的整个执行过程中都可以访问它们。

函数调用结果的范围类似于临时变量的范围。如果稍后需要访问返回值,则应将结果复制到局部变量。

如果您收到临时变量已被撤销的错误,您可以尝试将其设为局部变量。

到这就完成了第二部分,约束中间计算过程的正确性

验证位置列表

让我们将其包装在一个循环(准确地说是递归)中,在整个位置列表上调用这两个函数。

func verify_location_list(loc_list: Location*, n_steps) {
    // Always verify that the location is valid, even if
    // n_steps = 0 (remember that there is always one more
    // location than steps).
    verify_valid_location(loc=loc_list);

    if (n_steps == 0) {
        return ();
    }

    verify_adjacent_locations(
        loc0=loc_list, loc1=loc_list + Location.SIZE
    );

    // Call verify_location_list recursively.
    verify_location_list(
        loc_list=loc_list + Location.SIZE, n_steps=n_steps - 1
    );
    return ();
}

添加虚拟main()函数

在我们继续之前,让我们编写一个虚拟主函数,让我们可以运行 verify_location_list(稍后我们将删除它,并用真正的主函数替换它):

from starkware.cairo.common.registers import get_fp_and_pc

func main() {
    alloc_locals;

    local loc_tuple: (
        Location, Location, Location, Location, Location
    ) = (
        Location(row=0, col=2),
        Location(row=1, col=2),
        Location(row=1, col=3),
        Location(row=2, col=3),
        Location(row=3, col=3),
    );

    // Get the value of the frame pointer register (fp) so that
    // we can use the address of loc_tuple.
    let (__fp__, _) = get_fp_and_pc();
    // Since the tuple elements are next to each other, we can
    // use the address of loc_tuple as a pointer to the 5
    // locations.
    verify_location_list(
        loc_list=cast(&loc_tuple, Location*), n_steps=4
    );
    return ();
}

此函数使用元组来定义和存储 Location 元素列表。元组是有序的有限列表,可以包含有效类型的任意组合,例如,五个 Location 结构。可以使用从零开始的索引访问每个元素(例如,loc_tuple[2] 是第三个元素。请参阅元组)

在函数的开头,我们使用类型化的局部变量分配了 5 个位置。 Cairo 查找常量 Location.SIZE 以找出每个变量需要多少个单元格,然后按照定义的顺序分配它们。由于 loc_tuple 是一个包含 5 个位置的元组,Cairo 分配了 5 * Location.SIZE 内存单元。每个 Location 实例都分配了一些坐标(根据上面的示例)。

由于 verify_location_list 需要指向位置列表的指针,因此我们传递 &loc_tuple,它表示 loc_tuple 在内存中的地址。由于 &loc_tuple 的类型是指向元组的指针而不是 Location*,我们需要强制转换操作来指示编译器将此地址视为 Location*。

由于技术原因,当 Cairo 需要检索局部变量 (&loc_tuple) 的地址时,需要告知帧指针寄存器 fp 的值(请参阅 fp 寄存器)。这可以通过语句 let ( __ fp__, _ ) = get_fp_and_pc() 来完成,它调用库函数 get_fp_and_pc() 来检索 fp。结果命名为 __ fp__,这是 Cairo 在必须知道 fp 时查找的名称。如果忘记写这行,可能会出现如下形式的错误:直接使用 fp 的值,需要定义一个名为 fp 的变量。

语法 let (…) = foo(…) 调用一个函数 foo,它返回一个值元组并将该元组的每个条目存储到一个单独的变量中。符号 _ 可用于跳过条目。

练习

  1. 使用位置坐标的值,如果它们代表非法值,请确保程序失败。例如,尝试将 loc_tuple[0].row 从 0 更改为 10。您应该会看到 verify_valid_location 中的断言失败。或者您可以将此值更改为 1,这将使第一个转换不合法(空块不能留在同一个地方)。
  2. 修改 verify_location_list 以便它检查最后一个位置确实是 (3, 3)。

原文连接: https://www.cairo-lang.org/docs/hello_cairo/puzzle.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值