diff --git a/Cargo.lock b/Cargo.lock index 6de14744..3488401b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2667,6 +2667,7 @@ dependencies = [ "pathfinder-crypto", "reqwest 0.12.24", "serde", + "serde_json", "starknet", "starknet-crypto 0.7.4", "starknet-types-core 0.1.9", diff --git a/crates/dry_hint_processor/Cargo.toml b/crates/dry_hint_processor/Cargo.toml index 91aa3625..77f0a7d5 100644 --- a/crates/dry_hint_processor/Cargo.toml +++ b/crates/dry_hint_processor/Cargo.toml @@ -21,4 +21,5 @@ strum_macros.workspace = true syscall_handler.workspace = true tokio.workspace = true tracing.workspace = true -types.workspace = true \ No newline at end of file +types.workspace = true +serde_json.workspace = true \ No newline at end of file diff --git a/crates/dry_hint_processor/src/lib.rs b/crates/dry_hint_processor/src/lib.rs index 75f5d435..295e36ac 100644 --- a/crates/dry_hint_processor/src/lib.rs +++ b/crates/dry_hint_processor/src/lib.rs @@ -34,6 +34,7 @@ use crate::syscall_handler::{injected_state, unconstrained}; pub struct CustomHintProcessor { inputs: HDPDryRunInput, + output_preimage_path: std::path::PathBuf, builtin_hint_proc: BuiltinHintProcessor, cairo1_builtin_hint_proc: Cairo1HintProcessor, hints: HashMap, @@ -44,6 +45,7 @@ impl CustomHintProcessor { pub fn new(inputs: HDPDryRunInput) -> Self { Self { inputs, + output_preimage_path: std::path::PathBuf::from("dry_run_output_preimage.json"), builtin_hint_proc: BuiltinHintProcessor::new_empty(), cairo1_builtin_hint_proc: Cairo1HintProcessor::new(Default::default(), Default::default(), true), hints: Self::hints(), @@ -51,6 +53,10 @@ impl CustomHintProcessor { } } + pub fn set_output_preimage_path(&mut self, path: std::path::PathBuf) { + self.output_preimage_path = path; + } + #[rustfmt::skip] fn hints() -> HashMap { let mut hints = hints(); @@ -91,6 +97,7 @@ impl HintProcessorLogic for CustomHintProcessor { let res = match hint_code { crate::input::HINT_INPUT => self.hint_input(vm, exec_scopes, hpd, constants), crate::output::HINT_OUTPUT => self.hint_output(vm, exec_scopes, hpd, constants), + crate::output::HINT_SAVE_OUTPUT_PREIMAGE => self.hint_save_output_preimage(vm, exec_scopes, hpd, constants), _ => Err(HintError::UnknownHint(hint_code.to_string().into_boxed_str())), }; diff --git a/crates/dry_hint_processor/src/output.rs b/crates/dry_hint_processor/src/output.rs index f26df6dc..af28e03c 100644 --- a/crates/dry_hint_processor/src/output.rs +++ b/crates/dry_hint_processor/src/output.rs @@ -1,18 +1,24 @@ use std::collections::HashMap; use cairo_vm::{ - hint_processor::builtin_hint_processor::{ - builtin_hint_processor_definition::HintProcessorData, hint_utils::get_relocatable_from_var_name, + hint_processor::{ + builtin_hint_processor::{ + builtin_hint_processor_definition::HintProcessorData, + hint_utils::{get_integer_from_var_name, get_relocatable_from_var_name}, + }, + hint_processor_utils::felt_to_usize, }, - types::exec_scope::ExecutionScopes, + types::{exec_scope::ExecutionScopes, relocatable::MaybeRelocatable}, vm::{errors::hint_errors::HintError, vm_core::VirtualMachine}, Felt252, }; use hints::vars; +use serde_json; use super::CustomHintProcessor; pub const HINT_OUTPUT: &str = "print(\"result\", [hex(ids.result.low), hex(ids.result.high)])"; +pub const HINT_SAVE_OUTPUT_PREIMAGE: &str = "save_output_preimage(ids.retdata, ids.retdata_size)"; impl CustomHintProcessor { pub fn hint_output( @@ -33,4 +39,53 @@ impl CustomHintProcessor { println!("result: {}, {}", result[0], result[1]); Ok(()) } + + pub fn hint_save_output_preimage( + &mut self, + vm: &mut VirtualMachine, + _exec_scopes: &mut ExecutionScopes, + hint_data: &HintProcessorData, + _constants: &HashMap, + ) -> Result<(), HintError> { + let retdata_ptr = get_relocatable_from_var_name("retdata", vm, &hint_data.ids_data, &hint_data.ap_tracking)?; + let retdata_size = get_integer_from_var_name("retdata_size", vm, &hint_data.ids_data, &hint_data.ap_tracking)?; + let len = felt_to_usize(&retdata_size)?; + + let cells = vm.get_continuous_range(retdata_ptr, len)?; + + let mut values: Vec = Vec::new(); + if len > 0 { + // Relocatable pointer at retdata[0] + let ptr = match &cells[0] { + MaybeRelocatable::RelocatableValue(ptr) => *ptr, + _ => { + return Err(HintError::CustomHint( + "Expected relocatable pointer at retdata[0]".to_owned().into_boxed_str(), + )); + } + }; + let slice = vm.get_continuous_range(ptr, len).map_err(HintError::Memory)?; + for el in slice.into_iter() { + match el { + MaybeRelocatable::Int(f) => values.push(f), + MaybeRelocatable::RelocatableValue(_) => { + return Err(HintError::CustomHint( + "Unexpected relocatable inside output array".to_owned().into_boxed_str(), + )); + } + } + } + } + + // Write directly to file using the path stored in the hint processor + let json_bytes = serde_json::to_vec(&values) + .map_err(|e| HintError::CustomHint(format!("Failed to serialize output preimage: {}", e).into_boxed_str()))?; + std::fs::write(&self.output_preimage_path, json_bytes).map_err(|e| { + HintError::CustomHint( + format!("Failed to write output preimage to {}: {}", self.output_preimage_path.display(), e).into_boxed_str(), + ) + })?; + + Ok(()) + } } diff --git a/crates/dry_run/src/lib.rs b/crates/dry_run/src/lib.rs index 66e092b0..f0094cb7 100644 --- a/crates/dry_run/src/lib.rs +++ b/crates/dry_run/src/lib.rs @@ -22,7 +22,7 @@ use syscall_handler::{SyscallHandler, SyscallHandlerWrapper}; use tokio as _; use tracing::{debug, info}; use tracing_subscriber as _; -use types::{error::Error, param::Param, CasmContractClass, HDPDryRunInput, HDPDryRunOutput, InjectedState}; +use types::{error::Error, output_preimage, param::Param, CasmContractClass, HDPDryRunInput, HDPDryRunOutput, InjectedState}; pub const DRY_RUN_COMPILED_JSON: &str = env!("DRY_RUN_COMPILED_JSON"); @@ -48,12 +48,24 @@ pub struct Args { help = "Path where the output JSON will be written" )] pub output: PathBuf, + #[arg( + long = "output_preimage", + default_value = "dry_run_output_preimage.json", + help = "Path where the output preimage JSON will be written" + )] + pub output_preimage: PathBuf, #[arg( long = "print_output", default_value_t = false, help = "Print program output to stdout [default: false]" )] pub print_output: bool, + #[arg( + long = "print_output_preimage", + default_value_t = false, + help = "Print deserialized output preimage to stdout [default: false]" + )] + pub print_output_preimage: bool, #[structopt(long = "allow_missing_builtins")] pub allow_missing_builtins: Option, } @@ -62,6 +74,7 @@ pub struct Args { pub fn run( program_path: PathBuf, input: HDPDryRunInput, + output_preimage_path: PathBuf, ) -> Result< ( SyscallHandler< @@ -86,6 +99,10 @@ pub fn run( let program = Program::from_bytes(&program_file, Some(cairo_run_config.entrypoint))?; let mut hint_processor = CustomHintProcessor::new(input); + + // Set output preimage path in execution scopes before running (will be used by the hint) + hint_processor.set_output_preimage_path(output_preimage_path.clone()); + let mut cairo_runner = cairo_run_program(&program, &cairo_run_config, &mut hint_processor).map_err(Box::new)?; debug!("{:?}", cairo_runner.get_execution_resources()); @@ -119,7 +136,8 @@ pub fn run( pub async fn run_with_args(args: Args) -> Result<(), Error> { info!("Starting dry run execution..."); info!("Reading compiled module from: {}", args.compiled_module.display()); - let compiled_class: CasmContractClass = serde_json::from_slice(&std::fs::read(args.compiled_module).map_err(Error::IO)?)?; + let compiled_module_path = args.compiled_module.clone(); + let compiled_class: CasmContractClass = serde_json::from_slice(&std::fs::read(&compiled_module_path).map_err(Error::IO)?)?; let params: Vec = if let Some(path) = args.inputs { serde_json::from_slice(&std::fs::read(path).map_err(Error::IO)?)? } else { @@ -139,12 +157,17 @@ pub async fn run_with_args(args: Args) -> Result<(), Error> { params, injected_state, }, + args.output_preimage.clone(), )?; if args.print_output { println!("{:#?}", output); } + if args.print_output_preimage { + output_preimage::print_output_preimage(&args.output_preimage, &compiled_module_path)?; + } + std::fs::write( args.output, serde_json::to_vec::< diff --git a/crates/sound_run/src/lib.rs b/crates/sound_run/src/lib.rs index dccf39b0..cc5b851f 100644 --- a/crates/sound_run/src/lib.rs +++ b/crates/sound_run/src/lib.rs @@ -17,7 +17,7 @@ use sound_hint_processor::CustomHintProcessor; use tokio as _; use tracing::info; use tracing_subscriber as _; -use types::{error::Error, param::Param, CasmContractClass, HDPInput, HDPOutput, InjectedState, ProofsData}; +use types::{error::Error, output_preimage, param::Param, CasmContractClass, HDPInput, HDPOutput, InjectedState, ProofsData}; use crate::prove::prover_input_from_runner; pub mod prove; @@ -51,6 +51,12 @@ pub struct Args { help = "Print program output to stdout [default: false]" )] pub print_output: bool, + #[arg( + long = "print_output_preimage", + default_value_t = false, + help = "Print deserialized output preimage to stdout [default: false]" + )] + pub print_output_preimage: bool, #[arg(long = "proof_mode", conflicts_with = "cairo_pie", help = "Configure runner in proof mode")] pub proof_mode: bool, @@ -103,7 +109,8 @@ pub async fn run_with_args(args: Args) -> Result<(), Error> { info!("Reading compiled module from: {}", args.compiled_module.display()); info!("Reading proofs from: {}", args.proofs.display()); - let compiled_class: CasmContractClass = serde_json::from_slice(&std::fs::read(args.compiled_module).map_err(Error::IO)?)?; + let compiled_module_path = args.compiled_module.clone(); + let compiled_class: CasmContractClass = serde_json::from_slice(&std::fs::read(&compiled_module_path).map_err(Error::IO)?)?; let params: Vec = if let Some(input_path) = args.inputs { serde_json::from_slice(&std::fs::read(input_path).map_err(Error::IO)?)? } else { @@ -143,6 +150,12 @@ pub async fn run_with_args(args: Args) -> Result<(), Error> { println!("{:#?}", output); } + if args.print_output_preimage { + // Use default path for sound run output preimage + let default_preimage_path = PathBuf::from("sound_run_output_preimage.json"); + output_preimage::print_output_preimage(&default_preimage_path, &compiled_module_path)?; + } + if let Some(ref relocated_trace) = cairo_runner.relocated_trace { info!( "Step count ({}): {:?}", diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 623c5ea6..f811a548 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -7,6 +7,7 @@ pub mod cairo; pub mod error; pub mod keys; +pub mod output_preimage; pub mod param; pub mod proofs; @@ -49,6 +50,10 @@ pub const OPTIMISM_MAINNET_CHAIN_ID: u128 = 0xa; pub const OPTIMISM_TESTNET_CHAIN_ID: u128 = 0xaa37dc; pub const STARKNET_MAINNET_CHAIN_ID: u128 = 0x534e5f4d41494e; pub const STARKNET_TESTNET_CHAIN_ID: u128 = 0x534e5f5345504f4c4941; +pub const ARBITRUM_MAINNET_CHAIN_ID: u128 = 0xa4b1; +pub const ARBITRUM_TESTNET_CHAIN_ID: u128 = 0x66eee; +pub const BASE_MAINNET_CHAIN_ID: u128 = 0x2105; +pub const BASE_TESTNET_CHAIN_ID: u128 = 0x14a34; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ProofsData { @@ -111,6 +116,10 @@ pub enum ChainIds { StarknetSepolia, OptimismMainnet, OptimismSepolia, + ArbitrumMainnet, + ArbitrumSepolia, + BaseMainnet, + BaseSepolia, } impl fmt::Display for ChainIds { @@ -122,6 +131,10 @@ impl fmt::Display for ChainIds { ChainIds::StarknetSepolia => write!(f, "starknet-sepolia"), ChainIds::OptimismMainnet => write!(f, "optimism-mainnet"), ChainIds::OptimismSepolia => write!(f, "optimism-sepolia"), + ChainIds::ArbitrumMainnet => write!(f, "arbitrum-mainnet"), + ChainIds::ArbitrumSepolia => write!(f, "arbitrum-sepolia"), + ChainIds::BaseMainnet => write!(f, "base-mainnet"), + ChainIds::BaseSepolia => write!(f, "base-sepolia"), } } } @@ -137,6 +150,10 @@ impl FromStr for ChainIds { "starknet-sepolia" | "starknet_sepolia" | "starknetsepolia" => Ok(Self::StarknetSepolia), "optimism-mainnet" | "optimism_mainnet" | "optimismmainnet" => Ok(Self::OptimismMainnet), "optimism-sepolia" | "optimism_sepolia" | "optimismsepolia" => Ok(Self::OptimismSepolia), + "arbitrum-mainnet" | "arbitrum_mainnet" | "arbitrummainnet" => Ok(Self::ArbitrumMainnet), + "arbitrum-sepolia" | "arbitrum_sepolia" | "arbitrumsepolia" => Ok(Self::ArbitrumSepolia), + "base-mainnet" | "base_mainnet" | "basemainnet" => Ok(Self::BaseMainnet), + "base-sepolia" | "base_sepolia" | "basesepolia" => Ok(Self::BaseSepolia), _ => Err(format!("Invalid chain ID: {}", s)), } } @@ -151,6 +168,10 @@ impl ChainIds { STARKNET_TESTNET_CHAIN_ID => Some(Self::StarknetSepolia), OPTIMISM_MAINNET_CHAIN_ID => Some(Self::OptimismMainnet), OPTIMISM_TESTNET_CHAIN_ID => Some(Self::OptimismSepolia), + ARBITRUM_MAINNET_CHAIN_ID => Some(Self::ArbitrumMainnet), + ARBITRUM_TESTNET_CHAIN_ID => Some(Self::ArbitrumSepolia), + BASE_MAINNET_CHAIN_ID => Some(Self::BaseMainnet), + BASE_TESTNET_CHAIN_ID => Some(Self::BaseSepolia), _ => None, } } diff --git a/crates/types/src/output_preimage.rs b/crates/types/src/output_preimage.rs new file mode 100644 index 00000000..b1c2e61e --- /dev/null +++ b/crates/types/src/output_preimage.rs @@ -0,0 +1,692 @@ +use std::path::PathBuf; + +use cairo_vm::Felt252; +use serde_json::Value; + +use crate::error::Error; + +/// Deserialize and pretty print output preimage using ABI from contract class +pub fn print_output_preimage( + preimage_path: &PathBuf, + compiled_module_path: &PathBuf, +) -> Result<(), Error> { + print_output_preimage_with_options(preimage_path, compiled_module_path, false) +} + +/// Deserialize and pretty print output preimage using ABI from contract class +/// with options to control formatting +pub fn print_output_preimage_with_options( + preimage_path: &PathBuf, + compiled_module_path: &PathBuf, + show_struct_names: bool, +) -> Result<(), Error> { + println!("\nOutput Preimage (deserialized via ABI):"); + println!("{}", "=".repeat(80)); + + // Read the preimage file + let preimage_data: Vec = if preimage_path.exists() { + let file_content = std::fs::read(preimage_path).map_err(|e| { + Error::IO(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to read output preimage file {}: {}", + preimage_path.display(), + e + ), + )) + })?; + + serde_json::from_slice(&file_content).map_err(|e| { + Error::IO(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to deserialize output preimage JSON: {}", e), + )) + })? + } else { + println!( + " Warning: Output preimage file not found at {}", + preimage_path.display() + ); + return Ok(()); + }; + + if preimage_data.is_empty() { + println!(" (empty)"); + return Ok(()); + } + + // Try to get ABI from contract class file + let (abi_output_type, full_abi) = get_abi_output_type(compiled_module_path)?; + + // Deserialize based on ABI + if let Some(output_type) = abi_output_type { + deserialize_with_abi_full(&preimage_data, &output_type, full_abi.as_ref(), show_struct_names)?; + } else { + // Fallback to basic pretty printing if ABI not found + println!(" Warning: Could not find output type in ABI, using basic format"); + print_basic_format(&preimage_data); + } + + println!(); + println!("{}", "=".repeat(80)); + println!(); + + Ok(()) +} + +fn get_abi_output_type(compiled_module_path: &PathBuf) -> Result<(Option, Option), Error> { + // Convert compiled_module path to contract_class path + // e.g., example.compiled_contract_class.json -> example.contract_class.json + let contract_class_path = compiled_module_path + .to_string_lossy() + .replace(".compiled_contract_class.json", ".contract_class.json") + .replace("compiled_contract_class.json", "contract_class.json"); + + let contract_class_path = PathBuf::from(contract_class_path); + + if !contract_class_path.exists() { + return Ok((None, None)); + } + + let contract_class_content = std::fs::read(&contract_class_path).map_err(|e| { + Error::IO(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to read contract class file {}: {}", + contract_class_path.display(), + e + ), + )) + })?; + + let contract_class: Value = serde_json::from_slice(&contract_class_content).map_err(|e| { + Error::IO(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to parse contract class JSON: {}", e), + )) + })?; + + // Find the main function or any function with outputs + let abi = match contract_class.get("abi").and_then(|v| v.as_array()) { + Some(abi) => abi, + None => return Ok((None, None)), + }; + + let full_abi = Value::Array(abi.clone()); + + // Look for main function first, then any function with outputs + let function = abi + .iter() + .find(|item| { + item.get("type") == Some(&Value::String("function".to_string())) + && item.get("name") == Some(&Value::String("main".to_string())) + }) + .or_else(|| { + abi.iter().find(|item| { + item.get("type") == Some(&Value::String("function".to_string())) + && item.get("outputs").is_some() + }) + }); + + if let Some(func) = function { + if let Some(outputs) = func.get("outputs") { + return Ok((Some(outputs.clone()), Some(full_abi))); + } + } + + Ok((None, Some(full_abi))) +} + +fn deserialize_with_abi_full( + preimage_data: &[Felt252], + output_type: &Value, + full_abi: Option<&Value>, + show_struct_names: bool, +) -> Result<(), Error> { + // Get the contract class to find struct definitions + // We need to get the full ABI to resolve struct types + if let Some(outputs_array) = output_type.as_array() { + if outputs_array.len() == 1 { + let output = &outputs_array[0]; + if let Some(type_str) = output.get("type").and_then(|v| v.as_str()) { + // Try to deserialize based on type + let mut offset = 0; + match deserialize_type(preimage_data, &mut offset, type_str, full_abi) { + Ok(deserialized) => { + // Extract struct name from type string (e.g., "example_rsi::module::SpinResult" -> "SpinResult") + let struct_name = type_str + .split("::") + .last() + .unwrap_or(type_str) + .to_string(); + + // Print with custom formatter that preserves order and adds bold formatting + print_formatted_struct(&struct_name, &deserialized, full_abi, 0, show_struct_names)?; + return Ok(()); + } + Err(e) => { + println!(" Warning: Failed to deserialize with ABI: {}", e); + println!(" Falling back to basic format"); + } + } + } + } + } + + // Fallback to basic format + println!(" Output Type: (unknown or complex)"); + print_basic_format(preimage_data); + Ok(()) +} + +// Helper function to find struct in ABI by name (handles both full and short names) +fn find_struct_in_abi<'a>(abi: Option<&'a Value>, struct_name: &str) -> Option<&'a Value> { + if let Some(abi_val) = abi { + if let Some(abi_array) = abi_val.as_array() { + // Try exact match first + if let Some(struct_def) = abi_array.iter().find(|item| { + item.get("type") == Some(&Value::String("struct".to_string())) + && item.get("name") == Some(&Value::String(struct_name.to_string())) + }) { + return Some(struct_def); + } + // Try matching by suffix (e.g., "module::FulfillmentCheckResult" matches "FulfillmentCheckResult") + if let Some(struct_def) = abi_array.iter().find(|item| { + if item.get("type") == Some(&Value::String("struct".to_string())) { + if let Some(name) = item.get("name").and_then(|v| v.as_str()) { + return name == struct_name || name.ends_with(&format!("::{}", struct_name)); + } + } + false + }) { + return Some(struct_def); + } + } + } + None +} + +fn deserialize_type( + data: &[Felt252], + offset: &mut usize, + type_str: &str, + abi: Option<&Value>, +) -> Result { + if *offset >= data.len() { + return Err(Error::IO(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "Unexpected end of data", + ))); + } + + // Handle basic types + if type_str == "core::felt252" { + let value = data[*offset]; + *offset += 1; + return Ok(Value::String(format!("0x{:x}", value))); + } + + if type_str == "core::bool" || type_str == "bool" { + let value = data[*offset]; + *offset += 1; + let bool_val = value != Felt252::ZERO; + return Ok(Value::Bool(bool_val)); + } + + // Handle integer types (u8, u16, u32, u64, u128) - all stored as single felt252 + if type_str == "core::integer::u8" || type_str == "u8" { + let value = data[*offset]; + *offset += 1; + let bytes = value.to_bytes_be(); + let u8_val = bytes[31]; + return Ok(Value::Number(u8_val.into())); + } + + if type_str == "core::integer::u16" || type_str == "u16" { + let value = data[*offset]; + *offset += 1; + let bytes = value.to_bytes_be(); + let u16_val = u16::from_be_bytes([bytes[30], bytes[31]]); + return Ok(Value::Number(u16_val.into())); + } + + if type_str == "core::integer::u32" || type_str == "u32" { + let value = data[*offset]; + *offset += 1; + let bytes = value.to_bytes_be(); + let u32_val = u32::from_be_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]); + return Ok(Value::Number(u32_val.into())); + } + + if type_str == "core::integer::u64" || type_str == "u64" { + let value = data[*offset]; + *offset += 1; + let bytes = value.to_bytes_be(); + let u64_val = u64::from_be_bytes([ + bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], + ]); + return Ok(Value::Number(u64_val.into())); + } + + if type_str == "core::integer::u128" || type_str == "u128" { + let value = data[*offset]; + *offset += 1; + let bytes = value.to_bytes_be(); + let u128_val = u128::from_be_bytes([ + bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], + bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], + ]); + return Ok(Value::String(format!("{}", u128_val))); + } + + // Handle u256 (two felt252: low, high) + if type_str == "core::integer::u256" || type_str == "u256" { + if *offset + 1 >= data.len() { + return Err(Error::IO(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "Not enough data for u256", + ))); + } + let low = data[*offset]; + let high = data[*offset + 1]; + *offset += 2; + + // Convert to hex string (high * 2^128 + low) + let u256_str = if high == Felt252::ZERO { + format!("0x{:x}", low) + } else { + format!("0x{:x}{:032x}", high, low) + }; + return Ok(Value::String(u256_str)); + } + + // Handle fixed-size arrays: [T; N] -> just elements (no length prefix) + if let Some(rest) = type_str.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + if let Some((inner_type, size_str)) = rest.split_once("; ") { + if let Ok(size) = size_str.trim().parse::() { + // Fixed-size array - deserialize N elements + let mut elements = Vec::new(); + for _ in 0..size { + elements.push(deserialize_type(data, offset, inner_type.trim(), abi)?); + } + return Ok(Value::Array(elements)); + } + } + } + + // Handle dynamic arrays: Array -> length (felt252) + elements + if let Some(inner_type) = type_str.strip_prefix("core::array::Array::<").and_then(|s| s.strip_suffix(">")) { + // Get length - felt252 stores length, extract as usize + let len_felt = data[*offset]; + let len_val = { + let bytes = len_felt.to_bytes_be(); + // Extract last 8 bytes as u64 (big-endian) + let mut buf = [0u8; 8]; + buf.copy_from_slice(&bytes[24..32]); + u64::from_be_bytes(buf) as usize + }; + *offset += 1; + + // Deserialize elements + let mut elements = Vec::new(); + for _ in 0..len_val { + elements.push(deserialize_type(data, offset, inner_type, abi)?); + } + return Ok(Value::Array(elements)); + } + + // Handle enums - serialized as variant index (felt252) + if let Some(abi_val) = abi { + if let Some(abi_array) = abi_val.as_array() { + // Try to find enum definition + if let Some(enum_def) = abi_array.iter().find(|item| { + item.get("type") == Some(&Value::String("enum".to_string())) + && (item.get("name") == Some(&Value::String(type_str.to_string())) + || item.get("name").and_then(|v| v.as_str()).map_or(false, |n| { + n == type_str || n.ends_with(&format!("::{}", type_str)) + })) + }) { + // Get variant index + let variant_idx = data[*offset]; + *offset += 1; + + // Extract variant index as usize + let bytes = variant_idx.to_bytes_be(); + let mut buf = [0u8; 8]; + buf.copy_from_slice(&bytes[24..32]); + let idx = u64::from_be_bytes(buf) as usize; + + // Get variants + if let Some(variants) = enum_def.get("variants").and_then(|v| v.as_array()) { + if idx < variants.len() { + if let Some(variant) = variants.get(idx) { + if let Some(variant_name) = variant.get("name").and_then(|v| v.as_str()) { + // If variant has a type, deserialize it; otherwise it's unit variant + if let Some(variant_type_val) = variant.get("type") { + if let Some(variant_type_str) = variant_type_val.as_str() { + if variant_type_str != "()" { + let inner_value = deserialize_type(data, offset, variant_type_str, abi)?; + let mut obj = serde_json::Map::new(); + obj.insert(variant_name.to_string(), inner_value); + return Ok(Value::Object(obj)); + } + } + } + // Unit variant - just return the variant name + return Ok(Value::String(variant_name.to_string())); + } + } + } + } + } + } + } + + // Handle structs - need to find struct definition in ABI + let struct_name = if let Some(name) = type_str.strip_prefix("struct ") { + name + } else { + type_str + }; + + if let Some(struct_def) = find_struct_in_abi(abi, struct_name) { + if let Some(members) = struct_def.get("members").and_then(|v| v.as_array()) { + // Use serde_json::Map which preserves insertion order (uses IndexMap internally) + // Iterate through members in ABI order to maintain struct field ordering + let mut obj = serde_json::Map::new(); + for member in members { + if let (Some(name), Some(member_type)) = ( + member.get("name").and_then(|v| v.as_str()), + member.get("type").and_then(|v| v.as_str()), + ) { + let value = deserialize_type(data, offset, member_type, abi)?; + // Insert in order - serde_json::Map preserves insertion order + obj.insert(name.to_string(), value); + } + } + return Ok(Value::Object(obj)); + } + } + + // Fallback: treat as felt252 + let value = data[*offset]; + *offset += 1; + Ok(Value::String(format!("0x{:x}", value))) +} + +// ANSI escape codes for formatting +const BOLD: &str = "\x1b[1m"; +const RESET: &str = "\x1b[0m"; + +// Custom formatter that preserves struct field order and makes values bold +fn print_formatted_struct( + struct_name: &str, + value: &Value, + abi: Option<&Value>, + indent: usize, + show_struct_names: bool, +) -> Result<(), Error> { + let indent_str = " ".repeat(indent); + + // If it's an object (struct), print with preserved order + if let Value::Object(map) = value { + // Print struct name with the given indent level (if enabled) + if show_struct_names { + print!("{}{}{}{} {{", indent_str, BOLD, struct_name, RESET); + } else { + print!("{}{{", indent_str); + } + + if map.is_empty() { + print!(" }}"); + return Ok(()); + } + + println!(); + + // Get struct definition from ABI to maintain field order + if let Some(abi_val) = abi { + if let Some(abi_array) = abi_val.as_array() { + if let Some(struct_def) = abi_array.iter().find(|item| { + item.get("type") == Some(&Value::String("struct".to_string())) + && (item.get("name").and_then(|v| v.as_str()).map_or(false, |n| { + n == struct_name || n.ends_with(&format!("::{}", struct_name)) + })) + }) { + if let Some(members) = struct_def.get("members").and_then(|v| v.as_array()) { + // Print fields in ABI order + // Fields should be indented one level more than the struct name + let field_indent_str = " ".repeat(indent + 1); + for (idx, member) in members.iter().enumerate() { + if let Some(name) = member.get("name").and_then(|v| v.as_str()) { + if let Some(field_value) = map.get(name) { + print!("{} \"{}\": ", field_indent_str, name); + // Field values should be at the same indent as the field name + // Fields are at indent+1, so pass indent+1 to the formatter + print_formatted_value(field_value, abi, indent + 1, show_struct_names)?; + if idx < members.len() - 1 { + println!(","); + } else { + println!(); + } + } + } + } + print!("{}}}", indent_str); + return Ok(()); + } + } + } + } + + // Fallback: print in map order (which should be preserved) + // Fields should be indented one level more than the struct name + let field_indent_str = " ".repeat(indent + 1); + let entries: Vec<_> = map.iter().collect(); + for (idx, (key, val)) in entries.iter().enumerate() { + print!("{} \"{}\": ", field_indent_str, key); + // Fields are at indent+1, so pass indent+1 to the formatter + print_formatted_value(val, abi, indent + 1, show_struct_names)?; + if idx < entries.len() - 1 { + println!(","); + } else { + println!(); + } + } + print!("{}}}", indent_str); + } else { + // Not a struct, just print the value + print_formatted_value(value, abi, indent, show_struct_names)?; + } + + Ok(()) +} + +fn print_formatted_value(value: &Value, abi: Option<&Value>, indent: usize, show_struct_names: bool) -> Result<(), Error> { + match value { + Value::Null => print!("{}null{}", BOLD, RESET), + Value::Bool(b) => print!("{}{}{}", BOLD, b, RESET), + Value::Number(n) => print!("{}{}{}", BOLD, n, RESET), + Value::String(s) => { + print!("{}\"{}\"{}", BOLD, s, RESET); + } + Value::Array(arr) => { + if arr.is_empty() { + print!("[]"); + } else { + // Check if all elements are small numeric values (u32 or smaller) + let is_small_numeric = arr.iter().all(|v| { + match v { + Value::Number(_) => true, + Value::String(s) => s.parse::().is_ok(), + _ => false, + } + }); + + // Check if all elements are enum strings (short string values, likely enum variants) + let is_enum_array = arr.iter().all(|v| { + if let Value::String(s) = v { + // Check if it's a short string (likely enum) and not a number + s.len() <= 10 && s.parse::().is_err() + } else { + false + } + }); + + if is_small_numeric && arr.len() <= 10 { + // Print small numeric arrays on one line + print!("["); + for (idx, item) in arr.iter().enumerate() { + print_formatted_value(item, abi, indent, show_struct_names)?; + if idx < arr.len() - 1 { + print!(", "); + } + } + print!("]"); + } else if is_enum_array { + // Print enum arrays compactly with multiple values per line (4 per line) + // Calculate column widths for alignment + let items_per_line = 4; + let num_lines = (arr.len() + items_per_line - 1) / items_per_line; + let mut column_widths = vec![0; items_per_line]; + + // Find maximum width for each column + for (idx, item) in arr.iter().enumerate() { + if let Value::String(s) = item { + let col = idx % items_per_line; + column_widths[col] = column_widths[col].max(s.len()); + } + } + + println!("["); + let indent_str = " ".repeat(indent); + for (idx, item) in arr.iter().enumerate() { + let is_line_start = idx % items_per_line == 0; + let is_line_end = (idx + 1) % items_per_line == 0 || idx == arr.len() - 1; + let col = idx % items_per_line; + + if is_line_start { + print!("{} ", indent_str); + } + + // Print value with padding for column alignment + if let Value::String(s) = item { + let width = column_widths[col]; + print!("{}\"{}\"{}", BOLD, s, RESET); + if !is_line_end && col < items_per_line - 1 { + // Add padding to align next column + let padding = width.saturating_sub(s.len()) + 2; // +2 for ", " + print!(",{}", " ".repeat(padding)); + } + } else { + print_formatted_value(item, abi, indent, show_struct_names)?; + if !is_line_end { + print!(", "); + } + } + + if is_line_end { + println!(","); + } else if idx == arr.len() - 1 { + println!(); + } + } + print!("{} ]", indent_str); + } else { + // Print larger arrays or non-numeric arrays with line breaks + println!("["); + let indent_str = " ".repeat(indent); + // Array elements are indented by 4 spaces from the array start + // So if array is at indent N, elements are at N+2 + let element_indent = indent + 2; + for (idx, item) in arr.iter().enumerate() { + // For array elements, we print them at element_indent level + // The formatter should use this indent level directly + print_formatted_value(item, abi, element_indent, show_struct_names)?; + if idx < arr.len() - 1 { + println!(","); + } else { + println!(); + } + } + print!("{} ]", indent_str); + } + } + } + Value::Object(map) => { + // Nested struct - try to find struct name from ABI + // Look for a struct in ABI that has matching field names + let struct_name = if let Some(abi_val) = abi { + if let Some(abi_array) = abi_val.as_array() { + // Try to find a struct that matches this object's fields + abi_array + .iter() + .find_map(|item| { + if item.get("type") == Some(&Value::String("struct".to_string())) { + if let Some(members) = item.get("members").and_then(|v| v.as_array()) { + // Check if all members match + let all_match = members.iter().all(|m| { + m.get("name").and_then(|v| v.as_str()).map_or(false, |name| { + map.contains_key(name) + }) + }); + if all_match && !members.is_empty() { + item.get("name").and_then(|v| v.as_str()) + .map(|n| n.split("::").last().unwrap_or(n).to_string()) + } else { + None + } + } else { + None + } + } else { + None + } + }) + .unwrap_or_else(|| "struct".to_string()) + } else { + "struct".to_string() + } + } else { + "struct".to_string() + }; + + // Use the struct formatter for nested structs + print_formatted_struct(&struct_name, value, abi, indent, show_struct_names)?; + } + } + Ok(()) +} + +fn print_basic_format(preimage_data: &[Felt252]) { + println!(" Values ({} total):", preimage_data.len()); + for (idx, felt) in preimage_data.iter().enumerate() { + let hex_str = format!("0x{:x}", felt); + + // Try to interpret as different types for better readability + let bytes = felt.to_bytes_be(); + let as_u64 = u64::from_be_bytes([ + bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], + ]); + let as_u32 = u32::from_be_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]); + let as_u16 = u16::from_be_bytes([bytes[30], bytes[31]]); + let as_u8 = bytes[31]; + + print!(" [{}] {}", idx, hex_str); + + // Show alternative interpretations if they're reasonable + if as_u64 < 1000000 { + print!(" (decimal: {})", as_u64); + } + if as_u32 < 1000000 && as_u32 > 0 { + print!(" (u32: {})", as_u32); + } + if as_u16 < 10000 && as_u16 > 0 { + print!(" (u16: {})", as_u16); + } + if as_u8 < 255 && as_u8 > 0 { + print!(" (u8: {})", as_u8); + } + println!(); + } +} diff --git a/src/contract_bootloader/contract_dry_run.cairo b/src/contract_bootloader/contract_dry_run.cairo index 34957350..ec444077 100644 --- a/src/contract_bootloader/contract_dry_run.cairo +++ b/src/contract_bootloader/contract_dry_run.cairo @@ -161,6 +161,8 @@ func main{ let (leafs: Uint256*) = alloc(); felt_array_to_uint256s(counter=retdata_size, retdata=retdata, leafs=leafs); + %{ save_output_preimage(ids.retdata, ids.retdata_size) %} + with keccak_ptr { let output_root = compute_merkle_root(leafs, retdata_size); } diff --git a/src/utils/chain_info.cairo b/src/utils/chain_info.cairo index 3227d3ba..d9dd2c12 100644 --- a/src/utils/chain_info.cairo +++ b/src/utils/chain_info.cairo @@ -60,6 +60,62 @@ func fetch_chain_info(chain_id: felt) -> (info: ChainInfo) { ); } + // Arbitrum Mainnet + if (chain_id == 42161) { + return ( + info=ChainInfo( + id=42161, + id_bytes_len=2, + encoded_id=0x82A4B1, + encoded_id_bytes_len=3, + byzantium=0, + layout=Layout.EVM, + ), + ); + } + + // Arbitrum Sepolia + if (chain_id == 421614) { + return ( + info=ChainInfo( + id=421614, + id_bytes_len=3, + encoded_id=0x83066EEE, + encoded_id_bytes_len=4, + byzantium=0, + layout=Layout.EVM, + ), + ); + } + + // Base Mainnet + if (chain_id == 8453) { + return ( + info=ChainInfo( + id=8453, + id_bytes_len=2, + encoded_id=0x822105, + encoded_id_bytes_len=3, + byzantium=0, + layout=Layout.EVM, + ), + ); + } + + // Base Sepolia + if (chain_id == 84532) { + return ( + info=ChainInfo( + id=84532, + id_bytes_len=3, + encoded_id=0x83014A34, + encoded_id_bytes_len=4, + byzantium=0, + layout=Layout.EVM, + ), + ); + } + // SN_MAIN if (chain_id == 23448594291968334) { return (