From 861b15500bb94c1c51ea837a72db13b2044b821f Mon Sep 17 00:00:00 2001 From: feltroid Prime Date: Mon, 29 Jan 2024 13:38:22 +0100 Subject: [PATCH] Modulo Builtin setup --- tests/cairo_programs/modulo.cairo | 136 +++ tests/cairo_programs/modulo_trick.cairo | 117 +++ tools/make/cairo/common/BUILD | 144 +++ tools/make/cairo/common/cairo_builtins.cairo | 60 ++ tools/make/cairo/common/modulo.cairo | 54 ++ tools/make/cairo/lang/builtins/BUILD | 62 ++ tools/make/cairo/lang/builtins/modulo/BUILD | 22 + .../lang/builtins/modulo/instance_def.py | 71 ++ .../builtins/modulo/mod_builtin_runner.py | 445 +++++++++ .../modulo/mod_builtin_runner_test.py | 117 +++ .../range_check/range_check_builtin_runner.py | 109 +++ tools/make/cairo/lang/instances.py | 572 +++++++++++ tools/make/cairo/lang/vm/builtin_runner.py | 319 ++++++ tools/make/cairo/lang/vm/cairo_runner.py | 917 ++++++++++++++++++ tools/make/cairo/lang/vm/memory_segments.py | 281 ++++++ tools/make/launch_cairo_files.py | 2 +- tools/make/requirements.txt | 3 +- tools/make/setup.sh | 5 +- 18 files changed, 3431 insertions(+), 5 deletions(-) create mode 100644 tests/cairo_programs/modulo.cairo create mode 100644 tests/cairo_programs/modulo_trick.cairo create mode 100644 tools/make/cairo/common/BUILD create mode 100644 tools/make/cairo/common/cairo_builtins.cairo create mode 100644 tools/make/cairo/common/modulo.cairo create mode 100644 tools/make/cairo/lang/builtins/BUILD create mode 100644 tools/make/cairo/lang/builtins/modulo/BUILD create mode 100644 tools/make/cairo/lang/builtins/modulo/instance_def.py create mode 100644 tools/make/cairo/lang/builtins/modulo/mod_builtin_runner.py create mode 100644 tools/make/cairo/lang/builtins/modulo/mod_builtin_runner_test.py create mode 100644 tools/make/cairo/lang/builtins/range_check/range_check_builtin_runner.py create mode 100644 tools/make/cairo/lang/instances.py create mode 100644 tools/make/cairo/lang/vm/builtin_runner.py create mode 100644 tools/make/cairo/lang/vm/cairo_runner.py create mode 100644 tools/make/cairo/lang/vm/memory_segments.py diff --git a/tests/cairo_programs/modulo.cairo b/tests/cairo_programs/modulo.cairo new file mode 100644 index 00000000..af5668fd --- /dev/null +++ b/tests/cairo_programs/modulo.cairo @@ -0,0 +1,136 @@ +%builtins range_check range_check96 add_mod mul_mod +from starkware.cairo.common.cairo_builtins import ModBuiltin + +from starkware.cairo.common.cairo_builtins import PoseidonBuiltin, BitwiseBuiltin, UInt384 +from starkware.cairo.common.registers import get_label_location +from starkware.cairo.common.modulo import run_mod_p_circuit +from starkware.cairo.common.memcpy import memcpy +from starkware.cairo.common.alloc import alloc + +// Computes the polynomial f(x) = x^8 + 5*x^2 + 1. +func apply_poly{ + range_check_ptr, range_check96_ptr: felt*, add_mod_ptr: ModBuiltin*, mul_mod_ptr: ModBuiltin* +}(x: UInt384*, p: UInt384) -> (res: UInt384*) { + // Copy inputs and constants into the values_ptr segment. + memcpy(dst=range_check96_ptr, src=x, len=UInt384.SIZE); + let (constants_ptr) = get_label_location(constants); + memcpy(dst=range_check96_ptr + UInt384.SIZE, src=constants_ptr, len=2 * UInt384.SIZE); + let values_ptr = cast(range_check96_ptr, UInt384*); + let range_check96_ptr = range_check96_ptr + 36; + + let (add_mod_offsets_ptr) = get_label_location(add_offsets); + let (mul_mod_offsets_ptr) = get_label_location(mul_offsets); + run_mod_p_circuit( + p=p, + values_ptr=values_ptr, + add_mod_offsets_ptr=add_mod_offsets_ptr, + add_mod_n=2, + mul_mod_offsets_ptr=mul_mod_offsets_ptr, + mul_mod_n=4, + ); + + return (res=values_ptr + 32); + + // values_ptr points to a segment within the range_check96_ptr segment that looks like this: + // + // offset value + // 0 x + // 4 1 + // 8 5 + // 12 x^2 + // 16 x^4 + // 20 x^8 + // 24 5*x^2 + // 28 x^8 + 5*x^2 + // 32 x^8 + 5*x^2 + 1 + + constants: + dw 1; + dw 0; + dw 0; + dw 0; + + dw 5; + dw 0; + dw 0; + dw 0; + + add_offsets: + dw 20; // x^8 + dw 24; // 5*x^2 + dw 28; // x^8 + 5*x^2 + + dw 4; // 1 + dw 28; // x^8 + 5*x^2 + dw 32; // x^8 + 5*x^2 + 1 + + // // Placeholders (copies of the first 3 offsets): + dw 20; + dw 24; + dw 28; + dw 20; + dw 24; + dw 28; + dw 20; + dw 24; + dw 28; + dw 20; + dw 24; + dw 28; + dw 20; + dw 24; + dw 28; + dw 20; + dw 24; + dw 28; + + mul_offsets: + dw 0; // x + dw 0; // x + dw 12; // x^2 + + dw 12; // x^2 + dw 12; // x^2 + dw 16; // x^4 + + dw 16; // x^4 + dw 16; // x^4 + dw 20; // x^8 + + dw 8; // 5 + dw 12; // x^2 + dw 24; // 5*x^2 + + // Placeholders (copies of the first 3 offsets): + dw 0; + dw 0; + dw 12; + dw 0; + dw 0; + dw 12; + dw 0; + dw 0; + dw 12; + dw 0; + dw 0; + dw 12; +} + +func main{ + range_check_ptr, + bitwise_ptr: BitwiseBuiltin*, + poseidon_ptr: PoseidonBuiltin*, + range_check96_ptr: felt*, + add_mod_ptr: ModBuiltin*, + mul_mod_ptr: ModBuiltin*, +}() { + alloc_locals; + + let p = UInt384(d0=0xffff, d1=0xffff, d2=0xffff, d3=0xffff); + let (local inputs: UInt384*) = alloc(); + assert inputs[0] = UInt384(d0=0xbbbb, d1=0xaaaa, d2=0x6666, d3=0xffff); + + let res: UInt384* = apply_poly(inputs, p); + %{ print("The result: ", hex(ids.res[0].d0), hex(ids.res[0].d1), hex(ids.res[0].d2), hex(ids.res[0].d3)) %} + return (); +} diff --git a/tests/cairo_programs/modulo_trick.cairo b/tests/cairo_programs/modulo_trick.cairo new file mode 100644 index 00000000..735b8819 --- /dev/null +++ b/tests/cairo_programs/modulo_trick.cairo @@ -0,0 +1,117 @@ +%builtins range_check bitwise poseidon range_check96 add_mod mul_mod + +from starkware.cairo.common.cairo_builtins import ( + ModBuiltin, + UInt384, + PoseidonBuiltin, + BitwiseBuiltin, +) +from starkware.cairo.common.registers import get_label_location +from starkware.cairo.common.modulo import run_mod_p_circuit +from starkware.cairo.common.memcpy import memcpy +from starkware.cairo.common.alloc import alloc + +const P0 = 32324006162389411176778628423; +const P1 = 57042285082623239461879769745; +const P2 = 3486998266802970665; +const P3 = 0; + +const N_LIMBS = 4; +const BASE = 2 ** 96; + +func main{ + range_check_ptr, + bitwise_ptr: BitwiseBuiltin*, + poseidon_ptr: PoseidonBuiltin*, + range_check96_ptr: felt*, + add_mod_ptr: ModBuiltin*, + mul_mod_ptr: ModBuiltin*, +}() { + alloc_locals; + + tempvar p = UInt384(d0=P0, d1=P1, d2=P2, d3=P3); + tempvar z = UInt384(d0=1, d1=1, d2=1, d3=1); + let values_ptr = cast(range_check96_ptr, UInt384*); + let (add_offsets_ptr: felt*) = alloc(); + let (mul_offsets_ptr: felt*) = alloc(); + assert values_ptr[0] = z; + + // Z**2 + assert mul_offsets_ptr[0] = 0; + assert mul_offsets_ptr[1] = 0; + assert mul_offsets_ptr[2] = 4; + // Z**3 + assert mul_offsets_ptr[3] = 0; + assert mul_offsets_ptr[4] = 4; + assert mul_offsets_ptr[5] = 8; + // Z**4 + assert mul_offsets_ptr[6] = 0; + assert mul_offsets_ptr[7] = 8; + assert mul_offsets_ptr[8] = 12; + // Z**5 + assert mul_offsets_ptr[9] = 0; + assert mul_offsets_ptr[10] = 12; + assert mul_offsets_ptr[11] = 16; + // Z**6 + assert mul_offsets_ptr[12] = 0; + assert mul_offsets_ptr[13] = 16; + assert mul_offsets_ptr[14] = 20; + + // Dummy until 24 + assert mul_offsets_ptr[15] = 0; + assert mul_offsets_ptr[16] = 0; + assert mul_offsets_ptr[17] = 24; + + assert mul_offsets_ptr[18] = 0; + assert mul_offsets_ptr[19] = 0; + assert mul_offsets_ptr[20] = 28; + + assert mul_offsets_ptr[21] = 0; + assert mul_offsets_ptr[22] = 0; + assert mul_offsets_ptr[23] = 32; + + // assert add_mod_ptr[0] = ModBuiltin( + // p=p, values_ptr=values_ptr, offsets_ptr=add_offsets_ptr, n=3 + // ); + assert mul_mod_ptr[0] = ModBuiltin( + p=p, values_ptr=values_ptr, offsets_ptr=mul_offsets_ptr, n=8 + ); + + let mul_mod_n = 8; + + %{ + from starkware.cairo.lang.builtins.modulo.mod_builtin_runner import ModBuiltinRunner + ModBuiltinRunner.fill_memory(memory=memory, add_mod=None, mul_mod=(ids.mul_mod_ptr.address_, builtin_runners['mul_mod_builtin'], ids.mul_mod_n)) + %} + + let mul_mod_ptr = mul_mod_ptr + ModBuiltin.SIZE; + let range_check96_ptr = range_check96_ptr + mul_mod_n * UInt384.SIZE + 4; + + tempvar z2 = [cast(values_ptr + 4, UInt384*)]; + tempvar z3 = [cast(values_ptr + 8, UInt384*)]; + tempvar z4 = [cast(values_ptr + 12, UInt384*)]; + tempvar z5 = [cast(values_ptr + 16, UInt384*)]; + + %{ + from src.hints.fq import bigint_pack, get_p + p = get_p(ids) + print(f'p = {p}') + z = bigint_pack(ids.z, 4, 2**96) + z2 = bigint_pack(ids.z2, 4, 2**96) + z3 = bigint_pack(ids.z3, 4, 2**96) + z4 = bigint_pack(ids.z4, 4, 2**96) + z5 = bigint_pack(ids.z5, 4, 2**96) + + print(f'z2 = {z2}') + print(f'z3 = {z3}') + print(f'z4 = {z4}') + print(f'z5 = {z5}') + + assert z**2 % p == z2 + assert z**3 % p == z3 + assert z**4 % p == z4 + assert z**5 % p == z5 + %} + + return (); +} diff --git a/tools/make/cairo/common/BUILD b/tools/make/cairo/common/BUILD new file mode 100644 index 00000000..c8f5892c --- /dev/null +++ b/tools/make/cairo/common/BUILD @@ -0,0 +1,144 @@ +load("//src/starkware/cairo:vars.bzl", "CAIRO_COMMON_LIB_ADDITIONAL_FILES", "CAIRO_COMMON_LIB_ADDITIONAL_LIBS") +load("//src/starkware/cairo/lang:cairo_rules.bzl", "cairo_library") +load("//bazel_utils/python:defs.bzl", "requirement") + +cairo_library( + name = "cairo_common_cairo_lib", + srcs = [ + "alloc.cairo", + "bitwise.cairo", + "modulo.cairo", + "bool.cairo", + "cairo_builtins.cairo", + "default_dict.cairo", + "dict.cairo", + "dict_access.cairo", + "ec.cairo", + "ec_point.cairo", + "find_element.cairo", + "hash.cairo", + "hash_chain.cairo", + "hash_state.cairo", + "hash_state_poseidon.cairo", + "invoke.cairo", + "keccak.cairo", + "keccak_state.cairo", + "math.cairo", + "math_cmp.cairo", + "memcpy.cairo", + "memset.cairo", + "merkle_multi_update.cairo", + "merkle_update.cairo", + "patricia.cairo", + "patricia_utils.cairo", + "patricia_with_poseidon.cairo", + "patricia_with_sponge.cairo", + "poseidon_state.cairo", + "pow.cairo", + "registers.cairo", + "segments.cairo", + "serialize.cairo", + "set.cairo", + "signature.cairo", + "small_merkle_tree.cairo", + "sponge_as_hash.cairo", + "squash_dict.cairo", + "uint256.cairo", + "usort.cairo", + "//src/starkware/cairo/common/builtin_keccak:keccak.cairo", + "//src/starkware/cairo/common/builtin_poseidon:poseidon.cairo", + "//src/starkware/cairo/common/cairo_blake2s:blake2s.cairo", + "//src/starkware/cairo/common/cairo_blake2s:packed_blake2s.cairo", + "//src/starkware/cairo/common/cairo_keccak:keccak.cairo", + "//src/starkware/cairo/common/cairo_keccak:packed_keccak.cairo", + "//src/starkware/cairo/common/keccak_utils:keccak_utils.cairo", + ] + CAIRO_COMMON_LIB_ADDITIONAL_FILES, + deps = [ + "//src/starkware/cairo/common/cairo_secp:cairo_secp256k1", + "//src/starkware/cairo/common/secp256r1:cairo_secp256r1", + ], +) + +py_library( + name = "poseidon_utils_lib", + srcs = [ + "poseidon_hash.py", + "poseidon_utils.py", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/starkware/cairo/lang:cairo_constants_lib", + "//src/starkware/python:starkware_python_utils_lib", + requirement("numpy"), + ], +) + +py_library( + name = "cairo_common_lib", + srcs = [ + "dict.py", + "hash_chain.py", + "hash_state.py", + "math_utils.py", + "patricia_utils.py", + "small_merkle_tree.py", + "structs.py", + "//src/starkware/cairo/common/cairo_blake2s:blake2s_utils.py", + "//src/starkware/cairo/common/cairo_keccak:keccak_utils.py", + "//src/starkware/cairo/common/cairo_secp:secp256r1_utils.py", + "//src/starkware/cairo/common/cairo_secp:secp_utils.py", + "//src/starkware/cairo/common/cairo_sha256:sha256_utils.py", + "//src/starkware/cairo/common/keccak_utils:keccak_utils.py", + ], + data = [ + ":cairo_common_cairo_lib", + ], + visibility = ["//visibility:public"], + deps = [ + "poseidon_utils_lib", + "//src/starkware/cairo/lang:cairo_constants_lib", + "//src/starkware/cairo/lang/vm:cairo_vm_crypto_lib", + "//src/starkware/python:starkware_merkle_tree_lib", + "//src/starkware/python:starkware_python_utils_lib", + ] + CAIRO_COMMON_LIB_ADDITIONAL_LIBS, +) + +package(default_visibility = ["//visibility:public"]) + +exports_files(glob([ + "*.cairo", + "*.py", +])) + +py_library( + name = "cairo_common_validate_utils_lib", + srcs = [ + "validate_utils.py", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/starkware/cairo/lang/builtins:cairo_run_builtins_lib", + "//src/starkware/cairo/lang/vm:cairo_run_lib", + "//src/starkware/cairo/lang/vm:cairo_vm_lib", + "//src/starkware/python:starkware_python_utils_lib", + ], +) + +py_library( + name = "cairo_function_runner_lib", + srcs = [ + "cairo_function_runner.py", + ], + visibility = ["//visibility:public"], + deps = [ + ":cairo_common_lib", + "//src/starkware/cairo/lang/builtins:cairo_run_builtins_lib", + "//src/starkware/cairo/lang/compiler:cairo_compile_lib", + "//src/starkware/cairo/lang/tracer:cairo_tracer_lib", + "//src/starkware/cairo/lang/vm:cairo_relocatable_lib", + "//src/starkware/cairo/lang/vm:cairo_run_lib", + "//src/starkware/cairo/lang/vm:cairo_vm_crypto_lib", + "//src/starkware/cairo/lang/vm:cairo_vm_lib", + "//src/starkware/python:starkware_python_utils_lib", + ], +) diff --git a/tools/make/cairo/common/cairo_builtins.cairo b/tools/make/cairo/common/cairo_builtins.cairo new file mode 100644 index 00000000..da52a289 --- /dev/null +++ b/tools/make/cairo/common/cairo_builtins.cairo @@ -0,0 +1,60 @@ +from starkware.cairo.common.ec_point import EcPoint +from starkware.cairo.common.keccak_state import KeccakBuiltinState +from starkware.cairo.common.poseidon_state import PoseidonBuiltinState + +// Specifies the hash builtin memory structure. +struct HashBuiltin { + x: felt, + y: felt, + result: felt, +} + +// Specifies the signature builtin memory structure. +struct SignatureBuiltin { + pub_key: felt, + message: felt, +} + +// Specifies the bitwise builtin memory structure. +struct BitwiseBuiltin { + x: felt, + y: felt, + x_and_y: felt, + x_xor_y: felt, + x_or_y: felt, +} + +// Specifies the EC operation builtin memory structure. +struct EcOpBuiltin { + p: EcPoint, + q: EcPoint, + m: felt, + r: EcPoint, +} + +// Specifies the Keccak builtin memory structure. +struct KeccakBuiltin { + input: KeccakBuiltinState, + output: KeccakBuiltinState, +} + +// Specifies the Poseidon builtin memory structure. +struct PoseidonBuiltin { + input: PoseidonBuiltinState, + output: PoseidonBuiltinState, +} + +struct UInt384 { + d0: felt, + d1: felt, + d2: felt, + d3: felt, +} + +// Specifies the Add and Mul Mod builtins memory structure. +struct ModBuiltin { + p: UInt384, + values_ptr: UInt384*, + offsets_ptr: felt*, + n: felt, +} diff --git a/tools/make/cairo/common/modulo.cairo b/tools/make/cairo/common/modulo.cairo new file mode 100644 index 00000000..57a765ac --- /dev/null +++ b/tools/make/cairo/common/modulo.cairo @@ -0,0 +1,54 @@ +from starkware.cairo.common.cairo_builtins import ModBuiltin, UInt384 +from starkware.cairo.common.math import safe_div, unsigned_div_rem +from starkware.cairo.common.registers import get_label_location + +const BATCH_SIZE = 8; + +func div_ceil{range_check_ptr}(x: felt, y: felt) -> felt { + let (q, r) = unsigned_div_rem(x, y); + if (r != 0) { + return q + 1; + } else { + return q; + } +} + +// Fills the first instance of the add_mod and mul_mod builtins and calls the fill_memory hint to +// fill the rest of the instances and the missing values in the values table. +// +// This function works only for batch_size=8. If you are using a mod builtin with a different +// batch_size, you should fill the first instances and use the fill_memory hint directly. +func run_mod_p_circuit{range_check_ptr, add_mod_ptr: ModBuiltin*, mul_mod_ptr: ModBuiltin*}( + p: UInt384, + values_ptr: UInt384*, + add_mod_offsets_ptr: felt*, + add_mod_n: felt, + mul_mod_offsets_ptr: felt*, + mul_mod_n: felt, +) { + let add_mod_n_instances = div_ceil(add_mod_n, BATCH_SIZE); + assert add_mod_ptr.p = p; + assert add_mod_ptr.values_ptr = values_ptr; + assert add_mod_ptr.offsets_ptr = add_mod_offsets_ptr; + assert add_mod_ptr.n = add_mod_n_instances * BATCH_SIZE; + + let mul_mod_n_instances = div_ceil(mul_mod_n, BATCH_SIZE); + assert mul_mod_ptr.p = p; + assert mul_mod_ptr.values_ptr = values_ptr; + assert mul_mod_ptr.offsets_ptr = mul_mod_offsets_ptr; + assert mul_mod_ptr.n = mul_mod_n_instances * BATCH_SIZE; + + %{ + from starkware.cairo.lang.builtins.modulo.mod_builtin_runner import ModBuiltinRunner + + ModBuiltinRunner.fill_memory( + memory=memory, + add_mod=(ids.add_mod_ptr.address_, builtin_runners["add_mod_builtin"], ids.add_mod_n), + mul_mod=(ids.mul_mod_ptr.address_, builtin_runners["mul_mod_builtin"], ids.mul_mod_n), + ) + %} + + let add_mod_ptr = add_mod_ptr + ModBuiltin.SIZE * add_mod_n_instances; + let mul_mod_ptr = mul_mod_ptr + ModBuiltin.SIZE * mul_mod_n_instances; + return (); +} diff --git a/tools/make/cairo/lang/builtins/BUILD b/tools/make/cairo/lang/builtins/BUILD new file mode 100644 index 00000000..d872d919 --- /dev/null +++ b/tools/make/cairo/lang/builtins/BUILD @@ -0,0 +1,62 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "instance_def_lib", + srcs = [ + "instance_def.py", + ], + visibility = ["//visibility:public"], +) + +py_library( + name = "cairo_run_builtins_lib", + srcs = [ + "//src/starkware/cairo/lang/builtins/bitwise:bitwise_builtin_runner.py", + "//src/starkware/cairo/lang/builtins/bitwise:instance_def.py", + "//src/starkware/cairo/lang/builtins/ec:ec_op_builtin_runner.py", + "//src/starkware/cairo/lang/builtins/ec:instance_def.py", + "//src/starkware/cairo/lang/builtins/hash:hash_builtin_runner.py", + "//src/starkware/cairo/lang/builtins/hash:instance_def.py", + "//src/starkware/cairo/lang/builtins/keccak:instance_def.py", + "//src/starkware/cairo/lang/builtins/keccak:keccak_builtin_runner.py", + "//src/starkware/cairo/lang/builtins/modulo:instance_def.py", + "//src/starkware/cairo/lang/builtins/modulo:mod_builtin_runner.py", + "//src/starkware/cairo/lang/builtins/poseidon:instance_def.py", + "//src/starkware/cairo/lang/builtins/poseidon:poseidon_builtin_runner.py", + "//src/starkware/cairo/lang/builtins/range_check:instance_def.py", + "//src/starkware/cairo/lang/builtins/range_check:range_check_builtin_runner.py", + "//src/starkware/cairo/lang/builtins/signature:instance_def.py", + "//src/starkware/cairo/lang/builtins/signature:signature_builtin_runner.py", + ], + visibility = ["//visibility:public"], + deps = [ + ":instance_def_lib", + "//src/starkware/cairo/common:cairo_common_lib", + "//src/starkware/cairo/lang/vm:cairo_relocatable_lib", + "//src/starkware/cairo/lang/vm:cairo_vm_crypto_lib", + "//src/starkware/cairo/lang/vm:cairo_vm_lib", + "//src/starkware/crypto:starkware_crypto_lib", + "//src/starkware/python:starkware_python_utils_lib", + ], +) + +py_library( + name = "cairo_all_builtins_lib", + srcs = [ + "all_builtins.py", + ], + visibility = ["//visibility:public"], +) + +py_library( + name = "cairo_run_builtins_test_utils_lib", + srcs = [ + "builtin_runner_test_utils.py", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/starkware/cairo/lang:cairo_instances_lib", + "//src/starkware/cairo/lang/compiler:cairo_compile_lib", + "//src/starkware/cairo/lang/vm:cairo_run_lib", + ], +) diff --git a/tools/make/cairo/lang/builtins/modulo/BUILD b/tools/make/cairo/lang/builtins/modulo/BUILD new file mode 100644 index 00000000..03f5a58d --- /dev/null +++ b/tools/make/cairo/lang/builtins/modulo/BUILD @@ -0,0 +1,22 @@ +load("//bazel_utils:python.bzl", "pytest_test") + +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "instance_def.py", + "mod_builtin_runner.py", +]) + +pytest_test( + name = "cairo_run_builtins_mod_test", + srcs = [ + "mod_builtin_runner_test.py", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/starkware/cairo/lang:cairo_instances_lib", + "//src/starkware/cairo/lang/builtins:cairo_run_builtins_lib", + "//src/starkware/cairo/lang/builtins:cairo_run_builtins_test_utils_lib", + "//src/starkware/cairo/lang/vm:cairo_vm_lib", + ], +) diff --git a/tools/make/cairo/lang/builtins/modulo/instance_def.py b/tools/make/cairo/lang/builtins/modulo/instance_def.py new file mode 100644 index 00000000..7a6f8be1 --- /dev/null +++ b/tools/make/cairo/lang/builtins/modulo/instance_def.py @@ -0,0 +1,71 @@ +import dataclasses +from abc import abstractmethod + +from starkware.cairo.lang.builtins.instance_def import BuiltinInstanceDef +from starkware.python.math_utils import div_ceil, safe_div + +HEIGHT = 1 + + +# Mypy has a problem with dataclasses that contain unimplemented abstract methods. +# See https://github.com/python/mypy/issues/5374 for details on this problem. +@dataclasses.dataclass # type: ignore[misc] +class ModInstanceDef(BuiltinInstanceDef): + # The parameters of the presentation of large integers in felts. Each number is composed of + # n_words felts, each containing a value in [0, 2**word_bit_len), to represent + # n_words * word_bit_len bit-long integers. + # E.g., for 384-bit numbers, use word_bit_len = 96 and n_words = 4. + word_bit_len: int + n_words: int + batch_size: int + + @property + def memory_cells_per_instance(self) -> int: + # The user-facing memory has n_words + 3 memory cells per instance (p, n and two pointers), + # and the additional memory contains 3*(n_words + 1) memory cells, for offsets and values + # of each of a, b, and c. + return self.n_words + 3 + self.batch_size * 3 * (self.n_words + 1) + + @property + @abstractmethod + def range_check_units_per_builtin(self) -> int: + pass + + @property + def invocation_height(self) -> int: + return HEIGHT + + def get_diluted_units_per_builtin(self, diluted_spacing: int, diluted_n_bits: int) -> int: + return 0 + + +@dataclasses.dataclass +class AddModInstanceDef(ModInstanceDef): + @property + def range_check_units_per_builtin(self) -> int: + # No range check units in the builtin itself. The range check is done externally. + return 0 + + +@dataclasses.dataclass +class MulModInstanceDef(ModInstanceDef): + # The bits in the basic range check units, needed to calculate the amount used by the builtin. + bits_per_part: int = 16 + + @property + def p_multiplier_n_parts(self): + return safe_div(self.word_bit_len, self.bits_per_part) + + @property + def carry_n_parts(self): + return self.p_multiplier_n_parts + div_ceil( + (self.n_words - 1).bit_length(), self.bits_per_part + ) + + @property + def range_check_units_per_builtin(self) -> int: + # The carry requires (2 * n_words - 2) * carry_n_parts range check units and p_multiplier + # requires n_words * p_multiplier_n_parts units. + return ( + self.n_words * self.p_multiplier_n_parts + (2 * self.n_words - 2) * self.carry_n_parts + ) diff --git a/tools/make/cairo/lang/builtins/modulo/mod_builtin_runner.py b/tools/make/cairo/lang/builtins/modulo/mod_builtin_runner.py new file mode 100644 index 00000000..1a40c0cf --- /dev/null +++ b/tools/make/cairo/lang/builtins/modulo/mod_builtin_runner.py @@ -0,0 +1,445 @@ +import operator +from math import ceil +from typing import Any, Dict, List, Optional, Tuple + +from starkware.cairo.lang.builtins.modulo.instance_def import ( + AddModInstanceDef, + ModInstanceDef, + MulModInstanceDef, +) +from starkware.cairo.lang.vm.builtin_runner import SimpleBuiltinRunner +from starkware.cairo.lang.vm.relocatable import MaybeRelocatable, RelocatableValue +from starkware.cairo.lang.vm.utils import MemorySegmentRelocatableAddresses +from starkware.python.math_utils import div_mod, safe_div + +# The maximum n value that the function fill_memory accepts. +MAX_N = 100000 + +INPUT_NAMES = [ + "p0", + "p1", + "p2", + "p3", + "values_ptr", + "offsets_ptr", + "n", +] + +MEMORY_VAR_NAMES = [ + "a_offset", + "b_offset", + "c_offset", + "a0", + "a1", + "a2", + "a3", + "b0", + "b1", + "b2", + "b3", + "c0", + "c1", + "c2", + "c3", +] + +INPUT_CELLS = len(INPUT_NAMES) +ADDITIONAL_MEMORY_UNITS = len(MEMORY_VAR_NAMES) + + +class ModBuiltinRunner(SimpleBuiltinRunner): + def __init__(self, name: str, included: bool, instance_def: ModInstanceDef): + super().__init__( + name=name, + included=included, + ratio=None if instance_def is None else instance_def.ratio, + cells_per_instance=INPUT_CELLS, + n_input_cells=INPUT_CELLS, + additional_memory_units_per_instance=ADDITIONAL_MEMORY_UNITS, + ) + self.instance_def: ModInstanceDef = instance_def + self.zero_value: Optional[RelocatableValue] = None + + def get_instance_def(self): + return self.instance_def + + def get_memory_accesses(self, runner): + """ + Returns memory accesses for the builtin on all segments, including values and offsets + addresses. Used by the cairo_runner to check for memory holes. + """ + segment_size = runner.segments.get_segment_size(self.base.segment_index) + n_instances = ceil(segment_size / self.cells_per_instance) + res = set() + for instance in range(n_instances): + offsets_ptr_addr = ( + self.base + instance * self.n_input_cells + INPUT_NAMES.index("offsets_ptr") + ) + offsets_addr = runner.vm_memory[offsets_ptr_addr] + values_ptr_addr = ( + self.base + instance * self.n_input_cells + INPUT_NAMES.index("values_ptr") + ) + values_addr = runner.vm_memory[values_ptr_addr] + for i in range(3 * self.instance_def.batch_size): + offset_addr = offsets_addr + i + res.add(offset_addr) + offset = runner.vm_memory[offset_addr] + for j in range(self.instance_def.n_words): + res.add(values_addr + offset + j) + return super().get_memory_accesses(runner).union(res) + + def initialize_segments(self, runner): + super().initialize_segments(runner) + self.zero_value = runner.segments.add_zero_segment(self.instance_def.n_words) + + def finalize_segments(self, runner): + super().finalize_segments(runner) + runner.segments.finalize_zero_segment() + + def get_memory_segment_addresses(self, runner): + assert self.zero_value is not None, "Uninitialized self.zero_value." + return super().get_memory_segment_addresses(runner) | { + f"{self.name}_zero_value": MemorySegmentRelocatableAddresses( + begin_addr=self.zero_value, + stop_ptr=self.zero_value + self.instance_def.n_words, + ), + } + + # The structure of the values in the returned dictionary is of the form: + # {keys = INPUT_NAMES, "batch": {index_in_batch: {keys = MEMORY_VAR_NAMES}}}. + def air_private_input(self, runner) -> Dict[str, Any]: + assert self.base is not None, "Uninitialized self.base." + res: Dict[int, Any] = {} + values_addr_per_instance = {} + offsets_addr_per_instance = {} + for addr, val in runner.vm_memory.items(): + if ( + not isinstance(addr, RelocatableValue) + or addr.segment_index != self.base.segment_index + ): + continue + + assert isinstance(val, int) + idx, typ = divmod(addr.offset, INPUT_CELLS) + res.setdefault(idx, {"index": idx})[INPUT_NAMES[typ]] = hex(val) + if typ == INPUT_NAMES.index("values_ptr"): + values_addr_per_instance[idx] = val + if typ == INPUT_NAMES.index("offsets_ptr"): + offsets_addr_per_instance[idx] = val + + for idx, offsets_addr in offsets_addr_per_instance.items(): + for index_in_batch in range(self.instance_def.batch_size): + for i, s in enumerate("abc"): + offset = runner.vm_memory[offsets_addr + i + 3 * index_in_batch] + res[idx]["batch"][index_in_batch][f"{s}_offset"] = hex(offset) + assert idx in values_addr_per_instance + values_addr = values_addr_per_instance[idx] + for d in range(self.instance_def.n_words): + value = runner.vm_memory[values_addr + offset + d] + res[idx]["batch"][index_in_batch][f"{s}{d}"] = hex(value) + + for index, item in res.items(): + for name in INPUT_NAMES: + assert name in item, f"Missing input '{name}' of {self.name} instance {index}." + for index_in_batch in range(self.instance_def.batch_size): + for name in MEMORY_VAR_NAMES: + assert name in item["batch"][index_in_batch], ( + f"Missing memory variable '{name}' of {self.name} instance {index}, " + + f"batch {index_in_batch}." + ) + + return {self.name: sorted(res.values(), key=lambda item: item["index"])} + + def read_n_words_value(self, memory, addr) -> Tuple[List[int], Optional[int]]: + """ + Reads self.instance_def.n_words from memory, starting at address=addr. + Returns the words and the value if all words are in memory. + Verifies that all words are integers and are bounded by 2**self.instance_def.word_bit_len. + """ + if addr not in memory: + return [], None + + words: List[int] = [] + value = 0 + for i in range(self.instance_def.n_words): + addr_i = addr + i + if addr_i not in memory: + return words, None + word = memory[addr_i] + assert isinstance(word, int), ( + f"Expected integer at address {addr_i}. " + f"Got: {word}." + ) + assert word < 2**self.instance_def.word_bit_len, ( + f"Expected integer at address {addr_i} to be smaller than " + + f"2^{self.instance_def.word_bit_len}. Got: {word}." + ) + words.append(word) + value += word * 2 ** (self.instance_def.word_bit_len * i) + + return words, value + + def run_security_checks(self, runner, op): + super().run_security_checks(runner) + segment_size = runner.segments.get_segment_used_size(self.base.segment_index) + n_instances = ceil(segment_size / self.cells_per_instance) + + prev_inputs = None + for instance in range(n_instances): + inputs = self.read_inputs(runner.vm_memory, self.base + instance * self.n_input_cells) + if prev_inputs is not None and prev_inputs["n"] > self.instance_def.batch_size: + assert all( + inputs[f"p{i}"] == prev_inputs[f"p{i}"] + for i in range(self.instance_def.n_words) + ) + assert inputs["values_ptr"] == prev_inputs["values_ptr"] + assert ( + inputs["offsets_ptr"] + == prev_inputs["offsets_ptr"] + 3 * self.instance_def.batch_size + ) + assert inputs["n"] == prev_inputs["n"] - self.instance_def.batch_size + assert isinstance(inputs["p"], int) + for index_in_batch in range(self.instance_def.batch_size): + values = self.read_memory_vars( + runner.vm_memory, inputs["values_ptr"], inputs["offsets_ptr"], index_in_batch + ) + assert op(values["a"], values["b"]) % inputs["p"] == values["c"] % inputs["p"], ( + f"{self.name} builtin: Expected a {op} b == c (mod p). Got: " + + f"instance={instance}, batch={index_in_batch}, inputs={inputs}, " + + f"values={values}." + ) + prev_inputs = inputs + if prev_inputs is not None: + assert prev_inputs["n"] == self.instance_def.batch_size + + def read_inputs(self, memory, addr) -> Dict[str, MaybeRelocatable]: + """ + Reads the inputs to the builtin (see INPUT_NAMES) from the memory at address=addr. + Returns a dictionary from input name to its value. Asserts that it exists in memory. + Returns also the value of p, not just its words. + """ + inputs = {} + inputs["values_ptr"] = memory[addr + INPUT_NAMES.index("values_ptr")] + assert isinstance(inputs["values_ptr"], RelocatableValue), ( + f"{self.name} builtin: Expected RelocatableValue at address " + + f"{addr + INPUT_NAMES.index('values_ptr')}. Got: {inputs['values_ptr']}." + ) + inputs["offsets_ptr"] = memory[addr + INPUT_NAMES.index("offsets_ptr")] + assert isinstance(inputs["offsets_ptr"], RelocatableValue), ( + f"{self.name} builtin: Expected RelocatableValue at address " + + f"{addr + INPUT_NAMES.index('offsets_ptr')}. Got: {inputs['offsets_ptr']}." + ) + inputs["n"] = memory[addr + INPUT_NAMES.index("n")] + assert isinstance(inputs["n"], int), ( + f"{self.name} builtin: Expected integer at address " + + f"{addr + INPUT_NAMES.index('n')}. Got: {inputs['n']}." + ) + assert inputs["n"] >= 1, f"{self.name} builtin: Expected n >= 1. Got: {inputs['n']}." + p_addr = addr + INPUT_NAMES.index("p0") + words, value = self.read_n_words_value(memory, p_addr) + assert ( + value is not None + ), f"{self.name} builtin: Missing value at address {p_addr + len(words)}." + inputs["p"] = value + for d, w in enumerate(words): + inputs[f"p{d}"] = w + return inputs + + def read_memory_vars(self, memory, values_ptr, offsets_ptr, index_in_batch) -> Dict[str, int]: + """ + Reads the memory variables to the builtin (see MEMORY_VAR_NAMES) from the memory given + the inputs (specifically, values_ptr and offsets_ptr). + Returns a dictionary from memory variable name to its value. Asserts if it doesn't exist in + memory. Returns also the values of a, b, and c, not just their words. + """ + memory_vars = {} + for i, s in enumerate("abc"): + offset = memory[offsets_ptr + i + 3 * index_in_batch] + assert isinstance(offset, int), ( + f"{self.name} builtin: Expected integer at address " + + f"{offsets_ptr + i}. Got: {offset}." + ) + memory_vars[f"{s}_offset"] = offset + value_addr = values_ptr + offset + words, value = self.read_n_words_value(memory, value_addr) + assert ( + value is not None + ), f"{self.name} builtin: Missing value at address {value_addr + len(words)}." + memory_vars[s] = value + for d, w in enumerate(words): + memory_vars[f"{s}{d}"] = w + return memory_vars + + @staticmethod + def fill_memory( + memory, + add_mod: Optional[Tuple[RelocatableValue, "AddModBuiltinRunner", int]], + mul_mod: Optional[Tuple[RelocatableValue, "MulModBuiltinRunner", int]], + ): + """ + Fills the memory with inputs to the builtin instances based on the inputs to the + first instance, pads the offsets table to fit the number of operations writen in the + input to the first instance, and caculates missing values in the values table. + + For each builtin, the given tuple is of the form (builtin_ptr, builtin_runner, n), + where n is the number of operations in the offsets table (i.e., the length of the + offsets table is 3*n). + + The number of operations written to the input of the first instance n' should be at + least n and a multiple of batch_size. Previous offsets are copied to the end of the + offsets table to make its length 3n'. + """ + # Check that the instance definitions of the builtins are the same. + if add_mod and mul_mod: + assert ( + add_mod[1].instance_def.n_words == mul_mod[1].instance_def.n_words + and add_mod[1].instance_def.word_bit_len == mul_mod[1].instance_def.word_bit_len + ), f"add_mod and mul_mod builtins must have the same n_words and word_bit_len." + + # Fill the inputs to the builtins. + add_mod_inputs = {} + mul_mod_inputs = {} + if add_mod: + add_mod_inputs = add_mod[1].read_inputs(memory, add_mod[0]) + add_mod[1].fill_inputs(memory, add_mod[0], add_mod_inputs) + add_mod[1].fill_offsets( + memory, add_mod_inputs, add_mod[2], add_mod_inputs["n"] - add_mod[2] + ) + if mul_mod: + mul_mod_inputs = mul_mod[1].read_inputs(memory, mul_mod[0]) + mul_mod[1].fill_inputs(memory, mul_mod[0], mul_mod_inputs) + mul_mod[1].fill_offsets( + memory, mul_mod_inputs, mul_mod[2], mul_mod_inputs["n"] - mul_mod[2] + ) + + # Get one of the builtin runners - the rest of this function doesn't depend on batch_size. + mod_builtin = add_mod if add_mod else mul_mod + assert mod_builtin is not None, "At least one of add_mod and mul_mod must be given." + mod_runner = mod_builtin[1] + assert isinstance(mod_runner, ModBuiltinRunner) + + # Fill the values table. + add_mod_n = add_mod[2] if add_mod else 0 + mul_mod_n = mul_mod[2] if mul_mod else 0 + add_mod_index = 0 + mul_mod_index = 0 + while add_mod_index < add_mod_n or mul_mod_index < mul_mod_n: + if add_mod_index < add_mod_n and mod_runner.fill_value( + memory, + add_mod_inputs, + add_mod_index, + operator.add, + operator.sub, + ): + add_mod_index += 1 + elif mul_mod_index < mul_mod_n and mod_runner.fill_value( + memory, + mul_mod_inputs, + mul_mod_index, + operator.mul, + lambda a, b: div_mod(a, b, mul_mod_inputs["p"]), + ): + mul_mod_index += 1 + else: + raise Exception( + f"Could not fill the values table, " + + f"add_mod_index={add_mod_index}, mul_mod_index={mul_mod_index}" + ) + + # Fills the inputs to the instances of the builtin given the inputs to the first instance. + def fill_inputs(self, memory, builtin_ptr, inputs): + assert inputs["n"] <= MAX_N, f"{self.name} builtin: n must be <= {MAX_N}" + n_instances = safe_div(inputs["n"], self.instance_def.batch_size) + for instance in range(1, n_instances): + instance_ptr = builtin_ptr + instance * len(INPUT_NAMES) + for i in range(self.instance_def.n_words): + memory[instance_ptr + INPUT_NAMES.index(f"p{i}")] = inputs[f"p{i}"] + memory[instance_ptr + INPUT_NAMES.index("values_ptr")] = inputs["values_ptr"] + memory[instance_ptr + INPUT_NAMES.index("offsets_ptr")] = ( + inputs["offsets_ptr"] + 3 * instance * self.instance_def.batch_size + ) + memory[instance_ptr + INPUT_NAMES.index("n")] = ( + inputs["n"] - instance * self.instance_def.batch_size + ) + + # Copies the first offsets in the offsets table to its end, n_copies times. + def fill_offsets(self, memory, inputs, index, n_copies): + offsets = {} + for i, s in enumerate("abc"): + s_offset = memory[inputs["offsets_ptr"] + i] + offsets[s] = s_offset + for i in range(n_copies): + for j, s in enumerate("abc"): + memory[inputs["offsets_ptr"] + 3 * (index + i) + j] = offsets[s] + + # Fills a value in the values table, if exactly one value is missing. + # Returns true on success or if all values are already known. + def fill_value(self, memory, inputs, index, op, inv_op) -> bool: + values = {} + addresses = {} + for i, s in enumerate("abc"): + s_offset = memory[inputs["offsets_ptr"] + 3 * index + i] + addresses[s] = inputs["values_ptr"] + s_offset + if inputs["values_ptr"] + s_offset in memory: + _words, value = self.read_n_words_value(memory, inputs["values_ptr"] + s_offset) + if value is not None: + values[s] = value + + if all(s in values for s in "abc"): + # All values are already known. + return True + + # Deduce c from a and b and write it to memory. + if "c" not in values and "a" in values and "b" in values: + value = op(values["a"], values["b"]) % inputs["p"] + self.write_n_words_value(memory, addresses["c"], value) + return True + # Deduce b from a and c and write it to memory. + if "b" not in values and "a" in values and "c" in values: + value = inv_op(values["c"], values["a"]) % inputs["p"] + self.write_n_words_value(memory, addresses["b"], value) + return True + # Deduce a from b and c and write it to memory. + if "a" not in values and "b" in values and "c" in values: + value = inv_op(values["c"], values["b"]) % inputs["p"] + self.write_n_words_value(memory, addresses["a"], value) + return True + + return False + + def write_n_words_value(self, memory, addr, value): + """ + Given a value, writes its n_words to memory, starting at address=addr. + """ + shift = 2**self.instance_def.word_bit_len + value_copy = value + for i in range(self.instance_def.n_words): + word = value_copy % shift + # The following line will raise InconsistentMemoryError if the address is already in + # memory and a different value is written. + memory[addr + i] = word + value_copy //= shift + assert value_copy == 0 + + +class AddModBuiltinRunner(ModBuiltinRunner): + def __init__(self, included: bool, instance_def: AddModInstanceDef): + super().__init__( + name="add_mod", + included=included, + instance_def=instance_def, + ) + + def run_security_checks(self, runner): + super().run_security_checks(runner, operator.add) + + +class MulModBuiltinRunner(ModBuiltinRunner): + def __init__(self, included: bool, instance_def: MulModInstanceDef): + super().__init__( + name="mul_mod", + included=included, + instance_def=instance_def, + ) + + def run_security_checks(self, runner): + super().run_security_checks(runner, operator.mul) diff --git a/tools/make/cairo/lang/builtins/modulo/mod_builtin_runner_test.py b/tools/make/cairo/lang/builtins/modulo/mod_builtin_runner_test.py new file mode 100644 index 00000000..12967999 --- /dev/null +++ b/tools/make/cairo/lang/builtins/modulo/mod_builtin_runner_test.py @@ -0,0 +1,117 @@ +import dataclasses + +import pytest + +from starkware.cairo.lang.builtins.builtin_runner_test_utils import compile_and_run +from starkware.cairo.lang.builtins.modulo.instance_def import AddModInstanceDef, MulModInstanceDef +from starkware.cairo.lang.instances import all_cairo_instance +from starkware.cairo.lang.vm.vm_exceptions import SecurityError, VmException + + +def test_add_mod_builtin_runner(): + CODE_FORMAT = """ +%builtins range_check add_mod mul_mod +from starkware.cairo.common.cairo_builtins import ModBuiltin, UInt384 +from starkware.cairo.common.registers import get_label_location +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.modulo import run_mod_p_circuit + +func main{{range_check_ptr, add_mod_ptr: ModBuiltin*, mul_mod_ptr: ModBuiltin*}}() {{ + alloc_locals; + + let p = UInt384(d0={p[0]}, d1={p[1]}, d2={p[2]}, d3={p[3]}); + let x1 = UInt384(d0={x1[0]}, d1={x1[1]}, d2={x1[2]}, d3={x1[3]}); + let x2 = UInt384(d0={x2[0]}, d1={x2[1]}, d2={x2[2]}, d3={x2[3]}); + let x3 = UInt384(d0={x3[0]}, d1={x3[1]}, d2={x3[2]}, d3={x3[3]}); + let res = UInt384(d0={res[0]}, d1={res[1]}, d2={res[2]}, d3={res[3]}); + + let (local values_arr: UInt384*) = alloc(); + assert values_arr[0] = x1; + assert values_arr[1] = x2; + assert values_arr[2] = x3; + assert values_arr[7] = res; + + let (local add_mod_offsets_arr: felt*) = alloc(); + assert add_mod_offsets_arr[0] = 0; // x1 + assert add_mod_offsets_arr[1] = 12; // x2 - x1 + assert add_mod_offsets_arr[2] = 4; // x2 + assert add_mod_offsets_arr[3] = 16; // (x2 - x1) * x3 + assert add_mod_offsets_arr[4] = 20; // x1 * x3 + assert add_mod_offsets_arr[5] = 24; // (x2 - x1) * x3 + x1 * x3 + + let (local mul_mod_offsets_arr: felt*) = alloc(); + assert mul_mod_offsets_arr[0] = 12; // x2 - x1 + assert mul_mod_offsets_arr[1] = 8; // x3 + assert mul_mod_offsets_arr[2] = 16; // (x2 - x1) * x3 + assert mul_mod_offsets_arr[3] = 0; // x1 + assert mul_mod_offsets_arr[4] = 8; // x3 + assert mul_mod_offsets_arr[5] = 20; // x1 * x3 + assert mul_mod_offsets_arr[6] = 8; // x3 + assert mul_mod_offsets_arr[7] = 28; // ((x2 - x1) * x3 + x1 * x3) / x3 = x2 mod p + assert mul_mod_offsets_arr[8] = 24; // (x2 - x1) * x3 + x1 * x3 + + run_mod_p_circuit( + p=p, + values_ptr=values_arr, + add_mod_offsets_ptr=add_mod_offsets_arr, + add_mod_n=2, + mul_mod_offsets_ptr=mul_mod_offsets_arr, + mul_mod_n=3, + ); + + return (); +}} +""" + + # Create a dummy layout. + layout = dataclasses.replace( + all_cairo_instance, + builtins={ + **all_cairo_instance.builtins, + "add_mod": AddModInstanceDef(ratio=1, word_bit_len=3, n_words=4, batch_size=8), + "mul_mod": MulModInstanceDef( + ratio=1, word_bit_len=3, n_words=4, batch_size=8, bits_per_part=1 + ), + }, + ) + + # A valid computation. + compile_and_run( + CODE_FORMAT.format( + p=[1, 1, 0, 0], + x1=[1, 0, 0, 0], + x2=[2, 1, 0, 0], + x3=[2, 0, 0, 0], + res=[1, 0, 0, 0], + ), + layout=layout, + secure_run=True, + ) + + # Test that the runner fails when a0 is too big. + with pytest.raises(VmException, match="Expected integer at address .* to be smaller"): + compile_and_run( + CODE_FORMAT.format( + p=[1, 1, 0, 0], + x1=[8, 0, 0, 0], + x2=[2, 1, 0, 0], + x3=[2, 0, 0, 0], + res=[1, 0, 0, 0], + ), + layout=layout, + secure_run=True, + ) + + # Test that the runner fails when an incorrect result is given. + with pytest.raises(SecurityError, match="Expected a .* b == c \(mod p\)"): + compile_and_run( + CODE_FORMAT.format( + p=[1, 1, 0, 0], + x1=[1, 0, 0, 0], + x2=[2, 1, 0, 0], + x3=[2, 0, 0, 0], + res=[2, 0, 0, 0], + ), + layout=layout, + secure_run=True, + ) diff --git a/tools/make/cairo/lang/builtins/range_check/range_check_builtin_runner.py b/tools/make/cairo/lang/builtins/range_check/range_check_builtin_runner.py new file mode 100644 index 00000000..b9fdb22c --- /dev/null +++ b/tools/make/cairo/lang/builtins/range_check/range_check_builtin_runner.py @@ -0,0 +1,109 @@ +from typing import Any, Dict, Optional, Tuple + +from starkware.cairo.lang.builtins.range_check.instance_def import RangeCheckInstanceDef +from starkware.cairo.lang.vm.builtin_runner import BuiltinVerifier, SimpleBuiltinRunner +from starkware.cairo.lang.vm.relocatable import RelocatableValue +from starkware.python.math_utils import safe_div + + +class RangeCheckBuiltinRunner(SimpleBuiltinRunner): + def __init__(self, name: str, included: bool, ratio, inner_rc_bound, n_parts): + super().__init__( + name=name, + included=included, + ratio=ratio, + cells_per_instance=1, + n_input_cells=1, + ) + self.inner_rc_bound = inner_rc_bound + self.bound = inner_rc_bound**n_parts + self.n_parts = n_parts + + def get_instance_def(self): + return RangeCheckInstanceDef(ratio=self.ratio, n_parts=self.n_parts) + + def add_validation_rules(self, runner): + def rule(memory, addr): + value = memory[addr] + assert isinstance(value, int), ( + f"Range-check builtin: Expected value at address {addr} to be an integer. " + f"Got: {value}." + ) + # The range check builtin asserts that 0 <= value < BOUND. + # For example, if the layout uses 8 16-bit range-checks per instance, + # bound will be 2**(16 * 8) = 2**128. + assert 0 <= value < self.bound, ( + f"Value {value}, in range check builtin {addr - self.base}, is out of range " + f"[0, {self.bound})." + ) + return {addr} + + runner.vm.add_validation_rule(self.base.segment_index, rule) + + def air_private_input(self, runner) -> Dict[str, Any]: + assert self.base is not None, "Uninitialized self.base." + res: Dict[int, Any] = {} + for addr, val in runner.vm_memory.items(): + if ( + not isinstance(addr, RelocatableValue) + or addr.segment_index != self.base.segment_index + ): + continue + idx = addr.offset + + assert isinstance(val, int) + res[idx] = {"index": idx, "value": hex(val)} + + return {self.name: sorted(res.values(), key=lambda item: item["index"])} + + def get_range_check_usage(self, runner) -> Optional[Tuple[int, int]]: + assert self.base is not None, "Uninitialized self.base." + rc_min = None + rc_max = None + for addr, val in runner.vm_memory.items(): + if ( + not isinstance(addr, RelocatableValue) + or addr.segment_index != self.base.segment_index + ): + continue + + # Split val into n_parts parts. + for _ in range(self.n_parts): + part_val = val % self.inner_rc_bound + + if rc_min is None: + rc_min = rc_max = part_val + else: + rc_min = min(rc_min, part_val) + rc_max = max(rc_max, part_val) + val //= self.inner_rc_bound + if rc_min is None or rc_max is None: + return None + return rc_min, rc_max + + def get_used_perm_range_check_units(self, runner) -> int: + used_cells, _ = self.get_used_cells_and_allocated_size(runner) + # Each cell in the range check segment requires n_parts range check units. + return used_cells * self.n_parts + + +class RangeCheckBuiltinVerifier(BuiltinVerifier): + def __init__(self, name: str, included: bool, ratio): + self.name = name + self.included = included + self.ratio = ratio + + def expected_stack(self, public_input): + if not self.included: + return [], [] + + addresses = public_input.memory_segments[self.name] + max_size = safe_div(public_input.n_steps, self.ratio) + assert ( + 0 + <= addresses.begin_addr + <= addresses.stop_ptr + <= addresses.begin_addr + max_size + < 2**64 + ) + return [addresses.begin_addr], [addresses.stop_ptr] diff --git a/tools/make/cairo/lang/instances.py b/tools/make/cairo/lang/instances.py new file mode 100644 index 00000000..3297e067 --- /dev/null +++ b/tools/make/cairo/lang/instances.py @@ -0,0 +1,572 @@ +import dataclasses +from dataclasses import field +from typing import Any, Dict, Optional, Union + +from starkware.cairo.lang.builtins.all_builtins import ( + OUTPUT_BUILTIN, + SUPPORTED_DYNAMIC_BUILTINS, + BuiltinList, +) +from starkware.cairo.lang.builtins.bitwise.instance_def import BitwiseInstanceDef +from starkware.cairo.lang.builtins.ec.instance_def import EcOpInstanceDef +from starkware.cairo.lang.builtins.hash.instance_def import PedersenInstanceDef +from starkware.cairo.lang.builtins.instance_def import BuiltinInstanceDef +from starkware.cairo.lang.builtins.keccak.instance_def import KECCAK_BATCH_SIZE, KeccakInstanceDef +from starkware.cairo.lang.builtins.modulo.instance_def import AddModInstanceDef, MulModInstanceDef +from starkware.cairo.lang.builtins.poseidon.instance_def import PoseidonInstanceDef +from starkware.cairo.lang.builtins.range_check.instance_def import RangeCheckInstanceDef +from starkware.cairo.lang.builtins.signature.instance_def import EcdsaInstanceDef +from starkware.python.math_utils import div_ceil, safe_div + +PRIME = 2**251 + 17 * 2**192 + 1 +COMPONENT_HEIGHT = 16 + +DYNAMIC_LAYOUT_NAME = "dynamic" + + +@dataclasses.dataclass +class CpuInstanceDef: + # Verifies that each 'call' instruction returns, even if the called function is malicious. + safe_call: bool = True + + +@dataclasses.dataclass +class DilutedPoolInstanceDef: + # The log of the ratio between the number of diluted cells in the pool + # and the number of cpu steps. + # The case of log_units_per_step < 0 is possible when there are only few + # builtins that require diluted units (as bitwise and keccak). + log_units_per_step: Optional[int] + + # In diluted form the binary sequence **** of length n_bits is represented as 00*00*00*00*, + # with (spacing - 1) zero bits between consecutive information carying bits. + spacing: int + + # The number of (information) bits (before diluting). + n_bits: int + + +@dataclasses.dataclass +class BuiltinsInfo: + output: bool = field(default=True) + # Dictionary of builtin definitions. Note that builtin definitions only exist for builtins that + # are implemented with a periodic constraint in the trace, i.e., all builtins except for the + # output builtin. Therefore, the output builtin is not included in this dictionary. + builtin_defs: Dict[str, BuiltinInstanceDef] = field(default_factory=lambda: {}) + + @property + def builtins_list(self) -> BuiltinList: + list_of_builtins = [] + if self.output: + list_of_builtins.append(OUTPUT_BUILTIN) + list_of_builtins.extend(self.builtin_defs.keys()) + + return BuiltinList(list_of_builtins) + + +@dataclasses.dataclass +class CairoLayout: + layout_name: str = "" + cpu_component_step: Optional[int] = 1 + # Range check units. + rc_units: Optional[int] = 16 + builtins: Dict[str, Any] = field(default_factory=lambda: {}) + # The ratio between the number of public memory cells and the total number of memory cells. + public_memory_fraction: int = 4 + memory_units_per_step: Optional[int] = 8 + diluted_pool_instance_def: Optional[DilutedPoolInstanceDef] = None + n_trace_columns: Optional[int] = None + cpu_instance_def: CpuInstanceDef = field(default=CpuInstanceDef()) + + @property + def diluted_units_row_ratio(self) -> Optional[int]: + assert self.diluted_pool_instance_def is not None + log_units_per_step = self.diluted_pool_instance_def.log_units_per_step + assert log_units_per_step is not None + assert self.cpu_component_step is not None + if log_units_per_step >= 0: + return safe_div(COMPONENT_HEIGHT * self.cpu_component_step, 2**log_units_per_step) + return COMPONENT_HEIGHT * self.cpu_component_step * 2**-log_units_per_step + + +def build_builtins_dict_with_default_params( + **ratios: Optional[int], +) -> Dict[str, Union[BuiltinInstanceDef, bool]]: + """ + Creates a builtins dictionary according to the given ratios. + The ratios are allowed to be None - this is used when constructing the AIR before knowing the + ratios. + """ + assert all(builtin in SUPPORTED_DYNAMIC_BUILTINS for builtin in ratios.keys()) + builtin_dict: Dict[str, Union[BuiltinInstanceDef, bool]] = {"output": True} + builtin_dict["pedersen"] = PedersenInstanceDef( + ratio=ratios.get("pedersen"), + repetitions=1, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ) + builtin_dict["range_check"] = RangeCheckInstanceDef( + ratio=ratios.get("range_check"), + n_parts=8, + ) + builtin_dict["ecdsa"] = EcdsaInstanceDef( + ratio=ratios.get("ecdsa"), + repetitions=1, + height=256, + n_hash_bits=251, + ) + builtin_dict["bitwise"] = BitwiseInstanceDef( + ratio=ratios.get("bitwise"), + total_n_bits=251, + ) + builtin_dict["ec_op"] = EcOpInstanceDef( + ratio=ratios.get("ec_op"), + scalar_height=256, + scalar_bits=252, + scalar_limit=PRIME, + ) + builtin_dict["keccak"] = KeccakInstanceDef( + ratio=ratios.get("keccak"), + state_rep=[200] * 8, + instances_per_component=16, + ) + builtin_dict["poseidon"] = PoseidonInstanceDef( + ratio=ratios.get("poseidon"), + partial_rounds_partition=[64, 22], + ) + + return builtin_dict + + +BATCH_SIZES: Dict[str, int] = { + "keccak": KECCAK_BATCH_SIZE, +} + + +def get_implemented_size_per_builtin(builtin_name: str, requested_usage: int = 0) -> int: + batch_size = BATCH_SIZES.get(builtin_name, 1) + return batch_size * div_ceil(requested_usage, batch_size) + + +DYNAMIC_DILUTED_SPACING = 4 +DYNAMIC_DILUTED_N_BITS = 16 +DYNAMIC_PUBLIC_MEMORY_FRACTION = 8 + + +def build_dynamic_layout( + log_diluted_units_per_step: Optional[int] = None, + cpu_component_step: Optional[int] = None, + rc_units: Optional[int] = None, + memory_units_per_step: Optional[int] = None, + **ratios: Optional[int], +) -> CairoLayout: + if len(ratios) != 0: + for builtin_name in SUPPORTED_DYNAMIC_BUILTINS.except_for(OUTPUT_BUILTIN): + if builtin_name not in ratios: + ratios[builtin_name] = 0 + + return CairoLayout( + layout_name=DYNAMIC_LAYOUT_NAME, + cpu_component_step=cpu_component_step, + rc_units=rc_units, + builtins=build_builtins_dict_with_default_params(**ratios), + public_memory_fraction=DYNAMIC_PUBLIC_MEMORY_FRACTION, + memory_units_per_step=memory_units_per_step, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=log_diluted_units_per_step, + spacing=DYNAMIC_DILUTED_SPACING, + n_bits=DYNAMIC_DILUTED_N_BITS, + ), + n_trace_columns=None, + ) + + +plain_instance = CairoLayout( + layout_name="plain", + n_trace_columns=8, +) + +small_instance = CairoLayout( + layout_name="small", + rc_units=16, + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=8, + repetitions=4, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=8, + n_parts=8, + ), + ecdsa=EcdsaInstanceDef( + ratio=512, + repetitions=1, + height=256, + n_hash_bits=251, + ), + ), + n_trace_columns=25, +) + +dex_instance = CairoLayout( + layout_name="dex", + rc_units=4, + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=8, + repetitions=4, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=8, + n_parts=8, + ), + ecdsa=EcdsaInstanceDef( + ratio=512, + repetitions=1, + height=256, + n_hash_bits=251, + ), + ), + n_trace_columns=22, +) + +starknet_instance = CairoLayout( + layout_name="starknet", + rc_units=4, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=1, + spacing=4, + n_bits=16, + ), + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=32, + repetitions=1, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=16, + n_parts=8, + ), + ecdsa=EcdsaInstanceDef( + ratio=2048, + repetitions=1, + height=256, + n_hash_bits=251, + ), + bitwise=BitwiseInstanceDef( + ratio=64, + total_n_bits=251, + ), + ec_op=EcOpInstanceDef( + ratio=1024, + scalar_height=256, + scalar_bits=252, + scalar_limit=PRIME, + ), + poseidon=PoseidonInstanceDef( + ratio=32, + partial_rounds_partition=[64, 22], + ), + ), + n_trace_columns=10, +) + +starknet_with_keccak_instance = CairoLayout( + layout_name="starknet_with_keccak", + rc_units=4, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=4, + spacing=4, + n_bits=16, + ), + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=32, + repetitions=1, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=16, + n_parts=8, + ), + ecdsa=EcdsaInstanceDef( + ratio=2048, + repetitions=1, + height=256, + n_hash_bits=251, + ), + bitwise=BitwiseInstanceDef( + ratio=64, + total_n_bits=251, + ), + ec_op=EcOpInstanceDef( + ratio=1024, + scalar_height=256, + scalar_bits=252, + scalar_limit=PRIME, + ), + keccak=KeccakInstanceDef( + ratio=2**11, + state_rep=[200] * 8, + instances_per_component=16, + ), + poseidon=PoseidonInstanceDef( + ratio=32, + partial_rounds_partition=[64, 22], + ), + ), + n_trace_columns=15, +) + +# A layout for a Cairo verification proof. +recursive_instance = CairoLayout( + layout_name="recursive", + rc_units=4, + public_memory_fraction=8, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=4, + spacing=4, + n_bits=16, + ), + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=128, + repetitions=1, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=8, + n_parts=8, + ), + bitwise=BitwiseInstanceDef( + ratio=8, + total_n_bits=251, + ), + ), + n_trace_columns=10, +) + +# A layout with a lot of bitwise and pedersen instances (e.g., for Cairo stark verification +# with long output). +recursive_large_output_instance = CairoLayout( + layout_name="recursive_large_output", + rc_units=4, + public_memory_fraction=8, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=4, + spacing=4, + n_bits=16, + ), + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=32, + repetitions=1, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=8, + n_parts=8, + ), + bitwise=BitwiseInstanceDef( + ratio=8, + total_n_bits=251, + ), + ), + n_trace_columns=13, +) + +# A layout optimized for a cairo verifier program that is being verified by a cairo verifier. +all_cairo_instance = CairoLayout( + layout_name="all_cairo", + rc_units=4, + public_memory_fraction=8, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=4, + spacing=4, + n_bits=16, + ), + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=256, + repetitions=1, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=8, + n_parts=8, + ), + ecdsa=EcdsaInstanceDef( + ratio=2048, + repetitions=1, + height=256, + n_hash_bits=251, + ), + bitwise=BitwiseInstanceDef( + ratio=16, + total_n_bits=251, + ), + ec_op=EcOpInstanceDef( + ratio=1024, + scalar_height=256, + scalar_bits=252, + scalar_limit=PRIME, + ), + keccak=KeccakInstanceDef( + ratio=2**11, + state_rep=[200] * 8, + instances_per_component=16, + ), + poseidon=PoseidonInstanceDef( + ratio=256, + partial_rounds_partition=[64, 22], + ), + ), + n_trace_columns=11, +) + +mod_builtin_layout_instance = CairoLayout( + layout_name="mod_builtin_layout", + rc_units=4, + public_memory_fraction=8, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=4, + spacing=4, + n_bits=16, + ), + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=256, + repetitions=1, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=8, + n_parts=8, + ), + ecdsa=EcdsaInstanceDef( + ratio=2048, + repetitions=1, + height=256, + n_hash_bits=251, + ), + bitwise=BitwiseInstanceDef( + ratio=16, + total_n_bits=251, + ), + ec_op=EcOpInstanceDef( + ratio=1024, + scalar_height=256, + scalar_bits=252, + scalar_limit=PRIME, + ), + keccak=KeccakInstanceDef( + ratio=2**11, + state_rep=[200] * 8, + instances_per_component=16, + ), + poseidon=PoseidonInstanceDef( + ratio=256, + partial_rounds_partition=[64, 22], + ), + range_check96=RangeCheckInstanceDef( + ratio=8, + n_parts=6, + ), + add_mod=AddModInstanceDef(ratio=128, word_bit_len=96, n_words=4, batch_size=8), + mul_mod=MulModInstanceDef(ratio=256, word_bit_len=96, n_words=4, batch_size=8, bits_per_part=16), + ), + n_trace_columns=11, +) + +all_solidity_instance = CairoLayout( + layout_name="all_solidity", + rc_units=8, + public_memory_fraction=8, + diluted_pool_instance_def=DilutedPoolInstanceDef( + log_units_per_step=4, + spacing=4, + n_bits=16, + ), + builtins=dict( + output=True, + pedersen=PedersenInstanceDef( + ratio=8, + repetitions=4, + element_height=256, + element_bits=252, + n_inputs=2, + hash_limit=PRIME, + ), + range_check=RangeCheckInstanceDef( + ratio=8, + n_parts=8, + ), + ecdsa=EcdsaInstanceDef( + ratio=512, + repetitions=1, + height=256, + n_hash_bits=251, + ), + bitwise=BitwiseInstanceDef( + ratio=256, + total_n_bits=251, + ), + ec_op=EcOpInstanceDef( + ratio=256, + scalar_height=256, + scalar_bits=252, + scalar_limit=PRIME, + ), + ), + n_trace_columns=27, +) + +LAYOUTS: Dict[str, CairoLayout] = { + "plain": plain_instance, + "small": small_instance, + "dex": dex_instance, + "recursive": recursive_instance, + "starknet": starknet_instance, + "recursive_large_output": recursive_large_output_instance, + "all_solidity": all_solidity_instance, + "starknet_with_keccak": starknet_with_keccak_instance, + "dynamic": build_dynamic_layout(), + "mod_builtin_layout": mod_builtin_layout_instance, +} diff --git a/tools/make/cairo/lang/vm/builtin_runner.py b/tools/make/cairo/lang/vm/builtin_runner.py new file mode 100644 index 00000000..35e5239b --- /dev/null +++ b/tools/make/cairo/lang/vm/builtin_runner.py @@ -0,0 +1,319 @@ +from abc import ABC, abstractmethod +from typing import Any, Callable, Dict, List, Optional, Tuple + +from starkware.cairo.lang.builtins.instance_def import BuiltinInstanceDef +from starkware.cairo.lang.vm.relocatable import MaybeRelocatable, RelocatableValue +from starkware.cairo.lang.vm.utils import MemorySegmentRelocatableAddresses +from starkware.python.math_utils import div_ceil, next_power_of_2, safe_div + + +class InsufficientAllocatedCells(Exception): + pass + + +class BuiltinRunner(ABC): + @abstractmethod + def initialize_segments(self, runner): + """ + Adds memory segments for the builtin. + """ + + @abstractmethod + def initial_stack(self) -> List[MaybeRelocatable]: + """ + Returns the initial stack elements enforced by this builtin. + """ + + @abstractmethod + def final_stack(self, runner, pointer: MaybeRelocatable) -> MaybeRelocatable: + """ + Reads values from the end of the stack ([pointer - 1], [pointer - 2], ...), and returns + the updated pointer (e.g., pointer - 2 if two values were read). + This function may also do builtin specific validation of said values. + """ + + @abstractmethod + def get_used_cells(self, runner) -> int: + """ + Returns the number of used cells. + """ + + @abstractmethod + def get_used_instances(self, runner) -> int: + """ + Returns the number of used instances. + """ + + @abstractmethod + def get_allocated_instances(self, runner) -> int: + """ + Returns the number of instances in the trace (including unused instances). + """ + + @abstractmethod + def get_allocated_memory_units(self, runner) -> int: + """ + Returns the number of memory units used by the builtin. + """ + + @abstractmethod + def get_used_cells_and_allocated_size(self, runner) -> Tuple[int, int]: + """ + Returns the number of used cells and the allocated size, and raises + InsufficientAllocatedCells if there are more used cells than allocated cells. + """ + + @abstractmethod + def finalize_segments(self, runner): + """ + Calls runner.segments.finalize for the memory segments added in initialize_segments. + """ + + @abstractmethod + def get_memory_segment_addresses(self, runner) -> Dict[str, MemorySegmentRelocatableAddresses]: + """ + Returns a dict from segment name to MemorySegmentRelocatableAddresses + (begin_addr and stop_ptr of the corresponding segment). + """ + + @abstractmethod + def run_security_checks(self, runner): + """ + Runs some security checks to make sure a proof can be generated given the memory. + """ + + def relocate(self, relocate_value: Callable[[MaybeRelocatable], MaybeRelocatable]): + """ + Relocates the internal values of the builtin using the given function relocate_value. + """ + return + + def add_auto_deduction_rules(self, runner): + """ + Adds auto-deduction rules for this builtin (if applicable). + Auto deduction rules are applied when an unknown memory cell in the builtin segment is + accessed. + """ + return + + def add_validation_rules(self, runner): + """ + Adds validation rules for this builtin (if applicable). + Validation rules are applied once a builtin instance is written to memory. + For more details, see 'add_validation_rule' in validated_memory_dict.py. + """ + return + + def air_private_input(self, runner) -> Dict[str, Any]: + """ + Returns information about the builtin that should be added to the AIR private input. + """ + return {} + + def get_range_check_usage(self, runner) -> Optional[Tuple[int, int]]: + """ + Returns (rc_min, rc_max), i.e., the minimal and maximal range-checked values, if the builtin + used any range check cells. Otherwise, returns None. + """ + return None + + def get_used_perm_range_check_units(self, runner) -> int: + """ + Returns the number of range check units used by the builtin. + """ + return 0 + + def get_used_diluted_check_units(self, diluted_spacing: int, diluted_n_bits: int) -> int: + """ + Returns the number of diluted check units used by the builtin. + """ + return 0 + + def get_additional_data(self) -> Any: + """ + Returns additional data that was created in the builtin runner. This data can be loaded + to another builtin runner of the same type using extend_additional_data(). + This data must be JSON-serializable. + """ + return + + def extend_additional_data( + self, + data: Any, + relocate_callback: Callable[[MaybeRelocatable], MaybeRelocatable], + data_is_trusted: bool = True, + ): + """ + Adds the additional data created by another instance of the builtin runner. + relocate_callback is a callback function used to relocate the addresses. + If data_is_trusted is False, the function does not assume that instances + that were processed by the other builtin runner were properly validated. + """ + return + + def get_memory_accesses(self, runner): + """ + Returns memory addresses that are used by the builtin itself and therefore should not count + as memory holes. + """ + return {} + + +class BuiltinVerifier(ABC): + @abstractmethod + def expected_stack(self, public_input) -> Tuple[List[int], List[int]]: + """ + Returns a pair (initial_stack, final_stack). + They contain the expected elements of the initial stack or final stack that are associated + with this builtin. + """ + + +class SimpleBuiltinRunner(BuiltinRunner): + """ + A base class for simple builtins that use a single segment. + """ + + def __init__( + self, + name: str, + included: bool, + ratio: Optional[int], + cells_per_instance: int, + n_input_cells: int, + instances_per_component: int = 1, + additional_memory_units_per_instance: int = 0, + ): + """ + Constructs a SimpleBuiltinRunner. + cells_per_instance is the number of memory cells per invocation. + n_input_cells is the number of the first memory cells in each invocation that form the + input. The rest of the cells are considered output. + instances_per_component is the number of invocations being handled in each call to the + corresponding component. It must divide the total number of invocations. + """ + self.name = name + self.included = included + self.ratio = ratio + self.instances_per_component = instances_per_component + self._base: Optional[RelocatableValue] = None + self.stop_ptr: Optional[RelocatableValue] = None + self.cells_per_instance = cells_per_instance + self.n_input_cells = n_input_cells + self.additional_memory_units_per_instance = additional_memory_units_per_instance + + def get_instance_def(self) -> Optional[BuiltinInstanceDef]: + """ + Returns a BuiltinInstanceDef object that represents the builtin if such object exists. + """ + return None + + def initialize_segments(self, runner): + self._base = runner.segments.add() + + @property + def base(self) -> RelocatableValue: + assert self._base is not None, "Uninitialized self.base." + return self._base + + def initial_stack(self) -> List[MaybeRelocatable]: + return [self.base] if self.included else [] + + def final_stack(self, runner, pointer): + if self.included: + self.stop_ptr = runner.vm_memory[pointer - 1] + used = self.get_used_instances(runner=runner) * self.cells_per_instance + assert self.stop_ptr == self.base + used, ( + f"Invalid stop pointer for {self.name}. " + + f"Expected: {self.base + used}, found: {self.stop_ptr}" + ) + return pointer - 1 + else: + self.stop_ptr = self.base + return pointer + + def get_used_cells(self, runner): + used = runner.segments.get_segment_used_size(self.base.segment_index) + return used + + def get_used_instances(self, runner): + return div_ceil(self.get_used_cells(runner), self.cells_per_instance) + + def get_allocated_instances(self, runner): + if self.ratio is None: + # Dynamic layout has the exact number of instances it needs (up to a power of 2). + instances = self.get_used_instances(runner) + needed_components = div_ceil(instances, self.instances_per_component) + components = next_power_of_2(needed_components) if needed_components > 0 else 0 + return self.instances_per_component * components + assert isinstance(self.ratio, int), "ratio is not an int" + if self.ratio == 0: + # The builtin is not used. + return 0 + min_steps = self.ratio * self.instances_per_component + if runner.vm.current_step < min_steps: + raise InsufficientAllocatedCells( + f"Number of steps must be at least {min_steps} for the {self.name} builtin." + ) + return safe_div(runner.vm.current_step, self.ratio) + + def get_allocated_memory_units(self, runner): + return ( + self.cells_per_instance + self.additional_memory_units_per_instance + ) * self.get_allocated_instances(runner) + + def get_used_cells_and_allocated_size(self, runner): + used = self.get_used_cells(runner) + size = self.cells_per_instance * self.get_allocated_instances(runner) + if used > size: + raise InsufficientAllocatedCells( + f"The {self.name} builtin used {used} cells but the capacity is {size}." + ) + return used, size + + def finalize_segments(self, runner): + _, size = self.get_used_cells_and_allocated_size(runner) + + runner.segments.finalize(segment_index=self.base.segment_index, size=size) + + def get_memory_segment_addresses(self, runner): + return { + self.name: MemorySegmentRelocatableAddresses( + begin_addr=self.base, + stop_ptr=self.stop_ptr, + ) + } + + def run_security_checks(self, runner): + offsets = { + addr.offset + for addr in runner.vm_memory.keys() + if isinstance(addr, RelocatableValue) and addr.segment_index == self.base.segment_index + } + n = (max(offsets) // self.cells_per_instance + 1) if len(offsets) > 0 else 0 + + # Verify that n is not too large to make sure the expected_offsets set that is constructed + # below is not too large. + assert n <= len(offsets) // self.n_input_cells, f"Missing memory cells for {self.name}." + + # Check that the two inputs (x and y) of each instance are set. + expected_offsets = { + self.cells_per_instance * i + j for i in range(n) for j in range(self.n_input_cells) + } + if not expected_offsets <= offsets: + missing_offsets = list(expected_offsets - offsets) + dots = "..." if len(missing_offsets) > 20 else "." + missing_offsets_str = ", ".join(map(str, missing_offsets[:20])) + dots + raise AssertionError(f"Missing memory cells for {self.name}: {missing_offsets_str}") + + # Verify auto deduction rules for the unassigned output cells. + # Assigned output cells are checked as part of the call to verify_auto_deductions(). + for i in range(n): + for j in range(self.n_input_cells, self.cells_per_instance): + addr = self.base + (self.cells_per_instance * i + j) + if runner.vm.validated_memory.get(addr) is None: + runner.vm.verify_auto_deductions_for_addr(addr) + + def get_memory_accesses(self, runner): + segment_size = runner.segments.get_segment_size(self.base.segment_index) + return {self.base + x for x in range(segment_size)} diff --git a/tools/make/cairo/lang/vm/cairo_runner.py b/tools/make/cairo/lang/vm/cairo_runner.py new file mode 100644 index 00000000..6c177c1e --- /dev/null +++ b/tools/make/cairo/lang/vm/cairo_runner.py @@ -0,0 +1,917 @@ +from typing import ( + Any, + Callable, + Dict, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, +) + +from starkware.cairo.lang.builtins.bitwise.bitwise_builtin_runner import BitwiseBuiltinRunner +from starkware.cairo.lang.builtins.ec.ec_op_builtin_runner import EcOpBuiltinRunner +from starkware.cairo.lang.builtins.hash.hash_builtin_runner import HashBuiltinRunner +from starkware.cairo.lang.builtins.keccak.keccak_builtin_runner import KeccakBuiltinRunner +from starkware.cairo.lang.builtins.modulo.mod_builtin_runner import ( + AddModBuiltinRunner, + MulModBuiltinRunner, +) +from starkware.cairo.lang.builtins.poseidon.poseidon_builtin_runner import PoseidonBuiltinRunner +from starkware.cairo.lang.builtins.range_check.range_check_builtin_runner import ( + RangeCheckBuiltinRunner, +) +from starkware.cairo.lang.builtins.signature.signature_builtin_runner import SignatureBuiltinRunner +from starkware.cairo.lang.compiler.cairo_compile import ( + compile_cairo, + compile_cairo_files, + get_module_reader, +) +from starkware.cairo.lang.compiler.debug_info import DebugInfo +from starkware.cairo.lang.compiler.expression_simplifier import to_field_element +from starkware.cairo.lang.compiler.preprocessor.default_pass_manager import default_pass_manager +from starkware.cairo.lang.compiler.preprocessor.preprocessor import Preprocessor +from starkware.cairo.lang.compiler.program import Program, ProgramBase +from starkware.cairo.lang.instances import LAYOUTS, CairoLayout +from starkware.cairo.lang.vm.builtin_runner import BuiltinRunner, InsufficientAllocatedCells +from starkware.cairo.lang.vm.cairo_pie import ( + CairoPie, + CairoPieMetadata, + ExecutionResources, + SegmentInfo, +) +from starkware.cairo.lang.vm.crypto import pedersen_hash, verify_ecdsa +from starkware.cairo.lang.vm.memory_dict import MemoryDict +from starkware.cairo.lang.vm.memory_segments import MemorySegmentManager +from starkware.cairo.lang.vm.output_builtin_runner import OutputBuiltinRunner +from starkware.cairo.lang.vm.relocatable import MaybeRelocatable, RelocatableValue, relocate_value +from starkware.cairo.lang.vm.trace_entry import relocate_trace +from starkware.cairo.lang.vm.utils import ( + MemorySegmentAddresses, + MemorySegmentRelocatableAddresses, + ResourcesError, + RunResources, +) +from starkware.cairo.lang.vm.vm import RunContext, VirtualMachine, get_perm_range_check_limits +from starkware.crypto.signature.signature import inv_mod_curve_size +from starkware.python.math_utils import next_power_of_2, safe_div +from starkware.python.utils import WriteOnceDict +from starkware.starkware_utils.subsequence import is_subsequence + +TCairoRunner = TypeVar("TCairoRunner", bound="CairoRunner") + + +def verify_ecdsa_sig(public_key, msg, signature) -> bool: + """ + Returns True if the given ECDSA signature is valid for the given public key and message hash. + Signature is a pair (r, s). + """ + r, s = signature + return verify_ecdsa(msg, r, s, public_key) + + +def process_ecdsa(public_key, msg, signature): + """ + Returns an (r, s) ECDSA signature in the {'r': hex(r), 'w': hex(s^-1)} format, as expected by + the ECDSA component. + """ + r, s = signature + return {"r": hex(r), "w": hex(inv_mod_curve_size(s))} + + +class CairoRunner: + def __init__( + self, + program: ProgramBase, + layout: Union[str, CairoLayout] = "plain", + memory: MemoryDict = None, + proof_mode: Optional[bool] = None, + allow_missing_builtins: Optional[bool] = None, + additional_builtin_factories: Optional[ + Dict[str, Callable[[str, bool], BuiltinRunner]] + ] = None, + ): + if additional_builtin_factories is None: + additional_builtin_factories = {} + + self.program = program + self.layout: CairoLayout = layout if isinstance(layout, CairoLayout) else LAYOUTS[layout] + self.builtin_runners: Dict[str, BuiltinRunner] = {} + self.original_steps = None + self.proof_mode = False if proof_mode is None else proof_mode + self.allow_missing_builtins = ( + False if allow_missing_builtins is None else allow_missing_builtins + ) + + if not allow_missing_builtins: + non_existing_builtins = set(self.program.builtins) - set(self.layout.builtins.keys()) + layout_name = self.layout.layout_name + assert ( + len(non_existing_builtins) == 0 + ), f'Builtins {non_existing_builtins} are not present in layout "{layout_name}"' + + builtin_factories = dict( + output=lambda name, included: OutputBuiltinRunner(included=included), + pedersen=lambda name, included: HashBuiltinRunner( + name=name, + included=included, + ratio=self.layout.builtins["pedersen"].ratio, + hash_func=pedersen_hash, + instance_def=self.layout.builtins["pedersen"], + ), + range_check=lambda name, included: RangeCheckBuiltinRunner( + name="range_check", + included=included, + ratio=self.layout.builtins["range_check"].ratio, + inner_rc_bound=2**16, + n_parts=self.layout.builtins["range_check"].n_parts, + ), + ecdsa=lambda name, included: SignatureBuiltinRunner( + name=name, + included=included, + ratio=self.layout.builtins["ecdsa"].ratio, + process_signature=process_ecdsa, + verify_signature=verify_ecdsa_sig, + instance_def=self.layout.builtins["ecdsa"], + ), + bitwise=lambda name, included: BitwiseBuiltinRunner( + included=included, bitwise_builtin=self.layout.builtins["bitwise"] + ), + ec_op=lambda name, included: EcOpBuiltinRunner( + included=included, ec_op_builtin=self.layout.builtins["ec_op"] + ), + keccak=lambda name, included: KeccakBuiltinRunner( + included=included, instance_def=self.layout.builtins["keccak"] + ), + poseidon=lambda name, included: PoseidonBuiltinRunner( + included=included, instance_def=self.layout.builtins["poseidon"] + ), + range_check96=lambda name, included: RangeCheckBuiltinRunner( + name="range_check96", + included=included, + ratio=self.layout.builtins["range_check96"].ratio, + inner_rc_bound=2**16, + n_parts=self.layout.builtins["range_check96"].n_parts, + ), + add_mod=lambda name, included: AddModBuiltinRunner( + included=included, instance_def=self.layout.builtins["add_mod"] + ), + mul_mod=lambda name, included: MulModBuiltinRunner( + included=included, instance_def=self.layout.builtins["mul_mod"] + ), + **additional_builtin_factories, + ) + + for name in self.layout.builtins: + factory = builtin_factories.get(name) + assert factory is not None, f"The {name} builtin is not supported." + included = name in self.program.builtins + # In proof mode all the builtin_runners are required. + if included or self.proof_mode: + self.builtin_runners[f"{name}_builtin"] = factory( # type: ignore + name=name, included=included + ) + + supported_builtin_list = list(builtin_factories.keys()) + err_msg = ( + f"The builtins specified by the %builtins directive must be subsequence of " + f"{supported_builtin_list}. Got {self.program.builtins}." + ) + assert is_subsequence(self.program.builtins, supported_builtin_list), err_msg + + self.memory = memory if memory is not None else MemoryDict() + self.segments = MemorySegmentManager(memory=self.memory, prime=self.program.prime) + self.segment_offsets: Optional[Dict[int, int]] = None + self.final_pc: Optional[RelocatableValue] = None + + # Flags used to ensure a safe use. + self._run_ended: bool = False + self._segments_finalized: bool = False + # A set of memory addresses accessed by the VM, after relocation of temporary segments into + # real ones. + self.accessed_addresses: Optional[Set[RelocatableValue]] = None + + @classmethod + def from_file( + cls: Type[TCairoRunner], + filename: str, + prime: int, + layout: str = "plain", + remove_hints: bool = False, + remove_builtins: bool = False, + memory: MemoryDict = None, + preprocessor_cls: Type[Preprocessor] = Preprocessor, + proof_mode: Optional[bool] = None, + ) -> TCairoRunner: + module_reader = get_module_reader(cairo_path=[]) + program = compile_cairo_files( + files=[filename], + debug_info=True, + pass_manager=default_pass_manager( + prime=prime, read_module=module_reader.read, preprocessor_cls=preprocessor_cls + ), + ) + if remove_hints: + program.hints = {} + if remove_builtins: + program.builtins = [] + return cls(program, layout, memory=memory, proof_mode=proof_mode) + + # Functions for the running sequence. + + def initialize_segments(self, program_base=None): + # Program segment. + self.program_base = self.segments.add() if program_base is None else program_base + + # Execution segment. + self.execution_base = self.segments.add() + + # Builtin segments. + for builtin_runner in self.builtin_runners.values(): + builtin_runner.initialize_segments(self) + + def initialize_main_entrypoint(self): + """ + Initializes state for running a program from the main() entrypoint. + If self.proof_mode == True, the execution starts from the start label rather then + the main() function. + + Returns the value of the program counter after returning from main. + """ + self.execution_public_memory: List[int] = [] + + stack: List[MaybeRelocatable] = [] + for builtin_name in self.program.builtins: + builtin_runner = self.builtin_runners.get(f"{builtin_name}_builtin") + if builtin_runner is None: + assert self.allow_missing_builtins, "Missing builtin." + stack += [0] + else: + stack += builtin_runner.initial_stack() + + if self.proof_mode: + # Add the dummy last fp and pc to the public memory, so that the verifier can enforce + # [fp - 2] = fp. + stack_prefix: List[MaybeRelocatable] = [self.execution_base + 2, 0] + stack = stack_prefix + stack + self.execution_public_memory = list(range(len(stack))) + + assert isinstance( + self.program, Program + ), "--proof_mode cannot be used with a StrippedProgram." + self.initialize_state(self.program.start, stack) + self.initial_fp = self.initial_ap = self.execution_base + 2 + return self.program_base + self.program.get_label("__end__") + else: + return_fp = self.segments.add() + + main = self.program.main + assert main is not None, "Missing main()." + return self.initialize_function_entrypoint(main, stack, return_fp=return_fp) + + def initialize_function_entrypoint( + self, + entrypoint: Union[str, int], + args: Sequence[MaybeRelocatable], + return_fp: MaybeRelocatable = 0, + ): + end = self.segments.add() + stack = list(args) + [return_fp, end] + self.initialize_state(entrypoint, stack) + self.initial_fp = self.initial_ap = self.execution_base + len(stack) + self.final_pc = end + return end + + def initialize_state(self, entrypoint: Union[str, int], stack: Sequence[MaybeRelocatable]): + self.initial_pc = self.program_base + self._to_pc(entrypoint) + # Load program. + self.load_data(self.program_base, self.program.data) + # Load stack. + self.load_data(self.execution_base, stack) + + def initialize_vm( + self, hint_locals, static_locals: Optional[Dict[str, Any]] = None, vm_class=None + ): + if vm_class is None: + vm_class = VirtualMachine + context = RunContext( + pc=self.initial_pc, + ap=self.initial_ap, + fp=self.initial_fp, + memory=self.memory, + prime=self.program.prime, + ) + + if static_locals is None: + static_locals = {} + + self.vm = vm_class( + self.program, + context, + hint_locals=hint_locals, + static_locals=dict(segments=self.segments, **static_locals), + builtin_runners=self.builtin_runners, + program_base=self.program_base, + ) + + for builtin_runner in self.builtin_runners.values(): + builtin_runner.add_validation_rules(self) + builtin_runner.add_auto_deduction_rules(self) + + self.vm.validate_existing_memory() + + def run_until_label( + self, label_or_pc: Union[str, int], run_resources: Optional[RunResources] = None + ): + """ + Runs the VM until label is reached, and stops right before that instruction is executed. + 'label_or_pc' should be either a label string or an integer offset from the program_base. + """ + label = self._to_pc(label_or_pc) + self.run_until_pc(self.program_base + label, run_resources=run_resources) + + def run_until_pc(self, addr: MaybeRelocatable, run_resources: Optional[RunResources] = None): + """ + Runs the VM until pc reaches 'addr', and stop right before that instruction is executed. + """ + if run_resources is None: + run_resources = RunResources(n_steps=None) + + while self.vm.run_context.pc != addr and not run_resources.consumed: + self.vm_step() + run_resources.consume_step() + + if self.vm.run_context.pc != addr: + raise self.vm.as_vm_exception( + ResourcesError("Error: End of program was not reached"), with_traceback=False + ) + + def vm_step(self): + if self.vm.run_context.pc == self.final_pc: + raise self.vm.as_vm_exception( + Exception("Error: Execution reached the end of the program."), + with_traceback=False, + ) + self.vm.step() + + def run_for_steps(self, steps: int): + """ + Runs the VM for 'steps' steps. + """ + for _ in range(steps): + self.vm_step() + + def run_until_steps(self, steps: int): + """ + Runs the VM (not necessarily from step 0) until 'steps' steps have been run. + Does nothing if 'steps' steps or more have been run already. + """ + self.run_for_steps(max(steps - self.vm.current_step, 0)) + + def run_until_next_power_of_2(self): + """ + Runs the VM until the step count reaches the next power of 2. + """ + self.run_until_steps(next_power_of_2(self.vm.current_step)) + + def end_run( + self, + disable_trace_padding: bool = True, + disable_finalize_all: bool = False, + allow_tmp_segments: bool = False, + ): + assert not self._run_ended, "end_run called twice." + + self.accessed_addresses = { + self.vm_memory.relocate_value(addr) for addr in self.vm.accessed_addresses + } + self.vm_memory.relocate_memory() + self.vm.end_run() + + if disable_finalize_all: + # For tests. + return + + # Freeze to enable caching; No changes in memory should be made from now on. + self.vm_memory.freeze() + # Deduce the size of each segment from its usage. + self.segments.compute_effective_sizes(allow_tmp_segments=allow_tmp_segments) + + if self.proof_mode and not disable_trace_padding: + self.run_until_next_power_of_2() + while not self.check_used_cells(): + self.run_for_steps(1) + self.run_until_next_power_of_2() + + self._run_ended = True + + def read_return_values(self): + """ + Reads builtin return values (end pointers) and adds them to the public memory. + Note: end_run() must precede a call to this method. + """ + assert self._run_ended, "Run must be ended before calling read_return_values." + + pointer = self.vm.run_context.ap + for builtin_name in self.program.builtins[::-1]: + builtin_runner = self.builtin_runners.get(f"{builtin_name}_builtin") + if builtin_runner is None: + assert self.allow_missing_builtins, "Missing builtin." + pointer -= 1 + assert ( + self.vm_memory[pointer] == 0 + ), f'The stop pointer of the missing builtin "{builtin_name}" must be 0.' + else: + pointer = builtin_runner.final_stack(self, pointer) + + assert ( + not self._segments_finalized + ), "Cannot add the return values to the public memory after segment finalization." + # Add return values to public memory. + self.execution_public_memory += list( + range(pointer - self.execution_base, self.vm.run_context.ap - self.execution_base) + ) + + def mark_as_accessed(self, address: RelocatableValue, size: int): + """ + Marks the memory range [address, address + size) as accessed. + + This is useful when a memory range is not accessed in a partial scenario + but is known to be accessed in the real use case. + + For example, a StarkNet contract entry point might not use all the information provided by + the StarkNet OS. + """ + assert self.accessed_addresses is not None + for i in range(size): + self.accessed_addresses.add(address + i) + + def check_used_cells(self): + """ + Returns True if there are enough allocated cells for the builtins. + If not, the number of steps should be increased or a different layout should be used. + """ + try: + for builtin_runner in self.builtin_runners.values(): + builtin_runner.get_used_cells_and_allocated_size(self) + self.check_range_check_usage() + self.check_memory_usage() + self.check_diluted_check_usage() + except InsufficientAllocatedCells as e: + print(f"Warning: {e} Increasing number of steps.") + return False + return True + + def finalize_segments(self): + """ + Finalizes the segments. + Note: + 1. end_run() must precede a call to this method. + 2. Call read_return_values() *before* finalize_segments(), otherwise the return values + will not be included in the public memory. + """ + if self._segments_finalized: + return + + assert self._run_ended, "Run must be ended before calling finalize_segments." + self.segments.finalize( + self.program_base.segment_index, + size=len(self.program.data), + public_memory=[(i, 0) for i in range(len(self.program.data))], + ) + self.segments.finalize( + self.execution_base.segment_index, + public_memory=[ + (x + self.execution_base.offset, 0) for x in self.execution_public_memory + ], + ) + + for builtin_runner in self.builtin_runners.values(): + builtin_runner.finalize_segments(self) + + self._segments_finalized = True + + def finalize_segments_by_cairo_pie(self, cairo_pie: CairoPie): + for segment_info in cairo_pie.metadata.all_segments(): + self.segments.finalize(segment_info.index, segment_info.size) + + def get_air_private_input(self): + return { + name: value + for builtin_runner in self.builtin_runners.values() + for name, value in builtin_runner.air_private_input(self).items() + } + + def get_perm_range_check_limits(self): + rc_min, rc_max = get_perm_range_check_limits(self.vm.trace, self.vm_memory) + for builtin_runner in self.builtin_runners.values(): + range_check_usage = builtin_runner.get_range_check_usage(self) + if range_check_usage is None: + continue + rc_min = min(rc_min, range_check_usage[0]) + rc_max = max(rc_max, range_check_usage[1]) + return rc_min, rc_max + + def check_range_check_usage(self): + """ + Checks that there are enough trace cells to fill the entire range checks range. + """ + assert self.layout.rc_units is not None, "RC units should not be None in proof mode." + + rc_min, rc_max = self.get_perm_range_check_limits() + rc_units_used_by_builtins = sum( + builtin_runner.get_used_perm_range_check_units(self) + for builtin_runner in self.builtin_runners.values() + ) + + # Out of the range check units allowed per step three are used for the instruction. + unused_rc_units = ( + self.layout.rc_units - 3 + ) * self.vm.current_step - rc_units_used_by_builtins + rc_usage_upper_bound = rc_max - rc_min + if unused_rc_units < rc_usage_upper_bound: + raise InsufficientAllocatedCells( + f"There are only {unused_rc_units} cells to fill the range checks holes, but " + f"potentially {rc_usage_upper_bound} are required." + ) + + def get_memory_holes(self): + assert self.accessed_addresses is not None + # Collect memory addresses that are accessed by the builtin (and therefore are not counted + # as memory holes). + builtin_accessed_addresses = { + addr + for builtin_runner in self.builtin_runners.values() + for addr in builtin_runner.get_memory_accesses(self) + } + return self.segments.get_memory_holes( + accessed_addresses=self.accessed_addresses | builtin_accessed_addresses + ) + + def check_memory_usage(self): + """ + Checks that there are enough trace cells to fill the entire memory range. + """ + assert ( + self.layout.memory_units_per_step is not None + ), "Memory units per step should not be None in proof mode." + + builtins_memory_units = sum( + builtin_runner.get_allocated_memory_units(self) + for builtin_runner in self.builtin_runners.values() + ) + + # Out of the memory units available per step, a fraction is used for public memory, and + # four are used for the instruction. + total_memory_units = self.layout.memory_units_per_step * self.vm.current_step + public_memory_units = safe_div(total_memory_units, self.layout.public_memory_fraction) + instruction_memory_units = 4 * self.vm.current_step + unused_memory_units = total_memory_units - ( + public_memory_units + instruction_memory_units + builtins_memory_units + ) + memory_address_holes = self.get_memory_holes() + if unused_memory_units < memory_address_holes: + raise InsufficientAllocatedCells( + f"There are only {unused_memory_units} cells to fill the memory address holes, but " + f"{memory_address_holes} are required." + ) + + def check_diluted_check_usage(self): + """ + Checks that there are enough trace cells to fill the entire diluted checks. + """ + if self.layout.diluted_pool_instance_def is None: + return + log_units_per_step = self.layout.diluted_pool_instance_def.log_units_per_step + assert ( + log_units_per_step is not None + ), "Diluted log units per step should not be None in proof mode." + + diluted_units_used_by_builtins = sum( + builtin_runner.get_used_diluted_check_units( + diluted_spacing=self.layout.diluted_pool_instance_def.spacing, + diluted_n_bits=self.layout.diluted_pool_instance_def.n_bits, + ) + * builtin_runner.get_allocated_instances(self) + for builtin_runner in self.builtin_runners.values() + ) + + diluted_units = ( + pow(2, log_units_per_step) * self.vm.current_step + if log_units_per_step >= 0 + else safe_div(self.vm.current_step, pow(2, -log_units_per_step)) + ) + unused_diluted_units = diluted_units - diluted_units_used_by_builtins + diluted_usage_upper_bound = 2**self.layout.diluted_pool_instance_def.n_bits + if unused_diluted_units < diluted_usage_upper_bound: + raise InsufficientAllocatedCells( + f"There are only {unused_diluted_units} cells to fill the diluted check holes, but " + f"potentially {diluted_usage_upper_bound} are required." + ) + + # Helper functions. + + @property + def vm_memory(self) -> MemoryDict: + return self.memory + + def _to_pc(self, label_or_pc: Union[str, int]) -> int: + """ + If the input is a string, treat it as a label and converts it to a PC. + Otherwise, return it unchanged. + """ + if isinstance(label_or_pc, str): + assert isinstance( + self.program, Program + ), "Label name cannot be used with a StrippedProgram." + return self.program.get_label(label_or_pc) + return label_or_pc + + def load_data( + self, ptr: MaybeRelocatable, data: Sequence[MaybeRelocatable] + ) -> MaybeRelocatable: + """ + Writes data into the memory at address ptr and returns the first address after the data. + """ + return self.segments.load_data(ptr, data) + + def gen_arg(self, arg, apply_modulo_to_args=True): + """ + Converts args to Cairo-friendly ones. + If an argument is Iterable it is replaced by a pointer to a new segment containing the items + in the Iterable arg (recursively). + If apply_modulo_to_args=True, all the integers are taken modulo the program's prime. + """ + return self.segments.gen_arg(arg=arg, apply_modulo_to_args=apply_modulo_to_args) + + def relocate_value(self, value: MaybeRelocatable) -> int: + assert self.segment_offsets is not None, "segment_offsets is not initialized." + relocated = relocate_value( + value=value, segment_offsets=self.segment_offsets, prime=self.program.prime + ) + assert isinstance(relocated, int) + return relocated + + def get_segment_offsets(self) -> Dict[int, int]: + assert self.segment_offsets is not None, "segment_offsets is not initialized." + return self.segment_offsets + + def relocate(self): + self.segment_offsets = self.segments.relocate_segments() + + initializer: Mapping[MaybeRelocatable, MaybeRelocatable] = { + self.relocate_value(addr): self.relocate_value(value) + for addr, value in self.vm_memory.items() + } + self.relocated_memory = MemoryDict(initializer) + self.relocated_trace = relocate_trace( + self.vm.trace, self.segment_offsets, self.program.prime + ) + for builtin_runner in self.builtin_runners.values(): + builtin_runner.relocate(self.relocate_value) + + def get_relocated_debug_info(self): + return DebugInfo( + instruction_locations={ + self.relocate_value(addr): location_info + for addr, location_info in self.vm.instruction_debug_info.items() + }, + file_contents=self.vm.debug_file_contents, + ) + + def get_memory_segment_addresses(self) -> Dict[str, MemorySegmentAddresses]: + def get_segment_addresses( + name: str, segment_addresses: MemorySegmentRelocatableAddresses + ) -> MemorySegmentAddresses: + stop_ptr = ( + segment_addresses.stop_ptr + if name in self.program.builtins + else segment_addresses.begin_addr + ) + + assert stop_ptr is not None, f"The {name} builtin stop pointer was not set." + return MemorySegmentAddresses( + begin_addr=self.relocate_value(segment_addresses.begin_addr), + stop_ptr=self.relocate_value(stop_ptr), + ) + + return { + name: get_segment_addresses(name, segment_addresses) + for builtin_runner in self.builtin_runners.values() + for name, segment_addresses in builtin_runner.get_memory_segment_addresses(self).items() + } + + def print_memory(self, relocated: bool): + print("Addr Value") + print("-----------") + old_addr = -1 + memory = self.relocated_memory if relocated else self.vm_memory + for addr in sorted(memory.keys()): + val = memory[addr] + if addr != old_addr + 1: + print("\u22ee") + if isinstance(val, int): + val = to_field_element(val=val, prime=self.program.prime) + print(f"{addr:<5} {val}") + old_addr = addr + print() + + def print_output(self, output_callback=to_field_element): + if "output_builtin" not in self.builtin_runners: + return + + output_runner = self.builtin_runners["output_builtin"] + assert isinstance(output_runner, OutputBuiltinRunner) + print("Program output:") + _, size = output_runner.get_used_cells_and_allocated_size(self) + for i in range(size): + val = self.vm_memory.get(output_runner.base + i) + if val is not None: + print(f" {output_callback(val=val, prime=self.program.prime)}") + else: + print(" ") + + print() + + def print_info(self, relocated: bool): + print(self.get_info(relocated=relocated)) + + def get_info(self, relocated: bool) -> str: + pc, ap, fp = self.vm.run_context.pc, self.vm.run_context.ap, self.vm.run_context.fp + if relocated: + pc = self.relocate_value(pc) + ap = self.relocate_value(ap) + fp = self.relocate_value(fp) + + info = f"""\ +Number of steps: {len(self.vm.trace)} { + '' if self.original_steps is None else f'(originally, {self.original_steps})'} +Used memory cells: {len(self.vm_memory)} +Register values after execution: +pc = {pc} +ap = {ap} +fp = {fp} +""" + return info + + def print_segment_relocation_table(self): + if self.segment_offsets is not None: + print("Segment relocation table:") + for segment_index in range(self.segments.n_segments): + print(f"{segment_index:<5} {self.segment_offsets[segment_index]}") + + def get_builtin_usage(self) -> str: + if len(self.builtin_runners) == 0: + return "" + + builtin_usage_str = "\nBuiltin usage:\n" + for name, builtin_runner in self.builtin_runners.items(): + used, size = builtin_runner.get_used_cells_and_allocated_size(self) + percentage = f"{used / size * 100:.2f}%" if size > 0 else "100%" + builtin_usage_str += f"{name:<30s} {percentage:>7s} (used {used} cells)\n" + + return builtin_usage_str + + def print_builtin_usage(self): + print(self.get_builtin_usage()) + + def get_builtin_segments_info(self): + builtin_segments: Dict[str, SegmentInfo] = {} + for builtin in self.builtin_runners.values(): + for name, segment_addresses in builtin.get_memory_segment_addresses(self).items(): + begin_addr = segment_addresses.begin_addr + assert isinstance( + begin_addr, RelocatableValue + ), f"{name} segment begin_addr is not a RelocatableValue {begin_addr}." + assert ( + begin_addr.offset == 0 + ), f"Unexpected {name} segment begin_addr {begin_addr.offset}." + assert segment_addresses.stop_ptr is not None, f"{name} segment stop ptr is None." + segment_index = begin_addr.segment_index + segment_size = segment_addresses.stop_ptr - begin_addr + assert isinstance(segment_size, int) + assert name not in builtin_segments, f"Builtin segment name collision: {name}." + builtin_segments[name] = SegmentInfo(index=segment_index, size=segment_size) + return builtin_segments + + def get_execution_resources(self) -> ExecutionResources: + n_steps = len(self.vm.trace) if self.original_steps is None else self.original_steps + n_memory_holes = self.get_memory_holes() + builtin_instance_counter = { + builtin_name: builtin_runner.get_used_instances(self) + for builtin_name, builtin_runner in self.builtin_runners.items() + } + return ExecutionResources( + n_steps=n_steps, + n_memory_holes=n_memory_holes, + builtin_instance_counter=builtin_instance_counter, + ) + + def get_cairo_pie(self) -> CairoPie: + """ + Constructs and returns a CairoPie representing the current VM run. + """ + builtin_segments = self.get_builtin_segments_info() + known_segment_indices = WriteOnceDict() + for segment_info in builtin_segments.values(): + known_segment_indices[segment_info.index] = None + + # Note that n_used_builtins might be smaller then len(builtin_segments). + n_used_builtins = len(self.program.builtins) + ret_fp, ret_pc = ( + self.vm_memory[self.execution_base + n_used_builtins + i] for i in range(2) + ) + + assert isinstance(ret_fp, RelocatableValue), f"Expecting a relocatable value got {ret_fp}." + assert isinstance(ret_pc, RelocatableValue), f"Expecting a relocatable value got {ret_pc}." + + assert self.segments.get_segment_size(ret_fp.segment_index) == 0, ( + "Unexpected ret_fp_segment size " + f"{self.segments.get_segment_size(ret_fp.segment_index)}" + ) + assert self.segments.get_segment_size(ret_pc.segment_index) == 0, ( + "Unexpected ret_pc_segment size " + f"{self.segments.get_segment_size(ret_pc.segment_index)}" + ) + + for addr in self.program_base, self.execution_base, ret_fp, ret_pc: + assert addr.offset == 0, "Expecting a 0 offset." + known_segment_indices[addr.segment_index] = None + + # Put all the remaining segments in extra_segments. + extra_segments = [ + SegmentInfo(index=index, size=self.segments.get_segment_size(index)) + for index in range(self.segments.n_segments) + if index not in known_segment_indices + ] + + execution_size = self.vm.run_context.ap - self.execution_base + cairo_pie_metadata = CairoPieMetadata( + program=self.program.stripped(), + program_segment=SegmentInfo( + index=self.program_base.segment_index, size=len(self.program.data) + ), + execution_segment=SegmentInfo( + index=self.execution_base.segment_index, size=execution_size + ), + ret_fp_segment=SegmentInfo(ret_fp.segment_index, size=0), + ret_pc_segment=SegmentInfo(ret_pc.segment_index, size=0), + builtin_segments=builtin_segments, + extra_segments=extra_segments, + ) + + execution_resources = self.get_execution_resources() + + return CairoPie( + metadata=cairo_pie_metadata, + memory=self.vm.run_context.memory, + additional_data={ + name: builtin.get_additional_data() + for name, builtin in self.builtin_runners.items() + }, + execution_resources=execution_resources, + ) + + +def get_runner_from_code( + code: Union[str, Sequence[Tuple[str, str]]], layout: str, prime: int +) -> CairoRunner: + """ + Given a code with some compile and run parameters (prime, layout, etc.), runs the code using + Cairo runner and returns the runner. + """ + program = compile_cairo(code=code, prime=prime, debug_info=True) + return get_main_runner(program=program, hint_locals={}, layout=layout) + + +def get_main_runner( + program: Program, + hint_locals: Dict[str, Any], + layout: str, + allow_missing_builtins: Optional[bool] = None, +) -> CairoRunner: + """ + Creates a Cairo runner and runs its main-entrypoint. + Returns the runner. + """ + runner = CairoRunner( + program=program, layout=layout, allow_missing_builtins=allow_missing_builtins + ) + run_main_entrypoint(runner=runner, hint_locals=hint_locals) + return runner + + +def run_main_entrypoint(runner: CairoRunner, hint_locals: Dict[str, Any]): + """ + Runs a main-entrypoint program using the given Cairo runner. + """ + runner.initialize_segments() + end = runner.initialize_main_entrypoint() + runner.initialize_vm(hint_locals=hint_locals) + runner.run_until_pc(end) + runner.end_run() + runner.read_return_values() diff --git a/tools/make/cairo/lang/vm/memory_segments.py b/tools/make/cairo/lang/vm/memory_segments.py new file mode 100644 index 00000000..9c963615 --- /dev/null +++ b/tools/make/cairo/lang/vm/memory_segments.py @@ -0,0 +1,281 @@ +from collections import defaultdict +from typing import Dict, Iterable, List, NamedTuple, Optional, Sequence, Set, Tuple + +from starkware.cairo.lang.compiler.ast.cairo_types import TypeFelt, TypePointer, TypeStruct +from starkware.cairo.lang.vm.memory_dict import MemoryDict +from starkware.cairo.lang.vm.relocatable import MaybeRelocatable, RelocatableValue +from starkware.cairo.lang.vm.vm_exceptions import SecurityError + +FIRST_MEMORY_ADDR = 1 +SEGMENT_SIZE_UPPER_BOUND = 2**64 + + +class MemorySegmentManager: + """ + Manages the list of memory segments, and allows relocating them once their sizes are known. + """ + + def __init__(self, memory: MemoryDict, prime: int): + self.memory = memory + self.prime = prime + # Number of segments. + self.n_segments = 0 + # A map from segment index to its size. + self._segment_sizes: Dict[int, int] = {} + self._segment_used_sizes: Optional[Dict[int, int]] = None + # A map from segment index to a list of pairs (offset, page_id) that constitute the + # public memory. Note that the offset is absolute (not based on the page_id). + self.public_memory_offsets: Dict[int, List[Tuple[int, int]]] = {} + # The number of temporary segments, see 'add_temp_segment' for more details. + self.n_temp_segments = 0 + # A zero segment can be used by any of the builtin runners. Any builtin that needs a zero + # segment can call add_zero_segment in its initialize_segments method, and call + # finalize_zero_segment in its finalize_segments method. Since all builtins are initialized + # together and finalized together in the cairo_runner, they can all use the same zero + # segment. The first builtin to call add_zero_segment will allocate the segment, and the + # first builtin to call finalize_zero_segment will finalize it. + self.zero_segment: Optional[RelocatableValue] = None + self.zero_segment_size = 0 + + def add(self, size: Optional[int] = None) -> RelocatableValue: + """ + Adds a new segment and returns its starting location as a RelocatableValue. + If size is not None the segment is finalized with the given size. + """ + segment_index = self.n_segments + self.n_segments += 1 + if size is not None: + self.finalize(segment_index=segment_index, size=size) + + return RelocatableValue(segment_index=segment_index, offset=0) + + def add_zero_segment(self, size: int) -> RelocatableValue: + if self.zero_segment is None: + self.zero_segment = self.add() + for i in range(self.zero_segment_size, size): + self.memory[self.zero_segment + i] = 0 + self.zero_segment_size = max(self.zero_segment_size, size) + return self.zero_segment + + def add_temp_segment(self) -> RelocatableValue: + """ + Adds a new temporary segment and returns its starting location as a RelocatableValue. + + A temporary segment is a segment that is relocated using memory.add_relocation_rule() + before the Cairo PIE is produced. + """ + + self.n_temp_segments += 1 + # Temporary segments have negative segment indices that start from -1. + segment_index = -self.n_temp_segments + + return RelocatableValue(segment_index=segment_index, offset=0) + + def finalize( + self, + segment_index: int, + size: Optional[int] = None, + public_memory: Sequence[Tuple[int, int]] = [], + ): + """ + Writes the following information for the given segment: + * size - The size of the segment (to be used in relocate_segments). + * public_memory - A list of offsets for memory cells that will be considered as public + memory. + """ + if size is not None: + self._segment_sizes[segment_index] = size + + self.public_memory_offsets[segment_index] = list(public_memory) + + def finalize_zero_segment(self): + if self.zero_segment is not None: + self.finalize(self.zero_segment.segment_index, self.zero_segment_size) + self.zero_segment = None + self.zero_segment_size = 0 + + def compute_effective_sizes(self, allow_tmp_segments: bool = False): + """ + Computes the current used size of the segments, and caches it. + """ + if self._segment_used_sizes is not None: + # segment_sizes is already cached. + return + + assert self.memory.is_frozen(), "Memory has to be frozen before calculating effective size." + + first_segment_index = -self.n_temp_segments if allow_tmp_segments else 0 + self._segment_used_sizes = { + index: 0 for index in range(first_segment_index, self.n_segments) + } + for addr in self.memory: + if not isinstance(addr, RelocatableValue): + raise SecurityError( + f"Expected memory address to be relocatable value. Found: {addr}." + ) + + previous_max_size = self._segment_used_sizes[addr.segment_index] + self._segment_used_sizes[addr.segment_index] = max(previous_max_size, addr.offset + 1) + + def relocate_segments(self) -> Dict[int, int]: + current_addr = FIRST_MEMORY_ADDR + res = {} + + assert ( + self._segment_used_sizes is not None + ), "compute_effective_sizes must be called before relocate_segments." + + for segment_index, used_size in self._segment_used_sizes.items(): + res[segment_index] = current_addr + size = self.get_segment_size(segment_index=segment_index) + assert size >= used_size, f"Segment {segment_index} exceeded its allocated size." + current_addr += size + return res + + def get_public_memory_addresses(self, segment_offsets: Dict[int, int]) -> List[Tuple[int, int]]: + """ + Returns a list of addresses of memory cells that constitute the public memory. + segment_offsets should be the dictionary returned by relocate_segments(). + """ + res = [] + for segment_index in range(self.n_segments): + offsets = self.public_memory_offsets.get(segment_index, []) + segment_start = segment_offsets[segment_index] + for offset, page_id in offsets: + res.append((segment_start + offset, page_id)) + return res + + def initialize_segments_from(self, other: "MemorySegmentManager"): + """ + Adds the segments used by the given MemorySegmentManager. + Note that this function must be called before any segments are added, to make the segment + indices identical. + """ + assert ( + self.n_segments == 0 + ), "initialize_segments_from() must be called before segments are added." + self.n_segments = other.n_segments + + def load_data( + self, ptr: MaybeRelocatable, data: Sequence[MaybeRelocatable] + ) -> MaybeRelocatable: + """ + Writes data into the memory at address ptr and returns the first address after the data. + """ + for i, v in enumerate(data): + self.memory[ptr + i] = v + return ptr + len(data) + + def gen_arg(self, arg, apply_modulo_to_args=True) -> MaybeRelocatable: + """ + Converts args to Cairo-friendly ones. + If an argument is Iterable it is replaced by a pointer to a new segment containing the items + in the Iterable arg (recursively). + If apply_modulo_to_args=True, all the integers are taken modulo the program's prime. + """ + if isinstance(arg, Iterable): + base = self.add() + self.write_arg(base, arg) + return base + if apply_modulo_to_args and isinstance(arg, int): + return arg % self.prime + return arg + + def gen_typed_args(self, args: NamedTuple) -> List[MaybeRelocatable]: + """ + Takes a Cairo typed NamedTuple generated with CairoStructFactory and + returns a Cairo-friendly argument list. + """ + cairo_args = [] + for value, field_type in zip(args, args.__annotations__.values()): + if field_type is TypePointer or field_type is TypeFelt: + # Pointer or felt. + cairo_args.append(self.gen_arg(arg=value)) + elif field_type is TypeStruct: + # Struct. + cairo_args += self.gen_typed_args(args=value) + else: + raise NotImplementedError(f"{field_type.__name__} is not supported.") + + return cairo_args + + def write_arg(self, ptr, arg, apply_modulo_to_args=True): + assert isinstance(arg, Iterable) + data = [self.gen_arg(arg=x, apply_modulo_to_args=apply_modulo_to_args) for x in arg] + return self.load_data(ptr, data) + + def get_memory_holes(self, accessed_addresses: Set[MaybeRelocatable]) -> int: + """ + Returns the total number of memory holes in all segments. + """ + # A map from segment index to the set of accessed offsets. + accessed_offsets_sets: Dict[int, Set] = defaultdict(set) + for addr in accessed_addresses: + assert isinstance( + addr, RelocatableValue + ), f"Expected memory address to be relocatable value. Found: {addr}." + index, offset = addr.segment_index, addr.offset + assert offset >= 0, f"Address offsets must be non-negative. Found: {offset}." + assert offset <= self.get_segment_size(segment_index=index), ( + f"Accessed address {addr} has higher offset than the maximal offset " + f"{self.get_segment_size(segment_index=index)} encountered in the memory segment." + ) + accessed_offsets_sets[index].add(offset) + + assert ( + self._segment_used_sizes is not None + ), "compute_effective_sizes must be called before get_memory_holes." + return sum( + self.get_segment_size(segment_index=index) - len(accessed_offsets_sets[index]) + for index in self._segment_sizes.keys() | self._segment_used_sizes.keys() + ) + + def get_segment_used_size(self, segment_index: int) -> int: + assert ( + self._segment_used_sizes is not None + ), "compute_effective_sizes must be called before get_segment_used_size." + + return self._segment_used_sizes[segment_index] + + def get_segment_size(self, segment_index: int) -> int: + """ + Returns the finalized size of the given segment. If the segment has not been finalized, + returns its used size. + """ + return ( + self._segment_sizes[segment_index] + if segment_index in self._segment_sizes + else self.get_segment_used_size(segment_index=segment_index) + ) + + def is_valid_memory_value(self, value: MaybeRelocatable) -> bool: + assert ( + self._segment_used_sizes is not None + ), "compute_effective_sizes must be called before is_valid_memory_value." + + return is_valid_memory_value(value=value, segment_sizes=self._segment_used_sizes) + + +def is_valid_memory_addr( + addr: MaybeRelocatable, segment_sizes: Dict[int, int], is_concrete_address: bool = True +): + """ + Returns True if addr is a relocatable value, such that its segment index appears in + segment_sizes and its offset is in the valid range (if is_concrete_address=False, offset + may exceed the segment size). + """ + return ( + isinstance(addr, RelocatableValue) + and isinstance(addr.segment_index, int) + and isinstance(addr.offset, int) + and addr.segment_index in segment_sizes + and 0 + <= addr.offset + < (segment_sizes[addr.segment_index] if is_concrete_address else SEGMENT_SIZE_UPPER_BOUND) + ) + + +def is_valid_memory_value(value: MaybeRelocatable, segment_sizes: Dict[int, int]): + return isinstance(value, int) or is_valid_memory_addr( + addr=value, segment_sizes=segment_sizes, is_concrete_address=False + ) diff --git a/tools/make/launch_cairo_files.py b/tools/make/launch_cairo_files.py index 737ceed1..b3746f7b 100755 --- a/tools/make/launch_cairo_files.py +++ b/tools/make/launch_cairo_files.py @@ -138,7 +138,7 @@ def compile_cairo_file(self): self.prompt_for_cairo_file() def construct_run_command(self, compiled_path): - cmd_base = f"cairo-run --program={compiled_path} --layout=starknet" + cmd_base = f"cairo-run --program={compiled_path} --layout=mod_builtin_layout" input_flag = ( f" --program_input={self.json_input_path}" if os.path.exists(self.json_input_path) diff --git a/tools/make/requirements.txt b/tools/make/requirements.txt index 5b4145b2..9679484f 100644 --- a/tools/make/requirements.txt +++ b/tools/make/requirements.txt @@ -1,5 +1,4 @@ -cairo-lang==0.12.2 +cairo-lang==0.13.0 Cython -gmpy2==2.1.5 protobuf==3.20.3 inquirer \ No newline at end of file diff --git a/tools/make/setup.sh b/tools/make/setup.sh index d15c755b..549aebf6 100755 --- a/tools/make/setup.sh +++ b/tools/make/setup.sh @@ -8,7 +8,8 @@ source venv/bin/activate pip install -r tools/make/requirements.txt echo "Patching poseidon_utils.py" patch venv/lib/python3.9/site-packages/starkware/cairo/common/poseidon_utils.py tools/make/poseidon_utils.patch -pip install gmpy2 --target ~/.protostar/dist/protostar +echo "Copying Modulo Builtin files into venv..." +rsync -avh --progress tools/make/cairo/ venv/lib/python3.9/site-packages/starkware/cairo/ if [ "$system" = "Darwin" ]; then brew install llvm @@ -19,4 +20,4 @@ protostar install echo "compiling Gnark..." cd ./tools/gnark go build main.go -echo "All done!" \ No newline at end of file +echo "All done!"