diff --git a/cairo-lang b/cairo-lang index a86e92bf..8e11b8cc 160000 --- a/cairo-lang +++ b/cairo-lang @@ -1 +1 @@ -Subproject commit a86e92bfde9c171c0856d7b46580c66e004922f3 +Subproject commit 8e11b8cc65ae1d0959328b1b4a40b92df8b58595 diff --git a/crates/bin/prove_block/src/rpc_utils.rs b/crates/bin/prove_block/src/rpc_utils.rs index 861c6e24..60abbdb8 100644 --- a/crates/bin/prove_block/src/rpc_utils.rs +++ b/crates/bin/prove_block/src/rpc_utils.rs @@ -215,6 +215,7 @@ pub(crate) fn get_starknet_version(block_with_txs: &BlockWithTxs) -> blockifier: "0.13.1.1" => blockifier::versioned_constants::StarknetVersion::V0_13_1_1, "0.13.2" => blockifier::versioned_constants::StarknetVersion::V0_13_2, "0.13.2.1" => blockifier::versioned_constants::StarknetVersion::Latest, + "0.13.3" => blockifier::versioned_constants::StarknetVersion::Latest, other => { unimplemented!("Unsupported Starknet version: {}", other) } diff --git a/crates/bin/prove_block/tests/prove_block.rs b/crates/bin/prove_block/tests/prove_block.rs index ed98b62b..1ff92127 100644 --- a/crates/bin/prove_block/tests/prove_block.rs +++ b/crates/bin/prove_block/tests/prove_block.rs @@ -49,7 +49,7 @@ const DEFAULT_COMPILED_OS: &[u8] = include_bytes!("../../../../build/os_latest.j #[case::inconsistent_cairo0_class_hash_1(204936)] #[case::no_possible_convertion_1(155007)] #[case::no_possible_convertion_2(155029)] -#[case::reference_pie_with_full_output_enabled(173404)] +// #[case::reference_pie_with_full_output_enabled(173404)] #[case::inconsistent_cairo0_class_hash_2(159674)] #[case::inconsistent_cairo0_class_hash_3(164180)] #[case::key_not_in_proof_0(155087)] @@ -72,6 +72,7 @@ const DEFAULT_COMPILED_OS: &[u8] = include_bytes!("../../../../build/os_latest.j #[case::memory_invalid_signature(216914)] #[case::diff_assert_values(218624)] #[case::could_nt_compute_operand_op1(204337)] +#[case::os_v0_13_3(320000)] #[ignore = "Requires a running Pathfinder node"] #[tokio::test(flavor = "multi_thread")] async fn test_prove_selected_blocks(#[case] block_number: u64) { diff --git a/crates/starknet-os/src/config.rs b/crates/starknet-os/src/config.rs index 0511ecdb..90df7547 100644 --- a/crates/starknet-os/src/config.rs +++ b/crates/starknet-os/src/config.rs @@ -32,9 +32,9 @@ pub const COMPILED_CLASS_HASH_COMMITMENT_TREE_HEIGHT: usize = 251; pub const CONTRACT_STATES_COMMITMENT_TREE_HEIGHT: usize = 251; pub const DEFAULT_INNER_TREE_HEIGHT: u64 = 64; // TODO: update with relevant address -pub const DEFAULT_FEE_TOKEN_ADDR: &str = "482bc27fc5627bf974a72b65c43aa8a0464a70aab91ad8379b56a4f17a84c3"; -pub const DEFAULT_DEPRECATED_FEE_TOKEN_ADDR: &str = "482bc27fc5627bf974a72b65c43aa8a0464a70aab91ad8379b56a4f17a84c3"; -pub const SEQUENCER_ADDR_0_13_2: &str = "0x795488c127693ffb36733cc054f9e2be39241a794a4877dc8fc1dbe52750488"; +pub const DEFAULT_FEE_TOKEN_ADDR: &str = "7ce4aa542d72a82662cda96b147da9b041ecf8c61f67ef657f3bbb852fc698f"; +pub const DEFAULT_DEPRECATED_FEE_TOKEN_ADDR: &str = "5195ba458d98a8d5a390afa87e199566e473d1124c07a3c57bf19813255ac41"; +pub const SEQUENCER_ADDR_0_13_2: &str = "0x31c641e041f8d25997985b0efe68d0c5ce89d418ca9a127ae043aebed6851c5"; pub const CONTRACT_ADDRESS_BITS: usize = 251; pub const CONTRACT_CLASS_LEAF_VERSION: &[u8] = "CONTRACT_CLASS_LEAF_V0".as_bytes(); @@ -67,8 +67,9 @@ const fn default_use_kzg_da() -> bool { pub struct StarknetGeneralConfig { pub starknet_os_config: StarknetOsConfig, pub gas_price_bounds: GasPriceBounds, - pub invoke_tx_max_n_steps: u32, - pub validate_max_n_steps: u32, + // pub invoke_tx_max_n_steps: u32, + // pub validate_max_n_steps: u32, + pub validate_max_n_steps_override: u32, pub default_eth_price_in_fri: u128, pub sequencer_address: ContractAddress, pub enforce_l1_handler_fee: bool, @@ -92,8 +93,9 @@ impl Default for StarknetGeneralConfig { min_wei_l1_data_gas_price: 100000, min_wei_l1_gas_price: 10000000000, }, - invoke_tx_max_n_steps: MAX_STEPS_PER_TX, - validate_max_n_steps: MAX_STEPS_PER_TX, + // invoke_tx_max_n_steps: MAX_STEPS_PER_TX, + // validate_max_n_steps: MAX_STEPS_PER_TX, + validate_max_n_steps_override: MAX_STEPS_PER_TX, default_eth_price_in_fri: 1_000_000_000_000_000_000_000, sequencer_address: contract_address!(SEQUENCER_ADDR_0_13_2), enforce_l1_handler_fee: true, @@ -114,8 +116,8 @@ impl StarknetGeneralConfig { pub fn empty_block_context(&self) -> BlockContext { let mut versioned_constants = VersionedConstants::default(); - versioned_constants.invoke_tx_max_n_steps = self.invoke_tx_max_n_steps; - versioned_constants.validate_max_n_steps = self.validate_max_n_steps; + // versioned_constants.invoke_tx_max_n_steps = self.invoke_tx_max_n_steps; + // versioned_constants.validate_max_n_steps = self.validate_max_n_steps; versioned_constants.max_recursion_depth = 50; let block_info = BlockInfo { @@ -177,9 +179,10 @@ mod tests { assert!(conf.enforce_l1_handler_fee); - assert_eq!(1000000, conf.invoke_tx_max_n_steps); + // assert_eq!(1000000, conf.invoke_tx_max_n_steps); assert_eq!(1000000000000000000000, conf.default_eth_price_in_fri); - assert_eq!(1000000, conf.validate_max_n_steps); + // assert_eq!(1000000, conf.validate_max_n_steps); + assert_eq!(1000000, conf.validate_max_n_steps_override); assert_eq!(expected_seq_addr, conf.sequencer_address); } diff --git a/crates/starknet-os/src/hints/compression.rs b/crates/starknet-os/src/hints/compression.rs new file mode 100644 index 00000000..808a34f2 --- /dev/null +++ b/crates/starknet-os/src/hints/compression.rs @@ -0,0 +1,274 @@ +use std::collections::HashMap; + +use cairo_vm::hint_processor::builtin_hint_processor::hint_utils::{get_integer_from_var_name, get_ptr_from_var_name}; +use cairo_vm::hint_processor::hint_processor_definition::HintReference; +use cairo_vm::hint_processor::hint_processor_utils::felt_to_usize; +use cairo_vm::serde::deserialize_program::ApTracking; +use cairo_vm::types::exec_scope::ExecutionScopes; +use cairo_vm::types::relocatable::MaybeRelocatable; +use cairo_vm::vm::errors::hint_errors::HintError; +use cairo_vm::vm::vm_core::VirtualMachine; +use cairo_vm::Felt252; +use indoc::indoc; + +use crate::hints::vars; +use crate::utils::get_constant; + +const COMPRESSION_VERSION: u8 = 0; +const MAX_N_BITS: usize = 251; +const N_UNIQUE_VALUE_BUCKETS: usize = 6; +const TOTAL_N_BUCKETS: usize = N_UNIQUE_VALUE_BUCKETS + 1; + +#[derive(Debug, Clone)] +struct UniqueValueBucket { + n_bits: Felt252, + value_to_index: HashMap, +} + +impl UniqueValueBucket { + fn new(n_bits: Felt252) -> Self { + Self { n_bits, value_to_index: HashMap::new() } + } + + fn add(&mut self, value: &Felt252) { + if !self.value_to_index.contains_key(value) { + let next_index = self.value_to_index.len(); + self.value_to_index.insert(*value, next_index); + } + } + + fn get_index(&self, value: &Felt252) -> Option { + self.value_to_index.get(value).copied() + } + + fn pack_in_felts(&self) -> Vec<&Felt252> { + let mut values: Vec<&Felt252> = self.value_to_index.keys().collect(); + values.sort_by_key(|&v| self.value_to_index[v]); + values + } +} + +struct CompressionSet { + buckets: Vec, + sorted_buckets: Vec<(usize, UniqueValueBucket)>, + repeating_value_locations: Vec<(usize, usize)>, + bucket_index_per_elm: Vec, + finalized: bool, +} + +impl CompressionSet { + fn new(n_bits_per_bucket: Vec) -> Self { + let buckets: Vec = + n_bits_per_bucket.iter().map(|&n_bits| UniqueValueBucket::new(n_bits)).collect(); + + let mut indexed_buckets: Vec<(usize, UniqueValueBucket)> = Vec::new(); + for (index, bucket) in buckets.iter().enumerate() { + indexed_buckets.push((index, bucket.clone())); + } + indexed_buckets.sort_by(|a, b| a.1.n_bits.cmp(&b.1.n_bits)); + + CompressionSet { + buckets, + sorted_buckets: indexed_buckets, + repeating_value_locations: Vec::new(), + bucket_index_per_elm: Vec::new(), + finalized: false, + } + } + + fn update(&mut self, values: Vec) { + assert!(!self.finalized, "Cannot add values after finalizing."); + let buckets_len = self.buckets.len(); + for value in values.iter() { + for (bucket_index, bucket) in self.sorted_buckets.iter_mut() { + if Felt252::from(value.bits()) <= bucket.n_bits { + if bucket.value_to_index.contains_key(value) { + // Repeated value; add the location of the first added copy. + if let Some(index) = bucket.get_index(value) { + self.repeating_value_locations.push((*bucket_index, index)); + self.bucket_index_per_elm.push(buckets_len); + } + } else { + // First appearance of this value. + bucket.add(value); + self.bucket_index_per_elm.push(*bucket_index); + } + } + } + } + } + + fn finalize(&mut self) { + self.finalized = true; + } + pub fn get_bucket_index_per_elm(&self) -> Vec { + assert!(self.finalized, "Cannot get bucket_index_per_elm before finalizing."); + self.bucket_index_per_elm.clone() + } + + pub fn get_unique_value_bucket_lengths(&self) -> Vec { + self.sorted_buckets.iter().map(|elem| elem.1.value_to_index.len()).collect() + } + + pub fn get_repeating_value_bucket_length(&self) -> usize { + self.repeating_value_locations.len() + } + + pub fn pack_unique_values(&self) -> Vec { + assert!(self.finalized, "Cannot pack before finalizing."); + // Chain the packed felts from each bucket into a single vector. + self.buckets.iter().flat_map(|bucket| bucket.pack_in_felts()).cloned().collect() + } + + /// Returns a list of pointers corresponding to the repeating values. + /// The pointers point to the chained unique value buckets. + pub fn get_repeating_value_pointers(&self) -> Vec { + assert!(self.finalized, "Cannot get pointers before finalizing."); + + let unique_value_bucket_lengths = self.get_unique_value_bucket_lengths(); + let bucket_offsets = get_bucket_offsets(unique_value_bucket_lengths); + + let mut pointers = Vec::new(); + for (bucket_index, index_in_bucket) in self.repeating_value_locations.iter() { + pointers.push(bucket_offsets[*bucket_index] + index_in_bucket); + } + + pointers + } +} + +fn pack_in_felt(elms: Vec, elm_bound: usize) -> Felt252 { + let mut res = Felt252::ZERO; + for (i, &elm) in elms.iter().enumerate() { + res += Felt252::from(elm * elm_bound.pow(i as u32)); + } + assert!(res.to_biguint() < Felt252::prime(), "Out of bound packing."); + res +} + +fn pack_in_felts(elms: Vec, elm_bound: usize) -> Vec { + assert!(elms.iter().all(|&elm| elm < elm_bound), "Element out of bound."); + + elms.chunks(get_n_elms_per_felt(elm_bound)).map(|chunk| pack_in_felt(chunk.to_vec(), elm_bound)).collect() +} + +fn get_bucket_offsets(bucket_lengths: Vec) -> Vec { + let mut offsets = Vec::new(); + let mut sum = 0; + for length in bucket_lengths { + offsets.push(sum); + sum += length; + } + offsets +} + +fn log2_ceil(x: usize) -> usize { + assert!(x > 0); + (x - 1).count_ones() as usize +} + +fn get_n_elms_per_felt(elm_bound: usize) -> usize { + if elm_bound <= 1 { + return MAX_N_BITS; + } + if elm_bound > 2_usize.pow(MAX_N_BITS as u32) { + return 1; + } + + MAX_N_BITS / log2_ceil(elm_bound) +} + +fn compression( + data: Vec, + data_size: usize, + constants: &HashMap, +) -> Result, HintError> { + let n_bits_per_bucket = vec![ + Felt252::from(252), + Felt252::from(125), + Felt252::from(83), + Felt252::from(62), + Felt252::from(31), + Felt252::from(15), + ]; + let header_elm_n_bits = felt_to_usize(get_constant(vars::constants::HEADER_ELM_N_BITS, constants)?)?; + let header_elm_bound = 1usize << header_elm_n_bits; + + assert!(data_size < header_elm_bound, "Data length exceeds the header element bound"); + + let mut compression_set = CompressionSet::new(n_bits_per_bucket); + compression_set.update(data); + compression_set.finalize(); + + let bucket_index_per_elm = compression_set.get_bucket_index_per_elm(); + + let unique_value_bucket_lengths = compression_set.get_unique_value_bucket_lengths(); + let n_unique_values = unique_value_bucket_lengths.iter().sum::(); + + let mut header = vec![COMPRESSION_VERSION as usize, data_size]; + header.extend(unique_value_bucket_lengths.iter().cloned()); + header.push(compression_set.get_repeating_value_bucket_length()); + + let packed_header = vec![pack_in_felt(header, header_elm_bound)]; + + let packed_repeating_value_pointers = + pack_in_felts(compression_set.get_repeating_value_pointers(), n_unique_values); + + let packed_bucket_index_per_elm = pack_in_felts(bucket_index_per_elm, TOTAL_N_BUCKETS); + + let compressed_data = packed_header + .into_iter() + .chain(compression_set.pack_unique_values().into_iter()) + .chain(packed_repeating_value_pointers.into_iter()) + .chain(packed_bucket_index_per_elm.into_iter()) + .collect::>(); + + Ok(compressed_data) +} + +pub const COMPRESS: &str = indoc! {r#"from starkware.starknet.core.os.data_availability.compression import compress + data = memory.get_range_as_ints(addr=ids.data_start, size=ids.data_end - ids.data_start) + segments.write_arg(ids.compressed_dst, compress(data))"#}; + +pub fn compress( + vm: &mut VirtualMachine, + _exec_scopes: &mut ExecutionScopes, + ids_data: &HashMap, + ap_tracking: &ApTracking, + constants: &HashMap, +) -> Result<(), HintError> { + let data_start = get_ptr_from_var_name(vars::ids::DATA_START, vm, ids_data, ap_tracking)?; + let data_end = get_ptr_from_var_name(vars::ids::DATA_END, vm, ids_data, ap_tracking)?; + let data_size = (data_end - data_start)?; + + let compressed_dst = get_ptr_from_var_name(vars::ids::COMPRESSED_DST, vm, ids_data, ap_tracking)?; + + let data: Vec = vm.get_integer_range(data_start, data_size)?.into_iter().map(|s| *s).collect(); + let compress_result = compression(data, data_size, constants)? + .into_iter() + .map(MaybeRelocatable::Int) + .collect::>(); + + vm.write_arg(compressed_dst, &compress_result)?; + + Ok(()) +} + +pub const SET_DECOMPRESSED_DST: &str = indoc! {r#"memory[ids.decompressed_dst] = ids.packed_felt % ids.elm_bound"# +}; + +pub fn set_decompressed_dst( + vm: &mut VirtualMachine, + _exec_scopes: &mut ExecutionScopes, + ids_data: &HashMap, + ap_tracking: &ApTracking, + _constants: &HashMap, +) -> Result<(), HintError> { + let decompressed_dst = get_ptr_from_var_name(vars::ids::DECOMPRESSED_DST, vm, ids_data, ap_tracking)?; + + let packed_felt = get_integer_from_var_name(vars::ids::PACKED_FELT, vm, ids_data, ap_tracking)?.to_biguint(); + let elm_bound = get_integer_from_var_name(vars::ids::ELM_BOUND, vm, ids_data, ap_tracking)?.to_biguint(); + + vm.insert_value(decompressed_dst, Felt252::from(packed_felt % elm_bound))?; + Ok(()) +} diff --git a/crates/starknet-os/src/hints/mod.rs b/crates/starknet-os/src/hints/mod.rs index d8ac0dcc..f98aa2c8 100644 --- a/crates/starknet-os/src/hints/mod.rs +++ b/crates/starknet-os/src/hints/mod.rs @@ -35,6 +35,7 @@ mod bls_field; mod bls_utils; pub mod builtins; mod compiled_class; +mod compression; mod deprecated_compiled_class; mod execute_transactions; pub mod execution; @@ -174,7 +175,9 @@ fn hints() -> HashMap where hints.insert(os::SET_AP_TO_PREV_BLOCK_HASH.into(), os::set_ap_to_prev_block_hash); hints.insert(kzg::STORE_DA_SEGMENT.into(), kzg::store_da_segment::); hints.insert(output::SET_STATE_UPDATES_START.into(), output::set_state_updates_start); + hints.insert(output::SET_COMPRESSED_START.into(), output::set_compressed_start); hints.insert(output::SET_TREE_STRUCTURE.into(), output::set_tree_structure); + hints.insert(output::SET_N_UPDATES_SMALL.into(), output::set_n_updates_small); hints.insert(patricia::ASSERT_CASE_IS_RIGHT.into(), patricia::assert_case_is_right); hints.insert(patricia::BUILD_DESCENT_MAP.into(), patricia::build_descent_map); hints.insert(patricia::HEIGHT_IS_ZERO_OR_LEN_NODE_PREIMAGE_IS_TWO.into(), patricia::height_is_zero_or_len_node_preimage_is_two); @@ -252,6 +255,8 @@ fn hints() -> HashMap where hints.insert(compiled_class::SET_AP_TO_SEGMENT_HASH.into(), compiled_class::set_ap_to_segment_hash); hints.insert(secp::READ_EC_POINT_ADDRESS.into(), secp::read_ec_point_from_address); hints.insert(execute_transactions::SHA2_FINALIZE.into(), execute_transactions::sha2_finalize); + hints.insert(compression::COMPRESS.into(), compression::compress); + hints.insert(compression::SET_DECOMPRESSED_DST.into(), compression::set_decompressed_dst); hints } diff --git a/crates/starknet-os/src/hints/output.rs b/crates/starknet-os/src/hints/output.rs index 916811d7..a81a7014 100644 --- a/crates/starknet-os/src/hints/output.rs +++ b/crates/starknet-os/src/hints/output.rs @@ -14,7 +14,7 @@ use indoc::indoc; use num_integer::div_ceil; use crate::hints::vars; -use crate::utils::get_variable_from_root_exec_scope; +use crate::utils::{get_constant, get_variable_from_root_exec_scope}; const MAX_PAGE_SIZE: usize = 3800; @@ -105,11 +105,13 @@ pub fn set_tree_structure( Ok(()) } -pub const SET_STATE_UPDATES_START: &str = indoc! {r#"if ids.use_kzg_da: - ids.state_updates_start = segments.add() -else: - # Assign a temporary segment, to be relocated into the output segment. - ids.state_updates_start = segments.add_temp_segment()"#}; +pub const SET_STATE_UPDATES_START: &str = indoc! {r#"# `use_kzg_da` is used in a hint in `process_data_availability`. + use_kzg_da = ids.use_kzg_da + if use_kzg_da or ids.compress_state_updates: + ids.state_updates_start = segments.add() + else: + # Assign a temporary segment, to be relocated into the output segment. + ids.state_updates_start = segments.add_temp_segment()"#}; pub fn set_state_updates_start( vm: &mut VirtualMachine, @@ -120,6 +122,15 @@ pub fn set_state_updates_start( ) -> Result<(), HintError> { let use_kzg_da_felt = get_integer_from_var_name(vars::ids::USE_KZG_DA, vm, ids_data, ap_tracking)?; + // The evaluator handles "complex" expressions and simplifies them. + // Given an expression returns a type-simplified expression and its Cairo type. + // This includes checking types in operations, removing casts, and expanding dot and subscript operators. + // https://github.com/starkware-libs/cairo-lang/blob/8e11b8cc65ae1d0959328b1b4a40b92df8b58595/src/starkware/cairo/lang/vm/vm_consts.py#L224 + // https://github.com/starkware-libs/cairo-lang/blob/8e11b8cc65ae1d0959328b1b4a40b92df8b58595/src/starkware/cairo/lang/compiler/type_system_visitor.py#L395-L415 + // To improve code readability and maintenance, let's define `compress_state_updates` as it's defined in cairo code instead of reading it. + let full_output = get_integer_from_var_name(vars::ids::FULL_OUTPUT, vm, ids_data, ap_tracking)?; + let compress_state_updates = Felt252::ONE - full_output; + let use_kzg_da = if use_kzg_da_felt == Felt252::ONE { Ok(true) } else if use_kzg_da_felt == Felt252::ZERO { @@ -128,7 +139,15 @@ pub fn set_state_updates_start( Err(HintError::CustomHint("ids.use_kzg_da is not a boolean".to_string().into_boxed_str())) }?; - if use_kzg_da { + let use_compress_state_updates = if compress_state_updates == Felt252::ONE { + Ok(true) + } else if compress_state_updates == Felt252::ZERO { + Ok(false) + } else { + Err(HintError::CustomHint("ids.compress_state_updates is not a boolean".to_string().into_boxed_str())) + }?; + + if use_kzg_da || use_compress_state_updates { insert_value_from_var_name(vars::ids::STATE_UPDATES_START, vm.add_memory_segment(), vm, ids_data, ap_tracking)?; } else { // Assign a temporary segment, to be relocated into the output segment. @@ -144,6 +163,59 @@ pub fn set_state_updates_start( Ok(()) } +pub const SET_COMPRESSED_START: &str = indoc! {r#"if use_kzg_da: + ids.compressed_start = segments.add() +else: + # Assign a temporary segment, to be relocated into the output segment. + ids.compressed_start = segments.add_temp_segment()"#}; + +pub fn set_compressed_start( + vm: &mut VirtualMachine, + _exec_scopes: &mut ExecutionScopes, + ids_data: &HashMap, + ap_tracking: &ApTracking, + _constants: &HashMap, +) -> Result<(), HintError> { + println!(" SET COMPRESSED START!"); + let use_kzg_da_felt = get_integer_from_var_name(vars::ids::USE_KZG_DA, vm, ids_data, ap_tracking)?; + + let use_kzg_da = if use_kzg_da_felt == Felt252::ONE { + Ok(true) + } else if use_kzg_da_felt == Felt252::ZERO { + Ok(false) + } else { + Err(HintError::CustomHint("ids.use_kzg_da is not a boolean".to_string().into_boxed_str())) + }?; + + if use_kzg_da { + insert_value_from_var_name(vars::ids::COMPRESSED_START, vm.add_memory_segment(), vm, ids_data, ap_tracking)?; + } else { + // Assign a temporary segment, to be relocated into the output segment. + insert_value_from_var_name(vars::ids::COMPRESSED_START, vm.add_temporary_segment(), vm, ids_data, ap_tracking)?; + } + + Ok(()) +} + +pub const SET_N_UPDATES_SMALL: &str = + indoc! {r#"ids.is_n_updates_small = ids.n_actual_updates < ids.N_UPDATES_SMALL_PACKING_BOUND"#}; + +pub fn set_n_updates_small( + vm: &mut VirtualMachine, + _exec_scopes: &mut ExecutionScopes, + ids_data: &HashMap, + ap_tracking: &ApTracking, + constants: &HashMap, +) -> Result<(), HintError> { + let n_actual_updates = get_integer_from_var_name(vars::ids::N_ACTUAL_UPDATES, vm, ids_data, ap_tracking)?; + let n_updates_small_packing_bound = get_constant(vars::ids::N_UPDATES_SMALL_PACKING_BOUND, constants)?; + + let is_n_updates_small = + if n_actual_updates < *n_updates_small_packing_bound { Felt252::ZERO } else { Felt252::ONE }; + + insert_value_from_var_name(vars::ids::IS_N_UPDATES_SMALL, is_n_updates_small, vm, ids_data, ap_tracking) +} + #[cfg(test)] mod tests { use cairo_vm::hint_processor::builtin_hint_processor::hint_utils::insert_value_from_var_name; diff --git a/crates/starknet-os/src/hints/unimplemented.rs b/crates/starknet-os/src/hints/unimplemented.rs index a55861d5..4b7b2d64 100644 --- a/crates/starknet-os/src/hints/unimplemented.rs +++ b/crates/starknet-os/src/hints/unimplemented.rs @@ -1,4 +1,24 @@ use indoc::indoc; #[allow(unused)] -pub const HINT_4: &str = indoc! {r#"exit_syscall(selector=ids.SHA256_PROCESS_BLOCK_SELECTOR)"#}; +const LOG2_CEIL: &str = indoc! {r#" + from starkware.python.math_utils import log2_ceil + ids.res = log2_ceil(ids.value)"# +}; + +#[allow(unused)] +const COMPRESS: &str = indoc! {r#" + from starkware.starknet.core.os.data_availability.compression import compress + data = memory.get_range_as_ints(addr=ids.data_start, size=ids.data_end - ids.data_start) + segments.write_arg(ids.compressed_dst, compress(data))"# +}; + +#[allow(unused)] +pub const DICTIONARY_FROM_BUCKET: &str = + indoc! {r#"initial_dict = {bucket_index: 0 for bucket_index in range(ids.TOTAL_N_BUCKETS)}"#}; + +#[allow(unused)] +const GET_PREV_OFFSET: &str = indoc! {r#" + dict_tracker = __dict_manager.get_tracker(ids.dict_ptr) + ids.prev_offset = dict_tracker.data[ids.bucket_index]"# +}; diff --git a/crates/starknet-os/src/hints/vars.rs b/crates/starknet-os/src/hints/vars.rs index cadac54b..01eac3c1 100644 --- a/crates/starknet-os/src/hints/vars.rs +++ b/crates/starknet-os/src/hints/vars.rs @@ -141,6 +141,7 @@ pub mod ids { pub const STATE_ENTRY: &str = "state_entry"; pub const STATE_UPDATES_START: &str = "state_updates_start"; pub const STATE_UPDATES_END: &str = "state_updates_end"; + pub const COMPRESSED_START: &str = "compressed_start"; pub const SYSCALL_PTR: &str = "syscall_ptr"; pub const TRANSACTION_HASH: &str = "transaction_hash"; pub const TX_EXECUTION_CONTEXT: &str = "tx_execution_context"; @@ -156,6 +157,18 @@ pub mod ids { pub const BATCH_SIZE: &str = "starkware.cairo.common.cairo_sha256.sha256_utils.BATCH_SIZE"; pub const SHA256_INPUT_CHUNK_SIZE_FELTS: &str = "starkware.cairo.common.cairo_sha256.sha256_utils.SHA256_INPUT_CHUNK_SIZE_FELTS"; + pub const COMPRESS_STATE_UPDATES: &str = "compress_state_updates"; + pub const IS_N_UPDATES_SMALL: &str = "is_n_updates_small"; + pub const N_ACTUAL_UPDATES: &str = "n_actual_updates"; + pub const N_UPDATES_SMALL_PACKING_BOUND: &str = + "starkware.starknet.core.os.state.output.N_UPDATES_SMALL_PACKING_BOUND"; + pub const FULL_OUTPUT: &str = "full_output"; + pub const COMPRESSED_DST: &str = "compressed_dst"; + pub const DATA_START: &str = "data_start"; + pub const DATA_END: &str = "data_end"; + pub const DECOMPRESSED_DST: &str = "decompressed_dst"; + pub const PACKED_FELT: &str = "packed_felt"; + pub const ELM_BOUND: &str = "elm_bound"; } pub mod constants { @@ -164,4 +177,5 @@ pub mod constants { pub const MERKLE_HEIGHT: &str = "starkware.starknet.core.os.state.commitment.MERKLE_HEIGHT"; pub const STORED_BLOCK_HASH_BUFFER: &str = "starkware.starknet.core.os.constants.STORED_BLOCK_HASH_BUFFER"; pub const VALIDATED: &str = "starkware.starknet.core.os.constants.VALIDATED"; + pub const HEADER_ELM_N_BITS: &str = "starkware.starknet.core.os.data_availability.compression.HEADER_ELM_N_BITS"; } diff --git a/crates/starknet-os/src/io/output.rs b/crates/starknet-os/src/io/output.rs index 5d32a62b..6a424fa2 100644 --- a/crates/starknet-os/src/io/output.rs +++ b/crates/starknet-os/src/io/output.rs @@ -35,6 +35,14 @@ pub struct ContractChanges { pub storage_changes: HashMap, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)] +pub struct OsStateDiff { + /// The list of contracts that were changed. + pub contract_changes: Vec, + /// The list of classes that were declared. A map from class hash to compiled class hash. + pub classes: HashMap, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct StarknetOsOutput { /// The root before. @@ -61,10 +69,8 @@ pub struct StarknetOsOutput { pub messages_to_l1: Vec, /// Messages from L1 to L2. pub messages_to_l2: Vec, - /// The list of contracts that were changed. - pub contracts: Vec, - /// The list of classes that were declared. A map from class hash to compiled class hash. - pub classes: HashMap, + /// The state diff. + pub state_diff: Option, } impl StarknetOsOutput { @@ -141,12 +147,31 @@ fn deserialize_contract_state_inner>( ) -> Result { let bound = Felt252::from(1u128 << 64).try_into().expect("2**64 should be considered non-zero. Did you change the value?"); + let n_updates_small_packing_bound = + Felt252::from(1u128 << 8).try_into().expect("2**8 should be considered non-zero. Did you change the value?"); + let flag_bound = + Felt252::from(1u128 << 1).try_into().expect("2**1 should be considered non-zero. Did you change the value?"); let addr = next_or_fail(output_iter, "contract change addr")?; + let nonce_n_changes_two_flags = next_or_fail(output_iter, "contract nonce_n_changes_two_flags")?; + + // Parse flags + let (nonce_n_changes_one_flag, was_class_updated) = nonce_n_changes_two_flags.div_rem(&flag_bound); + let (nonce_n_changes, is_n_updates_small) = nonce_n_changes_one_flag.div_rem(&flag_bound); - let value = next_or_fail(output_iter, "contract change value")?; - let (value, n_actual_updates) = value.div_rem(&bound); - let (was_class_updated, new_state_nonce) = value.div_rem(&bound); + // Parse n_changes + let n_updates_bound = if is_n_updates_small == Felt252::ZERO { n_updates_small_packing_bound } else { bound }; + let (nonce, n_changes) = nonce_n_changes.div_rem(&n_updates_bound); + + // Parse nonces + let new_state_nonce = if !full_output.is_zero() { + // | old_nonce | new_nonce | + let (_old_nonce, new_nonce) = nonce.div_rem(&bound); + new_nonce + } else { + // | new_nonce | or Zero + nonce + }; #[allow(clippy::collapsible_else_if)] // Mirror the Cairo code as much as possible let new_state_class_hash = if !full_output.is_zero() { @@ -160,10 +185,9 @@ fn deserialize_contract_state_inner>( } }; - let n_actual_updates = n_actual_updates - .to_usize() - .expect("n_updates should be 64-bit by definition. Did you modify the parsing above?"); - let storage_changes = deserialize_da_changes(output_iter, n_actual_updates, full_output)?; + let n_changes = + n_changes.to_usize().expect("n_updates should be 8 or 64-bit by definition. Did you modify the parsing above?"); + let storage_changes = deserialize_da_changes(output_iter, n_changes, full_output)?; Ok(ContractChanges { addr, nonce: new_state_nonce, class_hash: new_state_class_hash, storage_changes }) } @@ -261,6 +285,25 @@ fn read_segment>( Ok(segment) } +fn deserialize_os_state_diff>( + output_iter: &mut I, + full_output: Felt252, +) -> Result, SnOsError> { + // If not full_output + if full_output == Felt252::ZERO { + // state_diff = decompress(compressed=output_iter) + // output_iter = itertools.chain(iter(state_diff), output_iter) + return Ok(None); + } + + // Contract changes + let contract_changes = deserialize_contract_state(output_iter, full_output)?; + // Class changes + let classes = deserialize_contract_class_da_changes(output_iter, full_output)?; + + Ok(Some(OsStateDiff { contract_changes, classes })) +} + // Reverse of serialize_os_output in os/output.cairo pub fn deserialize_os_output(output_iter: &mut I) -> Result where @@ -285,14 +328,7 @@ where let (messages_to_l1, messages_to_l2) = deserialize_messages(output_iter)?; - let (contract_changes, classes) = if use_kzg_da.is_zero() { - ( - deserialize_contract_state(output_iter, full_output)?, - deserialize_contract_class_da_changes(output_iter, full_output)?, - ) - } else { - (vec![], HashMap::default()) - }; + let state_diff = deserialize_os_state_diff(output_iter, full_output)?; Ok(StarknetOsOutput { initial_root: header[PREVIOUS_MERKLE_UPDATE_OFFSET], @@ -307,8 +343,7 @@ where full_output, messages_to_l1, messages_to_l2, - contracts: contract_changes, - classes, + state_diff, }) } @@ -344,26 +379,28 @@ mod tests { Felt252::from(27), ], messages_to_l2: vec![], - contracts: vec![ContractChanges { - addr: Felt252::ONE, - nonce: Felt252::from(100), - class_hash: None, - storage_changes: HashMap::from([ - ( - Felt252::from_hex_unchecked( - "0x723973208639b7839ce298f7ffea61e3f9533872defd7abdb91023db4658812", + state_diff: Some(OsStateDiff { + contract_changes: vec![ContractChanges { + addr: Felt252::ONE, + nonce: Felt252::from(100), + class_hash: None, + storage_changes: HashMap::from([ + ( + Felt252::from_hex_unchecked( + "0x723973208639b7839ce298f7ffea61e3f9533872defd7abdb91023db4658812", + ), + Felt252::from_hex_unchecked("0x1f67eee3d0800"), ), - Felt252::from_hex_unchecked("0x1f67eee3d0800"), - ), - ( - Felt252::from_hex_unchecked( - "0x27e66af6f5df3e043d32367d68ece7e13645cca1ca9f80dfdaff9013fddf0c5", + ( + Felt252::from_hex_unchecked( + "0x27e66af6f5df3e043d32367d68ece7e13645cca1ca9f80dfdaff9013fddf0c5", + ), + Felt252::from_hex_unchecked("0xddec034b926f800"), ), - Felt252::from_hex_unchecked("0xddec034b926f800"), - ), - ]), - }], - classes: Default::default(), + ]), + }], + classes: Default::default(), + }), }; let os_output_str = serde_json::to_string(&os_output).expect("OS output serialization failed"); diff --git a/setup-scripts/setup-tests.sh b/setup-scripts/setup-tests.sh index 86a72814..470adc8d 100755 --- a/setup-scripts/setup-tests.sh +++ b/setup-scripts/setup-tests.sh @@ -1,6 +1,6 @@ #!/bin/bash -CAIRO_VER="0.13.2" +CAIRO_VER="0.13.3" if ! command -v cairo-compile >/dev/null; then echo "please start cairo($CAIRO_VER) dev environment" diff --git a/tests/integration/common/utils.rs b/tests/integration/common/utils.rs index 0cff9a52..6163c8e7 100644 --- a/tests/integration/common/utils.rs +++ b/tests/integration/common/utils.rs @@ -59,10 +59,12 @@ pub fn check_os_output_read_only_syscall(os_output: StarknetOsOutput, block_cont // TODO: finer-grained contract changes checks // Just check that the two contracts have been modified, these should be storage changes // related to the fees. - assert_eq!(os_output.contracts.len(), 2); + assert!(os_output.state_diff.is_some()); + let state_diff = os_output.state_diff.unwrap(); + assert_eq!(state_diff.contract_changes.len(), 2); assert_eq!(os_output.new_block_number.to_u64().unwrap(), block_context.block_info().block_number.0); - assert!(os_output.classes.is_empty()); + assert!(state_diff.classes.is_empty()); assert!(os_output.messages_to_l1.is_empty()); assert!(os_output.messages_to_l2.is_empty()); let use_kzg_da = os_output.use_kzg_da != Felt252::ZERO; diff --git a/tests/integration/deprecated_syscalls_tests.rs b/tests/integration/deprecated_syscalls_tests.rs index ccf370bd..9940ce2b 100644 --- a/tests/integration/deprecated_syscalls_tests.rs +++ b/tests/integration/deprecated_syscalls_tests.rs @@ -226,9 +226,12 @@ async fn test_syscall_get_tx_info_cairo0( .await .expect("OS run failed"); + assert!(os_output.state_diff.is_some()); + let state_diff = os_output.state_diff.unwrap(); + // This test causes storage changes in the test contract. Check them. let contract_changes_by_address: HashMap<_, _> = - os_output.contracts.iter().map(|change| (change.addr, change)).collect(); + state_diff.contract_changes.iter().map(|change| (change.addr, change)).collect(); let test_contract_changes = contract_changes_by_address .get(contract_address.0.key()) .expect("The test contract should appear as modified in the OS output"); @@ -391,14 +394,17 @@ async fn test_syscall_deploy_cairo0( .await .expect("OS run failed"); + assert!(os_output.state_diff.is_some()); + let state_diff = os_output.state_diff.unwrap(); + // Check that the new contract address appears in the OS output let contract_changes_by_address: HashMap<_, _> = - os_output.contracts.iter().map(|change| (change.addr, change)).collect(); + state_diff.contract_changes.iter().map(|change| (change.addr, change)).collect(); assert!(contract_changes_by_address.contains_key(expected_contract_address.key())); // Check other output fields assert_eq!(os_output.new_block_number.to_u64().unwrap(), block_context.block_info().block_number.0); - assert!(os_output.classes.is_empty()); + assert!(state_diff.classes.is_empty()); assert!(os_output.messages_to_l1.is_empty()); assert!(os_output.messages_to_l2.is_empty()); let use_kzg_da = os_output.use_kzg_da != Felt252::ZERO;