diff --git a/README.md b/README.md index fa89a62b..38d0a0b7 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,11 @@ Run all benchmarks and compare: make build-compare-benchmarks ``` +Run all programs and compare output memory/trace for Zig/Rust cairo-vm: +```bash +make build-compare-output +``` + Run all unit tests with test summary: diff --git a/build.zig b/build.zig index b6accc87..6aac0356 100644 --- a/build.zig +++ b/build.zig @@ -142,6 +142,7 @@ pub fn build(b: *std.Build) void { // Creates a step for unit testing. This only builds the test executable // but does not run it. const unit_tests = b.addTest(.{ + .name = "unit_tests", .root_source_file = b.path("src/tests.zig"), .target = target, .optimize = optimize, @@ -155,6 +156,13 @@ pub fn build(b: *std.Build) void { mod.module, ); + const build_unit_tests = b.addInstallArtifact(unit_tests, .{}); + + // Creating run step for building unit tests + const build_test_step = b.step("build-test", "Build test binary"); + build_test_step.dependOn(&lib.step); + build_test_step.dependOn(&build_unit_tests.step); + const run_unit_tests = b.addRunArtifact(unit_tests); // Similar to creating the run step earlier, this exposes a `test` step to @@ -164,6 +172,7 @@ pub fn build(b: *std.Build) void { "test", "Run unit tests", ); + test_step.dependOn(&lib.step); test_step.dependOn(&run_unit_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 7f6085d4..2af1cd6c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,8 +14,8 @@ .hash = "1220ab73fb7cc11b2308edc3364988e05efcddbcac31b707f55e6216d1b9c0da13f1", }, .starknet = .{ - .url = "https://github.com/StringNick/starknet-zig/archive/7d51aed59982146df3581d3d3320d509b0b10f54.zip", - .hash = "1220b00c055ce40da237598e0f1c8758e3434470be648816fa4701a09f683146d4cd", + .url = "https://github.com/StringNick/starknet-zig/archive/d9e95579ce9f61a8acf42da03d9371af62925a9e.zip", + .hash = "12205f6b98ed2d3b99420d95f94a28d4e2f25f49001aca5db539d6a23e7b1840710e", }, }, } diff --git a/scripts/benchmarks.sh b/scripts/benchmarks.sh index 2e4a0edd..aa91a06e 100644 --- a/scripts/benchmarks.sh +++ b/scripts/benchmarks.sh @@ -14,6 +14,6 @@ for file in $(ls ${BENCH_DIR} | grep .cairo | sed -E 's/\.cairo//'); do cairo-compile --cairo_path="${CAIRO_DIR}:../${CAIRO_DIR}" ${BENCH_DIR}/${file}.cairo --output ${BENCH_DIR}/${file}.json --proof_mode echo "Running ${file} benchmark" hyperfine --show-output --warmup 2 \ - -n "cairo-vm (Rust)" "${CAIRO_VM_CLI} ${BENCH_DIR}/${file}.json --memory_file /dev/null --trace_file /dev/null --layout all_cairo" \ - -n "cairo-vm (Zig)" "${ZIG_CLI} execute --filename ${BENCH_DIR}/${file}.json --enable-trace=true --output-memory=/dev/null --output-trace=/dev/null --layout all_cairo" + -n "cairo-vm (Rust)" "${CAIRO_VM_CLI} ${BENCH_DIR}/${file}.json --memory_file /dev/null --trace_file /dev/null --proof_mode --layout all_cairo" \ + -n "cairo-vm (Zig)" "${ZIG_CLI} execute --filename ${BENCH_DIR}/${file}.json --memory-file=/dev/null --trace-file=/dev/null --proof-mode=true --layout all_cairo" done diff --git a/scripts/test_compare_output.sh b/scripts/test_compare_output.sh index 15d192a1..2cfb48e4 100644 --- a/scripts/test_compare_output.sh +++ b/scripts/test_compare_output.sh @@ -22,6 +22,10 @@ ZIG_MEMORY_OUTPUT="./tmp/zig_memory.tmp" RUST_TRACE_OUTPUT="./tmp/rust_trace.tmp" ZIG_TRACE_OUTPUT="./tmp/zig_trace.tmp" +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +NC='\033[0m' # No Color + trap ctrl_c INT ctrl_c() { @@ -32,24 +36,24 @@ ctrl_c() { mkdir tmp for file in $(ls ${CAIRO_PROGRAMS_DIR} | grep .cairo | sed -E 's/\.cairo//'); do - echo "Compiling ${file} program..." + echo "${NC}Compiling ${file} program..." cairo-compile --cairo_path="${CAIRO_DIR}:" ${CAIRO_PROGRAMS_DIR}/${file}.cairo --output ${CAIRO_PROGRAMS_DIR}/${file}.json --proof_mode echo "Running ${file}" - ${CAIRO_VM_CLI} ${CAIRO_PROGRAMS_DIR}/${file}.json --memory_file $RUST_MEMORY_OUTPUT --trace_file $RUST_TRACE_OUTPUT --layout all_cairo + ${CAIRO_VM_CLI} ${CAIRO_PROGRAMS_DIR}/${file}.json --memory_file $RUST_MEMORY_OUTPUT --trace_file $RUST_TRACE_OUTPUT --proof_mode --layout all_cairo - ${ZIG_CLI} execute --filename ${CAIRO_PROGRAMS_DIR}/${file}.json --enable-trace=true --output-memory=$ZIG_MEMORY_OUTPUT --output-trace=$ZIG_TRACE_OUTPUT --layout all_cairo + ${ZIG_CLI} execute --filename ${CAIRO_PROGRAMS_DIR}/${file}.json --memory-file=$ZIG_MEMORY_OUTPUT --trace-file=$ZIG_TRACE_OUTPUT --proof-mode=true --layout=all_cairo if sameContents $RUST_TRACE_OUTPUT $ZIG_TRACE_OUTPUT; then - echo "Rust & Zig output trace is same" + echo "${Green}Rust & Zig output trace is same" else - echo "Zig have different output trace" + echo "${Red}Zig have different output trace" fi if sameContents $RUST_MEMORY_OUTPUT $ZIG_MEMORY_OUTPUT; then - echo "Rust & Zig memory output is same" + echo "${Green}Rust & Zig memory output is same" else - echo "Zig have different output memory" + echo "${Red}Zig have different output memory" fi done diff --git a/src/build_options.zig b/src/build_options.zig deleted file mode 100644 index cdda1b98..00000000 --- a/src/build_options.zig +++ /dev/null @@ -1,7 +0,0 @@ -/// Whether tracing should be disabled globally. This prevents the -/// user from enabling tracing via the command line but it might -/// improve performance slightly. -pub const trace_disable = false; -/// The initial capacity of the buffer responsible for gathering execution trace -/// data. -pub const trace_initial_capacity: usize = 4096; diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index ac696dc0..28ceb9b1 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -9,21 +9,36 @@ const vm_core = @import("../vm/core.zig"); const RunContext = @import("../vm/run_context.zig").RunContext; const relocatable = @import("../vm/memory/relocatable.zig"); const Config = @import("../vm/config.zig").Config; -const build_options = @import("../build_options.zig"); const cairo_runner = @import("../vm/runners/cairo_runner.zig"); const CairoRunner = cairo_runner.CairoRunner; const ProgramJson = @import("../vm/types/programjson.zig").ProgramJson; const cairo_run = @import("../vm/cairo_run.zig"); +const layout_lib = @import("../vm/types/layout.zig"); +const errors = @import("../vm/error.zig"); // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const global_allocator = std.heap.c_allocator; // Configuration settings for the CLI. -var config = Config{ - .proof_mode = false, - .enable_trace = false, +const Args = struct { + filename: []const u8 = undefined, + trace_file: ?[]const u8 = null, + print_output: bool = false, + entrypoint: []const u8 = "main", + memory_file: ?[]const u8 = null, + layout: []const u8 = layout_lib.LayoutName.plain.toString(), + proof_mode: bool = false, + secure_run: ?bool = null, + // require proof_mode=true + air_public_input: ?[]const u8 = null, + // requires proof_mode, trace_file, memory_file + air_private_input: ?[]const u8 = null, + + allow_missing_builtins: ?bool = null, }; +var cfg: Args = .{}; + /// Runs the Command-Line Interface application. /// /// This function initializes and executes the CLI app with the provided configuration @@ -38,15 +53,25 @@ pub fn run() !void { var r = try cli.AppRunner.init(global_allocator); // Command-line option for enabling proof mode. - const execute_proof_mode_option = cli.Option{ + const proof_mode = cli.Option{ // The full name of the option. .long_name = "proof-mode", // Description of the option's purpose. .help = "Whether to run in proof mode or not.", - // Short alias for the option. - .short_alias = 'p', // Reference to the proof mode configuration. - .value_ref = r.mkRef(&config.proof_mode), + .value_ref = r.mkRef(&cfg.proof_mode), + // Indicates if the option is required. + .required = false, + }; + + // Command-line option for enabling secure run. + const secure_run = cli.Option{ + // The full name of the option. + .long_name = "secure-run", + // Description of the option's purpose. + .help = "Whether to run in secure mode or not.", + // Reference to the proof mode configuration. + .value_ref = r.mkRef(&cfg.secure_run), // Indicates if the option is required. .required = false, }; @@ -57,36 +82,32 @@ pub fn run() !void { .long_name = "filename", // Description of the option's purpose. .help = "The location of the program to be evaluated.", - // Short alias for the option. - .short_alias = 'f', // Reference to the program filename. - .value_ref = r.mkRef(&config.filename), + .value_ref = r.mkRef(&cfg.filename), // Indicates if the option is required. .required = true, }; - // Command-line option for enabling trace mode. - const enable_trace = cli.Option{ + // Command-line option for specifying the filename. + const air_input_public = cli.Option{ // The full name of the option. - .long_name = "enable-trace", + .long_name = "air-input-public", // Description of the option's purpose. - .help = "Enable trace mode.", - // Short alias for the option. - .short_alias = 't', - // Reference to the trace mode configuration. - .value_ref = r.mkRef(&config.enable_trace), + .help = "The location where we wrote air input public.", + // Reference to the program filename. + .value_ref = r.mkRef(&cfg.air_public_input), // Indicates if the option is required. .required = false, }; - // Command-line option for enabling trace mode. - const print_output = cli.Option{ + // Command-line option for specifying the filename. + const air_input_private = cli.Option{ // The full name of the option. - .long_name = "print-output", + .long_name = "air-input-private", // Description of the option's purpose. - .help = "Print output from Output runner", - // Reference to the trace mode configuration. - .value_ref = r.mkRef(&config.print_output), + .help = "The location where we wrote air input private.", + // Reference to the program filename. + .value_ref = r.mkRef(&cfg.air_private_input), // Indicates if the option is required. .required = false, }; @@ -94,27 +115,49 @@ pub fn run() !void { // Command-line option for specifying the output trace file. const output_trace = cli.Option{ // The full name of the option. - .long_name = "output-trace", + .long_name = "trace-file", // Description of the option's purpose. .help = "File where the register execution cycles are written.", // Short alias for the option. .short_alias = 'o', // Reference to the output trace file. - .value_ref = r.mkRef(&config.output_trace), + .value_ref = r.mkRef(&cfg.trace_file), + // Indicates if the option is required. + .required = false, + }; + + // Command-line option for specifying print output or not. + const print_output = cli.Option{ + // The full name of the option. + .long_name = "print-output", + // Description of the option's purpose. + .help = "Do we need to print output or not", + // Reference to the output trace file. + .value_ref = r.mkRef(&cfg.print_output), + // Indicates if the option is required. + .required = false, + }; + + // Command-line option for specifying entrypoint of program. + const entrypoint = cli.Option{ + // The full name of the option. + .long_name = "entrypoint", + // Description of the option's purpose. + .help = "Specifying entrypoint of program", + // Reference to the output trace file. + .value_ref = r.mkRef(&cfg.entrypoint), // Indicates if the option is required. .required = false, }; // Command-line option for specifying the output memory file. - const output_memory = cli.Option{ + const memory_file = cli.Option{ // The full name of the option. - .long_name = "output-memory", + .long_name = "memory-file", // Description of the option's purpose. .help = "File where the memory post-execution is written.", - // Short alias for the option. - .short_alias = 'm', // Reference to the output memory file. - .value_ref = r.mkRef(&config.output_memory), + .value_ref = r.mkRef(&cfg.memory_file), // Indicates if the option is required. .required = false, }; @@ -125,10 +168,8 @@ pub fn run() !void { .long_name = "layout", // Description of the option's purpose. .help = "The memory layout to use.", - // Short alias for the option. - .short_alias = 'l', // Reference to the memory layout. - .value_ref = r.mkRef(&config.layout), + .value_ref = r.mkRef(&cfg.layout), // Indicates if the option is required. .required = false, }; @@ -167,13 +208,16 @@ pub fn run() !void { }, // Options for the subcommand. .options = &.{ - execute_proof_mode_option, + proof_mode, layout, - enable_trace, program_option, output_trace, - output_memory, + secure_run, + memory_file, + air_input_public, + air_input_private, print_output, + entrypoint, }, // Action to be executed for the subcommand. .target = .{ .action = .{ .exec = execute } }, @@ -204,13 +248,135 @@ const UsageError = error{ /// Returns a `UsageError` if there's a misuse of the CLI, specifically if tracing is attempted /// while it's disabled in the build. fn execute() anyerror!void { - var arena = std.heap.ArenaAllocator.init(global_allocator); + try runProgram(global_allocator, cfg); +} + +fn runProgram(allocator: std.mem.Allocator, _cfg: Args) !void { + var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - if (build_options.trace_disable and config.enable_trace) { - std.log.err("Tracing is disabled in this build.\n", .{}); - return UsageError.IncompatibleBuildOptions; + const trace_enabled = _cfg.trace_file != null or _cfg.air_public_input != null; + + const file = try std.fs.cwd().openFile(_cfg.filename, .{}); + defer file.close(); + + // Read the entire file content into a buffer using the provided allocator + const buffer = try file.readToEndAlloc( + arena.allocator(), + try file.getEndPos(), + ); + defer arena.allocator().free(buffer); + + var runner = try cairo_run.cairoRun( + arena.allocator(), + buffer, + .{ + .entrypoint = _cfg.entrypoint, + .trace_enabled = trace_enabled, + .relocate_mem = _cfg.memory_file != null or _cfg.air_public_input != null, + .layout = _cfg.layout, + .proof_mode = _cfg.proof_mode, + .secure_run = _cfg.secure_run, + .allow_missing_builtins = _cfg.allow_missing_builtins, + }, + @constCast(&.{}), + ); + defer runner.deinit(arena.allocator()); + defer runner.vm.segments.memory.deinitData(arena.allocator()); + + if (_cfg.print_output) { + var output_buffer = try std.ArrayList(u8).initCapacity(arena.allocator(), 100); + output_buffer.appendSliceAssumeCapacity("Program Output:\n"); + + try runner.vm.writeOutput(output_buffer.writer()); } - try cairo_run.runConfig(arena.allocator(), config); + var writer: std.io.BufferedWriter(5 * 1024 * 1024, std.fs.File.Writer) = .{ .unbuffered_writer = undefined }; + + if (_cfg.trace_file) |trace_path| { + const relocated_trace = runner.relocated_trace orelse return errors.TraceError.TraceNotRelocated; + + const trace_file = try std.fs.cwd().createFile(trace_path, .{}); + defer trace_file.close(); + + writer.unbuffered_writer = trace_file.writer(); + + try cairo_run.writeEncodedTrace(relocated_trace, &writer); + + try writer.flush(); + } + + if (_cfg.memory_file) |memory_path| { + const memory_file = try std.fs.cwd().createFile(memory_path, .{}); + defer memory_file.close(); + + writer.unbuffered_writer = memory_file.writer(); + + try cairo_run.writeEncodedMemory(runner.relocated_memory.items, &writer); + + try writer.flush(); + } + + if (_cfg.air_public_input) |file_path| { + var public_input = try runner.getAirPublicInput(); + defer public_input.deinit(); + + const public_input_json = try public_input.serialize(arena.allocator()); + defer arena.allocator().free(public_input_json); + + var air_file = try std.fs.cwd().createFile(file_path, .{}); + defer air_file.close(); + + try air_file.writeAll(public_input_json); + } +} + +test "RunOK" { + for ([_][]const u8{ + "plain", + // "small", + // "dex", + // "starknet", + // "starknet_with_keccak", + // "recursive_large_output", + "all_cairo", + // "all_solidity", + }) |layout| { + inline for ([_]bool{ false, true }) |memory_file| { + inline for ([_]bool{ false, true }) |_trace_file| { + inline for ([_]bool{ false, true }) |proof_mode| { + inline for ([_]bool{ false, true }) |print_out| { + inline for ([_]bool{ false, true }) |entrypoint| { + inline for ([_]bool{ false, true }) |air_input_public| { + var trace_file = _trace_file; + + var _cfg: Args = + .{ + .layout = layout, + .air_public_input = if (air_input_public) "/dev/null" else null, + .proof_mode = if (proof_mode) val: { + trace_file = true; + break :val true; + } else false, + .memory_file = if (memory_file) "/dev/null" else null, + .trace_file = if (trace_file) "/dev/null" else null, + .print_output = print_out, + .filename = "cairo_programs/proof_programs/fibonacci.json", + }; + + if (entrypoint) _cfg.entrypoint = "main"; + + runProgram( + std.testing.allocator, + _cfg, + ) catch |err| { + if (air_input_public and !proof_mode) return {} else return err; + }; + } + } + } + } + } + } + } } diff --git a/src/hint_processor/ec_utils.zig b/src/hint_processor/ec_utils.zig index 55018516..e822b682 100644 --- a/src/hint_processor/ec_utils.zig +++ b/src/hint_processor/ec_utils.zig @@ -118,13 +118,11 @@ fn randomEcPointSeeded(allocator: std.mem.Allocator, seed_bytes: []const u8) !st const x = std.mem.readInt(u256, &hash_buffer, .big); - // const y_coef = std.math.pow(i32, -1, seed[0] & 1); - // Calculate y if (recoverY(Felt252.fromInt(u256, x))) |y| { return .{ Felt252.fromInt(u256, x), - y, + if (seed[0] & 1 == 1) y.neg() else y, }; } } diff --git a/src/lib.zig b/src/lib.zig index dbca3be0..f2b3b5c0 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -7,6 +7,8 @@ pub const vm = struct { pub usingnamespace @import("vm/instructions.zig"); pub usingnamespace @import("vm/run_context.zig"); pub usingnamespace @import("vm/trace_context.zig"); + pub usingnamespace @import("vm/security.zig"); + pub usingnamespace @import("vm/air_input_public.zig"); pub usingnamespace @import("vm/types/program.zig"); pub usingnamespace @import("vm/types/programjson.zig"); pub usingnamespace @import("vm/types/pedersen_instance_def.zig"); @@ -47,8 +49,6 @@ pub const utils = struct { pub usingnamespace @import("utils/time.zig"); }; -pub const build_options = @import("build_options.zig"); - pub const hint_processor = struct { pub usingnamespace @import("hint_processor/hint_processor_def.zig"); pub usingnamespace @import("hint_processor/hint_processor_utils.zig"); diff --git a/src/tests.zig b/src/tests.zig index 2e24a01b..55d20936 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -1,8 +1,10 @@ const std = @import("std"); const lib = @import("lib.zig"); +const cmd = @import("cmd/cmd.zig"); // Automatically run all tests in files declared in the `lib.zig` file. test { std.testing.log_level = std.log.Level.err; std.testing.refAllDecls(lib); + std.testing.refAllDecls(cmd); } diff --git a/src/vm/air_input_public.zig b/src/vm/air_input_public.zig new file mode 100644 index 00000000..46770677 --- /dev/null +++ b/src/vm/air_input_public.zig @@ -0,0 +1,268 @@ +const std = @import("std"); +const Felt252 = @import("starknet").fields.Felt252; +const RelocatedTraceEntry = @import("trace_context.zig").RelocatedTraceEntry; +const HintProcessor = @import("../hint_processor/hint_processor_def.zig").CairoVMHintProcessor; + +const Config = @import("config.zig").Config; +const cairo_run = @import("cairo_run.zig"); +const cairo_runner = @import("runners/cairo_runner.zig"); + +const errors = @import("error.zig"); + +pub const PublicInput = struct { + const Self = @This(); + + layout: []const u8, + rc_min: isize, + rc_max: isize, + n_steps: usize, + + // we use struct here only for json serialization/deserialization abstraction + memory_segments: struct { + value: std.StringHashMap(MemorySegmentAddresses), + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + if (.object_begin != try source.next()) return error.UnexpectedToken; + var result = std.StringHashMap(MemorySegmentAddresses).init(allocator); + errdefer result.deinit(); + + while (true) { + const name_token: ?std.json.Token = try source.nextAllocMax(allocator, .alloc_if_needed, options.max_value_len.?); + const field_name = switch (name_token.?) { + inline .string, .allocated_string => |slice| slice, + .object_end => { // No more fields. + break; + }, + else => { + return error.UnexpectedToken; + }, + }; + + try result.put(field_name, try std.json.innerParse(MemorySegmentAddresses, allocator, source, options)); + } + + return .{ .value = result }; + } + + pub fn jsonStringify(self: @This(), out: anytype) !void { + try out.beginObject(); + var it = self.value.iterator(); + while (it.next()) |x| { + try out.objectField(x.key_ptr.*); + try out.write(x.value_ptr.*); + } + try out.endObject(); + } + }, + + public_memory: struct { + value: std.ArrayList(PublicMemoryEntry), + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + if (.array_begin != try source.next()) return error.UnexpectedToken; + var result = std.ArrayList(PublicMemoryEntry).init(allocator); + errdefer result.deinit(); + + while (true) { + switch (try source.peekNextTokenType()) { + inline .object_begin => try result.append(try std.json.innerParse(PublicMemoryEntry, allocator, source, options)), + inline .array_end => { + _ = try source.next(); + return .{ .value = result }; + }, + else => return error.UnexpectedToken, + } + } + + return .{ .value = result }; + } + + pub fn jsonStringify(self: @This(), out: anytype) !void { + try out.beginArray(); + for (self.value.items) |x| try out.write(x); + try out.endArray(); + } + }, + + // serializing PublicInput to json + pub fn serialize(self: *Self, allocator: std.mem.Allocator) ![]const u8 { + var buffer = std.ArrayList(u8).init(allocator); + errdefer buffer.deinit(); + + try std.json.stringify(self, .{}, buffer.writer()); + + return try buffer.toOwnedSlice(); + } + + pub fn deserialize(allocator: std.mem.Allocator, data: []const u8) !std.json.Parsed(Self) { + const parsed = try std.json.parseFromSlice( + Self, + allocator, + data, + .{ + // Always allocate memory during parsing + .allocate = .alloc_always, + }, + ); + + // Return the parsed `ProgramJson` instance + return parsed; + } + + pub fn deinit(self: *Self) void { + self.memory_segments.value.deinit(); + self.public_memory.value.deinit(); + } + + // new - creating new PublicInput, all arguments caller is owner + pub fn new( + allocator: std.mem.Allocator, + memory: []const ?Felt252, + layout: []const u8, + public_memory_addresses: []const std.meta.Tuple(&.{ usize, usize }), + memory_segment_addresses: std.StringHashMap(std.meta.Tuple(&.{ usize, usize })), + trace: []const RelocatedTraceEntry, + rc_limits: std.meta.Tuple(&.{ isize, isize }), + ) !Self { + const memory_entry = (struct { + fn func(mem: []const ?Felt252, addresses: std.meta.Tuple(&.{ usize, usize })) !PublicMemoryEntry { + const address, const page = addresses; + return .{ + .address = address, + .page = page, + .value = .{ .value = if (mem.len <= address) return errors.PublicInputError.MemoryNotFound else mem[address] }, + }; + } + }).func; + + var public_memory = try std.ArrayList(PublicMemoryEntry).initCapacity(allocator, public_memory_addresses.len); + errdefer public_memory.deinit(); + + for (public_memory_addresses) |maddr| public_memory.appendAssumeCapacity(try memory_entry(memory, maddr)); + + const rc_min, const rc_max = rc_limits; + + if (trace.len < 2) return errors.PublicInputError.EmptyTrace; + + const trace_first = trace[0]; + const trace_last = trace[trace.len - 1]; + + return .{ + .layout = layout, + .rc_min = rc_min, + .rc_max = rc_max, + .n_steps = trace.len, + .memory_segments = .{ .value = blk: { + var msa = std.StringHashMap(MemorySegmentAddresses).init(allocator); + errdefer msa.deinit(); + + var it = memory_segment_addresses.iterator(); + while (it.next()) |x| { + const begin_addr, const stop_ptr = x.value_ptr.*; + + try msa.put(x.key_ptr.*, .{ + .begin_addr = begin_addr, + .stop_ptr = stop_ptr, + }); + } + + try msa.put("program", .{ .begin_addr = trace_first.pc, .stop_ptr = trace_last.pc }); + + try msa.put("execution", .{ .begin_addr = trace_first.ap, .stop_ptr = trace_last.ap }); + + break :blk msa; + } }, + .public_memory = .{ .value = public_memory }, + }; + } +}; + +pub const PublicMemoryEntry = struct { + address: usize, + value: struct { + /// using struct only for json parse abstraction + value: ?Felt252, + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + _ = allocator; // autofix + _ = options; // autofix + switch (try source.next()) { + inline .string => |data| { + const val = try std.fmt.parseInt(u256, data, 0); + + return .{ .value = Felt252.fromInt(u256, val) }; + }, + inline .null => return .{ + .value = null, + }, + else => return error.UnexpectedToken, + } + } + + pub fn jsonStringify(self: @This(), out: anytype) !void { + if (self.value) |v| try out.print("\"0x{x}\"", .{v.toU256()}) else try out.write(null); + } + }, + page: usize, +}; + +pub const MemorySegmentAddresses = struct { + begin_addr: usize, + stop_ptr: usize, +}; + +test "AirInputPublic" { + const serialize_and_deserialize_air_input_public = (struct { + fn func(program_content: []const u8) !void { + const cfg = cairo_run.CairoRunConfig{ + .proof_mode = true, + .relocate_mem = true, + .trace_enabled = true, + .layout = "all_cairo", + }; + + var processor: HintProcessor = .{}; + + var runner = try cairo_run.cairoRun(std.testing.allocator, program_content, cfg, &processor); + + defer std.testing.allocator.destroy(runner.vm); + defer runner.deinit(std.testing.allocator); + defer runner.vm.segments.memory.deinitData(std.testing.allocator); + + var public_input = try runner.getAirPublicInput(); + defer public_input.deinit(); + + const public_input_json = try public_input.serialize(std.testing.allocator); + defer std.testing.allocator.free(public_input_json); + + var deserialized_public_input = try PublicInput.deserialize(std.testing.allocator, public_input_json); + + defer deserialized_public_input.deinit(); + defer deserialized_public_input.value.deinit(); + + try std.testing.expectEqualSlices(u8, public_input.layout, deserialized_public_input.value.layout); + try std.testing.expectEqual(public_input.rc_max, deserialized_public_input.value.rc_max); + try std.testing.expectEqual(public_input.rc_min, deserialized_public_input.value.rc_min); + try std.testing.expectEqual(public_input.n_steps, deserialized_public_input.value.n_steps); + + try std.testing.expectEqualSlices(PublicMemoryEntry, public_input.public_memory.value.items, deserialized_public_input.value.public_memory.value.items); + + try std.testing.expectEqual(public_input.memory_segments.value.count(), deserialized_public_input.value.memory_segments.value.count()); + + var it = public_input.memory_segments.value.iterator(); + while (it.next()) |kv| { + try std.testing.expect(deserialized_public_input.value.memory_segments.value.get(kv.key_ptr.*) != null); + try std.testing.expectEqual(kv.value_ptr.*, deserialized_public_input.value.memory_segments.value.get(kv.key_ptr.*).?); + } + } + }).func; + + const file = try std.fs.cwd().openFile("cairo_programs/proof_programs/fibonacci.json", .{ .mode = .read_only }); + + const stat = try file.stat(); + + const data = try file.readToEndAlloc(std.testing.allocator, stat.size); + defer std.testing.allocator.free(data); + + try serialize_and_deserialize_air_input_public(data); +} diff --git a/src/vm/builtins/builtin_runner/builtin_runner.zig b/src/vm/builtins/builtin_runner/builtin_runner.zig index 2654cd87..198f8fa5 100644 --- a/src/vm/builtins/builtin_runner/builtin_runner.zig +++ b/src/vm/builtins/builtin_runner/builtin_runner.zig @@ -24,6 +24,8 @@ const EcdsaInstanceDef = @import("../../types/ecdsa_instance_def.zig").EcdsaInst const BitwiseInstanceDef = @import("../../types/bitwise_instance_def.zig").BitwiseInstanceDef; const EcOpInstanceDef = @import("../../types/ec_op_instance_def.zig").EcOpInstanceDef; +const programjson = @import("../../types/programjson.zig"); + const ArrayList = std.ArrayList; const expectError = std.testing.expectError; @@ -41,6 +43,9 @@ pub const HASH_BUILTIN_NAME = "pedersen_builtin"; /// The name of the range check builtin. pub const RANGE_CHECK_BUILTIN_NAME = "range_check_builtin"; +/// The name of the range check 96 builtin. +pub const RANGE_CHECK96_BUILTIN_NAME = "range_check96_builtin"; + /// The name of the ECDSA signature verification builtin. pub const SIGNATURE_BUILTIN_NAME = "ecdsa_builtin"; @@ -70,6 +75,8 @@ pub const BuiltinName = enum { Output, /// Range Check built-in runner for range check operations. RangeCheck, + /// Range Check 96 built-in runner for range check operations. + RangeCheck96, /// Keccak built-in runner for Keccak operations. Keccak, /// Signature built-in runner for signature operations. @@ -78,6 +85,21 @@ pub const BuiltinName = enum { Poseidon, /// Segment Arena built-in runner for segment arena operations. SegmentArena, + + pub fn fromProgramJsonName(name: programjson.BuiltinName) BuiltinName { + return switch (name) { + inline .output => .Output, + inline .range_check => .RangeCheck, + inline .pedersen => .Hash, + inline .ecdsa => .Signature, + inline .keccak => .Keccak, + inline .bitwise => .Bitwise, + inline .ec_op => .EcOp, + inline .poseidon => .Poseidon, + inline .segment_arena => .SegmentArena, + inline .range_check96 => .RangeCheck96, + }; + } }; /// Built-in runner @@ -94,6 +116,8 @@ pub const BuiltinRunner = union(BuiltinName) { Output: OutputBuiltinRunner, /// Range Check built-in runner for range check operations. RangeCheck: RangeCheckBuiltinRunner, + /// Range Check built-in runner for range check operations. + RangeCheck96: RangeCheckBuiltinRunner, /// Keccak built-in runner for Keccak operations. Keccak: KeccakBuiltinRunner, /// Signature built-in runner for signature operations. @@ -533,7 +557,7 @@ pub const BuiltinRunner = union(BuiltinName) { /// # Returns /// /// A null-terminated byte slice representing the name of the built-in runner. - pub fn name(self: *Self) []const u8 { + pub fn name(self: *const Self) []const u8 { return switch (self.*) { .Bitwise => BITWISE_BUILTIN_NAME, .EcOp => EC_OP_BUILTIN_NAME, @@ -544,6 +568,7 @@ pub const BuiltinRunner = union(BuiltinName) { .Signature => SIGNATURE_BUILTIN_NAME, .Poseidon => POSEIDON_BUILTIN_NAME, .SegmentArena => SEGMENT_ARENA_BUILTIN_NAME, + .RangeCheck96 => RANGE_CHECK96_BUILTIN_NAME, }; } @@ -633,6 +658,77 @@ pub const BuiltinRunner = union(BuiltinName) { } } + pub fn runSecurityChecks(self: *Self, allocator: std.mem.Allocator, vm: *CairoVM) !void { + switch (self.*) { + inline .Output, .SegmentArena => return, + else => {}, + } + + // if let BuiltinRunner::Mod(modulo) = self { + // modulo.run_additional_security_checks(vm)?; + // } + + const cells_per_instance: usize = self.cellsPerInstance(); + const n_input_cells: usize = self.getNumberInputCells(); + const builtin_segment_index = self.base(); + // If the builtin's segment is empty, there are no security checks to run + + if (vm.segments.memory.data.items.len <= builtin_segment_index or vm.segments.memory.data.items[builtin_segment_index].items.len == 0) return; + + const builtin_segment = vm.segments.memory.data.items[builtin_segment_index].items; + + // The builtin segment's size - 1 is the maximum offset within the segment's addresses + // Assumption: The last element is not a None value + // It is safe to asume this for normal program execution + // If there are trailing None values at the end, the following security checks will fail + + const offset_max = builtin_segment.len -| 1; + // offset_len is the amount of non-None values in the segment + + const offset_len = blk: { + var counter: usize = 0; + for (builtin_segment) |cell| { + if (cell.isSome()) counter += 1; + } + break :blk counter; + }; + + const n = if (offset_len == 0) 0 else @divFloor(offset_max, cells_per_instance) + 1; + + // Verify that n is not too large to make sure the expected_offsets set that is constructed + // below is not too large. + if (n > @divFloor(offset_len, n_input_cells)) + return MemoryError.MissingMemoryCells; + + // Check that the two inputs (x and y) of each instance are set. + var missing_offsets = try std.ArrayList(usize).initCapacity(allocator, n); + defer missing_offsets.deinit(); + + // Check for missing expected offsets (either their address is no present, or their value is None) + for (0..n) |i| { + for (0..n_input_cells) |j| { + const offset = cells_per_instance * i + j; + if (builtin_segment.len <= offset or builtin_segment[offset].isNone()) { + try missing_offsets.append(offset); + } + } + } + + if (missing_offsets.items.len != 0) + return MemoryError.MissingMemoryCellsWithOffsets; + + // Verify auto deduction rules for the unasigned output cells + // Assigned output cells are checked as part of the call to verify_auto_deductions(). + for (0..n) |i| { + for (n_input_cells..cells_per_instance) |j| { + const offset = cells_per_instance * i + j; + if (builtin_segment.len <= offset or builtin_segment[offset].isNone()) { + try vm.verifyAutoDeductionsForAddr(allocator, Relocatable.init(@intCast(builtin_segment_index), offset), self); + } + } + } + } + /// Deinitializes the built-in runner. /// /// This method is used to deinitialize the specific type of built-in runner, @@ -1943,3 +2039,364 @@ test "BuiltinRunner: getMemorySegmentAddresses after set stop ptr" { try expectEqual(ptr, builtin.getMemorySegmentAddresses()[1]); } } + +test "BuiltinRunner: runSecurityChecks for output" { + var builtin = BuiltinRunner{ .Output = OutputBuiltinRunner.init(std.testing.allocator, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try builtin.runSecurityChecks(std.testing.allocator, &vm); +} + +test "BuiltinRunner: runSecurityChecks empty memory" { + const instance_def = BitwiseInstanceDef.init(256); + var builtin = BuiltinRunner{ .Bitwise = BitwiseBuiltinRunner.init(&instance_def, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try builtin.runSecurityChecks(std.testing.allocator, &vm); +} + +test "BuiltinRunner: runSecurityChecks empty offsets" { + const instance_def = BitwiseInstanceDef.init(256); + var builtin = BuiltinRunner{ .Bitwise = BitwiseBuiltinRunner.init(&instance_def, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + // vm.segments.memory.setUpMemory(std.testing.allocator, .{}); + + try builtin.runSecurityChecks(std.testing.allocator, &vm); +} + +test "BuiltinRunner: runSecurityChecks bitwise missing memory cells with offsets" { + const instance_def = BitwiseInstanceDef.init(256); + var builtin = BuiltinRunner{ .Bitwise = BitwiseBuiltinRunner.init(&instance_def, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 1 }, .{ 0, 1 } }, + .{ .{ 0, 2 }, .{ 0, 2 } }, + .{ .{ 0, 3 }, .{ 0, 3 } }, + .{ .{ 0, 4 }, .{ 0, 4 } }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + // vm.segments.memory.setUpMemory(std.testing.allocator, .{}); + + try std.testing.expectError( + MemoryError.MissingMemoryCellsWithOffsets, + builtin.runSecurityChecks(std.testing.allocator, &vm), + ); +} + +test "BuiltinRunner: runSecurityChecks bitwise missing memory cells" { + const instance_def = BitwiseInstanceDef.init(256); + var builtin = BuiltinRunner{ .Bitwise = BitwiseBuiltinRunner.init(&instance_def, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 4 }, .{ 0, 5 } }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + // vm.segments.memory.setUpMemory(std.testing.allocator, .{}); + + try std.testing.expectError( + MemoryError.MissingMemoryCells, + builtin.runSecurityChecks(std.testing.allocator, &vm), + ); +} + +test "BuiltinRunner: runSecurityChecks hash missing memory cells with offsets" { + var builtin = BuiltinRunner{ .Hash = HashBuiltinRunner.init(std.testing.allocator, 8, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 1 }, .{ 0, 1 } }, + .{ .{ 0, 2 }, .{ 0, 2 } }, + .{ .{ 0, 3 }, .{ 0, 3 } }, + .{ .{ 0, 4 }, .{ 0, 4 } }, + .{ .{ 0, 5 }, .{ 0, 5 } }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + // vm.segments.memory.setUpMemory(std.testing.allocator, .{}); + + try std.testing.expectError( + MemoryError.MissingMemoryCellsWithOffsets, + builtin.runSecurityChecks(std.testing.allocator, &vm), + ); +} + +test "BuiltinRunner: runSecurityChecks hash missing memory cells" { + var builtin = BuiltinRunner{ .Hash = HashBuiltinRunner.init(std.testing.allocator, 8, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 0 }, .{ 0, 0 } }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + // vm.segments.memory.setUpMemory(std.testing.allocator, .{}); + + try std.testing.expectError( + MemoryError.MissingMemoryCells, + builtin.runSecurityChecks(std.testing.allocator, &vm), + ); +} + +test "BuiltinRunner: runSecurityChecks rangeCheck missing memory cells with offsets" { + var builtin = BuiltinRunner{ .RangeCheck = RangeCheckBuiltinRunner.init(8, 8, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 1 }, .{100} }, + .{ .{ 0, 2 }, .{2} }, + .{ .{ 0, 3 }, .{3} }, + .{ .{ 0, 5 }, .{5} }, + .{ .{ 0, 6 }, .{17} }, + .{ .{ 0, 7 }, .{22} }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + // vm.segments.memory.setUpMemory(std.testing.allocator, .{}); + + try std.testing.expectError( + MemoryError.MissingMemoryCells, + builtin.runSecurityChecks(std.testing.allocator, &vm), + ); +} + +test "BuiltinRunner: runSecurityChecks rangeCheck missing memory cells" { + var builtin = BuiltinRunner{ .RangeCheck = RangeCheckBuiltinRunner.init(8, 8, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 1 }, .{1} }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + // vm.segments.memory.setUpMemory(std.testing.allocator, .{}); + + try std.testing.expectError( + MemoryError.MissingMemoryCells, + builtin.runSecurityChecks(std.testing.allocator, &vm), + ); +} + +test "BuiltinRunner: runSecurityChecks rangeCheck check empty" { + const MemoryCell = @import("../../memory/memory.zig").MemoryCell; + var builtin = BuiltinRunner{ .RangeCheck = RangeCheckBuiltinRunner.init(8, 8, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + defer vm.segments.memory.deinitData(std.testing.allocator); + + _ = try vm.segments.addSegment(); + + try vm.segments.memory.data.items[0].appendSlice(std.testing.allocator, &.{ + MemoryCell.NONE, + MemoryCell.NONE, + MemoryCell.NONE, + }); + + try builtin.runSecurityChecks(std.testing.allocator, &vm); +} + +test "BuiltinRunner: runSecurityChecks validate auto deductions" { + const instance_def = BitwiseInstanceDef.init(256); + var builtin = BuiltinRunner{ .Bitwise = BitwiseBuiltinRunner.init(&instance_def, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.validated_addresses.addAddresses(&.{.{ .segment_index = 0, .offset = 2 }}); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 0 }, .{ 0, 0 } }, + .{ .{ 0, 1 }, .{ 0, 1 } }, + .{ .{ 0, 2 }, .{ 0, 2 } }, + .{ .{ 0, 3 }, .{ 0, 3 } }, + .{ .{ 0, 4 }, .{ 0, 4 } }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + try builtin.runSecurityChecks(std.testing.allocator, &vm); +} + +test "BuiltinRunner: runSecurityChecks ecOp check memory empty" { + var builtin = BuiltinRunner{ .EcOp = EcOpBuiltinRunner.init(std.testing.allocator, .{ .ratio = 256 }, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try builtin.runSecurityChecks(std.testing.allocator, &vm); +} + +test "BuiltinRunner: runSecurityChecks ecOp check memory 1 element" { + var builtin = BuiltinRunner{ .EcOp = EcOpBuiltinRunner.init(std.testing.allocator, .{ .ratio = 256 }, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 0 }, .{0} }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + try std.testing.expectError(MemoryError.MissingMemoryCells, builtin.runSecurityChecks(std.testing.allocator, &vm)); +} + +test "BuiltinRunner: runSecurityChecks ecOp check memory 3 element" { + var builtin = BuiltinRunner{ .EcOp = EcOpBuiltinRunner.init(std.testing.allocator, .{ .ratio = 256 }, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 0 }, .{0} }, + .{ .{ 0, 1 }, .{0} }, + .{ .{ 0, 2 }, .{0} }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + try std.testing.expectError(MemoryError.MissingMemoryCells, builtin.runSecurityChecks(std.testing.allocator, &vm)); +} + +test "BuiltinRunner: runSecurityChecks ecOp missing memory cells with offsets" { + var builtin = BuiltinRunner{ .EcOp = EcOpBuiltinRunner.init(std.testing.allocator, .{ .ratio = 256 }, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 1 }, .{ 0, 1 } }, + .{ .{ 0, 2 }, .{ 0, 2 } }, + .{ .{ 0, 3 }, .{ 0, 3 } }, + .{ .{ 0, 4 }, .{ 0, 4 } }, + .{ .{ 0, 5 }, .{ 0, 5 } }, + .{ .{ 0, 6 }, .{ 0, 6 } }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + try std.testing.expectError(MemoryError.MissingMemoryCellsWithOffsets, builtin.runSecurityChecks(std.testing.allocator, &vm)); +} + +test "BuiltinRunner: runSecurityChecks ecOp check memory gap" { + var builtin = BuiltinRunner{ .EcOp = EcOpBuiltinRunner.init(std.testing.allocator, .{ .ratio = 256 }, true) }; + defer builtin.deinit(); + + // Initialize Cairo VM + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + + try vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 0 }, .{0} }, + .{ .{ 0, 1 }, .{1} }, + .{ .{ 0, 2 }, .{2} }, + .{ .{ 0, 3 }, .{3} }, + .{ .{ 0, 4 }, .{4} }, + .{ .{ 0, 5 }, .{5} }, + .{ .{ 0, 6 }, .{6} }, + .{ .{ 0, 8 }, .{8} }, + .{ .{ 0, 9 }, .{9} }, + .{ .{ 0, 10 }, .{10} }, + .{ .{ 0, 11 }, .{11} }, + }); + defer vm.segments.memory.deinitData(std.testing.allocator); + + try std.testing.expectError(MemoryError.MissingMemoryCellsWithOffsets, builtin.runSecurityChecks(std.testing.allocator, &vm)); +} diff --git a/src/vm/builtins/builtin_runner/output.zig b/src/vm/builtins/builtin_runner/output.zig index ea1a1b65..54b4567c 100644 --- a/src/vm/builtins/builtin_runner/output.zig +++ b/src/vm/builtins/builtin_runner/output.zig @@ -155,6 +155,29 @@ pub const OutputBuiltinRunner = struct { ) orelse MemoryError.MissingSegmentUsedSizes; } + // return ArrayList owner is caller, so he should call free + pub fn getPublicMemory( + self: *const Self, + allocator: std.mem.Allocator, + segments: *MemorySegmentManager, + ) !std.ArrayList(std.meta.Tuple(&.{ usize, usize })) { + const size = try self.getUsedCells(segments); + + var public_memory = try std.ArrayList(std.meta.Tuple(&.{ usize, usize })).initCapacity(allocator, size); + errdefer public_memory.deinit(); + + for (0..size) |i| public_memory.appendAssumeCapacity(.{ i, 0 }); + + var it = self.pages.iterator(); + + while (it.next()) |entry| { + for (0..entry.value_ptr.size) |index| + public_memory.items[entry.value_ptr.start + index][1] = entry.key_ptr.*; + } + + return public_memory; + } + /// Retrieves the count of used instances for the OutputBuiltinRunner instance. /// /// This function acts as an alias for `getUsedCells`, obtaining the number of used instances @@ -850,3 +873,42 @@ test "OutputBuiltinRunner: clearStateWithBase should set a new base and clear pa // Verifies if the count of pages in the OutputBuiltinRunner's hashmap is 0 after the state clearing. try expectEqual(@as(usize, 0), output_builtin.pages.count()); } + +test "OutputBuiltinRunner: getPublicMemory" { + // Creates a new OutputBuiltinRunner instance and initializes it with a base address of 10. + var output_builtin = OutputBuiltinRunner.initDefault(std.testing.allocator); + // Deinitializes the runner when the function scope ends. + defer output_builtin.deinit(); + + // Sets up the initial pages in the runner's hashmap with various IDs and configurations. + try output_builtin.addPage(1, MaybeRelocatable.fromRelocatable( + Relocatable.init( + @intCast(output_builtin.base), + 2, + ), + ), 2); + try output_builtin.addPage(2, MaybeRelocatable.fromRelocatable( + Relocatable.init( + @intCast(output_builtin.base), + 4, + ), + ), 3); + + var segments = try MemorySegmentManager.init(std.testing.allocator); + defer segments.deinit(); + + try segments.segment_used_sizes.append(7); + + const public_memory = try output_builtin.getPublicMemory(std.testing.allocator, segments); + defer public_memory.deinit(); + + try expectEqualSlices(std.meta.Tuple(&.{ usize, usize }), &.{ + .{ 0, 0 }, + .{ 1, 0 }, + .{ 2, 1 }, + .{ 3, 1 }, + .{ 4, 2 }, + .{ 5, 2 }, + .{ 6, 2 }, + }, public_memory.items); +} diff --git a/src/vm/cairo_run.zig b/src/vm/cairo_run.zig index b5e716cb..7a732699 100644 --- a/src/vm/cairo_run.zig +++ b/src/vm/cairo_run.zig @@ -2,13 +2,16 @@ const std = @import("std"); const json = std.json; const Allocator = std.mem.Allocator; -const CairoRunner = @import("./runners/cairo_runner.zig").CairoRunner; -const CairoVM = @import("./core.zig").CairoVM; -const Config = @import("./config.zig").Config; +const security = @import("security.zig"); +const CairoRunner = @import("runners/cairo_runner.zig").CairoRunner; +const CairoVM = @import("core.zig").CairoVM; +const Config = @import("config.zig").Config; const Felt252 = @import("../math/fields/starknet.zig").Felt252; -const Program = @import("./types/program.zig").Program; -const ProgramJson = @import("./types/programjson.zig").ProgramJson; +const Program = @import("types/program.zig").Program; +const ProgramJson = @import("types/programjson.zig").ProgramJson; const HintProcessor = @import("../hint_processor/hint_processor_def.zig").CairoVMHintProcessor; +const ExecutionScopes = @import("types/execution_scopes.zig").ExecutionScopes; +const MaybeRelocatable = @import("./memory/relocatable.zig").MaybeRelocatable; const trace_context = @import("./trace_context.zig"); const RelocatedTraceEntry = trace_context.RelocatedTraceEntry; @@ -52,13 +55,133 @@ pub fn writeEncodedMemory(relocated_memory: []?Felt252, dest: anytype) !void { } } +pub const CairoRunConfig = struct { + entrypoint: []const u8 = "main", + trace_enabled: bool = false, + relocate_mem: bool = false, + layout: []const u8 = "plain", + proof_mode: bool = false, + secure_run: ?bool = null, + disable_trace_padding: bool = false, + allow_missing_builtins: ?bool = null, +}; + +// cairoRun - running cairo program, after call u own runner.vm, own runner and own runner.vm.segments.memory +// so after call u need deallocate runner.vm, calll runner.vm.segments.memory.deinitData and runner.deinit +pub fn cairoRun( + allocator: std.mem.Allocator, + program_content: []const u8, + cairo_run_config: CairoRunConfig, + hint_processor: *HintProcessor, +) !CairoRunner { + var parsed_program = try ProgramJson.parseFromString(allocator, program_content); + defer parsed_program.deinit(); + + var program = try parsed_program.value.parseProgramJson(allocator, @constCast(&cairo_run_config.entrypoint)); + + const instructions = parsed_program.value.readData(allocator) catch |err| { + program.deinit(allocator); + return err; + }; + + return cairoRunProgram(allocator, program, cairo_run_config, hint_processor, instructions); +} + +pub fn cairoRunProgram( + allocator: std.mem.Allocator, + program: Program, + cairo_run_config: CairoRunConfig, + hint_processor: *HintProcessor, + instructions: std.ArrayList(MaybeRelocatable), +) !CairoRunner { + const execution_scopes = ExecutionScopes.init(allocator) catch |err| { + // hack to deallocate program (TODO: think how to do it better) + var p = program; + p.deinit(allocator); + instructions.deinit(); + return err; + }; + + return cairoRunProgramWithInitialScope(allocator, program, cairo_run_config, hint_processor, instructions, execution_scopes); +} + +/// Runs a program with a customized execution scope. +/// put exec_scopes as argument, func take controll on it deallocation due error, due success CairoRunner will control deallocate +/// also as passed program, CairoRunner take control on program on error/success run +pub fn cairoRunProgramWithInitialScope( + allocator: std.mem.Allocator, + program: Program, + cairo_run_config: CairoRunConfig, + hint_processor: *HintProcessor, + instructions: std.ArrayList(MaybeRelocatable), + exec_scopes: ExecutionScopes, +) !CairoRunner { + const secure_run = cairo_run_config.secure_run orelse !cairo_run_config.proof_mode; + + const allow_missing_builtins = cairo_run_config.allow_missing_builtins orelse cairo_run_config.proof_mode; + + var runner = val: { + errdefer instructions.deinit(); + errdefer exec_scopes.deinit(); + errdefer { + // hack to deallocate + var p = program; + p.deinit(allocator); + } + + var vm = try allocator.create(CairoVM); + errdefer allocator.destroy(vm); + + vm.* = try CairoVM.initV2(allocator, cairo_run_config.trace_enabled); + errdefer vm.deinit(); + + break :val try CairoRunner.initV2( + allocator, + program, + cairo_run_config.layout, + instructions, + vm, + cairo_run_config.proof_mode, + exec_scopes, + ); + }; + errdefer runner.deinit(allocator); + + const end = try runner.setupExecutionState(allow_missing_builtins); + + try runner.runUntilPC(end, hint_processor); + + if (cairo_run_config.proof_mode) try runner.runForSteps(1, hint_processor); + + try runner.endRun( + allocator, + cairo_run_config.disable_trace_padding, + false, + hint_processor, + ); + + try runner.vm.verifyAutoDeductions(allocator); + try runner.readReturnValue(allow_missing_builtins); + + // cairo_runner.read_return_values(allow_missing_builtins)?; + if (cairo_run_config.proof_mode) try runner.finalizeSegments(); + + if (secure_run) try security.verifySecureRunner(allocator, &runner, true, null); + + try runner.relocate(cairo_run_config.relocate_mem); + + return runner; +} + /// Instruments the `CairoRunner` to initialize an execution of a cairo program based on Config params. /// /// # Arguments /// /// - `allocator`: The allocator to initialize the CairoRunner and parsing of the program json. /// - `config`: The config struct that defines the params that the CairoRunner uses to instantiate the vm state for running. -pub fn runConfig(allocator: Allocator, config: Config) !void { +pub fn runConfig(allocator: Allocator, config: Config) !CairoRunner { + const secure_run = config.secure_run orelse !config.proof_mode; + var vm = try CairoVM.init( allocator, config, @@ -78,12 +201,15 @@ pub fn runConfig(allocator: Allocator, config: Config) !void { &vm, config.proof_mode, ); - defer runner.deinit(allocator); + errdefer runner.deinit(allocator); const end = try runner.setupExecutionState(config.allow_missing_builtins orelse config.proof_mode); var hint_processor: HintProcessor = .{}; try runner.runUntilPC(end, &hint_processor); + + if (config.proof_mode) try runner.runForSteps(1, &hint_processor); + try runner.endRun( allocator, false, @@ -91,6 +217,18 @@ pub fn runConfig(allocator: Allocator, config: Config) !void { &hint_processor, ); + + try runner.vm.verifyAutoDeductions(allocator); + + // cairo_runner.read_return_values(allow_missing_builtins)?; + + if (config.proof_mode) + try runner.finalizeSegments(); + + if (secure_run) + try security.verifySecureRunner(allocator, &runner, true, null); + + if (config.print_output) { var buf = try std.ArrayList(u8).initCapacity(allocator, 100); defer buf.deinit(); @@ -102,6 +240,7 @@ pub fn runConfig(allocator: Allocator, config: Config) !void { } // TODO readReturnValues necessary for builtins + if (config.output_trace != null or config.output_memory != null) { try runner.relocate(); @@ -128,6 +267,8 @@ pub fn runConfig(allocator: Allocator, config: Config) !void { try writer.flush(); } } + + return runner; } const expect = std.testing.expect; diff --git a/src/vm/config.zig b/src/vm/config.zig index 2ce794ce..2797a911 100644 --- a/src/vm/config.zig +++ b/src/vm/config.zig @@ -4,6 +4,7 @@ /// Config used to initiate CairoVM pub const Config = struct { + secure_run: ?bool = false, /// Generate a proof for execution of the program proof_mode: bool = false, /// When enabled trace is generated diff --git a/src/vm/core.zig b/src/vm/core.zig index 9e62f792..6fd4613d 100644 --- a/src/vm/core.zig +++ b/src/vm/core.zig @@ -19,7 +19,6 @@ const Config = @import("config.zig").Config; const TraceContext = @import("trace_context.zig").TraceContext; const TraceEntry = @import("trace_context.zig").TraceEntry; const RelocatedTraceEntry = @import("trace_context.zig").RelocatedTraceEntry; -const build_options = @import("../build_options.zig"); const RangeCheckBuiltinRunner = @import("builtins/builtin_runner/range_check.zig").RangeCheckBuiltinRunner; const SignatureBuiltinRunner = @import("builtins/builtin_runner/signature.zig").SignatureBuiltinRunner; const BuiltinRunner = @import("builtins/builtin_runner/builtin_runner.zig").BuiltinRunner; @@ -76,20 +75,13 @@ pub const CairoVM = struct { // * MEMORY ALLOCATION AND DEALLOCATION * // ************************************************************ - /// Creates a new Cairo VM. - /// # Arguments - /// - `allocator`: The allocator to use for the VM. - /// - `config`: Configurations used to initialize the VM. - /// # Returns - /// - `CairoVM`: The created VM. - /// # Errors - /// - If a memory allocation fails. - pub fn init(allocator: Allocator, config: Config) !Self { + pub fn initV2(allocator: Allocator, trace_enabled: bool) !Self { + // Initialize the memory segment manager. const memory_segment_manager = try segments.MemorySegmentManager.init(allocator); errdefer memory_segment_manager.deinit(); // Initialize the trace context. - var trace: ?std.ArrayList(TraceEntry) = if (config.enable_trace) try std.ArrayList(TraceEntry).initCapacity(allocator, 100) else null; + var trace: ?std.ArrayList(TraceEntry) = if (trace_enabled) try std.ArrayList(TraceEntry).initCapacity(allocator, 100) else null; errdefer if (trace != null) trace.?.deinit(); // Initialize the run context. const run_context = .{}; @@ -110,6 +102,18 @@ pub const CairoVM = struct { }; } + /// Creates a new Cairo VM. + /// # Arguments + /// - `allocator`: The allocator to use for the VM. + /// - `config`: Configurations used to initialize the VM. + /// # Returns + /// - `CairoVM`: The created VM. + /// # Errors + /// - If a memory allocation fails. + pub fn init(allocator: Allocator, config: Config) !Self { + return Self.initV2(allocator, config.enable_trace); + } + /// Safely deallocates resources used by the CairoVM instance. /// /// This function ensures safe deallocation of various components within the CairoVM instance, diff --git a/src/vm/core_test.zig b/src/vm/core_test.zig index 1af05223..b0b3777b 100644 --- a/src/vm/core_test.zig +++ b/src/vm/core_test.zig @@ -22,7 +22,6 @@ const MathError = @import("error.zig").MathError; const Config = @import("config.zig").Config; const TraceEntry = @import("trace_context.zig").TraceEntry; const RelocatedTraceEntry = @import("trace_context.zig").RelocatedTraceEntry; -const build_options = @import("../build_options.zig"); const BuiltinRunner = @import("./builtins/builtin_runner/builtin_runner.zig").BuiltinRunner; const BitwiseBuiltinRunner = @import("./builtins/builtin_runner/bitwise.zig").BitwiseBuiltinRunner; const KeccakBuiltinRunner = @import("./builtins/builtin_runner/keccak.zig").KeccakBuiltinRunner; @@ -3092,13 +3091,13 @@ test "CairoVM: getPublicMemoryAddresses should return Cairo VM Memory error if s // Initialize inner lists to store specific offsets for segments. var inner_list_1 = std.ArrayList(std.meta.Tuple(&.{ usize, usize })).init(allocator); // Ensure proper deallocation of resources. - defer inner_list_1.deinit(); + errdefer inner_list_1.deinit(); try inner_list_1.append(.{ 0, 0 }); try inner_list_1.append(.{ 1, 1 }); var inner_list_2 = std.ArrayList(std.meta.Tuple(&.{ usize, usize })).init(allocator); // Ensure proper deallocation of resources. - defer inner_list_2.deinit(); + errdefer inner_list_2.deinit(); // Inline to append specific offsets to the list. inline for (0..8) |i| { try inner_list_2.append(.{ i, 0 }); @@ -3106,7 +3105,7 @@ test "CairoVM: getPublicMemoryAddresses should return Cairo VM Memory error if s var inner_list_5 = std.ArrayList(std.meta.Tuple(&.{ usize, usize })).init(allocator); // Ensure proper deallocation of resources. - defer inner_list_5.deinit(); + errdefer inner_list_5.deinit(); try inner_list_5.append(.{ 1, 2 }); // Append inner lists containing offsets to public_memory_offsets. @@ -3182,13 +3181,13 @@ test "CairoVM: getPublicMemoryAddresses should return a proper ArrayList if succ // Initialize inner lists to store specific offsets for segments. var inner_list_1 = std.ArrayList(std.meta.Tuple(&.{ usize, usize })).init(allocator); // Ensure proper deallocation of resources. - defer inner_list_1.deinit(); + errdefer inner_list_1.deinit(); try inner_list_1.append(.{ 0, 0 }); try inner_list_1.append(.{ 1, 1 }); var inner_list_2 = std.ArrayList(std.meta.Tuple(&.{ usize, usize })).init(allocator); // Ensure proper deallocation of resources. - defer inner_list_2.deinit(); + errdefer inner_list_2.deinit(); // Inline to append specific offsets to the list. inline for (0..8) |i| { try inner_list_2.append(.{ i, 0 }); @@ -3196,7 +3195,7 @@ test "CairoVM: getPublicMemoryAddresses should return a proper ArrayList if succ var inner_list_5 = std.ArrayList(std.meta.Tuple(&.{ usize, usize })).init(allocator); // Ensure proper deallocation of resources. - defer inner_list_5.deinit(); + errdefer inner_list_5.deinit(); try inner_list_5.append(.{ 1, 2 }); // Append inner lists containing offsets to public_memory_offsets. diff --git a/src/vm/error.zig b/src/vm/error.zig index 9cadf939..fbc13817 100644 --- a/src/vm/error.zig +++ b/src/vm/error.zig @@ -1,5 +1,11 @@ /// Represents different error conditions that occur in the Cairo VM. pub const CairoVMError = error{ + // Failed to find index in the vm's relocation table + RelocationNotFound, + // Out of bounds access to program segment + OutOfBoundsProgramSegmentAccess, + // Out of bounds access to builtin segment + OutOfBoundsBuiltinSegmentAccess, // Expected integer, found ExpectedIntAtRange, // Failed to compile hint @@ -80,6 +86,10 @@ pub const CairoVMError = error{ /// Represents different error conditions that are memory-related. pub const MemoryError = error{ + // Missing memory cells for + MissingMemoryCells, + // Missing memory cells for + MissingMemoryCellsWithOffsets, /// Occurs when the ratio of the builtin operation does not divide evenly into the current VM steps. ErrorCalculatingMemoryUnits, /// The amount of used cells associated with the Range Check runner is not available. @@ -148,6 +158,22 @@ pub const CairoRunnerError = error{ /// Represents different error conditions that occur in the built-in runners. pub const RunnerError = error{ + // Missing execution public memory + NoExecPublicMemory, + // Cannot add the return values to the public memory after segment finalization. + FailedAddingReturnValues, + // is missing + MissingBuiltin, + // The stop pointer of the missing builtin {0} must be 0 + MissingBuiltinStopPtrNotZero, + // end_run must be called before read_return_values. + ReadReturnValuesNoEndRun, + // Error while finalizing segments + FinalizeSegements, + // Finalize_segments called but proof_mode is not enabled + FinalizeSegmentsNoProofMode, + // end_run must be called before finalize_segments. + FinalizeNoEndRun, // Given builtins are not in appropiate order DisorderedBuiltins, // Builtin(s) not present in layout @@ -402,3 +428,14 @@ pub const ExecScopeError = error{ ExitMainScopeError, NoScopeError, }; + +pub const PublicInputError = error{ + // The trace slice provided is empty")] + EmptyTrace, + // The provided memory doesn't contain public address {0}")] + MemoryNotFound, + // Range check values are missing")] + NoRangeCheckLimits, + // Failed to (de)serialize data + Serialize, +}; diff --git a/src/vm/memory/memory.zig b/src/vm/memory/memory.zig index 728f562f..64b72e1e 100644 --- a/src/vm/memory/memory.zig +++ b/src/vm/memory/memory.zig @@ -30,7 +30,7 @@ pub const MemoryCell = extern struct { const RELOCATABLE_MASK: u64 = 1 << 63; const NONE_MASK: u64 = 1 << 61; - const NONE: Self = .{ .data = .{ + pub const NONE: Self = .{ .data = .{ 0, 0, 0, @@ -380,6 +380,9 @@ pub const Memory = struct { var data = self.getDataFromSegmentIndex(address.segment_index); const insert_segment_index = address.getAdjustedSegmentIndex(); + // if (address.segment_index == 0 and value.isFelt()) + // std.debug.panic("set {any}, value {any}", .{ address, value }); + // Check if the data segment is allocated for the given segment index. if (data.len <= insert_segment_index) return MemoryError.UnallocatedSegment; diff --git a/src/vm/memory/segments.zig b/src/vm/memory/segments.zig index 2e3ec45f..b9355edb 100644 --- a/src/vm/memory/segments.zig +++ b/src/vm/memory/segments.zig @@ -45,6 +45,12 @@ pub const MemorySegmentManager = struct { std.ArrayList(Tuple(&.{ usize, usize })), ), + // Segment index of the zero segment index, a memory segment filled with zeroes, used exclusively by builtin runners + // This segment will never have index 0 so we use 0 to represent uninitialized value + zero_segment_index: usize = 0, + // Segment size of the zero segment index + zero_segment_size: usize = 0, + // ************************************************************ // * MEMORY ALLOCATION AND DEALLOCATION * // ************************************************************ @@ -88,6 +94,10 @@ pub const MemorySegmentManager = struct { // Clear the hash maps self.segment_used_sizes.deinit(); self.segment_sizes.deinit(); + + var it = self.public_memory_offsets.valueIterator(); + while (it.next()) |v| v.deinit(); + self.public_memory_offsets.deinit(); // Deallocate the memory. self.memory.deinit(); @@ -228,7 +238,8 @@ pub const MemorySegmentManager = struct { var relocatable_table = try ArrayList(usize).initCapacity(allocator, 1 + self.segment_used_sizes.items.len); errdefer relocatable_table.deinit(); - try relocatable_table.append(1); + relocatable_table.appendAssumeCapacity(1); + for (0..self.segment_used_sizes.items.len) |i| { const segment_size = self.getSegmentSize(i) orelse return MemoryError.MissingSegmentUsedSizes; relocatable_table.appendAssumeCapacity(relocatable_table.items[i] + segment_size); @@ -289,6 +300,15 @@ pub const MemorySegmentManager = struct { return ptr.addUint(data.len) catch MemoryError.Math; } + // Finalizes the zero segment and clears it's tracking data from the manager + pub fn finalizeZeroSegment(self: *Self) !void { + if (self.zero_segment_index != 0) { + try self.finalize(self.zero_segment_index, self.zero_segment_size, null); + self.zero_segment_index = 0; + self.zero_segment_size = 0; + } + } + /// Records details for a specified segment, facilitating relocation: /// - `segment_index`: The index of the segment to finalize. /// - `size`: The size of the segment for `relocate_segments`. @@ -307,18 +327,17 @@ pub const MemorySegmentManager = struct { if (size) |s| { if (s > std.math.maxInt(u32)) return MathError.ValueTooLarge; try self.segment_sizes.put( - @intCast(segment_index), - @intCast(s), + segment_index, + s, ); } + try self.public_memory_offsets.put( segment_index, if (public_memory) |p| p else blk: { - var default = std.ArrayList(Tuple(&.{ usize, usize })).init(self.allocator); - defer default.deinit(); - break :blk default; + break :blk std.ArrayList(Tuple(&.{ usize, usize })).init(self.allocator); }, ); } @@ -336,9 +355,7 @@ pub const MemorySegmentManager = struct { segment_offsets: *const std.ArrayList(usize), ) !std.ArrayList(Tuple(&.{ usize, usize })) { // Initialize a list to store the resulting public memory addresses - var public_memory_addresses = std.ArrayList(Tuple(&.{ usize, usize })).init(self.allocator); - // Ensure that the list has enough capacity to accommodate the addresses - try public_memory_addresses.ensureTotalCapacity(self.numSegments()); + var public_memory_addresses = try std.ArrayList(Tuple(&.{ usize, usize })).initCapacity(self.allocator, self.numSegments()); // Defer deallocation of the list to handle potential errors errdefer public_memory_addresses.deinit(); @@ -1072,18 +1089,18 @@ test "MemorySegmentManager: getPublicMemoryAddresses with correct segment offset // Initialize inner lists to store specific offsets for segments. var inner_list_1 = std.ArrayList(Tuple(&.{ usize, usize })).init(allocator); - defer inner_list_1.deinit(); + errdefer inner_list_1.deinit(); try inner_list_1.append(.{ 0, 0 }); try inner_list_1.append(.{ 1, 1 }); var inner_list_2 = std.ArrayList(Tuple(&.{ usize, usize })).init(allocator); - defer inner_list_2.deinit(); + errdefer inner_list_2.deinit(); inline for (0..8) |i| { try inner_list_2.append(.{ i, 0 }); } var inner_list_5 = std.ArrayList(Tuple(&.{ usize, usize })).init(allocator); - defer inner_list_5.deinit(); + errdefer inner_list_5.deinit(); try inner_list_5.append(.{ 1, 2 }); // Append inner lists containing offsets to public_memory_offsets. @@ -1169,18 +1186,18 @@ test "MemorySegmentManager: getPublicMemoryAddresses with incorrect segment offs // Initialize inner lists to store specific offsets for segments. var inner_list_1 = std.ArrayList(Tuple(&.{ usize, usize })).init(allocator); - defer inner_list_1.deinit(); + errdefer inner_list_1.deinit(); try inner_list_1.append(.{ 0, 0 }); try inner_list_1.append(.{ 1, 1 }); var inner_list_2 = std.ArrayList(Tuple(&.{ usize, usize })).init(allocator); - defer inner_list_2.deinit(); + errdefer inner_list_2.deinit(); inline for (0..8) |i| { try inner_list_2.append(.{ i, 0 }); } var inner_list_5 = std.ArrayList(Tuple(&.{ usize, usize })).init(allocator); - defer inner_list_5.deinit(); + errdefer inner_list_5.deinit(); try inner_list_5.append(.{ 1, 2 }); // Append inner lists containing offsets to public_memory_offsets. diff --git a/src/vm/runners/cairo_runner.zig b/src/vm/runners/cairo_runner.zig index 292530b7..d61a487d 100644 --- a/src/vm/runners/cairo_runner.zig +++ b/src/vm/runners/cairo_runner.zig @@ -28,6 +28,7 @@ const CairoVMError = @import("../error.zig").CairoVMError; const InsufficientAllocatedCellsError = @import("../error.zig").InsufficientAllocatedCellsError; const RunnerError = @import("../error.zig").RunnerError; const MemoryError = @import("../error.zig").MemoryError; +const PublicInputError = @import("../error.zig").PublicInputError; const trace_context = @import("../trace_context.zig"); const RelocatedTraceEntry = trace_context.RelocatedTraceEntry; const RelocatedFelt252 = trace_context.RelocatedFelt252; @@ -36,6 +37,7 @@ const Felt252 = starknet_felt.Felt252; const ExecutionScopes = @import("../types/execution_scopes.zig").ExecutionScopes; const TraceEntry = @import("../trace_context.zig").TraceEntry; const TestingUtils = @import("../../utils/testing.zig"); +const air_input_public = @import("../air_input_public.zig"); const cfg = @import("cfg"); @@ -132,7 +134,7 @@ const ResourceTracker = struct { pub const RunnerMode = enum { execution_mode, proof_mode_canonical, proof_mode_cairo1 }; -const BuiltinInfo = struct { segment_index: usize, stop_pointer: usize }; +pub const BuiltinInfo = struct { segment_index: usize, stop_pointer: usize }; pub const CairoRunner = struct { const Self = @This(); @@ -156,14 +158,17 @@ pub const CairoRunner = struct { relocated_trace: ?[]RelocatedTraceEntry = null, relocated_memory: ArrayList(?Felt252), execution_scopes: ExecutionScopes = undefined, + segments_finalized: bool, - pub fn init( + // taking own on instructions and exec_scopes + pub fn initV2( allocator: Allocator, program: Program, layout: []const u8, instructions: std.ArrayList(MaybeRelocatable), vm: *CairoVM, proof_mode: bool, + exec_scopes: ExecutionScopes, ) !Self { const Case = enum { plain, small, dynamic, all_cairo }; return .{ @@ -180,11 +185,24 @@ pub const CairoRunner = struct { .vm = vm, .runner_mode = if (proof_mode) .proof_mode_canonical else .execution_mode, .relocated_memory = ArrayList(?Felt252).init(allocator), - .execution_scopes = try ExecutionScopes.init(allocator), + .execution_scopes = exec_scopes, .entrypoint = program.shared_program_data.main, + .segments_finalized = false, + .execution_public_memory = if (proof_mode) std.ArrayList(usize).init(allocator) else null, }; } + pub fn init( + allocator: Allocator, + program: Program, + layout: []const u8, + instructions: std.ArrayList(MaybeRelocatable), + vm: *CairoVM, + proof_mode: bool, + ) !Self { + return Self.initV2(allocator, program, layout, instructions, vm, proof_mode, try ExecutionScopes.init(allocator)); + } + pub fn isProofMode(self: *Self) bool { return self.runner_mode == .proof_mode_canonical or self.runner_mode == .proof_mode_cairo1; } @@ -208,6 +226,7 @@ pub const CairoRunner = struct { .ec_op, .keccak, .poseidon, + .range_check96, }; for (self.program.builtins.items) |builtin| { @@ -295,6 +314,15 @@ pub const CairoRunner = struct { }); } + if (self.layout.builtins.range_check96) |instance_def| { + const included = program_builtins.remove(.range_check96); + + if (included or self.isProofMode()) + try self.vm.builtin_runners.append(.{ + .RangeCheck96 = RangeCheckBuiltinRunner.init(instance_def.ratio, instance_def.n_parts, included), + }); + } + if (program_builtins.count() != 0 and !allow_missing_builtins) return RunnerError.NoBuiltinForInstance; } @@ -426,22 +454,23 @@ pub const CairoRunner = struct { var target_offset: usize = 2; // If in canonical proof mode, adjust stack and execution state accordingly. - if (self.runner_mode == .proof_mode_canonical) { + if (self.runner_mode != .proof_mode_cairo1) { // Prepare stack prefix with additional execution context. var stack_prefix = try std.ArrayList(MaybeRelocatable).initCapacity(self.allocator, 2 + stack.items.len); defer stack_prefix.deinit(); - try stack_prefix.append(MaybeRelocatable.fromRelocatable( + stack_prefix.appendAssumeCapacity(MaybeRelocatable.fromRelocatable( try (self.execution_base orelse return RunnerError.NoExecBase).addUint(target_offset), )); - try stack_prefix.append(MaybeRelocatable.fromFelt(Felt252.zero())); - try stack_prefix.appendSlice(stack.items); + stack_prefix.appendAssumeCapacity(MaybeRelocatable.fromFelt(Felt252.zero())); + + stack_prefix.appendSliceAssumeCapacity(stack.items); // Initialize public memory for execution context. var execution_public_memory = try std.ArrayList(usize).initCapacity(self.allocator, stack_prefix.items.len); for (0..stack_prefix.items.len) |v| { - try execution_public_memory.append(v); + execution_public_memory.appendAssumeCapacity(v); } self.execution_public_memory = execution_public_memory; @@ -704,8 +733,7 @@ pub const CairoRunner = struct { // Loop until all steps are executed or program ends while (remaining_steps >= 1) { // Check if the program has reached its end - if (self.final_pc.?.eq(self.vm.run_context.pc)) - return CairoVMError.EndOfProgram; + if (self.final_pc) |final_pc| if (final_pc.eq(self.vm.run_context.pc)) return CairoVMError.EndOfProgram; // Execute a single step of the program, considering extensive or non-extensive hints if (@import("cfg").extensive) { @@ -924,12 +952,57 @@ pub const CairoRunner = struct { } } - pub fn relocate(self: *Self) !void { + pub fn readReturnValue(self: *Self, allow_missing_builtins: bool) !void { + if (!self.run_ended) return RunnerError.ReadReturnValuesNoEndRun; + + var pointer = self.vm.run_context.getAP(); + const len = self.program.builtins.items.len; + for (0..len) |idx| { + const builtin_name = builtin_runner_import.BuiltinName.fromProgramJsonName(self.program.builtins.items[len - idx - 1]); + + for (self.vm.builtin_runners.items) |*builtin| { + if (std.meta.activeTag(builtin.*) == builtin_name) { + pointer = try builtin.finalStack(self.vm.segments, pointer); + break; + } + } else { + // builtin not found + if (!allow_missing_builtins) + return RunnerError.MissingBuiltin; + + pointer.offset = pointer.offset -| 1; + + if (!(try self.vm.getFelt(pointer)).isZero()) + return RunnerError.MissingBuiltinStopPtrNotZero; + } + } + + if (self.segments_finalized) return RunnerError.FailedAddingReturnValues; + + if (self.isProofMode()) { + const exec_base = self.execution_base orelse return RunnerError.NoExecBase; + + const begin = pointer.offset - exec_base.offset; + const ap = self.vm.run_context.getAP(); + const end = ap.offset - exec_base.offset; + + if (end - begin > 0) + if (self.execution_public_memory) |*memory| { + try memory.ensureUnusedCapacity(end - begin); + for (begin..end) |x| { + memory.appendAssumeCapacity(x); + } + } else return RunnerError.NoExecPublicMemory; + } + } + + pub fn relocate(self: *Self, relocate_mem: bool) !void { // Presuming the default case of `allow_tmp_segments` in python version _ = try self.vm.segments.computeEffectiveSize(false); + if (!relocate_mem and self.vm.trace == null) return; const totalSize, const relocation_table = try self.vm.segments.relocateSegments(self.allocator); - defer relocation_table.deinit(); + errdefer relocation_table.deinit(); try self.relocateMemory(totalSize, relocation_table.items); @@ -937,6 +1010,8 @@ pub const CairoRunner = struct { try self.vm.relocateTrace(relocation_table.items); self.relocated_trace = try self.vm.getRelocatedTrace(); } + + self.vm.relocation_table = relocation_table; } /// Retrieves information about the builtin segments. @@ -967,19 +1042,13 @@ pub const CairoRunner = struct { const memory_segment_addresses = builtin.getMemorySegmentAddresses(); // Uncomment the following line for debugging purposes. - // std.debug.print("memory_segment_addresses = {any}\n", .{memory_segment_addresses}); - - // Check if the stop pointer is present. - if (memory_segment_addresses[1]) |stop_pointer| { - // Append information about the segment to the ArrayList. - try builtin_segment_info.append(.{ - .segment_index = memory_segment_addresses[0], - .stop_pointer = stop_pointer, - }); - } else { - // Return an error if a stop pointer is missing. - return RunnerError.NoStopPointer; - } + // std.debug.print("memory_segment_addresses = {any}, {s}\n", .{ memory_segment_addresses, builtin.name() }); + + // Append information about the segment to the ArrayList. + try builtin_segment_info.append(.{ + .segment_index = memory_segment_addresses[0], + .stop_pointer = memory_segment_addresses[1] orelse return RunnerError.NoStopPointer, + }); } // Return the ArrayList containing information about the builtin segments. @@ -1032,6 +1101,65 @@ pub const CairoRunner = struct { ); } + pub fn getAirPublicInput(self: *Self) !air_input_public.PublicInput { + const public_memory_address = try self.vm.getPublicMemoryAddresses(); + defer public_memory_address.deinit(); + + var memory_segment_addresses = try self.getMemorySegmentAddresses(self.allocator); + defer memory_segment_addresses.deinit(); + + return air_input_public.PublicInput.new( + self.allocator, + self.relocated_memory.items, + self.layout.name, + public_memory_address.items, + memory_segment_addresses, + self.relocated_trace orelse return PublicInputError.EmptyTrace, + (try self.getPermRangeCheckLimits(self.allocator)) orelse return PublicInputError.NoRangeCheckLimits, + ); + } + + pub fn getMemorySegmentAddresses( + self: *Self, + allocator: std.mem.Allocator, + ) !std.StringHashMap(std.meta.Tuple(&.{ usize, usize })) { + const reloc_table = self + .vm + .relocation_table orelse return MemoryError.UnrelocatedMemory; + + const _relocate = (struct { + fn reloc(relocation_table: std.ArrayList(usize), segment: std.meta.Tuple(&.{ usize, usize })) !std.meta.Tuple(&.{ usize, usize }) { + const index, const stop_ptr_offset = segment; + const base = if (relocation_table.items.len <= index) return CairoVMError.RelocationNotFound else relocation_table.items[index]; + + return .{ base, base + stop_ptr_offset }; + } + }).reloc; + + var result = std.StringHashMap(std.meta.Tuple(&.{ usize, usize })).init(allocator); + errdefer result.deinit(); + + // TODO optimize contains? + const contains = (struct { + fn func(builtins: []BuiltinName, x: []const u8) bool { + for (builtins) |builtin| + if (builtin == std.meta.stringToEnum(BuiltinName, x)) return true; + + return false; + } + }).func; + + for (self.vm.builtin_runners.items) |*builtin| { + const base, const stop_ptr_ = builtin.getMemorySegmentAddresses(); + + const stop_ptr = if (contains(self.program.builtins.items, builtin.name())) stop_ptr_ orelse return RunnerError.NoStopPointer else stop_ptr_ orelse 0; + + try result.put(builtin.name(), try _relocate(reloc_table, .{ base, stop_ptr })); + } + + return result; + } + /// Retrieves the permanent range check limits from the CairoRunner instance. /// /// This function iterates through the builtin runners of the CairoRunner and gathers @@ -1151,6 +1279,77 @@ pub const CairoRunner = struct { } } + // 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. + pub fn finalizeSegments(self: *Self) !void { + if (self.segments_finalized) { + return; + } + + if (!self.run_ended) { + return RunnerError.FinalizeNoEndRun; + } + + const size = self.program.shared_program_data.data.items.len; + { + var public_memory = try std.ArrayList(std.meta.Tuple(&.{ usize, usize })).initCapacity(self.allocator, size); + errdefer public_memory.deinit(); + + for (0..size) |i| { + public_memory.appendAssumeCapacity(.{ i, 0 }); + } + + try self.vm.segments.finalize( + @intCast((self.program_base orelse return RunnerError.NoProgBase) + .segment_index), + size, + public_memory, + ); + } + + { + var public_memory = std.ArrayList(std.meta.Tuple(&.{ usize, usize })).init(self.allocator); + errdefer public_memory.deinit(); + + const exec_base = self + .execution_base orelse return RunnerError.NoExecBase; + + for ((self + .execution_public_memory orelse return RunnerError.FinalizeSegmentsNoProofMode).items) |elem| + { + try public_memory.append(.{ elem + exec_base.offset, 0 }); + } + + try self.vm + .segments + .finalize(@intCast(exec_base.segment_index), null, public_memory); + } + + for (self.vm.builtin_runners.items) |builtin_runner| { + _, const s = (builtin_runner + .getUsedCellsAndAllocatedSize(self.vm) catch return RunnerError.FinalizeSegements); + + if (builtin_runner == .Output) { + var public_memory = try builtin_runner.Output.getPublicMemory(self.allocator, self.vm.segments); + errdefer public_memory.deinit(); + + try self.vm + .segments + .finalize(builtin_runner.base(), s, public_memory); + } else { + try self.vm + .segments + .finalize(builtin_runner.base(), s, null); + } + } + + try self.vm.segments.finalizeZeroSegment(); + self.segments_finalized = true; + } + /// Retrieves a pointer to the list of built-in functions defined in the program associated with the CairoRunner. /// /// This function returns a pointer to an ArrayList containing the names of the built-in functions defined in the program. @@ -3993,7 +4192,7 @@ test "CairoRunner: initialize and run relocate trace with output builtin" { var hint_processor: HintProcessor = .{}; try cairo_runner.runUntilPC(end, &hint_processor); - try cairo_runner.relocate(); + try cairo_runner.relocate(true); // Perform assertions to check if memory relocation is correct. try expectEqualSlices( @@ -5255,3 +5454,237 @@ test "CairoRunner: endRun in proof mode with insufficient allocated cells" { // Run the program until reaching the specified PC. try cairo_runner.runUntilPC(end, &hint_processor); } + +test "CairoRunner: finalizeSegments run not ended" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + try Program.initDefault(std.testing.allocator), + "all_cairo", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + + defer cairo_runner.deinit(std.testing.allocator); + + try expectError(RunnerError.FinalizeNoEndRun, cairo_runner.finalizeSegments()); +} + +test "CairoRunner: finalizeSegments run ended empty no prog base" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + try Program.initDefault(std.testing.allocator), + "all_cairo", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + + cairo_runner.execution_base = Relocatable.init(1, 0); + cairo_runner.run_ended = true; + defer cairo_runner.deinit(std.testing.allocator); + + try expectError(RunnerError.NoProgBase, cairo_runner.finalizeSegments()); +} + +test "CairoRunner: finalizeSegments run ended empty no exec base" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + try Program.initDefault(std.testing.allocator), + "all_cairo", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + + defer cairo_runner.deinit(std.testing.allocator); + + cairo_runner.runner_mode = .proof_mode_canonical; + cairo_runner.program_base = Relocatable.init(0, 0); + cairo_runner.run_ended = true; + + try expectError(RunnerError.NoExecBase, cairo_runner.finalizeSegments()); +} + +test "CairoRunner: finalizeSegments run ended empty no proof mode" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + try Program.initDefault(std.testing.allocator), + "all_cairo", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + + defer cairo_runner.deinit(std.testing.allocator); + + cairo_runner.program_base = Relocatable.init(0, 0); + cairo_runner.execution_base = Relocatable.init(1, 0); + cairo_runner.run_ended = true; + + try expectError(RunnerError.FinalizeSegmentsNoProofMode, cairo_runner.finalizeSegments()); +} + +test "CairoRunner: finalizeSegments run ended empty empty proof mode" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + try Program.initDefault(std.testing.allocator), + "plain", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + true, + ); + + defer cairo_runner.deinit(std.testing.allocator); + + cairo_runner.program_base = Relocatable.init(0, 0); + cairo_runner.execution_base = Relocatable.init(1, 0); + cairo_runner.run_ended = true; + + try cairo_runner.finalizeSegments(); + try expectEqual(true, cairo_runner.segments_finalized); + try expectEqual(0, cairo_runner.execution_public_memory.?.items.len); +} + +test "CairoRunner: finalizeSegments run ended not emptyproof mode empty execution_public_memory" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + + program.shared_program_data.data.clearRetainingCapacity(); + try program.shared_program_data.data.appendSlice( + &val: { + var data: [8]MaybeRelocatable = undefined; + + inline for (0..data.len) |i| { + data[i] = .{ .felt = Felt252.fromInt(usize, i + 1) }; + } + + break :val data; + }, + ); + + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + program, + "plain", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + true, + ); + + defer cairo_runner.deinit(std.testing.allocator); + + cairo_runner.program_base = Relocatable.init(0, 0); + cairo_runner.execution_base = Relocatable.init(1, 0); + cairo_runner.run_ended = true; + + try cairo_runner.finalizeSegments(); + try expectEqual(true, cairo_runner.segments_finalized); + try expectEqual(8, cairo_runner.vm.segments.segment_sizes.get(0)); + + var expected = blk: { + var data: [8]std.meta.Tuple(&.{ usize, usize }) = undefined; + inline for (0..8) |i| + data[i] = .{ i, 0 }; + break :blk data; + }; + + try expectEqualSlices(std.meta.Tuple(&.{ usize, usize }), &expected, cairo_runner.vm.segments.public_memory_offsets.get(0).?.items); + + try expectEqual(null, cairo_runner.vm.segments.segment_sizes.get(1)); + try expectEqualSlices(std.meta.Tuple(&.{ usize, usize }), &.{}, cairo_runner.vm.segments.public_memory_offsets.get(1).?.items); +} + +test "CairoRunner: finalizeSegments run ended not emptyproof mode with execution_public_memory" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + + program.shared_program_data.data.clearRetainingCapacity(); + try program.shared_program_data.data.appendSlice( + &val: { + var data: [4]MaybeRelocatable = undefined; + + inline for (0..data.len) |i| { + data[i] = .{ .felt = Felt252.fromInt(usize, i + 1) }; + } + + break :val data; + }, + ); + + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + program, + "plain", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + true, + ); + + defer cairo_runner.deinit(std.testing.allocator); + + cairo_runner.program_base = Relocatable.init(0, 0); + cairo_runner.execution_base = Relocatable.init(1, 1); + try cairo_runner.execution_public_memory.?.appendSlice(&.{ 1, 3, 5, 4 }); + cairo_runner.run_ended = true; + + try cairo_runner.finalizeSegments(); + try expectEqual(true, cairo_runner.segments_finalized); + + try expectEqual(4, cairo_runner.vm.segments.segment_sizes.get(0)); + + var expected = blk: { + var data: [4]std.meta.Tuple(&.{ usize, usize }) = undefined; + inline for (0..4) |i| + data[i] = .{ i, 0 }; + break :blk data; + }; + try expectEqualSlices(std.meta.Tuple(&.{ usize, usize }), &expected, cairo_runner.vm.segments.public_memory_offsets.get(0).?.items); + + //Check values written by second call to segments.finalize() + try expectEqual(null, cairo_runner.vm.segments.segment_sizes.get(1)); + + expected = .{ .{ 2, 0 }, .{ 4, 0 }, .{ 6, 0 }, .{ 5, 0 } }; + + try expectEqualSlices(std.meta.Tuple(&.{ usize, usize }), &expected, cairo_runner.vm.segments.public_memory_offsets.get(1).?.items); +} diff --git a/src/vm/security.zig b/src/vm/security.zig new file mode 100644 index 00000000..558b423f --- /dev/null +++ b/src/vm/security.zig @@ -0,0 +1,470 @@ +const std = @import("std"); + +const cairo_runner_lib = @import("runners/cairo_runner.zig"); +const CairoVM = @import("core.zig").CairoVM; +const Program = @import("types/program.zig").Program; +const Relocatable = @import("memory/relocatable.zig").Relocatable; +const MaybeRelocatable = @import("memory/relocatable.zig").MaybeRelocatable; +const Felt252 = @import("starknet").fields.Felt252; +const errors = @import("error.zig"); + +/// Verify that the completed run in a runner is safe to be relocated and be +/// used by other Cairo programs. +/// +/// Checks include: +/// - (Only if `verify_builtins` is set to true) All accesses to the builtin segments must be within the range defined by +/// the builtins themselves. +/// - There must not be accesses to the program segment outside the program +/// data range. This check will use the `program_segment_size` instead of the program data length if available. +/// - All addresses in memory must be real (not temporary) +/// +/// Note: Each builtin is responsible for checking its own segments' data. +pub fn verifySecureRunner( + allocator: std.mem.Allocator, + runner: *cairo_runner_lib.CairoRunner, + verify_builtins: bool, + _program_segment_size: ?usize, +) !void { + const builtins_segment_info = if (verify_builtins) + try runner.getBuiltinSegmentsInfo(allocator) + else + std.ArrayList(cairo_runner_lib.BuiltinInfo).init(allocator); + defer builtins_segment_info.deinit(); + + // Check builtin segment out of bounds. + for (builtins_segment_info.items) |bi| { + const current_size = runner + .vm + .segments + .memory + .data.items[bi.segment_index].items.len; + // + 1 here accounts for maximum segment offset being segment.len() -1 + if (current_size >= bi.stop_pointer + 1) + return errors.CairoVMError.OutOfBoundsBuiltinSegmentAccess; + } + // Check out of bounds for program segment. + const program_segment_index: usize = if (runner + .program_base) |rel| @intCast(rel.segment_index) else return errors.RunnerError.NoProgBase; + + const program_segment_size = + _program_segment_size orelse runner.program.shared_program_data.data.items.len; + + const program_length = runner + .vm + .segments + .memory + .data + .items[program_segment_index].items.len; + + // + 1 here accounts for maximum segment offset being segment.len() -1 + if (program_length >= program_segment_size + 1) + return errors.CairoVMError.OutOfBoundsProgramSegmentAccess; + + // Check that the addresses in memory are valid + // This means that every temporary address has been properly relocated to a real address + // Asumption: If temporary memory is empty, this means no temporary memory addresses were generated and all addresses in memory are real + if (runner.vm.segments.memory.temp_data.items.len != 0) { + for (runner.vm.segments.memory.data.items) |segment| { + for (segment.items) |value| { + if (value.getValue()) |v| switch (v) { + .relocatable => |addr| if (addr.segment_index < 0) return errors.CairoVMError.InvalidMemoryAddress, + else => {}, + }; + } + } + } + + for (runner.vm.builtin_runners.items) |*builtin| { + try builtin.runSecurityChecks(allocator, runner.vm); + } +} + +test "Security: VerifySecureRunner without program base" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + const program = try Program.initDefault(std.testing.allocator); + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + + defer cairo_runner.deinit(std.testing.allocator); + + try std.testing.expectError(errors.RunnerError.NoProgBase, verifySecureRunner(std.testing.allocator, &cairo_runner, true, null)); +} + +test "Security: VerifySecureRunner empty memory" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + // defer vm.segments.memory.deinitData(std.testing.allocator); + + var program = try Program.initDefault(std.testing.allocator); + program.shared_program_data.main = 0; + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + _ = try cairo_runner.vm.segments.computeEffectiveSize(false); + + try verifySecureRunner(std.testing.allocator, &cairo_runner, true, null); +} + +test "Security: VerifySecureRunner program access out of bounds" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + program.shared_program_data.main = 0; + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ + .{ 0, 0 }, + .{100}, + }, + }); + + // used_sizes already empty, making arraylist equal [1] + try cairo_runner.vm.segments.segment_used_sizes.append(1); + + try std.testing.expectError(errors.CairoVMError.OutOfBoundsProgramSegmentAccess, verifySecureRunner(std.testing.allocator, &cairo_runner, true, null)); +} + +test "Security: VerifySecureRunner program with program size" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + program.shared_program_data.main = 0; + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ + .{ 0, 0 }, + .{100}, + }, + }); + + // used_sizes already empty, making arraylist equal [1] + try cairo_runner.vm.segments.segment_used_sizes.append(1); + + try verifySecureRunner(std.testing.allocator, &cairo_runner, true, 1); +} + +test "Security: VerifySecureRunner program builtin access out of bounds" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + program.shared_program_data.main = 0; + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ + .{ 0, 0 }, + .{100}, + }, + }); + + // used_sizes already empty, making arraylist equal [1] + try cairo_runner.vm.segments.segment_used_sizes.append(1); + + try verifySecureRunner(std.testing.allocator, &cairo_runner, true, 1); +} + +test "Security: VerifySecureRunner builtin access out of bounds" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + try program.builtins.append(.range_check); + program.shared_program_data.main = 0; + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + try cairo_runner.endRun(std.testing.allocator, false, false, @constCast(&.{})); + + cairo_runner.vm.builtin_runners.items[0].setStopPtr(0); + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ + .{ 2, 0 }, + .{1}, + }, + }); + + // used_sizes already empty, making arraylist equal [1] + try cairo_runner.vm.segments.segment_used_sizes.appendSlice(&.{ 0, 0, 0, 0 }); + + try std.testing.expectError( + errors.CairoVMError.OutOfBoundsBuiltinSegmentAccess, + verifySecureRunner(std.testing.allocator, &cairo_runner, true, null), + ); +} + +test "Security: VerifySecureRunner builtin access correct" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + try program.builtins.append(.range_check); + program.shared_program_data.main = 0; + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + try cairo_runner.endRun(std.testing.allocator, false, false, @constCast(&.{})); + + cairo_runner.vm.builtin_runners.items[0].setStopPtr(1); + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ + .{ 2, 0 }, + .{1}, + }, + }); + + // used_sizes already empty, making arraylist equal [1] + try cairo_runner.vm.segments.segment_used_sizes.appendSlice(&.{ 0, 0, 1, 0 }); + + try verifySecureRunner(std.testing.allocator, &cairo_runner, true, null); +} + +test "Security: VerifySecureRunner success" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + program.shared_program_data.main = 0; + try program.shared_program_data.data.appendSlice(&.{ + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + }); + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + cairo_runner.vm.segments.memory.data.clearRetainingCapacity(); + + _ = try cairo_runner.vm.segments.addSegment(); + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 0 }, .{ 1, 0 } }, + .{ .{ 0, 1 }, .{ 2, 1 } }, + .{ .{ 0, 2 }, .{ 3, 2 } }, + .{ .{ 0, 3 }, .{ 4, 3 } }, + }); + + // used_sizes already empty, making arraylist equal + try cairo_runner.vm.segments.segment_used_sizes.appendSlice(&.{ 5, 1, 2, 3, 4 }); + + try verifySecureRunner(std.testing.allocator, &cairo_runner, true, null); +} + +test "Security: VerifySecureRunner temporary memory properly relocated" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + program.shared_program_data.main = 0; + try program.shared_program_data.data.appendSlice(&.{ + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + }); + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + // clearing old memory data, to set new one + cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + cairo_runner.vm.segments.memory.data.clearRetainingCapacity(); + + _ = try cairo_runner.vm.segments.addSegment(); + // end of set + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 1 }, .{ 1, 0 } }, + .{ .{ 0, 2 }, .{ 2, 1 } }, + .{ .{ 0, 3 }, .{ 3, 2 } }, + .{ .{ -1, 0 }, .{ 1, 2 } }, + }); + + // used_sizes already empty, making arraylist equal + try cairo_runner.vm.segments.segment_used_sizes.appendSlice(&.{ 5, 1, 2, 3, 4 }); + + try verifySecureRunner(std.testing.allocator, &cairo_runner, true, null); +} + +test "Security: VerifySecureRunner temporary memory not fully relocated" { + var vm = + try CairoVM.init( + std.testing.allocator, + .{}, + ); + + var program = try Program.initDefault(std.testing.allocator); + program.shared_program_data.main = 0; + try program.shared_program_data.data.appendSlice(&.{ + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + .{ .felt = Felt252.zero() }, + }); + + var cairo_runner = try cairo_runner_lib.CairoRunner.init( + std.testing.allocator, + program, + "all_cairo", + std.ArrayList(MaybeRelocatable).init(std.testing.allocator), + &vm, + false, + ); + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + _ = try cairo_runner.setupExecutionState(false); + + // clearing old memory data, to set new one + cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + cairo_runner.vm.segments.memory.data.clearRetainingCapacity(); + + _ = try cairo_runner.vm.segments.addSegment(); + // end of set + + try cairo_runner.vm.segments.memory.setUpMemory(std.testing.allocator, .{ + .{ .{ 0, 0 }, .{ 1, 0 } }, + .{ .{ 0, 1 }, .{ 2, 1 } }, + .{ .{ 0, 2 }, .{ -3, 2 } }, + .{ .{ 0, 3 }, .{ 4, 3 } }, + .{ .{ -1, 0 }, .{ 1, 2 } }, + }); + + // used_sizes already empty, making arraylist equal + try cairo_runner.vm.segments.segment_used_sizes.appendSlice(&.{ 5, 1, 2, 3, 4 }); + + try std.testing.expectError(errors.CairoVMError.InvalidMemoryAddress, verifySecureRunner(std.testing.allocator, &cairo_runner, true, null)); +} diff --git a/src/vm/types/builtins_instance_def.zig b/src/vm/types/builtins_instance_def.zig index 3d10047a..dbbf29ca 100644 --- a/src/vm/types/builtins_instance_def.zig +++ b/src/vm/types/builtins_instance_def.zig @@ -32,6 +32,11 @@ pub const BuiltinsInstanceDef = struct { /// /// If present, contains the instance definition for the 'range_check' builtin; otherwise, it's `null`. range_check: ?RangeCheckInstanceDef = null, + /// Represents the instance of the 'range_check96' builtin. + /// + /// If present, contains the instance definition for the 'range_check' builtin; otherwise, it's `null`. + range_check96: ?RangeCheckInstanceDef = null, + /// Represents the instance of the 'ECDSA' builtin. /// /// If present, contains the instance definition for the 'ECDSA' builtin; otherwise, it's `null`. @@ -87,6 +92,7 @@ pub const BuiltinsInstanceDef = struct { .ec_op = EcOpInstanceDef.init(1024), .keccak = KeccakInstanceDef.init(2048, state_rep_keccak), .poseidon = PoseidonInstanceDef.init(256), + .range_check96 = RangeCheckInstanceDef.init(8, 6), }; } diff --git a/src/vm/types/execution_scopes.zig b/src/vm/types/execution_scopes.zig index 9544012b..27970c11 100644 --- a/src/vm/types/execution_scopes.zig +++ b/src/vm/types/execution_scopes.zig @@ -301,7 +301,7 @@ pub const ExecutionScopes = struct { } /// Deinitializes the execution scope. - pub fn deinit(self: *Self) void { + pub fn deinit(self: *const Self) void { for (self.data.items) |*m| { var it = m.valueIterator(); diff --git a/src/vm/types/layout.zig b/src/vm/types/layout.zig index df618899..77c5f79c 100644 --- a/src/vm/types/layout.zig +++ b/src/vm/types/layout.zig @@ -33,6 +33,36 @@ const expectEqual = std.testing.expectEqual; const expectError = std.testing.expectError; const expectEqualSlices = std.testing.expectEqualSlices; +pub const LayoutName = enum { + plain, + small, + dex, + recursive, + starknet, + starknet_with_keccak, + recursive_large_output, + recursive_with_poseidon, + all_solidity, + all_cairo, + dynamic, + + pub fn toString(self: @This()) []const u8 { + return switch (self) { + .plain => "plain", + .small => "small", + .dex => "dex", + .recursive => "recursive", + .starknet => "starknet", + .starknet_with_keccak => "starknet_with_keccak", + .recursive_large_output => "recursive_large_output", + .recursive_with_poseidon => "recursive_with_poseidon", + .all_solidity => "all_solidity", + .all_cairo => "all_cairo", + .dynamic => "all_cairo", + }; + } +}; + /// Represents the layout configuration for Cairo programs. pub const CairoLayout = struct { const Self = @This(); diff --git a/src/vm/types/program.zig b/src/vm/types/program.zig index 8b1457bd..30efdb22 100644 --- a/src/vm/types/program.zig +++ b/src/vm/types/program.zig @@ -322,7 +322,7 @@ pub const SharedProgramData = struct { // Iterate through each instruction location. while (it.next()) |kv| { // Check if the parent_location_instruction exists. - if (instruction_locations.getPtr(kv.key_ptr.*).?.inst.parent_location_instruction) |*p| { + if (kv.value_ptr.inst.parent_location_instruction) |*p| { // Retrieve and remove the first element of the list. var it_list = p.popFirst(); diff --git a/src/vm/types/programjson.zig b/src/vm/types/programjson.zig index f6d9d12f..4b3f370b 100644 --- a/src/vm/types/programjson.zig +++ b/src/vm/types/programjson.zig @@ -25,6 +25,8 @@ pub const BuiltinName = enum { output, /// Represents the range check builtin. range_check, + /// Represents the range check 96 builtin. + range_check96, /// Represents the Pedersen builtin. pedersen, /// Represents the ECDSA builtin.