diff --git a/.cairofmtignore b/.cairofmtignore new file mode 100644 index 0000000..52d9fd6 --- /dev/null +++ b/.cairofmtignore @@ -0,0 +1 @@ +cairo/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 2851cbf..ba62b9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ "crates/delegator", "crates/executor", "crates/peer", - "crates/prover", + "crates/prover", "crates/runner", ] exclude = [] @@ -28,18 +28,21 @@ futures-core = "0.3.30" futures-util = "0.3.30" hex = "0.4.3" itertools = "0.12.1" -libp2p = { version = "0.53.2", features = ["tokio","gossipsub","kad","mdns","noise","macros","tcp","yamux","quic"]} +libp2p = { version = "0.53.2", features = ["secp256k1", "tokio","gossipsub","kad","mdns","noise","macros","tcp","yamux","quic"]} +libsecp256k1 = "0.7.1" num-bigint = "0.4.4" serde = "1.0.197" serde_json = "1.0.115" -starknet = "0.10.0" +starknet = "0.9.0" +strum = { version = "0.26", features = ["derive"] } tempfile = "3.10.1" thiserror = "1.0.58" tokio = { version = "1.36", features = ["full"] } tokio-util = "0.7.10" tracing = "0.1.37" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -cairo1-run = { git = "https://github.com/iosis-tech/cairo-vm", rev = "091e1b3acf62a6e6baa9784105ae1125231b29e7"} +zip-extensions = "0.6.2" + sharp-p2p-common = { path = "crates/common" } sharp-p2p-delegator = { path = "crates/delegator" } diff --git a/cairo/.gitignore b/cairo/.gitignore new file mode 100644 index 0000000..03d56cb --- /dev/null +++ b/cairo/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +!/lang/compiler/lib/ \ No newline at end of file diff --git a/cairo/bootloader/__init__.py b/cairo/bootloader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cairo/bootloader/hash_program.py b/cairo/bootloader/hash_program.py new file mode 100644 index 0000000..de59519 --- /dev/null +++ b/cairo/bootloader/hash_program.py @@ -0,0 +1,55 @@ +import argparse +import json + +from starkware.cairo.common.hash_chain import compute_hash_chain +from starkware.cairo.lang.compiler.program import Program, ProgramBase +from starkware.cairo.lang.version import __version__ +from starkware.cairo.lang.vm.crypto import get_crypto_lib_context_manager, poseidon_hash_many +from starkware.python.utils import from_bytes + + +def compute_program_hash_chain(program: ProgramBase, use_poseidon: bool, bootloader_version=0): + """ + Computes a hash chain over a program, including the length of the data chain. + """ + builtin_list = [from_bytes(builtin.encode("ascii")) for builtin in program.builtins] + # The program header below is missing the data length, which is later added to the data_chain. + program_header = [bootloader_version, program.main, len(program.builtins)] + builtin_list + data_chain = program_header + program.data + + if use_poseidon: + return poseidon_hash_many(data_chain) + return compute_hash_chain([len(data_chain)] + data_chain) + + +def main(): + parser = argparse.ArgumentParser(description="A tool to compute the hash of a cairo program") + parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}") + parser.add_argument( + "--program", + type=argparse.FileType("r"), + required=True, + help="The name of the program json file.", + ) + parser.add_argument( + "--flavor", + type=str, + default="Release", + choices=["Debug", "Release", "RelWithDebInfo"], + help="Build flavor", + ) + parser.add_argument( + "--use_poseidon", + type=bool, + default=False, + help="Use Poseidon hash.", + ) + args = parser.parse_args() + + with get_crypto_lib_context_manager(args.flavor): + program = Program.Schema().load(json.load(args.program)) + print(hex(compute_program_hash_chain(program=program, use_poseidon=args.use_poseidon))) + + +if __name__ == "__main__": + main() diff --git a/cairo/bootloader/objects.py b/cairo/bootloader/objects.py new file mode 100644 index 0000000..ada6173 --- /dev/null +++ b/cairo/bootloader/objects.py @@ -0,0 +1,97 @@ +import dataclasses +from abc import abstractmethod +from dataclasses import field +from typing import ClassVar, Dict, List, Optional, Type + +import marshmallow +import marshmallow.fields as mfields +import marshmallow_dataclass +from marshmallow_oneofschema import OneOfSchema + +from starkware.cairo.lang.compiler.program import Program, ProgramBase, StrippedProgram +from starkware.cairo.lang.vm.cairo_pie import CairoPie +from starkware.starkware_utils.marshmallow_dataclass_fields import additional_metadata +from starkware.starkware_utils.validated_dataclass import ValidatedMarshmallowDataclass + + +class TaskSpec(ValidatedMarshmallowDataclass): + """ + Contains task's specification. + """ + + @abstractmethod + def load_task(self) -> "Task": + """ + Returns the corresponding task. + """ + + +class Task: + @abstractmethod + def get_program(self) -> ProgramBase: + """ + Returns the task's Cairo program. + """ + + +@marshmallow_dataclass.dataclass(frozen=True) +class RunProgramTask(TaskSpec, Task): + TYPE: ClassVar[str] = "RunProgramTask" + program: Program + program_input: dict + use_poseidon: bool + + def get_program(self) -> Program: + return self.program + + def load_task(self) -> "Task": + return self + + +@marshmallow_dataclass.dataclass(frozen=True) +class CairoPiePath(TaskSpec): + TYPE: ClassVar[str] = "CairoPiePath" + path: str + use_poseidon: bool + + def load_task(self) -> "CairoPieTask": + """ + Loads the PIE to memory. + """ + return CairoPieTask(cairo_pie=CairoPie.from_file(self.path), use_poseidon=self.use_poseidon) + + +class TaskSchema(OneOfSchema): + """ + Schema for Task/CairoPiePath. + OneOfSchema adds a "type" field. + """ + + type_schemas: Dict[str, Type[marshmallow.Schema]] = { + RunProgramTask.TYPE: RunProgramTask.Schema, + CairoPiePath.TYPE: CairoPiePath.Schema, + } + + def get_obj_type(self, obj): + return obj.TYPE + + +@dataclasses.dataclass(frozen=True) +class CairoPieTask(Task): + cairo_pie: CairoPie + use_poseidon: bool + + def get_program(self) -> StrippedProgram: + return self.cairo_pie.program + + +@marshmallow_dataclass.dataclass(frozen=True) +class SimpleBootloaderInput(ValidatedMarshmallowDataclass): + tasks: List[TaskSpec] = field( + metadata=additional_metadata(marshmallow_field=mfields.List(mfields.Nested(TaskSchema))) + ) + fact_topologies_path: Optional[str] + + # If true, the bootloader will put all the outputs in a single page, ignoring the + # tasks' fact topologies. + single_page: bool \ No newline at end of file diff --git a/cairo/bootloader/recursive_with_poseidon/__init__.py b/cairo/bootloader/recursive_with_poseidon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cairo/bootloader/recursive_with_poseidon/builtins.py b/cairo/bootloader/recursive_with_poseidon/builtins.py new file mode 100644 index 0000000..bb4b337 --- /dev/null +++ b/cairo/bootloader/recursive_with_poseidon/builtins.py @@ -0,0 +1,11 @@ +from starkware.cairo.lang.builtins.all_builtins import * + +ALL_BUILTINS = BuiltinList( + [ + OUTPUT_BUILTIN, + PEDERSEN_BUILTIN, + RANGE_CHECK_BUILTIN, + BITWISE_BUILTIN, + POSEIDON_BUILTIN, + ] +) \ No newline at end of file diff --git a/cairo/bootloader/recursive_with_poseidon/execute_task.cairo b/cairo/bootloader/recursive_with_poseidon/execute_task.cairo new file mode 100644 index 0000000..ee1d1e5 --- /dev/null +++ b/cairo/bootloader/recursive_with_poseidon/execute_task.cairo @@ -0,0 +1,253 @@ +from builtin_selection.inner_select_builtins import inner_select_builtins +from builtin_selection.select_input_builtins import select_input_builtins +from builtin_selection.validate_builtins import validate_builtins +from common.builtin_poseidon.poseidon import PoseidonBuiltin, poseidon_hash_many +from common.cairo_builtins import HashBuiltin +from common.hash_chain import hash_chain +from common.registers import get_ap, get_fp_and_pc + +const BOOTLOADER_VERSION = 0; + +// Use an empty struct to encode an arbitrary-length array. +struct BuiltinList { +} + +struct ProgramHeader { + // The data length field specifies the length of the data (i.e., program header + program) + // and guarantees unique decoding of the program hash. + data_length: felt, + bootloader_version: felt, + program_main: felt, + n_builtins: felt, + // 'builtin_list' is a continuous memory segment containing the ASCII encoding of the (ordered) + // builtins used by the program. + builtin_list: BuiltinList, +} + +struct BuiltinData { + output: felt, + pedersen: felt, + range_check: felt, + bitwise: felt, + poseidon: felt, +} + +// Computes the hash of a program. +// Arguments: +// * program_data_ptr - the pointer to the program to be hashed. +// * use_poseidon - a flag that determines whether the hashing will use Poseidon hash. +// Return values: +// * hash - the computed program hash. +func compute_program_hash{pedersen_ptr: HashBuiltin*, poseidon_ptr: PoseidonBuiltin*}( + program_data_ptr: felt*, use_poseidon: felt +) -> (hash: felt) { + if (use_poseidon == 1) { + let (hash) = poseidon_hash_many{poseidon_ptr=poseidon_ptr}( + n=program_data_ptr[0], elements=&program_data_ptr[1] + ); + return (hash=hash); + } else { + let (hash) = hash_chain{hash_ptr=pedersen_ptr}(data_ptr=program_data_ptr); + return (hash=hash); + } +} + +// Executes a single task. +// The task is passed in the 'task' hint variable. +// Outputs of the task are prefixed by: +// a. Output size (including this prefix) +// b. hash_chain(ProgramHeader || task.program.data) where ProgramHeader is defined below. +// The function returns a pointer to the updated builtin pointers after executing the task. +func execute_task{builtin_ptrs: BuiltinData*, self_range_check_ptr}( + builtin_encodings: BuiltinData*, builtin_instance_sizes: BuiltinData*, use_poseidon: felt +) { + // Allocate memory for local variables. + alloc_locals; + + // Get the value of fp. + let (local __fp__, _) = get_fp_and_pc(); + + // Pointer to the program data (which starts with ProgramHeader). + local program_data_ptr: felt*; + %{ ids.program_data_ptr = program_data_base = segments.add() %} + + // The struct of input builtin pointers pointed by the given builtin_ptrs. + let input_builtin_ptrs: BuiltinData* = builtin_ptrs; + local output_ptr = input_builtin_ptrs.output; + + let program_header = cast(program_data_ptr, ProgramHeader*); + %{ + from bootloader.utils import load_program + + # Call load_program to load the program header and code to memory. + program_address, program_data_size = load_program( + task=task, memory=memory, program_header=ids.program_header, + builtins_offset=ids.ProgramHeader.builtin_list) + segments.finalize(program_data_base.segment_index, program_data_size) + %} + + // Verify that the bootloader version is compatible with the bootloader. + assert program_header.bootloader_version = BOOTLOADER_VERSION; + + // Call hash_chain, to verify the program hash. + let pedersen_ptr = cast(input_builtin_ptrs.pedersen, HashBuiltin*); + let poseidon_ptr = cast(input_builtin_ptrs.poseidon, PoseidonBuiltin*); + with pedersen_ptr, poseidon_ptr { + let (hash) = compute_program_hash( + program_data_ptr=program_data_ptr, use_poseidon=use_poseidon + ); + } + + // Write hash_chain result to output_ptr + 1. + assert [output_ptr + 1] = hash; + %{ + # Validate hash. + from starkware.cairo.bootloaders.hash_program import compute_program_hash_chain + + assert memory[ids.output_ptr + 1] == compute_program_hash_chain( + program=task.get_program(), + use_poseidon=bool(ids.use_poseidon)), 'Computed hash does not match input.' + %} + + // Set the program entry point, so the bootloader can later run the program. + local builtin_list: felt* = &program_header.builtin_list; + local n_builtins = program_header.n_builtins; + tempvar program_address = builtin_list + n_builtins; + %{ + # Sanity check. + assert ids.program_address == program_address + %} + tempvar program_main = program_header.program_main; + // The address in memory where the main function of the task is loaded. + local program_entry_point: felt* = program_address + program_main; + + // Fill in all builtin pointers which may be used by the task. + // Skip the 2 slots prefix that we add to the task output. + local pre_execution_builtin_ptrs: BuiltinData = BuiltinData( + output=output_ptr + 2, + pedersen=cast(pedersen_ptr, felt), + range_check=input_builtin_ptrs.range_check, + bitwise=input_builtin_ptrs.bitwise, + poseidon=cast(poseidon_ptr, felt), + ); + + // Call select_input_builtins to get the relevant input builtin pointers for the task. + select_input_builtins( + all_encodings=builtin_encodings, + all_ptrs=&pre_execution_builtin_ptrs, + n_all_builtins=BuiltinData.SIZE, + selected_encodings=builtin_list, + n_selected_builtins=n_builtins, + ); + + call_task: + %{ + from bootloader.objects import ( + CairoPieTask, + RunProgramTask, + Task, + ) + from bootloader.utils import ( + load_cairo_pie, + prepare_output_runner, + ) + + assert isinstance(task, Task) + n_builtins = len(task.get_program().builtins) + new_task_locals = {} + if isinstance(task, RunProgramTask): + new_task_locals['program_input'] = task.program_input + new_task_locals['WITH_BOOTLOADER'] = True + + vm_load_program(task.program, program_address) + elif isinstance(task, CairoPieTask): + ret_pc = ids.ret_pc_label.instruction_offset_ - ids.call_task.instruction_offset_ + pc + load_cairo_pie( + task=task.cairo_pie, memory=memory, segments=segments, + program_address=program_address, execution_segment_address= ap - n_builtins, + builtin_runners=builtin_runners, ret_fp=fp, ret_pc=ret_pc) + else: + raise NotImplementedError(f'Unexpected task type: {type(task).__name__}.') + + output_runner_data = prepare_output_runner( + task=task, + output_builtin=output_builtin, + output_ptr=ids.pre_execution_builtin_ptrs.output) + vm_enter_scope(new_task_locals) + %} + + // Call the inner program's main() function. + call abs program_entry_point; + + ret_pc_label: + %{ + vm_exit_scope() + # Note that bootloader_input will only be available in the next hint. + %} + + // Note that used_builtins_addr cannot be set in a hint because doing so will allow a malicious + // prover to lie about the outputs of a valid program. + let (ap_val) = get_ap(); + local used_builtins_addr: felt* = cast(ap_val - n_builtins, felt*); + + // Call inner_select_builtins to validate that the values of the builtin pointers for the next + // task are updated according to the task return builtin pointers. + + // Allocate a struct containing all builtin pointers just after the program returns. + local return_builtin_ptrs: BuiltinData; + %{ + from bootloader.recursive_with_poseidon.builtins import ALL_BUILTINS + from bootloader.utils import write_return_builtins + + # Fill the values of all builtin pointers after executing the task. + builtins = task.get_program().builtins + write_return_builtins( + memory=memory, return_builtins_addr=ids.return_builtin_ptrs.address_, + used_builtins=builtins, used_builtins_addr=ids.used_builtins_addr, + pre_execution_builtins_addr=ids.pre_execution_builtin_ptrs.address_, task=task, all_builtins=ALL_BUILTINS) + + vm_enter_scope({'n_selected_builtins': n_builtins}) + %} + let select_builtins_ret = inner_select_builtins( + all_encodings=builtin_encodings, + all_ptrs=&return_builtin_ptrs, + selected_encodings=builtin_list, + selected_ptrs=used_builtins_addr, + n_builtins=BuiltinData.SIZE, + ); + %{ vm_exit_scope() %} + + // Assert that the correct number of builtins was selected. + // Note that builtin_list is a pointer to the list containing the selected encodings. + assert n_builtins = select_builtins_ret.selected_encodings_end - builtin_list; + + // Call validate_builtins to validate that the builtin pointers have advanced correctly. + validate_builtins{range_check_ptr=self_range_check_ptr}( + prev_builtin_ptrs=&pre_execution_builtin_ptrs, + new_builtin_ptrs=&return_builtin_ptrs, + builtin_instance_sizes=builtin_instance_sizes, + n_builtins=BuiltinData.SIZE, + ); + + // Verify that [output_ptr] = return_builtin_ptrs.output - output_ptr. + // Output size should be 2 + the number of output slots that were consumed by the task. + local output_size = return_builtin_ptrs.output - output_ptr; + assert [output_ptr] = output_size; + + %{ + from bootloader.utils import get_task_fact_topology + + # Add the fact topology of the current task to 'fact_topologies'. + output_start = ids.pre_execution_builtin_ptrs.output + output_end = ids.return_builtin_ptrs.output + fact_topologies.append(get_task_fact_topology( + output_size=output_end - output_start, + task=task, + output_builtin=output_builtin, + output_runner_data=output_runner_data, + )) + %} + + let builtin_ptrs = &return_builtin_ptrs; + return (); +} \ No newline at end of file diff --git a/cairo/bootloader/recursive_with_poseidon/run_simple_bootloader.cairo b/cairo/bootloader/recursive_with_poseidon/run_simple_bootloader.cairo new file mode 100644 index 0000000..83aa8d7 --- /dev/null +++ b/cairo/bootloader/recursive_with_poseidon/run_simple_bootloader.cairo @@ -0,0 +1,160 @@ +from bootloader.recursive_with_poseidon.execute_task import BuiltinData, execute_task +from common.cairo_builtins import HashBuiltin, PoseidonBuiltin +from common.registers import get_fp_and_pc + +// Loads the programs and executes them. +// +// Hint Arguments: +// simple_bootloader_input - contains the tasks to execute. +// +// Returns: +// Updated builtin pointers after executing all programs. +// fact_topologies - that corresponds to the tasks (hint variable). +func run_simple_bootloader{ + output_ptr: felt*, + pedersen_ptr: HashBuiltin*, + range_check_ptr, + bitwise_ptr, + poseidon_ptr: PoseidonBuiltin*, +}() { + alloc_locals; + local task_range_check_ptr; + + %{ + n_tasks = len(simple_bootloader_input.tasks) + memory[ids.output_ptr] = n_tasks + + # Task range checks are located right after simple bootloader validation range checks, and + # this is validated later in this function. + ids.task_range_check_ptr = ids.range_check_ptr + ids.BuiltinData.SIZE * n_tasks + + # A list of fact_toplogies that instruct how to generate the fact from the program output + # for each task. + fact_topologies = [] + %} + + let n_tasks = [output_ptr]; + let output_ptr = output_ptr + 1; + + // A struct containing the pointer to each builtin. + local builtin_ptrs_before: BuiltinData = BuiltinData( + output=cast(output_ptr, felt), + pedersen=cast(pedersen_ptr, felt), + range_check=task_range_check_ptr, + bitwise=bitwise_ptr, + poseidon=cast(poseidon_ptr, felt), + ); + + // A struct containing the encoding of each builtin. + local builtin_encodings: BuiltinData = BuiltinData( + output='output', + pedersen='pedersen', + range_check='range_check', + bitwise='bitwise', + poseidon='poseidon', + ); + + local builtin_instance_sizes: BuiltinData = BuiltinData( + output=1, + pedersen=3, + range_check=1, + bitwise=5, + poseidon=6, + ); + + // Call execute_tasks. + let (__fp__, _) = get_fp_and_pc(); + + %{ tasks = simple_bootloader_input.tasks %} + let builtin_ptrs = &builtin_ptrs_before; + let self_range_check_ptr = range_check_ptr; + with builtin_ptrs, self_range_check_ptr { + execute_tasks( + builtin_encodings=&builtin_encodings, + builtin_instance_sizes=&builtin_instance_sizes, + n_tasks=n_tasks, + ); + } + + // Verify that the task range checks appear after the self range checks of execute_task. + assert self_range_check_ptr = task_range_check_ptr; + + // Return the updated builtin pointers. + local builtin_ptrs: BuiltinData* = builtin_ptrs; + let output_ptr = cast(builtin_ptrs.output, felt*); + let pedersen_ptr = cast(builtin_ptrs.pedersen, HashBuiltin*); + let range_check_ptr = builtin_ptrs.range_check; + let bitwise_ptr = builtin_ptrs.bitwise; + let poseidon_ptr = cast(builtin_ptrs.poseidon, PoseidonBuiltin*); + + // 'execute_tasks' runs untrusted code and uses the range_check builtin to verify that + // the builtin pointers were advanced correctly by said code. + // Since range_check itself is used for the verification, we cannot assume that the verification + // above is sound unless we know that the self range checks that were used during verification + // are indeed valid (that is, within the segment of the range_check builtin). + // Following the Cairo calling convention, we can guarantee the validity of the self range + // checks by making sure that range_check_ptr >= self_range_check_ptr. + // The following check validates that the inequality above holds without using the range check + // builtin. + let additional_range_checks = range_check_ptr - self_range_check_ptr; + verify_non_negative(num=additional_range_checks, n_bits=64); + + return (); +} + +// Verifies that a field element is in the range [0, 2^n_bits), without relying on the range_check +// builtin. +func verify_non_negative(num: felt, n_bits: felt) { + if (n_bits == 0) { + assert num = 0; + return (); + } + + tempvar num_div2 = nondet %{ ids.num // 2 %}; + tempvar bit = num - (num_div2 + num_div2); + // Check that bit is 0 or 1. + assert bit = bit * bit; + return verify_non_negative(num=num_div2, n_bits=n_bits - 1); +} + +// Executes the last n_tasks from simple_bootloader_input.tasks. +// +// Arguments: +// builtin_encodings - String encodings of the builtins. +// builtin_instance_sizes - Mapping to builtin sizes. +// n_tasks - The number of tasks to execute. +// +// Implicit arguments: +// builtin_ptrs - Pointer to the builtin pointers before/after executing the tasks. +// self_range_check_ptr - range_check pointer (used for validating the builtins). +// +// Hint arguments: +// tasks - A list of tasks to execute. +func execute_tasks{builtin_ptrs: BuiltinData*, self_range_check_ptr}( + builtin_encodings: BuiltinData*, builtin_instance_sizes: BuiltinData*, n_tasks: felt +) { + if (n_tasks == 0) { + return (); + } + + %{ + from bootloader.objects import Task + + # Pass current task to execute_task. + task_id = len(simple_bootloader_input.tasks) - ids.n_tasks + task = simple_bootloader_input.tasks[task_id].load_task() + %} + tempvar use_poseidon = nondet %{ 1 if task.use_poseidon else 0 %}; + // Call execute_task to execute the current task. + execute_task( + builtin_encodings=builtin_encodings, + builtin_instance_sizes=builtin_instance_sizes, + use_poseidon=use_poseidon, + ); + + return execute_tasks( + builtin_encodings=builtin_encodings, + builtin_instance_sizes=builtin_instance_sizes, + n_tasks=n_tasks - 1, + ); +} \ No newline at end of file diff --git a/cairo/bootloader/recursive_with_poseidon/simple_bootloader.cairo b/cairo/bootloader/recursive_with_poseidon/simple_bootloader.cairo new file mode 100644 index 0000000..efec7e5 --- /dev/null +++ b/cairo/bootloader/recursive_with_poseidon/simple_bootloader.cairo @@ -0,0 +1,48 @@ +%builtins output pedersen range_check bitwise poseidon + +from bootloader.recursive_with_poseidon.run_simple_bootloader import ( + run_simple_bootloader, +) +from common.cairo_builtins import HashBuiltin, PoseidonBuiltin +from common.registers import get_fp_and_pc + +func main{ + output_ptr: felt*, + pedersen_ptr: HashBuiltin*, + range_check_ptr, + bitwise_ptr, + poseidon_ptr: PoseidonBuiltin*, +}() { + %{ + from bootloader.objects import SimpleBootloaderInput + simple_bootloader_input = SimpleBootloaderInput.Schema().load(program_input) + %} + + // Execute tasks. + run_simple_bootloader(); + + %{ + # Dump fact topologies to a json file. + from bootloader.utils import ( + configure_fact_topologies, + write_to_fact_topologies_file, + ) + + # The task-related output is prefixed by a single word that contains the number of tasks. + tasks_output_start = output_builtin.base + 1 + + if not simple_bootloader_input.single_page: + # Configure the memory pages in the output builtin, based on fact_topologies. + configure_fact_topologies( + fact_topologies=fact_topologies, output_start=tasks_output_start, + output_builtin=output_builtin, + ) + + if simple_bootloader_input.fact_topologies_path is not None: + write_to_fact_topologies_file( + fact_topologies_path=simple_bootloader_input.fact_topologies_path, + fact_topologies=fact_topologies, + ) + %} + return (); +} \ No newline at end of file diff --git a/cairo/bootloader/utils.py b/cairo/bootloader/utils.py new file mode 100644 index 0000000..2adf4b5 --- /dev/null +++ b/cairo/bootloader/utils.py @@ -0,0 +1,295 @@ +import json +import os +from typing import Any, List, Union + +import aiofiles + +from starkware.cairo.bootloaders.fact_topology import ( + FactTopologiesFile, + FactTopology, + get_fact_topology_from_additional_data, +) +from bootloader.objects import CairoPieTask, RunProgramTask, Task +from starkware.cairo.common.hash_state import compute_hash_on_elements +from starkware.cairo.lang.compiler.program import Program +from starkware.cairo.lang.vm.cairo_pie import CairoPie, ExecutionResources +from starkware.cairo.lang.vm.output_builtin_runner import OutputBuiltinRunner +from starkware.cairo.lang.vm.relocatable import MaybeRelocatable, RelocatableValue, relocate_value +from starkware.python.utils import WriteOnceDict, from_bytes + +SIMPLE_BOOTLOADER_COMPILED_PATH = os.path.join( + os.path.dirname(__file__), "simple_bootloader_compiled.json" +) + +# Upper bounds on the numbers of builtin instances and steps that the simple_bootloader uses. +SIMPLE_BOOTLOADER_N_OUTPUT = 2 +SIMPLE_BOOTLOADER_N_PEDERSEN = 20 +SIMPLE_BOOTLOADER_N_RANGE_CHECKS = 20 +SIMPLE_BOOTLOADER_N_STEPS_CONSTANT = 400 +SIMPLE_BOOTLOADER_N_STEPS_RATIO = 8 + + +async def get_simple_bootloader_program_json() -> str: + async with aiofiles.open(SIMPLE_BOOTLOADER_COMPILED_PATH, "r") as file: + return json.loads(await file.read()) + + +async def get_simple_bootloader_program() -> Program: + async with aiofiles.open(SIMPLE_BOOTLOADER_COMPILED_PATH, "r") as file: + return Program.Schema().loads(await file.read()) + + +async def get_simple_bootloader_program_hash() -> int: + """ + Returns the hash of the simple bootloader program. Matches the Cairo verifier's expected simple + bootloader hash. + """ + simple_bootloader_program: Program = await get_simple_bootloader_program() + return compute_hash_on_elements(data=simple_bootloader_program.data) + + +def load_program(task: Task, memory, program_header, builtins_offset): + """ + Fills the memory with the following: + 1. program header. + 2. program code. + Returns the program address and the size of the written program data. + """ + + builtins = task.get_program().builtins + n_builtins = len(builtins) + program_data = task.get_program().data + + # Fill in the program header. + header_address = program_header.address_ + # The program header ends with a list of builtins used by the program. + header_size = builtins_offset + n_builtins + # data_length does not include the data_length header field in the calculation. + program_header.data_length = (header_size - 1) + len(program_data) + program_header.program_main = task.get_program().main + program_header.n_builtins = n_builtins + # Fill in the builtin list in memory. + builtins_address = header_address + builtins_offset + for index, builtin in enumerate(builtins): + assert isinstance(builtin, str) + memory[builtins_address + index] = from_bytes(builtin.encode("ascii")) + + # Fill in the program code in memory. + program_address = header_address + header_size + for index, opcode in enumerate(program_data): + memory[program_address + index] = opcode + + return program_address, header_size + len(program_data) + + +def write_return_builtins( + memory, + return_builtins_addr, + used_builtins, + used_builtins_addr, + pre_execution_builtins_addr, + task, + all_builtins +): + """ + Writes the updated builtin pointers after the program execution to the given return builtins + address. + used_builtins is the list of builtins used by the program and thus updated by it. + """ + + used_builtin_offset = 0 + for index, builtin in enumerate(all_builtins): + if builtin in used_builtins: + memory[return_builtins_addr + index] = memory[used_builtins_addr + used_builtin_offset] + used_builtin_offset += 1 + + if isinstance(task, CairoPie): + assert task.metadata.builtin_segments[builtin].size == ( + memory[return_builtins_addr + index] + - memory[pre_execution_builtins_addr + index] + ), "Builtin usage is inconsistent with the CairoPie." + else: + # The builtin is unused, hence its value is the same as before calling the program. + memory[return_builtins_addr + index] = memory[pre_execution_builtins_addr + index] + + +def load_cairo_pie( + task: CairoPie, + memory, + segments, + program_address, + execution_segment_address, + builtin_runners, + ret_fp, + ret_pc, +): + """ + Load memory entries of the inner program. + This replaces executing hints in a non-trusted program. + """ + segment_offsets = WriteOnceDict() + + segment_offsets[task.metadata.program_segment.index] = program_address + segment_offsets[task.metadata.execution_segment.index] = execution_segment_address + segment_offsets[task.metadata.ret_fp_segment.index] = ret_fp + segment_offsets[task.metadata.ret_pc_segment.index] = ret_pc + + def extract_segment(value: MaybeRelocatable, value_name: str): + """ + Returns the segment index for the given value. + Verifies that value is a RelocatableValue with offset 0. + """ + assert isinstance(value, RelocatableValue), f"{value_name} is not relocatable." + assert value.offset == 0, f"{value_name} has a non-zero offset." + return value.segment_index + + orig_execution_segment = RelocatableValue( + segment_index=task.metadata.execution_segment.index, offset=0 + ) + + # Set initial stack relocations. + for idx, name in enumerate(task.program.builtins): + segment_offsets[ + extract_segment( + value=task.memory[orig_execution_segment + idx], + value_name=f"{name} builtin start address", + ) + ] = memory[execution_segment_address + idx] + + for segment_info in task.metadata.extra_segments: + segment_offsets[segment_info.index] = segments.add(size=segment_info.size) + + def local_relocate_value(value): + return relocate_value(value, segment_offsets, task.program.prime) + + # Relocate builtin additional data. + # This should occur before the memory relocation, since the signature builtin assumes that a + # signature is added before the corresponding public key and message are both written to memory. + esdsa_additional_data = task.additional_data.get("ecdsa_builtin") + if esdsa_additional_data is not None: + ecdsa_builtin = builtin_runners.get("ecdsa_builtin") + assert ecdsa_builtin is not None, "The task requires the ecdsa builtin but it is missing." + ecdsa_builtin.extend_additional_data(esdsa_additional_data, local_relocate_value) + + for addr, val in task.memory.items(): + memory[local_relocate_value(addr)] = local_relocate_value(val) + + +def prepare_output_runner( + task: Task, output_builtin: OutputBuiltinRunner, output_ptr: RelocatableValue +): + """ + Prepares the output builtin if the type of task is Task, so that pages of the inner program + will be recorded separately. + If the type of task is CairoPie, nothing should be done, as the program does not contain + hints that may affect the output builtin. + The return value of this function should be later passed to get_task_fact_topology(). + """ + + if isinstance(task, RunProgramTask): + output_state = output_builtin.get_state() + output_builtin.new_state(base=output_ptr) + return output_state + elif isinstance(task, CairoPieTask): + return None + else: + raise NotImplementedError(f"Unexpected task type: {type(task).__name__}.") + + +def get_task_fact_topology( + output_size: int, + task: Union[RunProgramTask, CairoPie], + output_builtin: OutputBuiltinRunner, + output_runner_data: Any, +) -> FactTopology: + """ + Returns the fact_topology that corresponds to 'task'. Restores output builtin state if 'task' is + a RunProgramTask. + """ + + # Obtain the fact_toplogy of 'task'. + if isinstance(task, RunProgramTask): + assert output_runner_data is not None + fact_topology = get_fact_topology_from_additional_data( + output_size=output_size, + output_builtin_additional_data=output_builtin.get_additional_data(), + ) + # Restore the output builtin runner to its original state. + output_builtin.set_state(output_runner_data) + elif isinstance(task, CairoPieTask): + assert output_runner_data is None + fact_topology = get_fact_topology_from_additional_data( + output_size=output_size, + output_builtin_additional_data=task.cairo_pie.additional_data["output_builtin"], + ) + else: + raise NotImplementedError(f"Unexpected task type: {type(task).__name__}.") + + return fact_topology + + +def add_consecutive_output_pages( + fact_topology: FactTopology, + output_builtin: OutputBuiltinRunner, + cur_page_id: int, + output_start: MaybeRelocatable, +) -> int: + offset = 0 + for i, page_size in enumerate(fact_topology.page_sizes): + output_builtin.add_page( + page_id=cur_page_id + i, page_start=output_start + offset, page_size=page_size + ) + offset += page_size + + return len(fact_topology.page_sizes) + + +def configure_fact_topologies( + fact_topologies: List[FactTopology], + output_start: MaybeRelocatable, + output_builtin: OutputBuiltinRunner, +): + """ + Given the fact_topologies of the tasks that were run by bootloader, configure the + corresponding pages in the output builtin. Assumes that the bootloader output 2 words per task. + """ + # Each task may use a few memory pages. Start from page 1 (as page 0 is reserved for the + # bootloader program and arguments). + cur_page_id = 1 + for fact_topology in fact_topologies: + # Skip bootloader output for each task. + output_start += 2 + cur_page_id += add_consecutive_output_pages( + fact_topology=fact_topology, + output_builtin=output_builtin, + cur_page_id=cur_page_id, + output_start=output_start, + ) + output_start += sum(fact_topology.page_sizes) + + +def write_to_fact_topologies_file(fact_topologies_path: str, fact_topologies: List[FactTopology]): + with open(fact_topologies_path, "w") as fp: + json.dump( + FactTopologiesFile.Schema().dump(FactTopologiesFile(fact_topologies=fact_topologies)), + fp, + indent=4, + sort_keys=True, + ) + fp.write("\n") + + +def calc_simple_bootloader_execution_resources(program_length: int) -> ExecutionResources: + """ + Returns an upper bound on the number of steps and builtin instances that the simple bootloader + uses. + """ + n_steps = SIMPLE_BOOTLOADER_N_STEPS_RATIO * program_length + SIMPLE_BOOTLOADER_N_STEPS_CONSTANT + builtin_instance_counter = { + "pedersen_builtin": SIMPLE_BOOTLOADER_N_PEDERSEN + program_length, + "range_check_builtin": SIMPLE_BOOTLOADER_N_RANGE_CHECKS, + "output_builtin": SIMPLE_BOOTLOADER_N_OUTPUT, + } + return ExecutionResources( + n_steps=n_steps, builtin_instance_counter=builtin_instance_counter, n_memory_holes=0 + ) \ No newline at end of file diff --git a/cairo/builtin_selection/__init__.py b/cairo/builtin_selection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cairo/builtin_selection/inner_select_builtins.cairo b/cairo/builtin_selection/inner_select_builtins.cairo new file mode 100644 index 0000000..3861579 --- /dev/null +++ b/cairo/builtin_selection/inner_select_builtins.cairo @@ -0,0 +1,71 @@ +// An helper function to extract selected_ptrs from all_ptrs according to the builtin encodings +// that appear in the selected_encodings list. +// The caller needs to pass n_selected_builtins as a hint. +// Returns a pointer to the next memory slot after the selected_encodings list, see "Assumptions". +// +// For example, given the following setup: +// - all_encodings points to ["output", "pedersen", "range-check"]. +// - selected_encodings points to ["output", "range-check"] +// - all_ptrs points to [output_ptr, pedersen_ptr, range_check_ptr] +// - The caller asserts that the return value is selected_encodings + n_selected_builtins(2). +// The function will check that selected_encodings points to [output_ptr, range_check_ptr]. +// +// n_builtins is the length of the list of *all pointers*. +// Assumptions: +// * The caller has to check that n_selected_builtins = selected_encodings_end - selected_encodings. +// * All lists are sorted according to the order of builtins input in Cairo programs. +// * len(selected_encodings) <= len(all_encodings) == len(all_ptrs). +func inner_select_builtins( + all_encodings: felt*, + all_ptrs: felt*, + selected_encodings: felt*, + selected_ptrs: felt*, + n_builtins, +) -> (selected_encodings_end: felt*) { + // Number of memory cells used when n_builtins = 0. + const FUNC_MEMORY_NO_BUILTINS = 1; + // Number of memory cells used *in a single iteration* when n_builtins > 0. + const FUNC_MEMORY_WITH_BUILTINS = 10; + + if (n_builtins == 0) { + // Return a pointer to the end of the selected_encodings list. + return (selected_encodings_end=selected_encodings); + } + + alloc_locals; + // select_builtin equals 1 if the first builtin should be selected and 0 otherwise. + local select_builtin; + %{ + # A builtin should be selected iff its encoding appears in the selected encodings list + # and the list wasn't exhausted. + # Note that testing inclusion by a single comparison is possible since the lists are sorted. + ids.select_builtin = int( + n_selected_builtins > 0 and memory[ids.selected_encodings] == memory[ids.all_encodings]) + if ids.select_builtin: + n_selected_builtins = n_selected_builtins - 1 + %} + // Verify that select_builtin is a bit. + select_builtin = select_builtin * select_builtin; + + local curr_builtin_encoding = [all_encodings]; + local curr_builtin_ptr = [all_ptrs]; + + if (select_builtin != 0) { + // Verify that the current builtin is indeed selected, by asserting that its encoding + // appears in the selected encodings list. + curr_builtin_encoding = [selected_encodings]; + // Copy the current builtin pointer between selected_ptrs and all_ptrs. + curr_builtin_ptr = [selected_ptrs]; + } + + // Advance all list pointers accordingly and continue selection by calling inner_select_builtins + // recursively. + // Lists of selected builtins/encodings should advance only if the current builtin was selected. + return inner_select_builtins( + all_encodings=all_encodings + 1, + all_ptrs=all_ptrs + 1, + selected_encodings=selected_encodings + select_builtin, + selected_ptrs=selected_ptrs + select_builtin, + n_builtins=n_builtins - 1, + ); +} diff --git a/cairo/builtin_selection/select_input_builtins.cairo b/cairo/builtin_selection/select_input_builtins.cairo new file mode 100644 index 0000000..f8a8d96 --- /dev/null +++ b/cairo/builtin_selection/select_input_builtins.cairo @@ -0,0 +1,40 @@ +from builtin_selection.inner_select_builtins import inner_select_builtins +from common.registers import get_fp_and_pc + +// A wrapper for 'inner_select_builtins' function (see its documentation). +// Returns the selected builtin pointers (e.g., if n_selected_builtins=2, returns two values). +func select_input_builtins( + all_encodings: felt*, + all_ptrs: felt*, + n_all_builtins: felt, + selected_encodings: felt*, + n_selected_builtins, +) { + // Number of memory cells used, without taking the inner function memory into account. + const FUNC_MEMORY_WITHOUT_INNER_FUNC = 11; + const INNER_FUNC_MEMORY_PER_ITERATION = inner_select_builtins.FUNC_MEMORY_WITH_BUILTINS; + const INNER_FUNC_MEMORY_FINAL_ITERATION = inner_select_builtins.FUNC_MEMORY_NO_BUILTINS; + // 'inner_select_builtins' has n_all_builtins iterations, until the final halting one, when + // called with n_builtins = n_all_builtins. + let inner_func_memory = n_all_builtins * INNER_FUNC_MEMORY_PER_ITERATION + + INNER_FUNC_MEMORY_FINAL_ITERATION; + let total_func_memory = inner_func_memory + FUNC_MEMORY_WITHOUT_INNER_FUNC; + + let frame = call get_fp_and_pc; + // The selected builtin pointers are the return values at the end of the function memory. + let selected_ptrs = frame.fp_val + total_func_memory; + %{ vm_enter_scope({'n_selected_builtins': ids.n_selected_builtins}) %} + let inner_ret = inner_select_builtins( + all_encodings=all_encodings, + all_ptrs=all_ptrs, + selected_encodings=selected_encodings, + selected_ptrs=selected_ptrs, + n_builtins=n_all_builtins, + ); + %{ vm_exit_scope() %} + // Assert that the correct number of builtins was selected. + n_selected_builtins = inner_ret.selected_encodings_end - selected_encodings; + + ap += n_selected_builtins; + ret; +} diff --git a/cairo/builtin_selection/validate_builtins.cairo b/cairo/builtin_selection/validate_builtins.cairo new file mode 100644 index 0000000..790a5fd --- /dev/null +++ b/cairo/builtin_selection/validate_builtins.cairo @@ -0,0 +1,56 @@ +// Validates that the builtin pointer of a single builtin was advanced correctly. +// The inputs are: +// The previous builtin pointer. +// The new builtin pointer. +// The size of the builtin instances. +// The function validates that the difference between the new builtin pointer and the old builtin +// pointer is a positive integer divisible by the given builtin instance size. +// +// The function consumes 1 range check instance starting at range_check_ptr and returns the +// updated range check pointer. +func validate_builtin{range_check_ptr}( + prev_builtin_ptr: felt*, new_builtin_ptr: felt*, builtin_instance_size: felt +) { + // Check that the difference is positive and divisible by builtin_instance_size by checking that + // 0 <= div_res < RANGE_CHECK_BOUND and diff = div_res * builtin_instance_size. + tempvar diff = new_builtin_ptr - prev_builtin_ptr; + tempvar div_res = diff / builtin_instance_size; + div_res = [range_check_ptr]; + let range_check_ptr = range_check_ptr + 1; + return (); +} + +// Validates that the builtin pointers were advanced correctly. +// +// The inputs are: +// The previous list of builtin pointers. +// The new list of builtin pointers. +// The sizes of the builtin instances. +// The number of builtins. +// +// For each builtin the function validates that the difference between the new builtin pointer and +// the old builtin pointer is a nonnegative integer divisible by the corresponding builtin +// instance size. +// +// The function consumes n_builtins range check instances starting at range_check_ptr and returns +// the updated range check pointer. +func validate_builtins{range_check_ptr}( + prev_builtin_ptrs: felt*, new_builtin_ptrs: felt*, builtin_instance_sizes: felt*, n_builtins +) { + if (n_builtins == 0) { + return (); + } + + validate_builtin( + prev_builtin_ptr=cast([prev_builtin_ptrs], felt*), + new_builtin_ptr=cast([new_builtin_ptrs], felt*), + builtin_instance_size=[builtin_instance_sizes], + ); + + return validate_builtins( + prev_builtin_ptrs=prev_builtin_ptrs + 1, + new_builtin_ptrs=new_builtin_ptrs + 1, + builtin_instance_sizes=builtin_instance_sizes + 1, + n_builtins=n_builtins - 1, + ); +} diff --git a/cairo/common/__init__.py b/cairo/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cairo/common/builtin_poseidon/poseidon.cairo b/cairo/common/builtin_poseidon/poseidon.cairo new file mode 100644 index 0000000..fdabd22 --- /dev/null +++ b/cairo/common/builtin_poseidon/poseidon.cairo @@ -0,0 +1,107 @@ +from starkware.cairo.common.cairo_builtins import PoseidonBuiltin +from starkware.cairo.common.poseidon_state import PoseidonBuiltinState + +// Hashes two elements and retrieves a single field element output. +func poseidon_hash{poseidon_ptr: PoseidonBuiltin*}(x: felt, y: felt) -> (res: felt) { + // To distinguish between the use cases the capacity element is initialized to 2. + assert poseidon_ptr.input = PoseidonBuiltinState(s0=x, s1=y, s2=2); + + let res = poseidon_ptr.output.s0; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + return (res=res); +} + +// Hashes one element and retrieves a single field element output. +func poseidon_hash_single{poseidon_ptr: PoseidonBuiltin*}(x: felt) -> (res: felt) { + // Pad the rate with a zero. + // To distinguish between the use cases the capacity element is initialized to 1. + assert poseidon_ptr.input = PoseidonBuiltinState(s0=x, s1=0, s2=1); + + let res = poseidon_ptr.output.s0; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + return (res=res); +} + +// Hashes n elements and retrieves a single field element output. +func poseidon_hash_many{poseidon_ptr: PoseidonBuiltin*}(n: felt, elements: felt*) -> (res: felt) { + let elements_end = &elements[n]; + // Apply the sponge construction to digest many elements. + // To distinguish between the use cases the capacity element is initialized to 0. + // To distinguish between different input sizes always pad with 1 and possibly with another 0 to + // complete to an even sized input. + tempvar state = PoseidonBuiltinState(s0=0, s1=0, s2=0); + tempvar elements = elements; + tempvar poseidon_ptr = poseidon_ptr; + + loop: + if (nondet %{ ids.elements_end - ids.elements >= 10 %} != 0) { + assert poseidon_ptr.input = PoseidonBuiltinState( + s0=state.s0 + elements[0], s1=state.s1 + elements[1], s2=state.s2 + ); + let state = poseidon_ptr.output; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + assert poseidon_ptr.input = PoseidonBuiltinState( + s0=state.s0 + elements[2], s1=state.s1 + elements[3], s2=state.s2 + ); + let state = poseidon_ptr.output; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + assert poseidon_ptr.input = PoseidonBuiltinState( + s0=state.s0 + elements[4], s1=state.s1 + elements[5], s2=state.s2 + ); + let state = poseidon_ptr.output; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + assert poseidon_ptr.input = PoseidonBuiltinState( + s0=state.s0 + elements[6], s1=state.s1 + elements[7], s2=state.s2 + ); + let state = poseidon_ptr.output; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + assert poseidon_ptr.input = PoseidonBuiltinState( + s0=state.s0 + elements[8], s1=state.s1 + elements[9], s2=state.s2 + ); + let state = poseidon_ptr.output; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + tempvar state = state; + tempvar elements = &elements[10]; + tempvar poseidon_ptr = poseidon_ptr; + jmp loop; + } + + if (nondet %{ ids.elements_end - ids.elements >= 2 %} != 0) { + assert poseidon_ptr.input = PoseidonBuiltinState( + s0=state.s0 + elements[0], s1=state.s1 + elements[1], s2=state.s2 + ); + let state = poseidon_ptr.output; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + + tempvar state = state; + tempvar elements = &elements[2]; + tempvar poseidon_ptr = poseidon_ptr; + jmp loop; + } + + tempvar n = elements_end - elements; + + if (n == 0) { + // Pad input with [1, 0]. + assert poseidon_ptr.input = PoseidonBuiltinState(s0=state.s0 + 1, s1=state.s1, s2=state.s2); + let res = poseidon_ptr.output.s0; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + return (res=res); + } + + assert n = 1; + // Pad input with [1]. + assert poseidon_ptr.input = PoseidonBuiltinState( + s0=state.s0 + elements[0], s1=state.s1 + 1, s2=state.s2 + ); + let res = poseidon_ptr.output.s0; + let poseidon_ptr = poseidon_ptr + PoseidonBuiltin.SIZE; + return (res=res); +} \ No newline at end of file diff --git a/cairo/common/cairo_builtins.cairo b/cairo/common/cairo_builtins.cairo new file mode 100644 index 0000000..baf3128 --- /dev/null +++ b/cairo/common/cairo_builtins.cairo @@ -0,0 +1,45 @@ +from starkware.cairo.common.ec_point import EcPoint +from starkware.cairo.common.keccak_state import KeccakBuiltinState +from starkware.cairo.common.poseidon_state import PoseidonBuiltinState + +// Specifies the hash builtin memory structure. +struct HashBuiltin { + x: felt, + y: felt, + result: felt, +} + +// Specifies the signature builtin memory structure. +struct SignatureBuiltin { + pub_key: felt, + message: felt, +} + +// Specifies the bitwise builtin memory structure. +struct BitwiseBuiltin { + x: felt, + y: felt, + x_and_y: felt, + x_xor_y: felt, + x_or_y: felt, +} + +// Specifies the EC operation builtin memory structure. +struct EcOpBuiltin { + p: EcPoint, + q: EcPoint, + m: felt, + r: EcPoint, +} + +// Specifies the Keccak builtin memory structure. +struct KeccakBuiltin { + input: KeccakBuiltinState, + output: KeccakBuiltinState, +} + +// Specifies the Poseidon builtin memory structure. +struct PoseidonBuiltin { + input: PoseidonBuiltinState, + output: PoseidonBuiltinState, +} \ No newline at end of file diff --git a/cairo/common/ec_point.cairo b/cairo/common/ec_point.cairo new file mode 100644 index 0000000..9c526fe --- /dev/null +++ b/cairo/common/ec_point.cairo @@ -0,0 +1,5 @@ +// Represents a point on an elliptic curve. +struct EcPoint { + x: felt, + y: felt, +} \ No newline at end of file diff --git a/cairo/common/hash_chain.cairo b/cairo/common/hash_chain.cairo new file mode 100644 index 0000000..dd255d0 --- /dev/null +++ b/cairo/common/hash_chain.cairo @@ -0,0 +1,47 @@ +from common.cairo_builtins import HashBuiltin + +// Computes a hash chain of a sequence whose length is given at [data_ptr] and the data starts at +// data_ptr + 1. The hash is calculated backwards (from the highest memory address to the lowest). +// For example, for the 3-element sequence [x, y, z] the hash is: +// h(3, h(x, h(y, z))) +// If data_length = 0, the function does not return (takes more than field prime steps). +func hash_chain{hash_ptr: HashBuiltin*}(data_ptr: felt*) -> (hash: felt) { + struct LoopLocals { + data_ptr: felt*, + hash_ptr: HashBuiltin*, + cur_hash: felt, + } + + tempvar data_length = [data_ptr]; + tempvar data_ptr_end = data_ptr + data_length; + // Prepare the loop_frame for the first iteration of the hash_loop. + tempvar loop_frame = LoopLocals( + data_ptr=data_ptr_end, hash_ptr=hash_ptr, cur_hash=[data_ptr_end] + ); + + hash_loop: + let curr_frame = cast(ap - LoopLocals.SIZE, LoopLocals*); + let current_hash: HashBuiltin* = curr_frame.hash_ptr; + + tempvar new_data = [curr_frame.data_ptr - 1]; + + let n_elements_to_hash = [ap]; + // Assign current_hash inputs and allocate space for n_elements_to_hash. + current_hash.x = new_data, ap++; + current_hash.y = curr_frame.cur_hash; + + // Set the frame for the next loop iteration (going backwards). + tempvar next_frame = LoopLocals( + data_ptr=curr_frame.data_ptr - 1, + hash_ptr=curr_frame.hash_ptr + HashBuiltin.SIZE, + cur_hash=current_hash.result, + ); + + // Update n_elements_to_hash and loop accordingly. Note that the hash is calculated backwards. + n_elements_to_hash = next_frame.data_ptr - data_ptr; + jmp hash_loop if n_elements_to_hash != 0; + + // Set the hash_ptr implicit argument and return the result. + let hash_ptr = next_frame.hash_ptr; + return (hash=next_frame.cur_hash); +} \ No newline at end of file diff --git a/cairo/common/keccak_state.cairo b/cairo/common/keccak_state.cairo new file mode 100644 index 0000000..3a26220 --- /dev/null +++ b/cairo/common/keccak_state.cairo @@ -0,0 +1,11 @@ +// Represents 1600 bits of a Keccak state (8 felts each containing 200 bits). +struct KeccakBuiltinState { + s0: felt, + s1: felt, + s2: felt, + s3: felt, + s4: felt, + s5: felt, + s6: felt, + s7: felt, +} \ No newline at end of file diff --git a/cairo/common/poseidon_state.cairo b/cairo/common/poseidon_state.cairo new file mode 100644 index 0000000..4481493 --- /dev/null +++ b/cairo/common/poseidon_state.cairo @@ -0,0 +1,6 @@ +// Represents a Poseidon state. +struct PoseidonBuiltinState { + s0: felt, + s1: felt, + s2: felt, +} \ No newline at end of file diff --git a/cairo/common/registers.cairo b/cairo/common/registers.cairo new file mode 100644 index 0000000..0b0ab8c --- /dev/null +++ b/cairo/common/registers.cairo @@ -0,0 +1,26 @@ +from starkware.cairo.lang.compiler.lib.registers import get_ap, get_fp_and_pc + +// Takes the value of a label (relative to program base) and returns the actual runtime address of +// that label in the memory. +// +// Usage example: +// +// func do_callback(...) { +// ... +// } +// +// func do_thing_then_callback(callback) { +// ... +// call abs callback; +// } +// +// func main() { +// let (callback_address) = get_label_location(do_callback); +// do_thing_then_callback(callback=callback_address); +// } +func get_label_location(label_value: codeoffset) -> (res: felt*) { + let (_, pc_val) = get_fp_and_pc(); + + ret_pc_label: + return (res=pc_val + (label_value - ret_pc_label)); +} \ No newline at end of file diff --git a/cairo/lang/__init__.py b/cairo/lang/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cairo/lang/compiler/__init__.py b/cairo/lang/compiler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cairo/lang/compiler/lib/__init__.py b/cairo/lang/compiler/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cairo/lang/compiler/lib/registers.cairo b/cairo/lang/compiler/lib/registers.cairo new file mode 100644 index 0000000..7f18f9d --- /dev/null +++ b/cairo/lang/compiler/lib/registers.cairo @@ -0,0 +1,18 @@ +// Returns the contents of the fp and pc registers of the calling function. +// The pc register's value is the address of the instruction that follows directly after the +// invocation of get_fp_and_pc(). +func get_fp_and_pc() -> (fp_val: felt*, pc_val: felt*) { + // The call instruction itself already places the old fp and the return pc at + // [ap - 2], [ap - 1]. + return (fp_val=cast([ap - 2], felt*), pc_val=cast([ap - 1], felt*)); +} + +// Returns the content of the ap register just before this function was invoked. +@known_ap_change +func get_ap() -> (ap_val: felt*) { + // Once get_ap() is invoked, fp points to ap + 2 (since the call instruction placed the old fp + // and pc in memory, advancing ap accordingly). + // Hence, the desired ap value is fp - 2. + let (fp_val, pc_val) = get_fp_and_pc(); + return (ap_val=fp_val - 2); +} \ No newline at end of file diff --git a/cairo/setup.py b/cairo/setup.py new file mode 100644 index 0000000..e0317bc --- /dev/null +++ b/cairo/setup.py @@ -0,0 +1,19 @@ +import setuptools + +setuptools.setup( + name="sharp_p2p_bootloader", + version="0.1", + description="sharp_p2p bootloader", + url="#", + author="Okm165", + packages=setuptools.find_packages(), + zip_safe=False, + package_data={ + "builtin_selection": ["*.cairo", "*/*.cairo"], + "common": ["*.cairo", "*/*.cairo"], + "common.builtin_poseidon": ["*.cairo", "*/*.cairo"], + "lang.compiler": ["cairo.ebnf", "lib/*.cairo"], + "bootloader": ["*.cairo", "*/*.cairo"], + "bootloader.recursive_with_poseidon": ["*.cairo", "*/*.cairo"], + } +) diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 6d964e3..6eeebdc 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -11,7 +11,9 @@ license-file.workspace = true cairo-felt.workspace = true hex.workspace = true libp2p.workspace = true +libsecp256k1.workspace = true num-bigint.workspace = true serde_json.workspace = true serde.workspace = true +tempfile.workspace = true thiserror.workspace = true \ No newline at end of file diff --git a/crates/common/src/hash_macro.rs b/crates/common/src/hash_macro.rs new file mode 100644 index 0000000..32dc4dd --- /dev/null +++ b/crates/common/src/hash_macro.rs @@ -0,0 +1,8 @@ +#[macro_export] +macro_rules! hash { + ($value:expr) => {{ + let mut hasher = DefaultHasher::new(); + $value.hash(&mut hasher); + hasher.finish() + }}; +} diff --git a/crates/common/src/job.rs b/crates/common/src/job.rs index 90c6c99..3f6e61b 100644 --- a/crates/common/src/job.rs +++ b/crates/common/src/job.rs @@ -1,22 +1,35 @@ +use libsecp256k1::{PublicKey, Signature}; use std::{ fmt::Display, hash::{DefaultHasher, Hash, Hasher}, }; -#[derive(Debug, PartialEq, Eq, Hash, Clone)] +use crate::hash; + +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Job { pub reward: u32, pub num_of_steps: u32, - pub private_input: Vec, - pub public_input: Vec, - pub cpu_air_prover_config: Vec, - pub cpu_air_params: Vec, + pub cairo_pie: Vec, + pub public_key: PublicKey, + pub signature: Signature, + // below fields not bounded by signature + pub cpu_air_params: Vec, // needed for proving + pub cpu_air_prover_config: Vec, // needed for proving +} + +impl Hash for Job { + fn hash(&self, state: &mut H) { + self.reward.hash(state); + self.num_of_steps.hash(state); + self.cairo_pie.hash(state); + self.cpu_air_prover_config.hash(state); + self.cpu_air_params.hash(state); + } } impl Display for Job { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut hasher = DefaultHasher::new(); - self.hash(&mut hasher); - write!(f, "{}", hex::encode(hasher.finish().to_be_bytes())) + write!(f, "{}", hex::encode(hash!(self).to_be_bytes())) } } diff --git a/crates/common/src/job_trace.rs b/crates/common/src/job_trace.rs new file mode 100644 index 0000000..bd7c627 --- /dev/null +++ b/crates/common/src/job_trace.rs @@ -0,0 +1,33 @@ +use crate::hash; +use std::{ + fmt::Display, + hash::{DefaultHasher, Hash, Hasher}, +}; +use tempfile::NamedTempFile; + +#[derive(Debug)] +pub struct JobTrace { + pub air_public_input: NamedTempFile, + pub air_private_input: NamedTempFile, + pub memory: NamedTempFile, // this is not used directly but needs to live for air_private_input to be valid + pub trace: NamedTempFile, // this is not used directly but needs to live for air_private_input to be valid + pub cpu_air_prover_config: NamedTempFile, + pub cpu_air_params: NamedTempFile, +} + +impl Hash for JobTrace { + fn hash(&self, state: &mut H) { + self.air_public_input.path().hash(state); + self.air_private_input.path().hash(state); + self.memory.path().hash(state); + self.trace.path().hash(state); + self.cpu_air_prover_config.path().hash(state); + self.cpu_air_params.path().hash(state); + } +} + +impl Display for JobTrace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(hash!(self).to_be_bytes())) + } +} diff --git a/crates/common/src/job_witness.rs b/crates/common/src/job_witness.rs index de95469..a038193 100644 --- a/crates/common/src/job_witness.rs +++ b/crates/common/src/job_witness.rs @@ -1,10 +1,10 @@ +use crate::hash; +use cairo_felt::Felt252; use std::{ fmt::Display, hash::{DefaultHasher, Hash, Hasher}, }; -use cairo_felt::Felt252; - #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct JobWitness { pub data: Vec, @@ -12,8 +12,6 @@ pub struct JobWitness { impl Display for JobWitness { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut hasher = DefaultHasher::new(); - self.hash(&mut hasher); - write!(f, "{}", hex::encode(hasher.finish().to_be_bytes())) + write!(f, "{}", hex::encode(hash!(self).to_be_bytes())) } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index e83a2cf..d40106a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,4 +1,6 @@ +pub mod hash_macro; pub mod job; +pub mod job_trace; pub mod job_witness; pub mod network; pub mod topic; diff --git a/crates/delegator/Cargo.toml b/crates/delegator/Cargo.toml index 889f138..69504f7 100644 --- a/crates/delegator/Cargo.toml +++ b/crates/delegator/Cargo.toml @@ -10,6 +10,7 @@ license-file.workspace = true [dependencies] futures-util.workspace = true libp2p.workspace = true +libsecp256k1.workspace = true sharp-p2p-common.workspace = true sharp-p2p-peer.workspace = true tokio.workspace = true diff --git a/crates/executor/Cargo.toml b/crates/executor/Cargo.toml index 152c39a..c99cdd3 100644 --- a/crates/executor/Cargo.toml +++ b/crates/executor/Cargo.toml @@ -10,8 +10,9 @@ license-file.workspace = true [dependencies] futures-util.workspace = true libp2p.workspace = true +libsecp256k1.workspace = true sharp-p2p-common.workspace = true sharp-p2p-peer.workspace = true tokio.workspace = true tracing-subscriber.workspace = true -tracing.workspace = true +tracing.workspace = true \ No newline at end of file diff --git a/crates/executor/src/main.rs b/crates/executor/src/main.rs index 8660dc8..422d2c6 100644 --- a/crates/executor/src/main.rs +++ b/crates/executor/src/main.rs @@ -14,7 +14,7 @@ async fn main() -> Result<(), Box> { let _ = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).try_init(); // 1. Generate keypair for the node - let p2p_local_keypair = libp2p::identity::Keypair::generate_ed25519(); + let p2p_local_keypair = libp2p::identity::Keypair::generate_secp256k1(); // 2. Generate topic let new_job_topic = gossipsub_ident_topic(Network::Sepolia, Topic::NewJob); diff --git a/crates/prover/src/lib.rs b/crates/prover/src/lib.rs index b92842a..20652e8 100644 --- a/crates/prover/src/lib.rs +++ b/crates/prover/src/lib.rs @@ -1,5 +1,4 @@ pub mod errors; +pub mod stone_prover; #[allow(async_fn_in_trait)] pub mod traits; - -pub mod stone_prover; diff --git a/crates/prover/src/stone_prover/mod.rs b/crates/prover/src/stone_prover/mod.rs index b750727..efad759 100644 --- a/crates/prover/src/stone_prover/mod.rs +++ b/crates/prover/src/stone_prover/mod.rs @@ -1,18 +1,20 @@ -use std::{collections::HashMap, io::Read}; - use crate::{ errors::ProverControllerError, traits::{Prover, ProverController}, }; use itertools::{chain, Itertools}; -use sharp_p2p_common::{job::Job, job_witness::JobWitness, vec252::VecFelt252}; -use std::io::Write; +use sharp_p2p_common::{hash, job_trace::JobTrace, job_witness::JobWitness, vec252::VecFelt252}; +use std::{ + collections::HashMap, + hash::{DefaultHasher, Hash, Hasher}, + io::Read, +}; use tempfile::NamedTempFile; use tokio::process::{Child, Command}; use tracing::{debug, trace}; pub struct StoneProver { - tasks: HashMap, + tasks: HashMap, } impl Prover for StoneProver { @@ -22,47 +24,52 @@ impl Prover for StoneProver { } impl ProverController for StoneProver { - async fn prove(&mut self, job: Job) -> Result { + async fn prove(&mut self, job_trace: JobTrace) -> Result { let mut out_file = NamedTempFile::new()?; - let mut private_input_file = NamedTempFile::new()?; - let mut public_input_file = NamedTempFile::new()?; - let mut prover_config_file = NamedTempFile::new()?; - let mut parameter_file = NamedTempFile::new()?; - - private_input_file.write_all(&job.private_input)?; - public_input_file.write_all(&job.public_input)?; - prover_config_file.write_all(&job.cpu_air_prover_config)?; - parameter_file.write_all(&job.cpu_air_params)?; - trace!("task {} environment prepared", job); let task = Command::new("cpu_air_prover") - .args(["out_file", out_file.path().to_string_lossy().as_ref()]) - .args(["private_input_file", private_input_file.path().to_string_lossy().as_ref()]) - .args(["public_input_file", public_input_file.path().to_string_lossy().as_ref()]) - .args(["prover_config_file", prover_config_file.path().to_string_lossy().as_ref()]) - .args(["parameter_file", parameter_file.path().to_string_lossy().as_ref()]) + .args(["--out_file", out_file.path().to_string_lossy().as_ref()]) + .args([ + "--air_private_input", + job_trace.air_private_input.path().to_string_lossy().as_ref(), + ]) + .args([ + "--air_public_input", + job_trace.air_public_input.path().to_string_lossy().as_ref(), + ]) + .args([ + "--cpu_air_prover_config", + job_trace.cpu_air_prover_config.path().to_string_lossy().as_ref(), + ]) + .args(["--cpu_air_params", job_trace.cpu_air_params.path().to_string_lossy().as_ref()]) .arg("--generate_annotations") .spawn()?; - debug!("task {} spawned", job); - self.tasks.insert(job.to_owned(), task); + let job_trace_hash = hash!(job_trace); + + debug!("task {} spawned", job_trace_hash); + self.tasks.insert(job_trace_hash.to_owned(), task); - let task_status = - self.tasks.get_mut(&job).ok_or(ProverControllerError::TaskNotFound)?.wait().await?; + let task_status = self + .tasks + .get_mut(&job_trace_hash) + .ok_or(ProverControllerError::TaskNotFound)? + .wait() + .await?; - trace!("task {} woke up", job); + trace!("task {} woke up", job_trace_hash); if !task_status.success() { - debug!("task terminated {}", job); + debug!("task terminated {}", job_trace_hash); return Err(ProverControllerError::TaskTerminated); } let task_output = self .tasks - .remove(&job) + .remove(&job_trace_hash) .ok_or(ProverControllerError::TaskNotFound)? .wait_with_output() .await?; - trace!("task {} output {:?}", job, task_output); + trace!("task {} output {:?}", job_trace_hash, task_output); let mut input = String::new(); out_file.read_to_string(&mut input)?; @@ -88,16 +95,23 @@ impl ProverController for StoneProver { Ok(JobWitness { data }) } - async fn terminate(&mut self, job: &Job) -> Result<(), ProverControllerError> { - self.tasks.get_mut(job).ok_or(ProverControllerError::TaskNotFound)?.start_kill()?; - trace!("task scheduled for termination {}", job); + async fn terminate(&mut self, job_trace_hash: u64) -> Result<(), ProverControllerError> { + self.tasks + .get_mut(&job_trace_hash) + .ok_or(ProverControllerError::TaskNotFound)? + .start_kill()?; + trace!("task scheduled for termination {}", job_trace_hash); Ok(()) } async fn drop(mut self) -> Result<(), ProverControllerError> { - let keys: Vec = self.tasks.keys().cloned().collect(); - for job in keys.iter() { - self.terminate(job).await?; + let keys: Vec = self.tasks.keys().cloned().collect(); + for job_trace_hash in keys.iter() { + self.tasks + .get_mut(job_trace_hash) + .ok_or(ProverControllerError::TaskNotFound)? + .start_kill()?; + trace!("task scheduled for termination {}", job_trace_hash); } Ok(()) } diff --git a/crates/prover/src/traits.rs b/crates/prover/src/traits.rs index 9d75910..2d78f22 100644 --- a/crates/prover/src/traits.rs +++ b/crates/prover/src/traits.rs @@ -1,13 +1,12 @@ -use sharp_p2p_common::{job::Job, job_witness::JobWitness}; - use crate::errors::ProverControllerError; +use sharp_p2p_common::{job_trace::JobTrace, job_witness::JobWitness}; pub trait Prover { fn init() -> impl ProverController; } pub trait ProverController { - async fn prove(&mut self, job: Job) -> Result; - async fn terminate(&mut self, job: &Job) -> Result<(), ProverControllerError>; + async fn prove(&mut self, job_trace: JobTrace) -> Result; + async fn terminate(&mut self, job_trace_hash: u64) -> Result<(), ProverControllerError>; async fn drop(self) -> Result<(), ProverControllerError>; } diff --git a/crates/runner/Cargo.toml b/crates/runner/Cargo.toml new file mode 100644 index 0000000..17b2e24 --- /dev/null +++ b/crates/runner/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sharp-p2p-runner" +version.workspace = true +edition.workspace = true +repository.workspace = true +license-file.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-process.workspace = true +cairo-proof-parser.workspace = true +futures.workspace= true +itertools.workspace = true +serde_json.workspace = true +serde.workspace = true +sharp-p2p-common.workspace = true +strum.workspace = true +tempfile.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true +zip-extensions.workspace = true \ No newline at end of file diff --git a/crates/runner/src/cairo_runner/mod.rs b/crates/runner/src/cairo_runner/mod.rs new file mode 100644 index 0000000..0702b1d --- /dev/null +++ b/crates/runner/src/cairo_runner/mod.rs @@ -0,0 +1,121 @@ +use crate::{ + errors::RunnerControllerError, + traits::{Runner, RunnerController}, + types::{ + input::{BootloaderInput, Task}, + layout::Layout, + }, +}; +use sharp_p2p_common::{hash, job::Job, job_trace::JobTrace}; +use std::io::Write; +use std::{ + collections::HashMap, + hash::{DefaultHasher, Hash, Hasher}, +}; +use tempfile::NamedTempFile; +use tokio::process::{Child, Command}; +use tracing::{debug, trace}; + +pub struct CairoRunner { + tasks: HashMap, +} + +impl Runner for CairoRunner { + fn init() -> impl RunnerController { + Self { tasks: HashMap::new() } + } +} + +impl RunnerController for CairoRunner { + async fn run(&mut self, job: Job) -> Result { + let program = NamedTempFile::new()?; + let layout: &str = Layout::Recursive.into(); + + let mut cairo_pie = NamedTempFile::new()?; + cairo_pie.write_all(&job.cairo_pie)?; + + let input = BootloaderInput { + tasks: vec![Task { path: cairo_pie.path().to_path_buf(), ..Default::default() }], + ..Default::default() + }; + + let mut program_input = NamedTempFile::new()?; + program_input.write_all(&serde_json::to_string(&input)?.into_bytes())?; + + // outputs + let air_public_input = NamedTempFile::new()?; + let air_private_input = NamedTempFile::new()?; + let trace = NamedTempFile::new()?; + let memory = NamedTempFile::new()?; + + let task = Command::new("cairo-run") + .args(["--program", program.path().to_string_lossy().as_ref()]) + .args(["--layout", layout]) + .args(["--program_input", program_input.path().to_string_lossy().as_ref()]) + .args(["--air_public_input", air_public_input.path().to_string_lossy().as_ref()]) + .args(["--air_private_input", air_private_input.path().to_string_lossy().as_ref()]) + .args(["--trace_file", trace.path().to_string_lossy().as_ref()]) + .args(["--memory_file", memory.path().to_string_lossy().as_ref()]) + .arg("--proof_mode") + .arg("--print_output") + .spawn()?; + + let job_hash = hash!(job); + + debug!("task {} spawned", job_hash); + self.tasks.insert(job_hash.to_owned(), task); + + let task_status = self + .tasks + .get_mut(&job_hash) + .ok_or(RunnerControllerError::TaskNotFound)? + .wait() + .await?; + + trace!("task {} woke up", job_hash); + if !task_status.success() { + debug!("task terminated {}", job_hash); + return Err(RunnerControllerError::TaskTerminated); + } + + let task_output = self + .tasks + .remove(&job_hash) + .ok_or(RunnerControllerError::TaskNotFound)? + .wait_with_output() + .await?; + trace!("task {} output {:?}", job_hash, task_output); + + let mut cpu_air_params = NamedTempFile::new()?; + let mut cpu_air_prover_config = NamedTempFile::new()?; + cpu_air_params.write_all(&job.cpu_air_params)?; + cpu_air_prover_config.write_all(&job.cpu_air_prover_config)?; + + Ok(JobTrace { + air_public_input, + air_private_input, + memory, + trace, + cpu_air_prover_config, + cpu_air_params, + }) + } + + async fn terminate(&mut self, job_hash: u64) -> Result<(), RunnerControllerError> { + self.tasks.get_mut(&job_hash).ok_or(RunnerControllerError::TaskNotFound)?.start_kill()?; + trace!("task scheduled for termination {}", job_hash); + Ok(()) + } + + async fn drop(mut self) -> Result<(), RunnerControllerError> { + let keys: Vec = self.tasks.keys().cloned().collect(); + for job_hash in keys.iter() { + self.tasks + .get_mut(job_hash) + .ok_or(RunnerControllerError::TaskNotFound)? + .start_kill()?; + trace!("task scheduled for termination {}", job_hash); + } + Ok(()) + } +} diff --git a/crates/runner/src/errors.rs b/crates/runner/src/errors.rs new file mode 100644 index 0000000..e115f1b --- /dev/null +++ b/crates/runner/src/errors.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RunnerControllerError { + #[error("task not found")] + TaskNotFound, + + #[error("task not found")] + TaskTerminated, + + #[error("io")] + Io(#[from] std::io::Error), + + #[error("serde")] + Serde(#[from] serde_json::Error), + + #[error("proof parsing error")] + ProofParseError(String), +} diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs new file mode 100644 index 0000000..26e9b70 --- /dev/null +++ b/crates/runner/src/lib.rs @@ -0,0 +1,5 @@ +pub mod cairo_runner; +pub mod errors; +#[allow(async_fn_in_trait)] +pub mod traits; +pub mod types; diff --git a/crates/runner/src/traits.rs b/crates/runner/src/traits.rs new file mode 100644 index 0000000..fe748e4 --- /dev/null +++ b/crates/runner/src/traits.rs @@ -0,0 +1,13 @@ +use sharp_p2p_common::{job::Job, job_trace::JobTrace}; + +use crate::errors::RunnerControllerError; + +pub trait Runner { + fn init() -> impl RunnerController; +} + +pub trait RunnerController { + async fn run(&mut self, job: Job) -> Result; + async fn terminate(&mut self, job_hash: u64) -> Result<(), RunnerControllerError>; + async fn drop(self) -> Result<(), RunnerControllerError>; +} diff --git a/crates/runner/src/types/input.rs b/crates/runner/src/types/input.rs new file mode 100644 index 0000000..af01ee3 --- /dev/null +++ b/crates/runner/src/types/input.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize)] +pub struct Task { + #[serde(rename = "type")] + pub type_: String, + pub path: PathBuf, + pub use_poseidon: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct BootloaderInput { + pub tasks: Vec, + pub single_page: bool, +} + +impl Default for Task { + fn default() -> Self { + Self { type_: "CairoPiePath".to_string(), path: PathBuf::default(), use_poseidon: false } + } +} + +impl Default for BootloaderInput { + fn default() -> Self { + Self { tasks: Vec::default(), single_page: true } + } +} + +pub fn write_cairo_pie_zip() {} diff --git a/crates/runner/src/types/layout.rs b/crates/runner/src/types/layout.rs new file mode 100644 index 0000000..d7edd3f --- /dev/null +++ b/crates/runner/src/types/layout.rs @@ -0,0 +1,7 @@ +use strum::IntoStaticStr; + +#[derive(Debug, PartialEq, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum Layout { + Recursive, +} diff --git a/crates/runner/src/types/mod.rs b/crates/runner/src/types/mod.rs new file mode 100644 index 0000000..1d2c2cd --- /dev/null +++ b/crates/runner/src/types/mod.rs @@ -0,0 +1,2 @@ +pub mod input; +pub mod layout;