diff --git a/CHANGELOG.md b/CHANGELOG.md index 806d9da4d6..c596c72cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ * Fix prover input. * Fix version reading when no version is supplied. +* feat: Add support for ByteArray in DebugPrint: [#1853](https://github.com/lambdaclass/cairo-vm/pull/1853) + * Debug print handler is swapped with the implementation from cairo-lang-runner * chore: bump `cairo-lang-` dependencies to 2.7.1 [#1823](https://github.com/lambdaclass/cairo-vm/pull/1823) diff --git a/Cargo.lock b/Cargo.lock index 4aef2cd4c3..a78ed93d2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -909,12 +909,14 @@ dependencies = [ "cairo-lang-casm", "cairo-lang-starknet", "cairo-lang-starknet-classes", + "cairo-lang-utils", "clap", "criterion", "generic-array", "hashbrown 0.14.5", "hex", "iai-callgrind", + "itertools 0.12.1", "keccak", "lazy_static", "mimalloc", diff --git a/Cargo.toml b/Cargo.toml index 7338b83ee0..f62303f907 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ serde_json = { version = "1.0", features = [ "alloc", ], default-features = false } hex = { version = "0.4.3", default-features = false } +itertools = { version = "0.12.1", default-feature = false } bincode = { version = "2.0.0-rc.3", default-features = false, features = [ "serde", ] } diff --git a/vm/Cargo.toml b/vm/Cargo.toml index 07bcde08cd..87ab740faa 100644 --- a/vm/Cargo.toml +++ b/vm/Cargo.toml @@ -24,6 +24,7 @@ cairo-1-hints = [ "dep:cairo-lang-starknet", "dep:cairo-lang-casm", "dep:cairo-lang-starknet-classes", + "dep:cairo-lang-utils", "dep:ark-ff", "dep:ark-std", ] @@ -46,6 +47,7 @@ serde = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } bincode = { workspace = true } +itertools = { workspace = true } starknet-crypto = { workspace = true } sha3 = { workspace = true } lazy_static = { workspace = true } @@ -67,6 +69,7 @@ bitvec = { workspace = true } cairo-lang-starknet = { workspace = true, optional = true } cairo-lang-starknet-classes = { workspace = true, optional = true } cairo-lang-casm = { workspace = true, optional = true } +cairo-lang-utils = { workspace = true, optional = true } # TODO: check these dependencies for wasm compatibility ark-ff = { workspace = true, optional = true } diff --git a/vm/src/hint_processor/cairo_1_hint_processor/debug_print.rs b/vm/src/hint_processor/cairo_1_hint_processor/debug_print.rs new file mode 100644 index 0000000000..2b4173a150 --- /dev/null +++ b/vm/src/hint_processor/cairo_1_hint_processor/debug_print.rs @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: StarkWare Industries +// +// SPDX-License-Identifier: Apache 2.0 + +//! DebugPrint hint handler, adapted from: +//! https://github.com/starkware-libs/cairo/blob/7cfecf38801416c19a431e3a5c21b7d68615ce93/crates/cairo-lang-runner/src/casm_run/mod.rs + +use cairo_lang_utils::byte_array::{BYTES_IN_WORD, BYTE_ARRAY_MAGIC}; +use itertools::Itertools; +use num_traits::{ToPrimitive, Zero}; +use starknet_types_core::felt::Felt; +use std::vec::IntoIter; + +/// Formats the given felts as a debug string. +pub(crate) fn format_for_debug(mut felts: IntoIter) -> String { + let mut items = Vec::new(); + while let Some(item) = format_next_item(&mut felts) { + items.push(item); + } + if let [item] = &items[..] { + if item.is_string { + return item.item.clone(); + } + } + items + .into_iter() + .map(|item| { + if item.is_string { + format!("{}\n", item.item) + } else { + format!("[DEBUG]\t{}\n", item.item) + } + }) + .join("") +} + +/// A formatted string representation of anything formattable (e.g. ByteArray, felt, short-string). +pub struct FormattedItem { + /// The formatted string representing the item. + item: String, + /// Whether the item is a string. + is_string: bool, +} +impl FormattedItem { + /// Returns the formatted item as is. + pub fn get(self) -> String { + self.item + } + /// Wraps the formatted item with quote, if it's a string. Otherwise returns it as is. + pub fn quote_if_string(self) -> String { + if self.is_string { + format!("\"{}\"", self.item) + } else { + self.item + } + } +} + +/// Formats a string or a short string / `felt252`. Returns the formatted string and a boolean +/// indicating whether it's a string. If can't format the item, returns None. +pub(crate) fn format_next_item(values: &mut T) -> Option +where + T: Iterator + Clone, +{ + let first_felt = values.next()?; + + if first_felt == Felt::from_hex(BYTE_ARRAY_MAGIC).unwrap() { + if let Some(string) = try_format_string(values) { + return Some(FormattedItem { + item: string, + is_string: true, + }); + } + } + Some(FormattedItem { + item: format_short_string(&first_felt), + is_string: false, + }) +} + +/// Formats a `Felt252`, as a short string if possible. +fn format_short_string(value: &Felt) -> String { + let hex_value = value.to_biguint(); + match as_cairo_short_string(value) { + Some(as_string) => format!("{hex_value:#x} ('{as_string}')"), + None => format!("{hex_value:#x}"), + } +} + +/// Tries to format a string, represented as a sequence of `Felt252`s. +/// If the sequence is not a valid serialization of a ByteArray, returns None and doesn't change the +/// given iterator (`values`). +fn try_format_string(values: &mut T) -> Option +where + T: Iterator + Clone, +{ + // Clone the iterator and work with the clone. If the extraction of the string is successful, + // change the original iterator to the one we worked with. If not, continue with the + // original iterator at the original point. + let mut cloned_values_iter = values.clone(); + + let num_full_words = cloned_values_iter.next()?.to_usize()?; + let full_words = cloned_values_iter + .by_ref() + .take(num_full_words) + .collect_vec(); + let pending_word = cloned_values_iter.next()?; + let pending_word_len = cloned_values_iter.next()?.to_usize()?; + + let full_words_string = full_words + .into_iter() + .map(|word| as_cairo_short_string_ex(&word, BYTES_IN_WORD)) + .collect::>>()? + .join(""); + let pending_word_string = as_cairo_short_string_ex(&pending_word, pending_word_len)?; + + // Extraction was successful, change the original iterator to the one we worked with. + *values = cloned_values_iter; + + Some(format!("{full_words_string}{pending_word_string}")) +} + +/// Converts a bigint representing a felt252 to a Cairo short-string. +pub(crate) fn as_cairo_short_string(value: &Felt) -> Option { + let mut as_string = String::default(); + let mut is_end = false; + for byte in value.to_biguint().to_bytes_be() { + if byte == 0 { + is_end = true; + } else if is_end { + return None; + } else if byte.is_ascii_graphic() || byte.is_ascii_whitespace() { + as_string.push(byte as char); + } else { + return None; + } + } + Some(as_string) +} + +/// Converts a bigint representing a felt252 to a Cairo short-string of the given length. +/// Nulls are allowed and length must be <= 31. +pub(crate) fn as_cairo_short_string_ex(value: &Felt, length: usize) -> Option { + if length == 0 { + return if value.is_zero() { + Some("".to_string()) + } else { + None + }; + } + if length > 31 { + // A short string can't be longer than 31 bytes. + return None; + } + + // We pass through biguint as felt252.to_bytes_be() does not trim leading zeros. + let bytes = value.to_biguint().to_bytes_be(); + let bytes_len = bytes.len(); + if bytes_len > length { + // `value` has more bytes than expected. + return None; + } + + let mut as_string = "".to_string(); + for byte in bytes { + if byte == 0 { + as_string.push_str(r"\0"); + } else if byte.is_ascii_graphic() + || byte.is_ascii_whitespace() + || ascii_is_escape_sequence(byte) + { + as_string.push(byte as char); + } else { + as_string.push_str(format!(r"\x{:02x}", byte).as_str()); + } + } + + // `to_bytes_be` misses starting nulls. Prepend them as needed. + let missing_nulls = length - bytes_len; + as_string.insert_str(0, &r"\0".repeat(missing_nulls)); + + Some(as_string) +} + +fn ascii_is_escape_sequence(byte: u8) -> bool { + byte == 0x1b +} diff --git a/vm/src/hint_processor/cairo_1_hint_processor/hint_processor.rs b/vm/src/hint_processor/cairo_1_hint_processor/hint_processor.rs index 0caf9c4e9c..e3b689afd3 100644 --- a/vm/src/hint_processor/cairo_1_hint_processor/hint_processor.rs +++ b/vm/src/hint_processor/cairo_1_hint_processor/hint_processor.rs @@ -109,10 +109,19 @@ impl Cairo1HintProcessor { quotient, remainder, })) => self.div_mod(vm, lhs, rhs, quotient, remainder), + + #[allow(unused_variables)] Hint::Core(CoreHintBase::Core(CoreHint::DebugPrint { start, end })) => { - self.debug_print(vm, start, end) + #[cfg(feature = "std")] + { + use crate::hint_processor::cairo_1_hint_processor::debug_print::format_for_debug; + print!( + "{}", + format_for_debug(read_felts(vm, start, end)?.into_iter()) + ); + } + Ok(()) } - Hint::Core(CoreHintBase::Core(CoreHint::Uint256SquareRoot { value_low, value_high, @@ -790,31 +799,6 @@ impl Cairo1HintProcessor { .map_err(HintError::from) } - #[allow(unused_variables)] - fn debug_print( - &self, - vm: &mut VirtualMachine, - start: &ResOperand, - end: &ResOperand, - ) -> Result<(), HintError> { - #[cfg(feature = "std")] - { - let mut curr = as_relocatable(vm, start)?; - let end = as_relocatable(vm, end)?; - while curr != end { - let value = vm.get_integer(curr)?; - if let Some(shortstring) = as_cairo_short_string(&value) { - println!("[DEBUG]\t{shortstring: <31}\t(raw: {value: <31})"); - } else { - println!("[DEBUG]\t{0: <31}\t(raw: {value: <31}) ", ' '); - } - curr += 1; - } - println!(); - } - Ok(()) - } - fn assert_all_accesses_used( &self, vm: &mut VirtualMachine, diff --git a/vm/src/hint_processor/cairo_1_hint_processor/hint_processor_utils.rs b/vm/src/hint_processor/cairo_1_hint_processor/hint_processor_utils.rs index 46abef74e9..32b07923c9 100644 --- a/vm/src/hint_processor/cairo_1_hint_processor/hint_processor_utils.rs +++ b/vm/src/hint_processor/cairo_1_hint_processor/hint_processor_utils.rs @@ -133,36 +133,22 @@ pub(crate) fn res_operand_get_val( } } +/// Reads a range of `Felt252`s from the VM. #[cfg(feature = "std")] -pub(crate) fn as_cairo_short_string(value: &Felt252) -> Option { - let mut as_string = String::default(); - let mut is_end = false; - for byte in value - .to_bytes_be() - .into_iter() - .skip_while(num_traits::Zero::is_zero) - { - if byte == 0 { - is_end = true; - } else if is_end || !byte.is_ascii() { - return None; - } else { - as_string.push(byte as char); - } - } - Some(as_string) -} +pub(crate) fn read_felts( + vm: &mut VirtualMachine, + start: &ResOperand, + end: &ResOperand, +) -> Result, HintError> { + let mut curr = as_relocatable(vm, start)?; + let end = as_relocatable(vm, end)?; -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn simple_as_cairo_short_string() { - // Values extracted from cairo book example - let s = "Hello, Scarb!"; - let x = Felt252::from(5735816763073854913753904210465_u128); - assert!(s.is_ascii()); - let cairo_string = as_cairo_short_string(&x).expect("call to as_cairo_short_string failed"); - assert_eq!(cairo_string, s); + let mut felts = Vec::new(); + while curr != end { + let value = *vm.get_integer(curr)?; + felts.push(value); + curr = (curr + 1)?; } + + Ok(felts) } diff --git a/vm/src/hint_processor/cairo_1_hint_processor/mod.rs b/vm/src/hint_processor/cairo_1_hint_processor/mod.rs index 29d5f47bd3..91e46dd27b 100644 --- a/vm/src/hint_processor/cairo_1_hint_processor/mod.rs +++ b/vm/src/hint_processor/cairo_1_hint_processor/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "std")] +pub mod debug_print; pub mod dict_manager; pub mod hint_processor; pub mod hint_processor_utils;