Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vm): support ByteArray in DebugPrint hint #1853

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
] }
Expand Down
3 changes: 3 additions & 0 deletions vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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 }
Expand All @@ -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 }
Expand Down
187 changes: 187 additions & 0 deletions vm/src/hint_processor/cairo_1_hint_processor/debug_print.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// SPDX-FileCopyrightText: StarkWare Industries <[email protected]>
//
// 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<Felt>) -> 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<T>(values: &mut T) -> Option<FormattedItem>
where
T: Iterator<Item = Felt> + 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<T>(values: &mut T) -> Option<String>
where
T: Iterator<Item = Felt> + 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::<Option<Vec<String>>>()?
.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<String> {
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<String> {
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
}
38 changes: 11 additions & 27 deletions vm/src/hint_processor/cairo_1_hint_processor/hint_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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<Vec<Felt252>, 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)
}
2 changes: 2 additions & 0 deletions vm/src/hint_processor/cairo_1_hint_processor/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(feature = "std")]
pub mod debug_print;
pub mod dict_manager;
pub mod hint_processor;
pub mod hint_processor_utils;