diff --git a/.gitignore b/.gitignore index 9c81814..b3801fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# python virtual environment +venv/ + # Generated by Cargo # will have compiled files and executables debug/ diff --git a/Cargo.toml b/Cargo.toml index 75869ef..7ece45c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,10 +47,14 @@ libp2p = { version = "0.53.2", features = [ ] } libsecp256k1 = "0.7.1" num-bigint = "0.4.4" +proptest = "1.4.0" +proptest-derive = "0.4.0" rand = "0.8.5" serde = "1.0.197" serde_json = "1.0.115" -starknet = "0.9.0" +serde_with = "3.7.0" +starknet = "0.10.0" +starknet-crypto = "0.6.2" strum = { version = "0.26", features = ["derive"] } tempfile = "3.10.1" thiserror = "1.0.58" diff --git a/cairo/bootloader/hash_program.py b/cairo/bootloader/hash_program.py index de59519..01755d5 100644 --- a/cairo/bootloader/hash_program.py +++ b/cairo/bootloader/hash_program.py @@ -1,20 +1,28 @@ 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.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): +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 + program_header = [ + bootloader_version, + program.main, + len(program.builtins), + ] + builtin_list data_chain = program_header + program.data if use_poseidon: @@ -23,8 +31,12 @@ def compute_program_hash_chain(program: ProgramBase, use_poseidon: bool, bootloa 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 = 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"), @@ -48,7 +60,13 @@ def main(): 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))) + print( + hex( + compute_program_hash_chain( + program=program, use_poseidon=args.use_poseidon + ) + ) + ) if __name__ == "__main__": diff --git a/cairo/bootloader/objects.py b/cairo/bootloader/objects.py index ecabe98..ff0e054 100644 --- a/cairo/bootloader/objects.py +++ b/cairo/bootloader/objects.py @@ -1,18 +1,20 @@ 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 +from typing import List, Optional import marshmallow_dataclass -from marshmallow_oneofschema import OneOfSchema - -from starkware.cairo.lang.compiler.program import Program, ProgramBase, StrippedProgram +from starkware.cairo.lang.compiler.program import 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): + @abstractmethod + def load_task(self) -> "Task": + """ + Returns the corresponding task. + """ + + class Task: @abstractmethod def get_program(self) -> ProgramBase: @@ -20,25 +22,47 @@ def get_program(self) -> ProgramBase: Returns the task's Cairo program. """ + @dataclasses.dataclass(frozen=True) -class Job(Task): - reward: int - num_of_steps: int +class CairoPieTask(Task): cairo_pie: CairoPie - registry_address: bytearray - public_key: bytearray - signature: bytearray + use_poseidon: bool def get_program(self) -> StrippedProgram: return self.cairo_pie.program + +@dataclasses.dataclass(frozen=True) +class JobData(Task): + reward: int + num_of_steps: int + cairo_pie_compressed: List[int] + registry_address: str + + def load_task(self) -> "CairoPieTask": + return CairoPieTask( + cairo_pie=CairoPie.deserialize(bytes(self.cairo_pie_compressed)), + use_poseidon=True, + ) + + +@dataclasses.dataclass(frozen=True) +class Job(Task): + job_data: JobData + public_key: List[int] + signature: List[int] + + def load_task(self) -> "CairoPieTask": + return self.job_data.load_task() + + @marshmallow_dataclass.dataclass(frozen=True) class SimpleBootloaderInput(ValidatedMarshmallowDataclass): - identity: bytearray + identity: str job: Job 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 + single_page: bool diff --git a/cairo/bootloader/recursive_with_poseidon/builtins.py b/cairo/bootloader/recursive_with_poseidon/builtins.py index bb4b337..78f19ac 100644 --- a/cairo/bootloader/recursive_with_poseidon/builtins.py +++ b/cairo/bootloader/recursive_with_poseidon/builtins.py @@ -8,4 +8,4 @@ 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 index ee1d1e5..06bf2bd 100644 --- a/cairo/bootloader/recursive_with_poseidon/execute_task.cairo +++ b/cairo/bootloader/recursive_with_poseidon/execute_task.cairo @@ -144,23 +144,17 @@ func execute_task{builtin_ptrs: BuiltinData*, self_range_check_ptr}( %{ 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): + + if 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, @@ -169,10 +163,6 @@ func execute_task{builtin_ptrs: BuiltinData*, self_range_check_ptr}( 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) %} @@ -243,8 +233,6 @@ func execute_task{builtin_ptrs: BuiltinData*, self_range_check_ptr}( 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, )) %} diff --git a/cairo/bootloader/recursive_with_poseidon/run_simple_bootloader.cairo b/cairo/bootloader/recursive_with_poseidon/run_simple_bootloader.cairo index 83aa8d7..17ada90 100644 --- a/cairo/bootloader/recursive_with_poseidon/run_simple_bootloader.cairo +++ b/cairo/bootloader/recursive_with_poseidon/run_simple_bootloader.cairo @@ -21,7 +21,7 @@ func run_simple_bootloader{ local task_range_check_ptr; %{ - n_tasks = len(simple_bootloader_input.tasks) + n_tasks = 1 memory[ids.output_ptr] = n_tasks # Task range checks are located right after simple bootloader validation range checks, and @@ -65,7 +65,6 @@ func run_simple_bootloader{ // 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 { @@ -141,8 +140,7 @@ func execute_tasks{builtin_ptrs: BuiltinData*, self_range_check_ptr}( 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() + task = simple_bootloader_input.job.load_task() %} tempvar use_poseidon = nondet %{ 1 if task.use_poseidon else 0 %}; // Call execute_task to execute the current task. diff --git a/cairo/bootloader/utils.py b/cairo/bootloader/utils.py index 2adf4b5..aade3ec 100644 --- a/cairo/bootloader/utils.py +++ b/cairo/bootloader/utils.py @@ -1,20 +1,22 @@ import json import os -from typing import Any, List, Union - +from typing import List 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 bootloader.objects import CairoPieTask, 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.cairo.lang.vm.relocatable import ( + MaybeRelocatable, + RelocatableValue, + relocate_value, +) from starkware.python.utils import WriteOnceDict, from_bytes SIMPLE_BOOTLOADER_COMPILED_PATH = os.path.join( @@ -89,7 +91,7 @@ def write_return_builtins( used_builtins_addr, pre_execution_builtins_addr, task, - all_builtins + all_builtins, ): """ Writes the updated builtin pointers after the program execution to the given return builtins @@ -100,7 +102,9 @@ def write_return_builtins( 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] + memory[return_builtins_addr + index] = memory[ + used_builtins_addr + used_builtin_offset + ] used_builtin_offset += 1 if isinstance(task, CairoPie): @@ -110,7 +114,9 @@ def write_return_builtins( ), "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] + memory[return_builtins_addr + index] = memory[ + pre_execution_builtins_addr + index + ] def load_cairo_pie( @@ -168,59 +174,31 @@ def local_relocate_value(value): 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) + 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, + task: CairoPie, ) -> FactTopology: """ - Returns the fact_topology that corresponds to 'task'. Restores output builtin state if 'task' is - a RunProgramTask. + Returns the fact_topology that corresponds to 'task'. """ - # 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 + if isinstance(task, CairoPieTask): fact_topology = get_fact_topology_from_additional_data( output_size=output_size, - output_builtin_additional_data=task.cairo_pie.additional_data["output_builtin"], + output_builtin_additional_data=task.cairo_pie.additional_data[ + "output_builtin" + ], ) else: raise NotImplementedError(f"Unexpected task type: {type(task).__name__}.") @@ -237,7 +215,9 @@ def add_consecutive_output_pages( 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 + page_id=cur_page_id + i, + page_start=output_start + offset, + page_size=page_size, ) offset += page_size @@ -268,10 +248,14 @@ def configure_fact_topologies( output_start += sum(fact_topology.page_sizes) -def write_to_fact_topologies_file(fact_topologies_path: str, fact_topologies: List[FactTopology]): +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)), + FactTopologiesFile.Schema().dump( + FactTopologiesFile(fact_topologies=fact_topologies) + ), fp, indent=4, sort_keys=True, @@ -279,17 +263,24 @@ def write_to_fact_topologies_file(fact_topologies_path: str, fact_topologies: Li fp.write("\n") -def calc_simple_bootloader_execution_resources(program_length: int) -> ExecutionResources: +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 + 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 + n_steps=n_steps, + builtin_instance_counter=builtin_instance_counter, + n_memory_holes=0, + ) diff --git a/compile.py b/compile.py new file mode 100644 index 0000000..ec4c4ed --- /dev/null +++ b/compile.py @@ -0,0 +1,9 @@ +import os +from install import log_and_run + +if __name__ == "__main__": + current_dir = os.getcwd() + + log_and_run([ + f"cairo-compile --cairo_path=. bootloader/recursive_with_poseidon/simple_bootloader.cairo --output {current_dir}/bootloader.json --proof_mode", + ], "Compile bootloader program", cwd="cairo") \ No newline at end of file diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index adeb8ac..3f78034 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -15,9 +15,14 @@ hex.workspace = true libp2p.workspace = true libsecp256k1.workspace = true num-bigint.workspace = true +proptest-derive.workspace = true +proptest.workspace = true serde_json.workspace = true +serde_with.workspace = true serde.workspace = true +starknet-crypto.workspace = true +starknet.workspace = true strum.workspace = true tempfile.workspace = true thiserror.workspace = true -tokio.workspace = true +tokio.workspace = true \ No newline at end of file diff --git a/crates/common/src/fuzzing.rs b/crates/common/src/fuzzing.rs new file mode 100644 index 0000000..67395e2 --- /dev/null +++ b/crates/common/src/fuzzing.rs @@ -0,0 +1,44 @@ +use proptest::{ + arbitrary::{any, Arbitrary}, + prop_compose, + strategy::{BoxedStrategy, Strategy}, +}; +use starknet::providers::sequencer::models::L1Address; + +use crate::job::{Job, JobData}; + +// This generates a random Job object for testing purposes. +prop_compose! { + fn arb_state()( + reward in any::(), + num_of_steps in any::(), + cairo_pie_compressed in any::>(), + secret_key in any::<[u8; 32]>() + ) -> (u32, u32, Vec, [u8; 32]) { + (reward, num_of_steps, cairo_pie_compressed, secret_key) + } +} + +impl Arbitrary for Job { + type Parameters = (); + type Strategy = BoxedStrategy; + fn arbitrary() -> Self::Strategy { + let abs_state = arb_state(); + abs_state + .prop_map(|(reward, num_of_steps, cairo_pie_compressed, secret_key)| { + Job::from_job_data( + JobData { + reward, + num_of_steps, + cairo_pie_compressed, + registry_address: L1Address::random(), + }, + libsecp256k1::SecretKey::parse(&secret_key).unwrap(), + ) + }) + .boxed() + } + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + Self::arbitrary() + } +} diff --git a/crates/common/src/job.rs b/crates/common/src/job.rs index 82bbd5b..6a33f56 100644 --- a/crates/common/src/job.rs +++ b/crates/common/src/job.rs @@ -1,10 +1,13 @@ use crate::hash; -use libsecp256k1::{curve::Scalar, sign, Message, PublicKey, SecretKey, Signature}; +use libsecp256k1::{Message, PublicKey, SecretKey, Signature}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use starknet::core::types::FromByteSliceError; +use starknet::providers::sequencer::models::L1Address; +use starknet_crypto::{poseidon_hash_many, FieldElement}; use std::{ fmt::Display, - fs, hash::{DefaultHasher, Hash, Hasher}, - path::PathBuf, }; /* @@ -16,61 +19,74 @@ use std::{ The Job object also includes the target registry where the delegator expects this proof to be verified. */ -#[derive(Debug, PartialEq, Eq, Clone)] +#[serde_as] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Job { - pub reward: u32, // The reward offered for completing the task - pub num_of_steps: u32, // The number of steps expected to complete the task (executor ensures that this number is greater than or equal to the actual steps; in the future, the executor may charge a fee to the delegator if not met) - pub cairo_pie_compressed: Vec, // The task bytecode in compressed zip format, to conserve memory - pub registry_address: String, // The address of the registry contract where the delegator expects the proof to be verified - pub public_key: PublicKey, // The public key of the delegator, used in the bootloader stage to confirm authenticity of the Job<->Delegator relationship - pub signature: Signature, // The signature of the delegator, used in the bootloader stage to confirm authenticity of the Job<->Delegator relationship + pub job_data: JobData, + #[serde_as(as = "[_; 65]")] + pub public_key: [u8; 65], // The public key of the delegator, used in the bootloader stage to confirm authenticity of the Job<->Delegator relationship + #[serde_as(as = "[_; 64]")] + pub signature: [u8; 64], // The signature of the delegator, used in the bootloader stage to confirm authenticity of the Job<->Delegator relationship } impl Job { + pub fn from_job_data(job_data: JobData, secret_key: SecretKey) -> Self { + let felts: Vec = job_data.to_owned().try_into().unwrap(); + let message = Message::parse(&poseidon_hash_many(&felts).to_bytes_be()); + let (signature, _recovery) = libsecp256k1::sign(&message, &secret_key); + + Self { + job_data, + public_key: PublicKey::from_secret_key(&secret_key).serialize(), + signature: signature.serialize(), + } + } + + pub fn verify_signature(&self) -> bool { + let felts: Vec = self.job_data.to_owned().try_into().unwrap(); + let message = Message::parse(&poseidon_hash_many(&felts).to_bytes_be()); + let signature = Signature::parse_overflowing(&self.signature); + let pubkey = PublicKey::parse(&self.public_key).unwrap(); + libsecp256k1::verify(&message, &signature, &pubkey) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct JobData { + pub reward: u32, + pub num_of_steps: u32, + pub cairo_pie_compressed: Vec, + pub registry_address: L1Address, +} + +impl JobData { pub fn new( reward: u32, num_of_steps: u32, - cairo_pie_file: PathBuf, - registry_address: &str, - secret_key: SecretKey, + cairo_pie_compressed: Vec, + registry_address: L1Address, ) -> Self { - Self { - reward, - num_of_steps, - cairo_pie_compressed: fs::read(cairo_pie_file).unwrap(), - registry_address: registry_address.to_string(), - public_key: PublicKey::from_secret_key(&secret_key), - signature: libsecp256k1::sign( - // TODO proper impl just mocked rn for tests - &Message::parse(&[ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - ]), - &secret_key, - ) - .0, - } + Self { reward, num_of_steps, cairo_pie_compressed, registry_address } } } -impl Default for Job { - fn default() -> Self { - let secret_key = &SecretKey::default(); - let public_key = PublicKey::from_secret_key(secret_key); - let (signature, _recovery_id) = - sign(&libsecp256k1::Message(Scalar([0, 0, 0, 0, 0, 0, 0, 0])), secret_key); - Self { - reward: 0, - num_of_steps: 0, - cairo_pie_compressed: vec![1, 2, 3], - public_key, - signature, - registry_address: "0x0".to_string(), - } +impl TryFrom for Vec { + type Error = FromByteSliceError; + fn try_from(value: JobData) -> Result { + let mut felts: Vec = + vec![FieldElement::from(value.reward), FieldElement::from(value.num_of_steps)]; + felts.extend( + value + .cairo_pie_compressed + .chunks(31) + .map(|chunk| FieldElement::from_byte_slice_be(chunk).unwrap()), + ); + felts.push(FieldElement::from_byte_slice_be(&value.registry_address.to_fixed_bytes())?); + Ok(felts) } } -impl Hash for Job { +impl Hash for JobData { fn hash(&self, state: &mut H) { self.reward.hash(state); self.num_of_steps.hash(state); @@ -79,18 +95,14 @@ impl Hash for Job { } } +impl Hash for Job { + fn hash(&self, state: &mut H) { + self.job_data.hash(state) + } +} + impl Display for Job { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", hex::encode(hash!(self).to_be_bytes())) } } - -// impl Job { -// pub fn serialize_job(&self) -> Vec { -// bincode::serialize(self).unwrap() -// } - -// pub fn deserialize_job(serialized_job: &[u8]) -> Self { -// bincode::deserialize(serialized_job).unwrap() -// } -// } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index e6170be..ea904f9 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -7,3 +7,8 @@ pub mod network; pub mod process; pub mod topic; pub mod vec252; + +mod fuzzing; + +#[cfg(test)] +pub mod tests; diff --git a/crates/common/src/network.rs b/crates/common/src/network.rs index 159dca5..fa7e666 100644 --- a/crates/common/src/network.rs +++ b/crates/common/src/network.rs @@ -1,3 +1,8 @@ +/* + Network + This defines the network that the node is connected to. +*/ + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Network { Mainnet, diff --git a/crates/common/src/tests/job.rs b/crates/common/src/tests/job.rs new file mode 100644 index 0000000..a7a0747 --- /dev/null +++ b/crates/common/src/tests/job.rs @@ -0,0 +1,20 @@ +use crate::job::Job; +use proptest::prelude::*; + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn job_verify_signature(job in any::()) { + assert!(job.verify_signature()); + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn job_serialization(job in any::()) { + let serialized_job = serde_json::to_string(&job).unwrap(); + let deserialized_job: Job = serde_json::from_str(&serialized_job).unwrap(); + assert_eq!(job, deserialized_job) + } +} diff --git a/crates/common/src/tests/mod.rs b/crates/common/src/tests/mod.rs new file mode 100644 index 0000000..80daa3e --- /dev/null +++ b/crates/common/src/tests/mod.rs @@ -0,0 +1 @@ +pub mod job; diff --git a/crates/common/src/topic.rs b/crates/common/src/topic.rs index 90d347a..6bdfa56 100644 --- a/crates/common/src/topic.rs +++ b/crates/common/src/topic.rs @@ -2,6 +2,14 @@ use libp2p::gossipsub::IdentTopic; use crate::network::Network; +/* + Topic + This defines the topic of the message used in the gossipsub protocol. + The topic is used to filter messages and route them to the correct subscribers. + `NewJob` is used to notify the network of a new job from Delegator to Executor. + `PickedJob` is used to notify the network of a job that has been picked up by an Executor. +*/ + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Topic { NewJob, diff --git a/crates/compiler/Cargo.toml b/crates/compiler/Cargo.toml index 346dddd..eead4a4 100644 --- a/crates/compiler/Cargo.toml +++ b/crates/compiler/Cargo.toml @@ -18,6 +18,8 @@ rand.workspace = true serde_json.workspace = true serde.workspace = true sharp-p2p-common.workspace = true +starknet-crypto.workspace = true +starknet.workspace = true strum.workspace = true tempfile.workspace = true thiserror.workspace = true diff --git a/crates/compiler/src/cairo_compiler/mod.rs b/crates/compiler/src/cairo_compiler/mod.rs index 862254f..70aa6a3 100644 --- a/crates/compiler/src/cairo_compiler/mod.rs +++ b/crates/compiler/src/cairo_compiler/mod.rs @@ -1,8 +1,11 @@ use crate::{errors::CompilerControllerError, traits::CompilerController}; use async_process::Stdio; use futures::Future; +use libsecp256k1::SecretKey; +use sharp_p2p_common::job::JobData; use sharp_p2p_common::layout::Layout; use sharp_p2p_common::{job::Job, process::Process}; +use starknet::providers::sequencer::models::L1Address; use std::path::PathBuf; use std::{io::Read, pin::Pin}; use tempfile::NamedTempFile; @@ -11,17 +14,13 @@ use tracing::debug; pub mod tests; -pub struct CairoCompiler {} - -impl CairoCompiler { - pub fn new() -> Self { - Self {} - } +pub struct CairoCompiler { + signing_key: SecretKey, } -impl Default for CairoCompiler { - fn default() -> Self { - Self::new() +impl CairoCompiler { + pub fn new(signing_key: SecretKey) -> Self { + Self { signing_key } } } @@ -104,8 +103,15 @@ impl CompilerController for CairoCompiler { let mut cairo_pie_bytes = Vec::new(); cairo_pie.read_to_end(&mut cairo_pie_bytes)?; - // TODO: calculate details - Ok(Job { cairo_pie_compressed: cairo_pie_bytes, ..Default::default() }) + Ok(Job::from_job_data( + JobData { + reward: 0, // TODO: calculate this properly + num_of_steps: 0, // TODO: calculate this properly + cairo_pie_compressed: cairo_pie_bytes, + registry_address: L1Address::random(), + }, + self.signing_key, + )) }); Ok(Process::new(future, terminate_tx)) diff --git a/crates/compiler/src/cairo_compiler/tests/single_job.rs b/crates/compiler/src/cairo_compiler/tests/single_job.rs index 06a4c55..38b32f3 100644 --- a/crates/compiler/src/cairo_compiler/tests/single_job.rs +++ b/crates/compiler/src/cairo_compiler/tests/single_job.rs @@ -1,3 +1,5 @@ +use rand::thread_rng; + use crate::{ cairo_compiler::{tests::models::fixture, CairoCompiler}, traits::CompilerController, @@ -5,18 +7,20 @@ use crate::{ #[tokio::test] async fn run_single_job() { + let mut rng = thread_rng(); let fixture = fixture(); - let compiler = CairoCompiler::new(); + let compiler = CairoCompiler::new(libsecp256k1::SecretKey::random(&mut rng)); compiler.run(fixture.program_path, fixture.program_input_path).unwrap().await.unwrap(); } #[tokio::test] async fn abort_single_jobs() { + let mut rng = thread_rng(); let fixture = fixture(); - let runner = CairoCompiler::new(); - let job = runner.run(fixture.program_path, fixture.program_input_path).unwrap(); + let compiler = CairoCompiler::new(libsecp256k1::SecretKey::random(&mut rng)); + let job = compiler.run(fixture.program_path, fixture.program_input_path).unwrap(); job.abort().await.unwrap(); job.await.unwrap_err(); } diff --git a/crates/runner/Cargo.toml b/crates/runner/Cargo.toml index 42300f5..614d2f7 100644 --- a/crates/runner/Cargo.toml +++ b/crates/runner/Cargo.toml @@ -16,6 +16,8 @@ itertools.workspace = true libsecp256k1.workspace = true rand.workspace = true serde_json.workspace = true +starknet.workspace = true +starknet-crypto.workspace = true serde.workspace = true sharp-p2p-common.workspace = true strum.workspace = true diff --git a/crates/runner/src/cairo_runner/mod.rs b/crates/runner/src/cairo_runner/mod.rs index 934acf8..bb92a17 100644 --- a/crates/runner/src/cairo_runner/mod.rs +++ b/crates/runner/src/cairo_runner/mod.rs @@ -1,7 +1,8 @@ -use self::types::input::{BootloaderInput, BootloaderTask}; +use self::types::input::SimpleBootloaderInput; use crate::{errors::RunnerControllerError, traits::RunnerController}; use async_process::Stdio; use futures::Future; +use libsecp256k1::PublicKey; use sharp_p2p_common::{hash, job::Job, job_trace::JobTrace, layout::Layout, process::Process}; use std::{ hash::{DefaultHasher, Hash, Hasher}, @@ -17,11 +18,12 @@ pub mod types; pub struct CairoRunner { program_path: PathBuf, + public_key: PublicKey, } impl CairoRunner { - pub fn new(program_path: PathBuf) -> Self { - Self { program_path } + pub fn new(program_path: PathBuf, public_key: PublicKey) -> Self { + Self { program_path, public_key } } } @@ -33,18 +35,14 @@ impl RunnerController for CairoRunner { let (terminate_tx, mut terminate_rx) = mpsc::channel::<()>(10); let future: Pin> + '_>> = Box::pin(async move { + let job_hash = hash!(job); let layout: &str = Layout::RecursiveWithPoseidon.into(); let mut cairo_pie = NamedTempFile::new()?; - cairo_pie.write_all(&job.cairo_pie_compressed)?; + cairo_pie.write_all(&job.job_data.cairo_pie_compressed)?; - let input = BootloaderInput { - tasks: vec![BootloaderTask { - path: cairo_pie.path().to_path_buf(), - ..Default::default() - }], - ..Default::default() - }; + let input = + SimpleBootloaderInput { identity: self.public_key, job, single_page: true }; let mut program_input = NamedTempFile::new()?; program_input.write_all(&serde_json::to_string(&input)?.into_bytes())?; @@ -75,8 +73,6 @@ impl RunnerController for CairoRunner { .stdout(Stdio::null()) .spawn()?; - let job_hash = hash!(job); - debug!("task {} spawned", job_hash); loop { diff --git a/crates/runner/src/cairo_runner/tests/models.rs b/crates/runner/src/cairo_runner/tests/models.rs index d42551a..fbd3bca 100644 --- a/crates/runner/src/cairo_runner/tests/models.rs +++ b/crates/runner/src/cairo_runner/tests/models.rs @@ -1,6 +1,7 @@ use rand::{thread_rng, Rng}; -use sharp_p2p_common::job::Job; -use std::{env, path::PathBuf}; +use sharp_p2p_common::job::{Job, JobData}; +use starknet::providers::sequencer::models::L1Address; +use std::{env, fs, path::PathBuf}; pub struct TestFixture { pub job: Job, @@ -16,11 +17,13 @@ pub fn fixture() -> TestFixture { let program_path = ws_root.join("target/bootloader.json"); TestFixture { - job: Job::new( - rng.gen(), - rng.gen(), - cairo_pie_path, - hex::encode(rng.gen::<[u8; 32]>()).as_str(), + job: Job::from_job_data( + JobData::new( + rng.gen(), + rng.gen(), + fs::read(cairo_pie_path).unwrap(), + L1Address::random(), + ), libsecp256k1::SecretKey::random(&mut rng), ), program_path, diff --git a/crates/runner/src/cairo_runner/tests/multiple_job.rs b/crates/runner/src/cairo_runner/tests/multiple_job.rs index 63f1979..322540d 100644 --- a/crates/runner/src/cairo_runner/tests/multiple_job.rs +++ b/crates/runner/src/cairo_runner/tests/multiple_job.rs @@ -3,13 +3,19 @@ use crate::{ traits::RunnerController, }; use futures::{stream::FuturesUnordered, StreamExt}; +use libsecp256k1::{PublicKey, SecretKey}; +use rand::thread_rng; #[tokio::test] async fn run_multiple_jobs() { + let mut rng = thread_rng(); let fixture1 = fixture(); let fixture2 = fixture(); - let runner = CairoRunner::new(fixture1.program_path); + let runner = CairoRunner::new( + fixture1.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); let mut futures = FuturesUnordered::new(); let job1 = runner.run(fixture1.job).unwrap(); @@ -25,10 +31,14 @@ async fn run_multiple_jobs() { #[tokio::test] async fn abort_multiple_jobs() { + let mut rng = thread_rng(); let fixture1 = fixture(); let fixture2 = fixture(); - let runner = CairoRunner::new(fixture1.program_path); + let runner = CairoRunner::new( + fixture1.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); let mut futures = FuturesUnordered::new(); let job1 = runner.run(fixture1.job).unwrap(); diff --git a/crates/runner/src/cairo_runner/tests/single_job.rs b/crates/runner/src/cairo_runner/tests/single_job.rs index 3c332e1..6b88d99 100644 --- a/crates/runner/src/cairo_runner/tests/single_job.rs +++ b/crates/runner/src/cairo_runner/tests/single_job.rs @@ -2,20 +2,30 @@ use crate::{ cairo_runner::{tests::models::fixture, CairoRunner}, traits::RunnerController, }; +use libsecp256k1::{PublicKey, SecretKey}; +use rand::thread_rng; #[tokio::test] async fn run_single_job() { + let mut rng = thread_rng(); let fixture = fixture(); - let runner = CairoRunner::new(fixture.program_path); + let runner = CairoRunner::new( + fixture.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); runner.run(fixture.job).unwrap().await.unwrap(); } #[tokio::test] async fn abort_single_jobs() { + let mut rng = thread_rng(); let fixture = fixture(); - let runner = CairoRunner::new(fixture.program_path); + let runner = CairoRunner::new( + fixture.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); let job = runner.run(fixture.job).unwrap(); job.abort().await.unwrap(); job.await.unwrap_err(); diff --git a/crates/runner/src/cairo_runner/types/input.rs b/crates/runner/src/cairo_runner/types/input.rs index eaabd77..d199b3b 100644 --- a/crates/runner/src/cairo_runner/types/input.rs +++ b/crates/runner/src/cairo_runner/types/input.rs @@ -1,30 +1,10 @@ +use libsecp256k1::PublicKey; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use sharp_p2p_common::job::Job; #[derive(Serialize, Deserialize)] -pub struct BootloaderTask { - #[serde(rename = "type")] - pub type_: String, - pub path: PathBuf, - pub use_poseidon: bool, -} - -#[derive(Serialize, Deserialize)] -pub struct BootloaderInput { - pub tasks: Vec, +pub struct SimpleBootloaderInput { + pub identity: PublicKey, + pub job: Job, pub single_page: bool, } - -impl Default for BootloaderTask { - fn default() -> Self { - Self { type_: "CairoPiePath".to_string(), path: PathBuf::default(), use_poseidon: true } - } -} - -impl Default for BootloaderInput { - fn default() -> Self { - Self { tasks: Vec::default(), single_page: true } - } -} - -pub fn write_cairo_pie_zip() {} diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 06bb165..a78ae9f 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -13,6 +13,8 @@ sharp-p2p-prover.workspace = true sharp-p2p-runner.workspace = true sharp-p2p-compiler.workspace = true tokio.workspace = true +libsecp256k1.workspace = true +rand.workspace = true [features] full_test = [] diff --git a/crates/tests/src/tests/compiler_runner_flow.rs b/crates/tests/src/tests/compiler_runner_flow.rs index 2186768..09dde35 100644 --- a/crates/tests/src/tests/compiler_runner_flow.rs +++ b/crates/tests/src/tests/compiler_runner_flow.rs @@ -1,5 +1,7 @@ use futures::stream::FuturesUnordered; use futures::{FutureExt, StreamExt}; +use libsecp256k1::{PublicKey, SecretKey}; +use rand::thread_rng; use sharp_p2p_compiler::cairo_compiler::tests::models::fixture as compiler_fixture; use sharp_p2p_compiler::cairo_compiler::CairoCompiler; use sharp_p2p_compiler::traits::CompilerController; @@ -9,11 +11,15 @@ use sharp_p2p_runner::traits::RunnerController; #[tokio::test] async fn run_single_job() { + let mut rng = thread_rng(); let compiler_fixture = compiler_fixture(); let runner_fixture = runner_fixture(); - let compiler = CairoCompiler::new(); - let runner = CairoRunner::new(runner_fixture.program_path); + let compiler = CairoCompiler::new(libsecp256k1::SecretKey::random(&mut rng)); + let runner = CairoRunner::new( + runner_fixture.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); compiler .run(compiler_fixture.program_path, compiler_fixture.program_input_path) @@ -29,12 +35,16 @@ async fn run_single_job() { #[tokio::test] async fn run_multiple_job() { + let mut rng = thread_rng(); let compiler_fixture1 = compiler_fixture(); let compiler_fixture2 = compiler_fixture(); let runner_fixture1 = runner_fixture(); - let compiler = CairoCompiler::new(); - let runner = CairoRunner::new(runner_fixture1.program_path); + let compiler = CairoCompiler::new(libsecp256k1::SecretKey::random(&mut rng)); + let runner = CairoRunner::new( + runner_fixture1.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); let mut futures = FuturesUnordered::new(); let job_trace1 = compiler diff --git a/crates/tests/src/tests/runner_prover_flow.rs b/crates/tests/src/tests/runner_prover_flow.rs index 8e7e312..87e4fea 100644 --- a/crates/tests/src/tests/runner_prover_flow.rs +++ b/crates/tests/src/tests/runner_prover_flow.rs @@ -1,4 +1,6 @@ use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; +use libsecp256k1::{PublicKey, SecretKey}; +use rand::thread_rng; use sharp_p2p_prover::{ stone_prover::{ types::{ @@ -35,9 +37,13 @@ pub fn params() -> Params { #[tokio::test] async fn run_single_job() { + let mut rng = thread_rng(); let runner_fixture = runner_fixture(); - let runner = CairoRunner::new(runner_fixture.program_path); + let runner = CairoRunner::new( + runner_fixture.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); let prover = StoneProver::new(config(), params()); runner @@ -51,10 +57,14 @@ async fn run_single_job() { #[tokio::test] async fn run_multiple_job() { + let mut rng = thread_rng(); let runner_fixture1 = runner_fixture(); let runner_fixture2 = runner_fixture(); - let runner = CairoRunner::new(runner_fixture1.program_path); + let runner = CairoRunner::new( + runner_fixture1.program_path, + PublicKey::from_secret_key(&SecretKey::random(&mut rng)), + ); let prover = StoneProver::new(config(), params()); let mut futures = FuturesUnordered::new(); diff --git a/install.py b/install.py index 485f661..3e6973d 100644 --- a/install.py +++ b/install.py @@ -1,5 +1,6 @@ import subprocess +from colorama import Fore, Style def log_and_run(commands, description, cwd=None): full_command = " && ".join(commands) diff --git a/run.py b/run.py new file mode 100644 index 0000000..9878503 --- /dev/null +++ b/run.py @@ -0,0 +1,16 @@ +from install import log_and_run + +if __name__ == "__main__": + log_and_run([ + "cairo-run \ + --program=bootloader.json \ + --layout=recursive_with_poseidon \ + --program_input=bootloader_input.json \ + --air_public_input=bootloader_public_input.json \ + --air_private_input=bootloader_private_input.json \ + --trace_file=bootloader.trace \ + --memory_file=bootloader.memory \ + --print_output \ + --proof_mode \ + --print_info" + ], "Running cairo1 pie in cairo0 bootloader", cwd=".") \ No newline at end of file