diff --git a/Cargo.toml b/Cargo.toml index c8df119c..ac471e1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "crates/cairo-program-runner-lib", "crates/stwo_run_and_prove", "crates/vm_runner", + "crates/concat-aggregator", + "crates/cairo-compile-utils", ] resolver = "2" diff --git a/crates/cairo-compile-utils/Cargo.toml b/crates/cairo-compile-utils/Cargo.toml new file mode 100755 index 00000000..8d95107e --- /dev/null +++ b/crates/cairo-compile-utils/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "cairo-compile-utils" +version = "0.1.0" +edition = "2021" +description = "The source (Cairo) code of the dummy concat aggregator." + +[features] +dump_source_files = [] + +[dependencies] +bincode.workspace = true +cairo-vm.workspace = true +cairo-lang-executable.workspace = true +cairo-lang-runner.workspace = true +cairo-lang-casm.workspace = true +cairo-lang-execute-utils.workspace = true +clap.workspace = true +num-traits.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +thiserror.workspace = true +thiserror-no-std.workspace = true +regex.workspace = true +num-bigint.workspace = true +walkdir = "2" +anyhow = "1.0" + +[build-dependencies] +serde_json.workspace = true + +[dev-dependencies] +assert_matches = "1.5.0" +rstest = "0.19.0" diff --git a/crates/cairo-compile-utils/src/lib.rs b/crates/cairo-compile-utils/src/lib.rs new file mode 100644 index 00000000..369c30e3 --- /dev/null +++ b/crates/cairo-compile-utils/src/lib.rs @@ -0,0 +1,116 @@ +use std::process::Command; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; // A useful crate for walking directories +use std::env; // Import the env module +use anyhow::{Result, Context}; + +use std::fs; + +fn find_main_file(project_root: &PathBuf) -> anyhow::Result { + for entry in WalkDir::new(project_root) + .into_iter() + .filter_map(Result::ok) + .filter(|e| e.file_type().is_file() && e.path().extension().map_or(false, |ext| ext == "cairo")) + { + let content = fs::read_to_string(entry.path())?; + if content.contains("func main") { + return Ok(entry.path().to_path_buf()); + } + } + + Err(anyhow::anyhow!( + "No Cairo 0 file with `func main` found under {:?}", + project_root + )) +} + +/// Builds a shell-like string representation of a Command for logging/debugging. +/// This is a simplified representation and might not handle all edge cases (e.g., complex quoting). +fn format_command_for_display(cmd: &Command) -> String { + let mut cmd_str = String::new(); + + // Add the program name + if let Some(program) = cmd.get_program().to_str() { + cmd_str.push_str(program); + } + + // Add arguments + for arg in cmd.get_args() { + if let Some(s) = arg.to_str() { + cmd_str.push(' '); + // Basic quoting for arguments that might contain spaces + if s.contains(' ') || s.contains('"') || s.contains('\'') { + cmd_str.push('"'); + cmd_str.push_str(&s.replace('"', "\\\"")); // Escape existing double quotes + cmd_str.push('"'); + } else { + cmd_str.push_str(s); + } + } + } + + cmd_str +} + +// current_dir should hold the relevant cairo code (only one main) +pub fn compile_cairo_code(current_dir: &PathBuf) -> anyhow::Result<()> { + + println!("Current working directory: {:?}", current_dir); + + // Define the output path for the compiled program (CASM) + let output_casm_file = PathBuf::from("./target/my_combined_program.json"); + + // Ensure the target directory exists + std::fs::create_dir_all(output_casm_file.parent().unwrap())?; + + println!("Searching for Cairo 0 files in: {:?}", current_dir); + + // Define the main Cairo file to compile + let main_cairo_file = find_main_file(¤t_dir)?; + println!("Detected main Cairo file: {:?}", main_cairo_file); + + // Check that it exists + if !main_cairo_file.exists() { + return Err(anyhow::anyhow!("Main Cairo file not found: {:?}", main_cairo_file)); + } + + println!("Attempting to compile Cairo 0 project to: {:?}", output_casm_file); + + // Build the `cairo-compile` command + let mut command = Command::new("cairo-compile"); + + // Add only the main file + command.arg(&main_cairo_file); + + println!("Executing the following command: {}", format_command_for_display(&command)); + // Specify the output file + command.arg("--output").arg(&output_casm_file); + + let cairo_project_root = current_dir.clone(); + // Add the --cairo_path argument using the determined project root + // This is crucial for resolving 'from starkware.cairo...' imports if 'starkware' + // is a top-level directory directly under your project root. + command.arg("--cairo_path").arg(&cairo_project_root); + + // Execute the command + let output = command.output()?; + + // Check if the command was successful + if output.status.success() { + println!("Cairo 0 compilation successful!"); + println!("Compiled program written to: {:?}", output_casm_file); + io::stdout().write_all(&output.stdout)?; + } else { + eprintln!("Cairo 0 compilation failed!"); + eprintln!("Status: {:?}", output.status); + eprintln!("Stderr:\n{}", String::from_utf8_lossy(&output.stderr)); + return Err(anyhow::anyhow!("Cairo 0 compilation failed: {}", String::from_utf8_lossy(&output.stderr))); + } + + println!("\nTo run the compiled Cairo 0 program, you would typically use `cairo-run`:"); + println!("`cairo-run --program {}`", output_casm_file.display()); + println!("If `main` is an external function for a StarkNet contract, you'd deploy it."); + + Ok(()) +} diff --git a/crates/concat-aggregator/Cargo.toml b/crates/concat-aggregator/Cargo.toml new file mode 100755 index 00000000..8d373765 --- /dev/null +++ b/crates/concat-aggregator/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "concat-aggregator" +version = "0.1.0" +edition = "2021" +description = "The source (Cairo) code of the dummy concat aggregator." + +[features] +dump_source_files = [] + +[dependencies] +bincode.workspace = true +cairo-vm.workspace = true +cairo-lang-executable.workspace = true +cairo-lang-runner.workspace = true +cairo-lang-casm.workspace = true +cairo-lang-execute-utils.workspace = true +clap.workspace = true +num-traits.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +thiserror.workspace = true +thiserror-no-std.workspace = true +regex.workspace = true +num-bigint.workspace = true +walkdir = "2" +anyhow = "1.0" +cairo-compile-utils = { path = "../cairo-compile-utils"} + +[build-dependencies] +serde_json.workspace = true + +[dev-dependencies] +assert_matches = "1.5.0" +rstest = "0.19.0" diff --git a/crates/concat-aggregator/build/main.rs b/crates/concat-aggregator/build/main.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/concat-aggregator/src/aggregator_tasks_utils.cairo b/crates/concat-aggregator/src/aggregator_tasks_utils.cairo new file mode 100644 index 00000000..b0d10cbb --- /dev/null +++ b/crates/concat-aggregator/src/aggregator_tasks_utils.cairo @@ -0,0 +1,60 @@ +// Parses the task outputs from the bootloader output. Writes their outputs to the output_ptr. +// Returns the number of tasks. +func parse_tasks{output_ptr: felt*}() -> felt { + alloc_locals; + + local n_tasks: felt; + + %{ + def parse_bootloader_tasks_outputs(output): + """ + Parses the output of the bootloader, returning the raw outputs of the tasks. + """ + output_iter = iter(output) + # Skip the bootloader_config. + [next(output_iter) for _ in range(3)] + + n_tasks = next(output_iter) + tasks_outputs = [] + for _ in range(n_tasks): + task_output_size = next(output_iter) + tasks_outputs.append([next(output_iter) for _ in range(task_output_size - 1)]) + + assert next(output_iter, None) is None, "Bootloader output wasn't fully consumed." + + return tasks_outputs + + tasks_outputs = parse_bootloader_tasks_outputs(program_input["bootloader_output"]) + assert len(tasks_outputs) > 0, "No tasks found in the bootloader output." + ids.n_tasks = len(tasks_outputs) + %} + + assert [output_ptr] = n_tasks; + let output_ptr = output_ptr + 1; + + // Output the task outputs as they are. + output_tasks(n_tasks=n_tasks); + + return n_tasks; +} + +// Outputs the task outputs, each with the size of the output (to match the bootloader output +// format). +func output_tasks{output_ptr: felt*}(n_tasks: felt) { + if (n_tasks == 0) { + return (); + } + + let output_size = output_ptr[0]; + let output_ptr = output_ptr + 1; + + %{ + task_index = len(tasks_outputs) - ids.n_tasks + segments.load_data(ptr=ids.output_ptr, data=tasks_outputs[task_index]) + ids.output_size = len(tasks_outputs[task_index]) + 1 + %} + + let output_ptr = output_ptr + output_size - 1; + + return output_tasks(n_tasks=n_tasks - 1); +} diff --git a/crates/concat-aggregator/src/concat_aggregator.cairo b/crates/concat-aggregator/src/concat_aggregator.cairo new file mode 100644 index 00000000..a6a10cb4 --- /dev/null +++ b/crates/concat-aggregator/src/concat_aggregator.cairo @@ -0,0 +1,69 @@ +%builtins output range_check poseidon + +from aggregator_tasks_utils import parse_tasks +from starkware.cairo.common.cairo_builtins import PoseidonBuiltin + +// Simple aggregation program that concatenates task outputs. Used for tests. It is not sound. +// +// Hint arguments: +// program_input - List of task outputs, in the format of the bootloader output. +func main{output_ptr: felt*, range_check_ptr, poseidon_ptr: PoseidonBuiltin*}() { + alloc_locals; + + let n_tasks = parse_tasks(); + local output_start: felt* = output_ptr; + + // Output the concatenated task outputs. + output_concatenated_output(n_tasks=n_tasks); + + %{ + from starkware.python.math_utils import div_ceil + + output_length = ids.output_ptr - ids.output_start + page_size = 10 + next_page_start = min(ids.output_start + page_size, ids.output_ptr) + next_page_id = 1 + while next_page_start < ids.output_ptr: + output_builtin.add_page( + page_id=next_page_id, + page_start=next_page_start, + page_size=min(ids.output_ptr - next_page_start, page_size), + ) + next_page_start += page_size + next_page_id += 1 + if next_page_id == 1: + # Single page. Use trivial fact topology. + output_builtin.add_attribute('gps_fact_topology', [ + 1, + 0, + ]) + else: + output_builtin.add_attribute('gps_fact_topology', [ + next_page_id, + next_page_id - 1, + 0, + 2, + ]) + %} + return (); +} + +// Outputs the task outputs, without the output sizes. +func output_concatenated_output{output_ptr: felt*}(n_tasks: felt) { + alloc_locals; + if (n_tasks == 0) { + return (); + } + + local output_size: felt; + + %{ + task_index = len(tasks_outputs) - ids.n_tasks + segments.load_data(ptr=ids.output_ptr, data=tasks_outputs[task_index]) + ids.output_size = len(tasks_outputs[task_index]) + %} + + let output_ptr = output_ptr + output_size; + + return output_concatenated_output(n_tasks=n_tasks - 1); +} diff --git a/crates/concat-aggregator/src/main.rs b/crates/concat-aggregator/src/main.rs new file mode 100644 index 00000000..2e6a2e3b --- /dev/null +++ b/crates/concat-aggregator/src/main.rs @@ -0,0 +1,20 @@ +use std::process::Command; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; // A useful crate for walking directories +use std::env; // Import the env module +use anyhow::{Result, Context}; + +use cairo_compile_utils::compile_cairo_code; +use std::fs; + +fn main() -> anyhow::Result<()> { + + // Get the current working directory of the Rust program + let current_dir = env::current_dir() + .context("Failed to get current working directory")?.join("crates").join("concat-aggregator").join("src"); + + compile_cairo_code(¤t_dir)?; + + Ok(()) +}