def test_nonpure_mul(): code = """ [ap] = [ap - 1] * 2; ap++ """ with pytest.raises(VmException, match='Could not complete computation *'): run_single(code, 1, ap=102, extra_mem={101: RelocatableValue(1, 0)})
def run_single(code: str, steps: int, *, pc=RelocatableValue(0, 10), ap=100, fp=100, extra_mem={}): program = compile_cairo(code, PRIME, debug_info=True) # Set memory[fp - 1] to an arbitrary value, since [fp - 1] is assumed to be set. memory: Dict[MaybeRelocatable, MaybeRelocatable] = { **{pc + i: v for i, v in enumerate(program.data)}, fp - 1: 1234, **extra_mem } context = RunContext( pc=pc, ap=ap, fp=fp, memory=MemoryDict(memory), prime=PRIME, ) vm = VirtualMachine(program, context, {}) for _ in range(steps): vm.step() return vm
def test_cairo_pie_memory_negative_address(cairo_pie: CairoPie): # Write to a negative address. cairo_pie.memory.set_without_checks(RelocatableValue( segment_index=cairo_pie.metadata.program_segment.index, offset=-5), 0) with pytest.raises( AssertionError, match='Invalid memory cell address.'): cairo_pie.run_validity_checks()
def test_nonpure_jmp_rel(): code = """ jmp rel [ap - 1] """ with pytest.raises(VmException, match='Could not complete computation jmp rel'): run_single(code, 1, ap=102, extra_mem={101: RelocatableValue(1, 0)})
def test_cairo_pie_memory_invalid_address(cairo_pie: CairoPie): # Write to an invalid address. cairo_pie.memory.unfreeze_for_testing() cairo_pie.memory[RelocatableValue( segment_index=cairo_pie.metadata.ret_pc_segment.index, offset=0)] = 0 with pytest.raises( AssertionError, match='Invalid memory cell address.'): cairo_pie.run_validity_checks()
def test_memory_dict_get(): md = MemoryDict({14: 15}) assert md.get(14, 'default') == 15 assert md.get(1234, 'default') == 'default' with pytest.raises(ValueError, match='must be nonnegative'): md.get(-10, 'default') # Attempting to read address with a negative offset is ok, it simply returns None. assert md.get(RelocatableValue(segment_index=10, offset=-2)) is None
def test_validated_memory_dict(): memory = MemoryDict() memory_validator = ValidatedMemoryDict(memory=memory) def rule_identical_pairs(mem, addr): """ Validates that the values in address pairs (i, i+1), where i is even, are identical. """ offset_diff = (-1) ** (addr.offset % 2) other_addr = RelocatableValue.from_tuple((addr.segment_index, addr.offset + offset_diff)) if other_addr in mem: assert mem[addr] == mem[other_addr] return {addr, other_addr} return set() def rule_constant_value(mem, addr, constant): assert mem[addr] == constant, \ f'Expected value in address {addr} to be {constant}, got {mem[addr]}.' return {addr} memory_validator.add_validation_rule(1, lambda memory, addr: set()) memory_validator.add_validation_rule(2, lambda memory, addr: {addr}) memory_validator.add_validation_rule(3, rule_identical_pairs) memory_validator.add_validation_rule(4, rule_constant_value, 0) addr0 = RelocatableValue.from_tuple((1, 0)) addr1 = RelocatableValue.from_tuple((2, 0)) addr2 = RelocatableValue.from_tuple((3, 0)) addr3 = RelocatableValue.from_tuple((3, 1)) addr4 = RelocatableValue.from_tuple((4, 0)) # Test validated_addresses update. memory_validator[addr0] = 0 assert memory_validator._ValidatedMemoryDict__validated_addresses == set() memory_validator[addr1] = 0 assert memory_validator._ValidatedMemoryDict__validated_addresses == {addr1} # Test validation rule application. memory_validator[addr2] = 1 assert memory_validator._ValidatedMemoryDict__validated_addresses == {addr1} memory_validator[addr3] = 1 assert memory_validator._ValidatedMemoryDict__validated_addresses == {addr1, addr2, addr3} with pytest.raises( AssertionError, match='Expected value in address 4:0 to be 0, got 1.'): memory_validator[addr4] = 1
def rule_identical_pairs(mem, addr): """ Validates that the values in address pairs (i, i+1), where i is even, are identical. """ offset_diff = (-1) ** (addr.offset % 2) other_addr = RelocatableValue.from_tuple((addr.segment_index, addr.offset + offset_diff)) if other_addr in mem: assert mem[addr] == mem[other_addr] return {addr, other_addr} return set()
def test_segment_relocation_failures(): memory = MemoryDict() relocation_target = RelocatableValue(segment_index=4, offset=25) with pytest.raises(AssertionError, match='src_ptr.segment_index must be < 0, src_ptr=1:2.'): memory.add_relocation_rule(src_ptr=RelocatableValue( segment_index=1, offset=2), dest_ptr=relocation_target) with pytest.raises(AssertionError, match='src_ptr.offset must be 0, src_ptr=-3:2.'): memory.add_relocation_rule(src_ptr=RelocatableValue( segment_index=-3, offset=2), dest_ptr=relocation_target) memory.add_relocation_rule(src_ptr=RelocatableValue( segment_index=-3, offset=0), dest_ptr=relocation_target) with pytest.raises( AssertionError, match='The segment with index -3 already has a relocation rule.'): memory.add_relocation_rule(src_ptr=RelocatableValue( segment_index=-3, offset=0), dest_ptr=relocation_target)
def add(self, size: Optional[int] = None) -> RelocatableValue: """ Adds a new segment and returns its starting location as a RelocatableValue. If size is not None the segment is finalized with the given size. """ segment_index = self.n_segments self.n_segments += 1 if size is not None: self.finalize(segment_index, size) return RelocatableValue(segment_index=segment_index, offset=0)
def test_negative_address(): runner = run_code_in_runner(""" main: [ap] = 0; ap++ ret """) # Access negative offset manually, so it is not taken modulo prime. runner.vm_memory.set_without_checks( RelocatableValue(segment_index=0, offset=-17), 0) with pytest.raises(SecurityError, match='Accessed address 0:-17 has negative offset.'): verify_secure_runner(runner)
def test_pages(runner_and_output_runner): """ Tests the add_page() functionality. """ runner, output_builtin_runner, base = runner_and_output_runner for i in range(15): runner.vm_memory[base + i] = i # Add two pages, with page_id 1 and 3. output_builtin_runner.add_page(page_id=1, page_start=base + 3, page_size=4) output_builtin_runner.add_page(page_id=3, page_start=base + 9, page_size=3) # page_start must be in the output segment (base). with pytest.raises(AssertionError, match='page_start must be in the output segment'): output_builtin_runner.add_page(page_id=4, page_start=RelocatableValue(999, 999), page_size=3) runner.end_run() runner.finalize_segments() # A list of output cells and their page id. offset_page_pairs = [ (0, 0), (1, 0), (2, 0), (3, 1), (4, 1), (5, 1), (6, 1), (7, 0), (8, 0), (9, 3), (10, 3), (11, 3), (12, 0), (13, 0), (14, 0), ] assert runner.segments.public_memory_offsets[base.segment_index] == \ offset_page_pairs # Check that get_public_memory_addresses() returns the correct page_id for each value. # The program and execution segments are always in page 0. segment_offsets = {0: 0, 1: 10, 2: 100} assert runner.segments.get_public_memory_addresses( segment_offsets=segment_offsets) == ( [(i, 0) for i in range(len(runner.program.data))] + # Program segment. [(10, 0), (11, 0), (12, 0)] + # Execution segment. [(100 + offset, page_id) for offset, page_id in offset_page_pairs]) # Output segment.
def test_memory_dict_setdefault(): memory = MemoryDict({14: 15}) memory.setdefault(14, 0) assert memory[14] == 15 memory.setdefault(123, 456) assert memory[123] == 456 with pytest.raises(ValueError, match='must be an int'): memory.setdefault(10, 'default') with pytest.raises(KeyError, match='must be nonnegative'): memory.setdefault(-10, 123) with pytest.raises(ValueError, match='The offset of a relocatable value must be nonnegative'): memory[RelocatableValue(segment_index=10, offset=-2)] = 13
def test_cairo_pie_serialize_deserialize(): program = compile_cairo( code=[('%builtins output pedersen range_check ecdsa\nmain:\n[ap] = [ap]\n', '')], prime=DEFAULT_PRIME) metadata = CairoPieMetadata( program=program.stripped(), program_segment=SegmentInfo(0, 10), execution_segment=SegmentInfo(1, 20), ret_fp_segment=SegmentInfo(6, 12), ret_pc_segment=SegmentInfo(7, 21), builtin_segments={ 'a': SegmentInfo(4, 15), }, extra_segments=[], ) memory = MemoryDict({ 1: 2, RelocatableValue(3, 4): RelocatableValue(6, 7), }) additional_data = {'c': ['d', 3]} execution_resources = ExecutionResources( n_steps=10, n_memory_holes=7, builtin_instance_counter={ 'output': 6, 'pedersen': 3, } ) cairo_pie = CairoPie( metadata=metadata, memory=memory, additional_data=additional_data, execution_resources=execution_resources ) fileobj = io.BytesIO() cairo_pie.to_file(fileobj) actual_cairo_pie = CairoPie.from_file(fileobj) assert cairo_pie == actual_cairo_pie
def add_temp_segment(self) -> RelocatableValue: """ Adds a new temporary segment and returns its starting location as a RelocatableValue. A temporary segment is a segment that is relocated using memory.add_relocation_rule() before the Cairo PIE is produced. """ self.n_temp_segments += 1 # Temporary segments have negative segment indices that start from -1. segment_index = -self.n_temp_segments return RelocatableValue(segment_index=segment_index, offset=0)
def test_auto_deduction_rules(): code = """ [fp + 1] = [fp] + [ap] """ program = compile_cairo(code=code, prime=PRIME, debug_info=True) memory = {i: v for i, v in enumerate(program.data)} initial_ap = RelocatableValue(segment_index=1, offset=200) initial_fp = RelocatableValue(segment_index=2, offset=100) context = RunContext( pc=0, ap=initial_ap, fp=initial_fp, memory=MemoryDict(memory), prime=PRIME, ) vm = VirtualMachine(program, context, {}) def rule_ap_segment(vm, addr, val): return val vm.add_auto_deduction_rule(1, rule_ap_segment, 100) vm.add_auto_deduction_rule(2, lambda vm, addr: None) vm.add_auto_deduction_rule( 2, lambda vm, addr: 200 if addr == initial_fp else None) vm.add_auto_deduction_rule(2, lambda vm, addr: 456) vm.step() assert vm.run_context.memory[initial_ap] == 100 assert vm.run_context.memory[initial_fp] == 200 assert vm.run_context.memory[initial_fp + 1] == 300 with pytest.raises(InconsistentAutoDeductionError, match='at address 2:100. 200 != 456'): vm.verify_auto_deductions()
def test_jnz_relocatables(offset: int): code = """ jmp body if [ap - 1] != 0 [ap] = 0; ap++ body: [ap] = 1; ap++ """ relocatable_value = RelocatableValue(segment_index=5, offset=offset) error_message = \ None if relocatable_value.offset >= 0 else \ f'Could not complete computation jmp != 0 of non pure value {relocatable_value}' with maybe_raises(expected_exception=VmException, error_message=error_message): vm = run_single(code, 2, ap=102, extra_mem={101: relocatable_value}) assert vm.run_context.memory[102] == 1
def test_cairo_pie_memory_invalid_value(cairo_pie: CairoPie): # Write a value after the execution segment. output_end = RelocatableValue( segment_index=cairo_pie.metadata.execution_segment.index, offset=cairo_pie.metadata.execution_segment.size) cairo_pie.memory[output_end] = output_end + 2 # It should fail because the address is outside the segment expected size. with pytest.raises( AssertionError, match='Invalid memory cell address.'): cairo_pie.run_validity_checks() # Increase the size. cairo_pie.metadata.execution_segment.size += 1 # Now it should fail because of the value. with pytest.raises(AssertionError, match='Invalid memory cell value.'): cairo_pie.run_validity_checks()
def test_get_segment_used_size(): memory = MemoryDict({ RelocatableValue(0, 0): 0, RelocatableValue(0, 2): 0, RelocatableValue(1, 5): 0, RelocatableValue(1, 7): 0, RelocatableValue(3, 0): 0, RelocatableValue(4, 1): 0, }) assert [get_segment_used_size(i, memory) for i in range(5)] == [3, 8, 0, 1, 2]
def get_program_output(cairo_pie: CairoPie) -> List[int]: """ Returns the program output. """ assert 'output' in cairo_pie.metadata.builtin_segments, 'The output builtin must be used.' output = cairo_pie.metadata.builtin_segments['output'] def verify_int(x: MaybeRelocatable) -> int: assert isinstance(x, int), \ f'Expected program output to contain absolute values, found: {x}.' return x return [ verify_int(cairo_pie.memory[RelocatableValue( segment_index=output.index, offset=i)]) for i in range(output.size) ]
def test_relocatable_operations(): x = RelocatableValue(1, 2) y = 3 assert x + y == RelocatableValue(1, 5) assert x - y == RelocatableValue(1, -1) assert (x + y) - x == y assert RelocatableValue(1, 101) % 10 == RelocatableValue(1, 1) with pytest.raises(TypeError): x * y with pytest.raises(AssertionError): x + x with pytest.raises(AssertionError): RelocatableValue(1, 2) - RelocatableValue(2, 2)
def test_get_segment_used_size(): memory = MemoryDict({ RelocatableValue(0, 0): 0, RelocatableValue(0, 2): 0, RelocatableValue(1, 5): 0, RelocatableValue(1, 7): 0, RelocatableValue(3, 0): 0, RelocatableValue(4, 1): 0, }) segments = MemorySegmentManager(memory=memory, prime=PRIME) segments.n_segments = 5 memory.freeze() segments.compute_effective_sizes() assert [segments.get_segment_used_size(i) for i in range(5)] == [3, 8, 0, 1, 2]
def test_relocate_segments(): segments = MemorySegmentManager(memory={}, prime=PRIME) for i in range(5): assert segments.add() == RelocatableValue(segment_index=i, offset=0) segment_sizes = [3, 8, 0, 1, 2] public_memory_offsets = [ [(0, 0), (1, 1)], [(i, 0) for i in range(8)], [], [], [(1, 2)], ] for i, (size, public_memory) in enumerate(zip(segment_sizes, public_memory_offsets)): segments.finalize(i, size=size, public_memory=public_memory) segment_offsets = segments.relocate_segments() assert segment_offsets == {0: 0, 1: 3, 2: 11, 3: 11, 4: 12} assert segments.get_public_memory_addresses(segment_offsets) == [ (0, 0), (1, 1), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0), (8, 0), (9, 0), (10, 0), (13, 2)]
def test_memory_validation_in_hints(): code = """ %{ memory[ap] = 0 %} [ap] = [ap]; ap++ %{ memory[ap] = 0 %} [ap] = [ap]; ap++ """ program = compile_cairo(code=code, prime=PRIME, debug_info=True) initial_ap_and_fp = RelocatableValue(segment_index=1, offset=200) memory = {i: v for i, v in enumerate(program.data)} # Set memory[fp - 1] to an arbitrary value, since [fp - 1] is assumed to be set. memory[initial_ap_and_fp - 1] = 1234 context = RunContext( pc=0, ap=initial_ap_and_fp, fp=initial_ap_and_fp, memory=MemoryDict(memory), prime=PRIME, ) vm = VirtualMachine(program, context, {}) vm.add_validation_rule(1, lambda memory, addr: {addr}) assert vm.validated_memory._ValidatedMemoryDict__validated_addresses == set( ) vm.step() assert vm.validated_memory._ValidatedMemoryDict__validated_addresses == { initial_ap_and_fp } def fail_validation(memory, addr): raise Exception('Validation failed.') vm.add_validation_rule(1, fail_validation) with pytest.raises(VmException, match='Exception: Validation failed.'): vm.step()
def get_additional_data(self): return [[list(RelocatableValue.to_tuple(addr)), signature] for addr, signature in sorted(self.signatures.items())]
def test_relocatable_value_frozen(): x = RelocatableValue(1, 2) with pytest.raises(dataclasses.FrozenInstanceError, match="cannot assign to field 'no_such_field'"): x.no_such_field = 5
def test_relocatable_value_serialization(byte_order, n_bytes): for num in [19, RelocatableValue(2, 5)]: assert RelocatableValue.from_bytes( RelocatableValue.to_bytes(num, n_bytes, byte_order), byte_order) == num
def get_segment(self, segment_info: SegmentInfo): return self.memory.get_range(RelocatableValue( segment_index=segment_info.index, offset=0), size=segment_info.size)
def extend_additional_data(self, data, relocate_callback): for addr, signature in data: self.signatures[relocate_callback(RelocatableValue.from_tuple(addr))] = signature
def serialize(self, field_bytes): return b''.join( RelocatableValue.to_bytes(addr, ADDR_SIZE_IN_BYTES, 'little') + RelocatableValue.to_bytes(value, field_bytes, 'little') for addr, value in self.items())