From e2a3dc544e6c24199705e480218a46849bdb684e Mon Sep 17 00:00:00 2001 From: Nikita Orlov Date: Thu, 29 Feb 2024 13:21:41 +0100 Subject: [PATCH] Feat/check range check usage (#412) * builtin runner methods * , merge conflict * temp code * change back imports * fix merge conflicts * fmt after merge --- .../builtin_runner/builtin_runner.zig | 103 +++++++++++++++--- src/vm/builtins/builtin_runner/ec_op.zig | 2 +- src/vm/builtins/builtin_runner/output.zig | 2 +- src/vm/error.zig | 8 ++ src/vm/run_context.zig | 2 +- src/vm/runners/cairo_runner.zig | 65 ++++++++++- 6 files changed, 160 insertions(+), 22 deletions(-) diff --git a/src/vm/builtins/builtin_runner/builtin_runner.zig b/src/vm/builtins/builtin_runner/builtin_runner.zig index b847c318..a0d4be3d 100644 --- a/src/vm/builtins/builtin_runner/builtin_runner.zig +++ b/src/vm/builtins/builtin_runner/builtin_runner.zig @@ -3,18 +3,19 @@ const Allocator = std.mem.Allocator; const Tuple = std.meta.Tuple; const MemorySegmentManager = @import("../../memory/segments.zig").MemorySegmentManager; -const CairoVM = @import("../../../vm/core.zig").CairoVM; -const MemoryError = @import("../../../vm/error.zig").MemoryError; -const InsufficientAllocatedCellsError = @import("../../../vm/error.zig").InsufficientAllocatedCellsError; -const BitwiseBuiltinRunner = @import("./bitwise.zig").BitwiseBuiltinRunner; -const EcOpBuiltinRunner = @import("./ec_op.zig").EcOpBuiltinRunner; -const HashBuiltinRunner = @import("./hash.zig").HashBuiltinRunner; -const KeccakBuiltinRunner = @import("./keccak.zig").KeccakBuiltinRunner; -const OutputBuiltinRunner = @import("./output.zig").OutputBuiltinRunner; -const PoseidonBuiltinRunner = @import("./poseidon.zig").PoseidonBuiltinRunner; -const RangeCheckBuiltinRunner = @import("./range_check.zig").RangeCheckBuiltinRunner; -const SegmentArenaBuiltinRunner = @import("./segment_arena.zig").SegmentArenaBuiltinRunner; -const SignatureBuiltinRunner = @import("./signature.zig").SignatureBuiltinRunner; +const BitwiseBuiltinRunner = @import("bitwise.zig").BitwiseBuiltinRunner; +const EcOpBuiltinRunner = @import("ec_op.zig").EcOpBuiltinRunner; +const HashBuiltinRunner = @import("hash.zig").HashBuiltinRunner; +const KeccakBuiltinRunner = @import("keccak.zig").KeccakBuiltinRunner; +const OutputBuiltinRunner = @import("output.zig").OutputBuiltinRunner; +const PoseidonBuiltinRunner = @import("poseidon.zig").PoseidonBuiltinRunner; +const RangeCheckBuiltinRunner = @import("range_check.zig").RangeCheckBuiltinRunner; +const SegmentArenaBuiltinRunner = @import("segment_arena.zig").SegmentArenaBuiltinRunner; +const SignatureBuiltinRunner = @import("signature.zig").SignatureBuiltinRunner; + +const InsufficientAllocatedCellsError = @import("../../error.zig").InsufficientAllocatedCellsError; +const MemoryError = @import("../../error.zig").MemoryError; +const CairoVM = @import("../../core.zig").CairoVM; const Relocatable = @import("../../memory/relocatable.zig").Relocatable; const MaybeRelocatable = @import("../../memory/relocatable.zig").MaybeRelocatable; const Memory = @import("../../memory/memory.zig").Memory; @@ -317,7 +318,7 @@ pub const BuiltinRunner = union(BuiltinName) { /// # Returns /// /// The total number of used memory cells as a `usize`, or an error if calculation fails. - pub fn getUsedCells(self: *Self, segments: *MemorySegmentManager) !usize { + pub fn getUsedCells(self: *const Self, segments: *MemorySegmentManager) !usize { return switch (self.*) { .SegmentArena => 0, inline else => |*builtin| try builtin.getUsedCells(segments), @@ -342,7 +343,7 @@ pub const BuiltinRunner = union(BuiltinName) { /// /// Returns an error if any error occurs during the computation. /// - pub fn getAllocatedMemoryUnits(self: *Self, vm: *CairoVM) !usize { + pub fn getAllocatedMemoryUnits(self: *const Self, vm: *CairoVM) !usize { switch (self.*) { // For Output and SegmentArena built-in runners, return 0 as they do not allocate memory units .Output, .SegmentArena => return 0, @@ -401,7 +402,7 @@ pub const BuiltinRunner = union(BuiltinName) { /// /// This function is used to determine the usage and allocation status of memory cells associated /// with the specific type of built-in runner. - pub fn getUsedCellsAndAllocatedSize(self: *Self, vm: *CairoVM) !Tuple(&.{ usize, ?usize }) { + pub fn getUsedCellsAndAllocatedSize(self: *const Self, vm: *CairoVM) !Tuple(&.{ usize, ?usize }) { switch (self.*) { // For output and segment arena built-in runners .Output, .SegmentArena => { @@ -442,7 +443,7 @@ pub const BuiltinRunner = union(BuiltinName) { /// /// This function is used to determine the usage of permanent range check units associated /// with the specific type of built-in runner. - pub fn getUsedPermRangeCheckUnits(self: *Self, vm: *CairoVM) !usize { + pub fn getUsedPermRangeCheckUnits(self: *const Self, vm: *CairoVM) !usize { return switch (self.*) { // For range check built-in runners .RangeCheck => |range_check| { @@ -519,7 +520,7 @@ pub const BuiltinRunner = union(BuiltinName) { /// # Returns /// /// A `usize` representing the number of instances per component for the built-in runner. - pub fn getInstancesPerComponent(self: *Self) u32 { + pub fn getInstancesPerComponent(self: *const Self) u32 { return switch (self.*) { .SegmentArena, .Output => 1, inline else => |*builtin| builtin.instances_per_component, @@ -945,6 +946,26 @@ test "BuiltinRunner: getMemoryAccesses with missing segment used sizes" { ); } +test "BuiltinRunner: getAllocataedMemoryUnits Output" { + var builtin = BuiltinRunner{ + .Output = OutputBuiltinRunner.init(std.testing.allocator, true), + }; + defer builtin.deinit(); + + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + // Ensure Cairo VM is properly deallocated at the end of the test + defer vm.deinit(); + + // Expecting an error of type MemoryError.MissingSegmentUsedSizes + try expectError( + MemoryError.MissingSegmentUsedSizes, + builtin.getMemoryAccesses(std.testing.allocator, &vm), + ); +} + test "BuiltinRunner: getMemoryAccesses with empty access" { // Initialize Cairo VM var vm = try CairoVM.init( @@ -969,6 +990,54 @@ test "BuiltinRunner: getMemoryAccesses with empty access" { try expect(actual.items.len == 0); } +test "BuiltinRunner: getAllocataedMemoryUnits Keccak" { + var def = try KeccakInstanceDef.initDefault(std.testing.allocator); + defer def.deinit(); + + var builtin = BuiltinRunner{ + .Keccak = try KeccakBuiltinRunner.init(std.testing.allocator, &def, true), + }; + defer builtin.deinit(); + + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + vm.current_step = 32768; + try std.testing.expectEqual(256, try builtin.getAllocatedMemoryUnits(&vm)); +} + +test "BuiltinRunner: getAllocataedMemoryUnits Bitwise" { + var builtin = BuiltinRunner{ + .Bitwise = BitwiseBuiltinRunner.init(&.{}, true), + }; + defer builtin.deinit(); + + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + vm.current_step = 256; + try std.testing.expectEqual(5, try builtin.getAllocatedMemoryUnits(&vm)); +} + +test "BuiltinRunner: getAllocataedMemoryUnits RangeCheck" { + var builtin = BuiltinRunner{ + .RangeCheck = RangeCheckBuiltinRunner.init(8, 8, true), + }; + defer builtin.deinit(); + + var vm = try CairoVM.init( + std.testing.allocator, + .{}, + ); + defer vm.deinit(); + vm.current_step = 8; + try std.testing.expectEqual(1, try builtin.getAllocatedMemoryUnits(&vm)); +} + test "BuiltinRunner: getMemoryAccesses with real data" { // Initialize Cairo VM var vm = try CairoVM.init(std.testing.allocator, .{}); diff --git a/src/vm/builtins/builtin_runner/ec_op.zig b/src/vm/builtins/builtin_runner/ec_op.zig index 284c940e..2f4c4176 100644 --- a/src/vm/builtins/builtin_runner/ec_op.zig +++ b/src/vm/builtins/builtin_runner/ec_op.zig @@ -198,7 +198,7 @@ pub const EcOpBuiltinRunner = struct { /// /// The number of used cells as a `u32`, or `MemoryError.MissingSegmentUsedSizes` if /// the size is not available. - pub fn getUsedCells(self: *Self, segments: *MemorySegmentManager) !u32 { + pub fn getUsedCells(self: *const Self, segments: *MemorySegmentManager) !u32 { return segments.getSegmentUsedSize( @intCast(self.base), ) orelse MemoryError.MissingSegmentUsedSizes; diff --git a/src/vm/builtins/builtin_runner/output.zig b/src/vm/builtins/builtin_runner/output.zig index 8c412ca5..b80452a5 100644 --- a/src/vm/builtins/builtin_runner/output.zig +++ b/src/vm/builtins/builtin_runner/output.zig @@ -149,7 +149,7 @@ pub const OutputBuiltinRunner = struct { /// /// The count of used memory cells associated with the OutputBuiltinRunner's base address. /// If the information is unavailable, it returns MemoryError.MissingSegmentUsedSizes. - pub fn getUsedCells(self: *Self, segments: *MemorySegmentManager) !u32 { + pub fn getUsedCells(self: *const Self, segments: *MemorySegmentManager) !u32 { return segments.getSegmentUsedSize( @intCast(self.base), ) orelse MemoryError.MissingSegmentUsedSizes; diff --git a/src/vm/error.zig b/src/vm/error.zig index c02e4c07..9e6152c0 100644 --- a/src/vm/error.zig +++ b/src/vm/error.zig @@ -281,6 +281,14 @@ pub const HintError = error{ }; pub const InsufficientAllocatedCellsError = error{ + // Number of steps must be at least for some builtin MinStepNotReached, + // The builtin used cells but the capacity is wrong BuiltinCells, + // There are only cells to fill the range checks holes, but potentially are required. + RangeCheckUnits, + // There are only cells to fill the diluted check holes, but potentially are required + DilutedCells, + // There are only cells to fill the memory address holes, but are required. + MemoryAddresses, }; diff --git a/src/vm/run_context.zig b/src/vm/run_context.zig index 73997dda..70bbc1ad 100644 --- a/src/vm/run_context.zig +++ b/src/vm/run_context.zig @@ -158,7 +158,7 @@ pub const RunContext = struct { return if (instruction.off_2 < 0) // Convert i16 to u64 safely and then negate - try base_addr.subUint(@intCast(-instruction.off_2)) + try base_addr.subUint(@abs(instruction.off_2)) else // Convert i16 to u64 safely return try base_addr.addUint(@intCast(instruction.off_2)); diff --git a/src/vm/runners/cairo_runner.zig b/src/vm/runners/cairo_runner.zig index 39253ada..d0b6504b 100644 --- a/src/vm/runners/cairo_runner.zig +++ b/src/vm/runners/cairo_runner.zig @@ -23,6 +23,7 @@ const Attribute = @import("../types/programjson.zig").Attribute; const ReferenceManager = @import("../types/programjson.zig").ReferenceManager; const CairoRunnerError = @import("../error.zig").CairoRunnerError; 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 trace_context = @import("../trace_context.zig"); @@ -652,6 +653,22 @@ pub const CairoRunner = struct { return builtin_segment_info; } + + /// Checks that there are enough trace cells to fill the entire range check + /// range. + pub fn checkRangeCheckUsage(self: *Self, allocator: Allocator, vm: *CairoVM) !void { + const rc_min_max = (try self.getPermRangeCheckLimits(allocator)) orelse return; + var rc_units_used_by_builtins: usize = 0; + + for (vm.builtin_runners.items) |runner| + rc_units_used_by_builtins = rc_units_used_by_builtins + try runner.getUsedPermRangeCheckUnits(vm); + + const unused_rc_units = (@as(usize, self.layout.rc_units) - 3) * vm.current_step - rc_units_used_by_builtins; + + if (unused_rc_units < @as(usize, @intCast(rc_min_max[1] - rc_min_max[0]))) + return InsufficientAllocatedCellsError.RangeCheckUnits; + } + /// Retrieves the number of memory holes in the CairoRunner's virtual machine (VM) segments. /// /// Memory holes are regions of memory that are unused or uninitialized. @@ -683,6 +700,7 @@ pub const CairoRunner = struct { ); } + /// Retrieves the permanent range check limits from the CairoRunner instance. /// /// This function iterates through the builtin runners of the CairoRunner and gathers @@ -1424,6 +1442,51 @@ test "CairoRunner: getPermRangeCheckLimits with null range limit" { ); } +test "CairoRunner: checkRangeCheckUsage perm range limits none" { + // Initialize a CairoRunner with an empty program, "plain" layout, and instructions. + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + try Program.initDefault(std.testing.allocator, true), + "plain", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + try CairoVM.init( + std.testing.allocator, + .{}, + ), + false, + ); + + // Defer the deinitialization of the CairoRunner to ensure cleanup. + defer cairo_runner.deinit(std.testing.allocator); + try cairo_runner.checkRangeCheckUsage(std.testing.allocator, &cairo_runner.vm); +} + +test "CairoRunner: checkRangeCheckUsage without builtins" { + // Initialize a CairoRunner with an empty program, "plain" layout, and instructions. + var cairo_runner = try CairoRunner.init( + std.testing.allocator, + try Program.initDefault(std.testing.allocator, true), + "plain", + ArrayList(MaybeRelocatable).init(std.testing.allocator), + try CairoVM.init( + std.testing.allocator, + .{}, + ), + false, + ); + cairo_runner.vm.current_step = 10000; + + var segm = std.ArrayListUnmanaged(?MemoryCell){}; + try segm.append(std.testing.allocator, MemoryCell.init(MaybeRelocatable.fromFelt(Felt252.fromInt(u256, 0x80FF80000530)))); + + try cairo_runner.vm.segments.memory.data.append(segm); + + // Defer the deinitialization of the CairoRunner to ensure cleanup. + defer cairo_runner.deinit(std.testing.allocator); + defer cairo_runner.vm.segments.memory.deinitData(std.testing.allocator); + + try cairo_runner.checkRangeCheckUsage(std.testing.allocator, &cairo_runner.vm); +} test "CairoRunner: get constants" { // Initialize a default program with built-ins enabled using the testing allocator. var program = try Program.initDefault(std.testing.allocator, true); @@ -1445,7 +1508,6 @@ test "CairoRunner: get constants" { ), false, ); - // Defer the deinitialization of the CairoRunner object to ensure cleanup after the test. defer cairo_runner.deinit(std.testing.allocator); @@ -1477,7 +1539,6 @@ test "CairoRunner: initBuiltins missing builtins allow missing" { ), false, ); - // Defer the deinitialization of the CairoRunner to ensure cleanup. defer cairo_runner.deinit(std.testing.allocator);