diff --git a/Cargo.lock b/Cargo.lock index 652e9b2b..7421d827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6156,6 +6156,7 @@ dependencies = [ "num-integer", "num-traits", "serde", + "strum_macros", "thiserror 1.0.69", "types", ] diff --git a/Cargo.toml b/Cargo.toml index da7cbfb0..d624bf97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,10 @@ futures = "0.3.31" hex = "0.4.3" http-body-util = "=0.1.0" indicatif = "0.17.9" +keccak = "0.1.5" +lazy_static = "1.5.0" num-bigint = "0.4.6" +num-integer = "0.1.46" num-traits = "0.2.19" rand = "0.8" reqwest = "0.12.9" @@ -53,8 +56,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } utoipa = { version = "5.3.1", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9", features = ["axum"] } version-compare = "=0.0.11" -num-integer = "0.1.46" -keccak = "0.1.5" dry_hint_processor = { path = "crates/dry_hint_processor" } eth_essentials_cairo_vm_hints = { path = "packages/eth_essentials/cairo_vm_hints" } diff --git a/crates/dry_hint_processor/src/syscall_handler/mod.rs b/crates/dry_hint_processor/src/syscall_handler/mod.rs index 7d416997..265f16d3 100644 --- a/crates/dry_hint_processor/src/syscall_handler/mod.rs +++ b/crates/dry_hint_processor/src/syscall_handler/mod.rs @@ -14,7 +14,8 @@ use hints::vars; use serde::{Deserialize, Serialize}; use starknet::CallContractHandler as StarknetCallContractHandler; use syscall_handler::{ - felt_from_ptr, keccak::KeccakHandler, run_handler, traits, SyscallExecutionError, SyscallResult, SyscallSelector, WriteResponseResult, + call_contract, call_contract::debug::DebugCallContractHandler, felt_from_ptr, keccak::KeccakHandler, run_handler, traits, + SyscallExecutionError, SyscallResult, SyscallSelector, WriteResponseResult, }; use tokio::{sync::RwLock, task}; use types::{ @@ -80,6 +81,8 @@ impl SyscallHandlerWrapper { pub struct CallContractHandlerRelay { pub evm_call_contract_handler: EvmCallContractHandler, pub starknet_call_contract_handler: StarknetCallContractHandler, + #[serde(skip)] + pub debug_call_contract_handler: DebugCallContractHandler, } impl traits::SyscallHandler for CallContractHandlerRelay { @@ -93,13 +96,18 @@ impl traits::SyscallHandler for CallContractHandlerRelay { } async fn execute(&mut self, request: Self::Request, vm: &mut VirtualMachine) -> SyscallResult { - let chain_id = >::try_into(*vm.get_integer((request.calldata_start + 2)?)?) - .map_err(|e| SyscallExecutionError::InternalError(e.to_string().into()))?; - - match chain_id { - ETHEREUM_MAINNET_CHAIN_ID | ETHEREUM_TESTNET_CHAIN_ID => self.evm_call_contract_handler.execute(request, vm).await, - STARKNET_MAINNET_CHAIN_ID | STARKNET_TESTNET_CHAIN_ID => self.starknet_call_contract_handler.execute(request, vm).await, - _ => Err(SyscallExecutionError::InternalError(Box::from("Unknown chain id"))), + match request.contract_address { + v if v == call_contract::debug::CONTRACT_ADDRESS => self.debug_call_contract_handler.execute(request, vm).await, + _ => { + let chain_id = >::try_into(*vm.get_integer((request.calldata_start + 2)?)?) + .map_err(|e| SyscallExecutionError::InternalError(e.to_string().into()))?; + + match chain_id { + ETHEREUM_MAINNET_CHAIN_ID | ETHEREUM_TESTNET_CHAIN_ID => self.evm_call_contract_handler.execute(request, vm).await, + STARKNET_MAINNET_CHAIN_ID | STARKNET_TESTNET_CHAIN_ID => self.starknet_call_contract_handler.execute(request, vm).await, + _ => Err(SyscallExecutionError::InternalError(Box::from("Unknown chain id"))), + } + } } } diff --git a/crates/sound_hint_processor/src/syscall_handler/mod.rs b/crates/sound_hint_processor/src/syscall_handler/mod.rs index 5eaaa0ea..63a16c5d 100644 --- a/crates/sound_hint_processor/src/syscall_handler/mod.rs +++ b/crates/sound_hint_processor/src/syscall_handler/mod.rs @@ -17,7 +17,8 @@ use cairo_vm::{ use hints::vars; use serde::{Deserialize, Serialize}; use syscall_handler::{ - felt_from_ptr, keccak::KeccakHandler, run_handler, traits, SyscallExecutionError, SyscallResult, SyscallSelector, WriteResponseResult, + call_contract, call_contract::debug::DebugCallContractHandler, felt_from_ptr, keccak::KeccakHandler, run_handler, traits, + SyscallExecutionError, SyscallResult, SyscallSelector, WriteResponseResult, }; use tokio::{sync::RwLock, task}; use types::{ @@ -85,6 +86,7 @@ impl CairoType for Memorizer { pub struct CallContractHandlerRelay { pub evm_call_contract_handler: EvmCallContractHandler, pub starknet_call_contract_handler: StarknetCallContractHandler, + pub debug_call_contract_handler: DebugCallContractHandler, } impl CallContractHandlerRelay { @@ -92,6 +94,7 @@ impl CallContractHandlerRelay { Self { evm_call_contract_handler: EvmCallContractHandler::new(dict_manager.clone()), starknet_call_contract_handler: StarknetCallContractHandler::new(dict_manager), + debug_call_contract_handler: DebugCallContractHandler, } } } @@ -107,13 +110,18 @@ impl traits::SyscallHandler for CallContractHandlerRelay { } async fn execute(&mut self, request: Self::Request, vm: &mut VirtualMachine) -> SyscallResult { - let chain_id = >::try_into(*vm.get_integer((request.calldata_start + 2)?)?) - .map_err(|e| SyscallExecutionError::InternalError(e.to_string().into()))?; - - match chain_id { - ETHEREUM_MAINNET_CHAIN_ID | ETHEREUM_TESTNET_CHAIN_ID => self.evm_call_contract_handler.execute(request, vm).await, - STARKNET_MAINNET_CHAIN_ID | STARKNET_TESTNET_CHAIN_ID => self.starknet_call_contract_handler.execute(request, vm).await, - _ => Err(SyscallExecutionError::InternalError(Box::from("Unknown chain id"))), + match request.contract_address { + v if v == call_contract::debug::CONTRACT_ADDRESS => self.debug_call_contract_handler.execute(request, vm).await, + _ => { + let chain_id = >::try_into(*vm.get_integer((request.calldata_start + 2)?)?) + .map_err(|e| SyscallExecutionError::InternalError(e.to_string().into()))?; + + match chain_id { + ETHEREUM_MAINNET_CHAIN_ID | ETHEREUM_TESTNET_CHAIN_ID => self.evm_call_contract_handler.execute(request, vm).await, + STARKNET_MAINNET_CHAIN_ID | STARKNET_TESTNET_CHAIN_ID => self.starknet_call_contract_handler.execute(request, vm).await, + _ => Err(SyscallExecutionError::InternalError(Box::from("Unknown chain id"))), + } + } } } @@ -129,6 +137,7 @@ pub struct SyscallHandler { #[serde(skip)] pub syscall_ptr: Option, pub call_contract_handler: CallContractHandlerRelay, + // pub debug_handler: DebugHandler, #[serde(skip)] pub keccak_handler: KeccakHandler, } diff --git a/crates/syscall_handler/Cargo.toml b/crates/syscall_handler/Cargo.toml index 60328b94..aadee551 100644 --- a/crates/syscall_handler/Cargo.toml +++ b/crates/syscall_handler/Cargo.toml @@ -5,10 +5,11 @@ edition = "2021" [dependencies] cairo-vm.workspace = true -thiserror.workspace = true -types.workspace = true -num-integer.workspace = true keccak.workspace = true num-bigint.workspace = true +num-integer.workspace = true num-traits.workspace = true -serde.workspace = true \ No newline at end of file +serde.workspace = true +strum_macros.workspace = true +thiserror.workspace = true +types.workspace = true diff --git a/crates/syscall_handler/src/call_contract/debug.rs b/crates/syscall_handler/src/call_contract/debug.rs new file mode 100644 index 00000000..fc8e772e --- /dev/null +++ b/crates/syscall_handler/src/call_contract/debug.rs @@ -0,0 +1,141 @@ +use cairo_vm::{types::relocatable::Relocatable, vm::vm_core::VirtualMachine, Felt252}; +use serde::{Deserialize, Serialize}; +use strum_macros::FromRepr; +use types::cairo::{ + new_syscalls::{CallContractRequest, CallContractResponse}, + traits::CairoType, +}; + +use crate::{traits, SyscallExecutionError, SyscallResult, WriteResponseResult}; + +pub const CONTRACT_ADDRESS: Felt252 = Felt252::from_hex_unchecked("0x6465627567"); // 'debug' in hex + +#[derive(FromRepr)] +pub enum CallHandlerId { + Print = 0, + PrintArray = 1, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct DebugCallContractHandler; + +impl traits::SyscallHandler for DebugCallContractHandler { + type Request = CallContractRequest; + type Response = CallContractResponse; + + fn read_request(&mut self, vm: &VirtualMachine, ptr: &mut Relocatable) -> SyscallResult { + let ret = Self::Request::from_memory(vm, *ptr)?; + *ptr = (*ptr + Self::Request::cairo_size())?; + Ok(ret) + } + + async fn execute(&mut self, request: Self::Request, vm: &mut VirtualMachine) -> SyscallResult { + let call_handler_id = CallHandlerId::try_from(request.selector)?; + match call_handler_id { + CallHandlerId::Print => { + let field_len = (request.calldata_end - request.calldata_start)?; + let fields = vm + .get_integer_range(request.calldata_start, field_len)? + .into_iter() + .map(|f| (*f.as_ref())) + .collect::>(); + + let str = decode_byte_array_felts(fields); + println!("{}", str); + Ok(Self::Response { + retdata_start: request.calldata_end, + retdata_end: request.calldata_end, + }) + } + CallHandlerId::PrintArray => { + let field_len = (request.calldata_end - request.calldata_start)?; + let fields = vm + .get_integer_range(request.calldata_start, field_len)? + .into_iter() + .map(|f| (*f.as_ref())) + .collect::>(); + + println!("{:?}", fields); + + Ok(Self::Response { + retdata_start: request.calldata_end, + retdata_end: request.calldata_end, + }) + } + } + } + + fn write_response(&mut self, _response: Self::Response, _vm: &mut VirtualMachine, _ptr: &mut Relocatable) -> WriteResponseResult { + Ok(()) + } +} + +// Decodes a serialized byte array of felts into a ascii string +fn decode_byte_array_felts(felts: Vec) -> String { + // 1) Parse how many full 31-byte chunks we have. + let n_full: usize = felts[0].try_into().expect("n_full not convertible"); + + // 2) Read each 31-byte chunk in big-endian order. + let mut bytes = Vec::new(); + for i in 0..n_full { + let chunk = &felts[1 + i]; + let chunk_be: Vec = chunk.to_bytes_be().to_vec(); + + // Convert if chain to match + match chunk_be.len().cmp(&31) { + std::cmp::Ordering::Less => { + // Prepend leading zeros if needed + let mut padded = vec![0u8; 31 - chunk_be.len()]; + padded.extend_from_slice(&chunk_be); + bytes.extend_from_slice(&padded); + } + std::cmp::Ordering::Greater => { + // If somehow bigger, take the last 31 bytes + bytes.extend_from_slice(&chunk_be[chunk_be.len() - 31..]); + } + std::cmp::Ordering::Equal => { + bytes.extend_from_slice(&chunk_be); + } + } + } + + // 3) The next felt is the pending word, followed by the pending length. + let pending_word = &felts[1 + n_full]; + let pending_len: usize = felts[1 + n_full + 1].try_into().unwrap(); + + if pending_len > 0 { + let pending_be: Vec = pending_word.to_bytes_be().to_vec(); + // Convert if chain to match + match pending_be.len().cmp(&pending_len) { + std::cmp::Ordering::Less => { + // Again pad if needed + let mut padded = vec![0u8; pending_len - pending_be.len()]; + padded.extend_from_slice(&pending_be); + bytes.extend_from_slice(&padded); + } + std::cmp::Ordering::Greater => { + bytes.extend_from_slice(&pending_be[pending_be.len() - pending_len..]); + } + std::cmp::Ordering::Equal => { + bytes.extend_from_slice(&pending_be); + } + } + } + + // 4) Convert raw bytes to a UTF-8 string (or ASCII if you know it is ASCII). + String::from_utf8(bytes).expect("Invalid UTF-8") +} + +impl TryFrom for CallHandlerId { + type Error = SyscallExecutionError; + fn try_from(value: Felt252) -> Result { + Self::from_repr(value.try_into().map_err(|e| Self::Error::InvalidSyscallInput { + input: value, + info: format!("{}", e), + })?) + .ok_or(Self::Error::InvalidSyscallInput { + input: value, + info: "Invalid function identifier".to_string(), + }) + } +} diff --git a/crates/syscall_handler/src/call_contract/mod.rs b/crates/syscall_handler/src/call_contract/mod.rs new file mode 100644 index 00000000..2f365233 --- /dev/null +++ b/crates/syscall_handler/src/call_contract/mod.rs @@ -0,0 +1 @@ +pub mod debug; diff --git a/crates/syscall_handler/src/lib.rs b/crates/syscall_handler/src/lib.rs index 56f48cc9..f6512301 100644 --- a/crates/syscall_handler/src/lib.rs +++ b/crates/syscall_handler/src/lib.rs @@ -3,6 +3,7 @@ #![warn(unused_crate_dependencies)] #![forbid(unsafe_code)] +pub mod call_contract; pub mod keccak; pub mod traits; diff --git a/hdp_cairo/src/debug.cairo b/hdp_cairo/src/debug.cairo new file mode 100644 index 00000000..d414462a --- /dev/null +++ b/hdp_cairo/src/debug.cairo @@ -0,0 +1,2 @@ +mod printer; +pub use printer::{print, print_array}; \ No newline at end of file diff --git a/hdp_cairo/src/debug/printer.cairo b/hdp_cairo/src/debug/printer.cairo new file mode 100644 index 00000000..3af6cbad --- /dev/null +++ b/hdp_cairo/src/debug/printer.cairo @@ -0,0 +1,25 @@ +use starknet::syscalls::call_contract_syscall; +use starknet::SyscallResultTrait; +use core::fmt::Display; + + +const DEBUG_CONTRACT_ADDRESS: felt252 = 'debug'; +const PRINT: felt252 = 0; +const PRINT_ARRAY: felt252 = 1; + +pub fn print, +Drop>(value: T) { + let msg: ByteArray = format!("{}", value); + let mut output_array = array![]; + msg.serialize(ref output_array); + call_contract_syscall(DEBUG_CONTRACT_ADDRESS.try_into().unwrap(), PRINT, output_array.span()) + .unwrap_syscall(); +} + +pub fn print_array(array: Array) { + call_contract_syscall( + DEBUG_CONTRACT_ADDRESS.try_into().unwrap(), + PRINT_ARRAY, + array.span() + ) + .unwrap_syscall(); +} \ No newline at end of file diff --git a/hdp_cairo/src/lib.cairo b/hdp_cairo/src/lib.cairo index d116cc68..84f18101 100644 --- a/hdp_cairo/src/lib.cairo +++ b/hdp_cairo/src/lib.cairo @@ -1,5 +1,6 @@ pub mod evm; pub mod starknet; +pub mod debug; #[derive(Serde, Drop)] pub struct HDP { diff --git a/src/contract_bootloader/execute_syscalls.cairo b/src/contract_bootloader/execute_syscalls.cairo index 3e53ad92..5ca76fb8 100644 --- a/src/contract_bootloader/execute_syscalls.cairo +++ b/src/contract_bootloader/execute_syscalls.cairo @@ -126,6 +126,11 @@ func execute_call_contract{ let state_access_type = request.contract_address; let field = request.selector; + // Debug Contract does not need to be executed + if (request.contract_address == 'debug') { + return (); + } + let layout = chain_id_to_layout(request.calldata_start[2]); let output_ptr = response.retdata_start; diff --git a/tests/src/lib.cairo b/tests/src/lib.cairo index 9f3dd66d..c1762f74 100644 --- a/tests/src/lib.cairo +++ b/tests/src/lib.cairo @@ -1,2 +1,3 @@ pub mod evm; pub mod starknet; +pub mod utils; \ No newline at end of file diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 77e63b09..e9e888ca 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -7,6 +7,9 @@ pub mod evm; #[cfg(test)] pub mod starknet; +#[cfg(test)] +pub mod utils; + #[cfg(test)] mod test_utils { use std::{env, path::PathBuf}; diff --git a/tests/src/utils.cairo b/tests/src/utils.cairo new file mode 100644 index 00000000..477cc78a --- /dev/null +++ b/tests/src/utils.cairo @@ -0,0 +1 @@ +pub mod debug; \ No newline at end of file diff --git a/tests/src/utils/debug.cairo b/tests/src/utils/debug.cairo new file mode 100644 index 00000000..c482f1dd --- /dev/null +++ b/tests/src/utils/debug.cairo @@ -0,0 +1,34 @@ +#[starknet::contract] +mod test_debug_print { + use hdp_cairo::HDP; + use hdp_cairo::debug::{print, print_array}; + use core::fmt::{Display, Formatter, Error}; + + #[storage] + struct Storage {} + + #[derive(Copy, Drop)] + struct Point { + x: u8, + y: u8, + } + + impl PointDisplay of Display { + fn fmt(self: @Point, ref f: Formatter) -> Result<(), Error> { + let str: ByteArray = format!("PointThatIAmMakingQuiteABitLongerToEnsureWeHaveMoreFelts ({}, {})", *self.x, *self.y); + f.buffer.append(@str); + Result::Ok(()) + } + } + + #[external(v0)] + pub fn main(ref self: ContractState, hdp: HDP) { + let p = Point { x: 1, y: 3 }; + + print(p); + print(1); + print_array(array![1, 2, 3]); + + + } +} diff --git a/tests/src/utils/debug.rs b/tests/src/utils/debug.rs new file mode 100644 index 00000000..80869676 --- /dev/null +++ b/tests/src/utils/debug.rs @@ -0,0 +1,10 @@ +use crate::test_utils::run; + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_modules_test_debug_print() { + run(serde_json::from_slice(include_bytes!( + "../../../target/dev/modules_test_debug_print.compiled_contract_class.json" + )) + .unwrap()) + .await +} diff --git a/tests/src/utils/mod.rs b/tests/src/utils/mod.rs new file mode 100644 index 00000000..2f365233 --- /dev/null +++ b/tests/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod debug;