|
1 | | -#![allow(dead_code)] |
2 | | - |
| 1 | +use std::collections::HashMap; |
3 | 2 | use std::sync::LazyLock; |
4 | 3 |
|
| 4 | +use apollo_starknet_os_program::AGGREGATOR_PROGRAM; |
5 | 5 | use ark_bls12_381::Fr; |
| 6 | +use cairo_vm::types::builtin_name::BuiltinName; |
| 7 | +use cairo_vm::types::layout_name::LayoutName; |
| 8 | +use cairo_vm::vm::runners::cairo_pie::{ |
| 9 | + BuiltinAdditionalData, |
| 10 | + OutputBuiltinAdditionalData, |
| 11 | + PublicMemoryPage, |
| 12 | +}; |
| 13 | +use itertools::Itertools; |
6 | 14 | use num_bigint::BigUint; |
7 | 15 | use num_integer::Integer; |
8 | 16 | use num_traits::ToPrimitive; |
| 17 | +use rstest::rstest; |
9 | 18 | use starknet_api::core::{ChainId, ClassHash, ContractAddress, Nonce}; |
10 | 19 | use starknet_api::state::StorageKey; |
11 | 20 | use starknet_types_core::felt::Felt; |
12 | 21 | use starknet_types_core::hash::{Poseidon, StarkHash}; |
| 22 | +use tempfile::NamedTempFile; |
13 | 23 |
|
| 24 | +use crate::hint_processor::aggregator_hint_processor::{ |
| 25 | + AggregatorHintProcessor, |
| 26 | + AggregatorInput, |
| 27 | + DataAvailability, |
| 28 | +}; |
14 | 29 | use crate::hints::hint_implementation::kzg::utils::{ |
15 | 30 | polynomial_coefficients_to_kzg_commitment, |
16 | 31 | BLS_PRIME, |
17 | 32 | }; |
| 33 | +use crate::hints::hint_implementation::output::{MAX_PAGE_SIZE, OUTPUT_ATTRIBUTE_FACT_TOPOLOGY}; |
18 | 34 | use crate::hints::hint_implementation::stateless_compression::utils::compress; |
19 | 35 | use crate::io::os_input::OsChainInfo; |
20 | 36 | use crate::io::os_output_types::{ |
21 | 37 | FullContractChanges, |
22 | 38 | FullContractStorageUpdate, |
23 | 39 | N_UPDATES_SMALL_PACKING_BOUND, |
24 | 40 | }; |
| 41 | +use crate::runner::{run_program, RunnerReturnObject}; |
| 42 | +use crate::test_utils::validations::validate_builtins; |
25 | 43 |
|
26 | 44 | // Dummy values for the test. |
27 | 45 | static OS_CONFIG_HASH: LazyLock<Felt> = LazyLock::new(|| { |
28 | 46 | OsChainInfo { |
29 | | - chain_id: ChainId::Other("0".to_string()), |
| 47 | + chain_id: ChainId::Other("\0".to_string()), |
30 | 48 | strk_fee_token_address: ContractAddress::default(), |
31 | 49 | } |
32 | 50 | .compute_os_config_hash(None) |
@@ -156,6 +174,107 @@ impl FailureModifier { |
156 | 174 | } |
157 | 175 | } |
158 | 176 |
|
| 177 | +#[derive(Debug, PartialEq)] |
| 178 | +pub(crate) struct FactTopology { |
| 179 | + pub(crate) tree_structure: Vec<usize>, |
| 180 | + pub(crate) page_sizes: Vec<usize>, |
| 181 | +} |
| 182 | + |
| 183 | +impl FactTopology { |
| 184 | + pub(crate) fn from_output_additional_data( |
| 185 | + output_size: usize, |
| 186 | + data: &OutputBuiltinAdditionalData, |
| 187 | + ) -> Self { |
| 188 | + let tree_structure = match data.attributes.get(OUTPUT_ATTRIBUTE_FACT_TOPOLOGY).cloned() { |
| 189 | + Some(tree_structure) => { |
| 190 | + let bound = 1usize << 30; |
| 191 | + assert_eq!(tree_structure.len() % 2, 0, "Tree structure should be of even length."); |
| 192 | + assert!(!tree_structure.is_empty()); |
| 193 | + assert!(tree_structure.len() <= 10); |
| 194 | + assert!(tree_structure.iter().all(|x| *x <= bound)); |
| 195 | + tree_structure |
| 196 | + } |
| 197 | + None => { |
| 198 | + assert!( |
| 199 | + data.pages.is_empty(), |
| 200 | + "Additional pages cannot be used since the '{OUTPUT_ATTRIBUTE_FACT_TOPOLOGY}' \ |
| 201 | + attribute is not specified." |
| 202 | + ); |
| 203 | + vec![1, 0] |
| 204 | + } |
| 205 | + }; |
| 206 | + Self { |
| 207 | + tree_structure, |
| 208 | + page_sizes: Self::get_page_sizes_from_page_dict(output_size, &data.pages), |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + pub(crate) fn trivial(page0_size: usize) -> Self { |
| 213 | + assert!( |
| 214 | + page0_size <= MAX_PAGE_SIZE, |
| 215 | + "Page size {page0_size} exceeded the maximum {MAX_PAGE_SIZE}." |
| 216 | + ); |
| 217 | + Self { tree_structure: vec![1, 0], page_sizes: vec![page0_size] } |
| 218 | + } |
| 219 | + |
| 220 | + /// Returns the sizes of the program output pages, given the pages dictionary that appears in |
| 221 | + /// the additional attributes of the output builtin. |
| 222 | + fn get_page_sizes_from_page_dict( |
| 223 | + output_size: usize, |
| 224 | + pages: &HashMap<usize, PublicMemoryPage>, |
| 225 | + ) -> Vec<usize> { |
| 226 | + // Make sure the pages are adjacent to each other. |
| 227 | + |
| 228 | + // The first page id is expected to be 1. |
| 229 | + let mut expected_page_id = 1; |
| 230 | + // We don't expect anything on its start value. |
| 231 | + let mut expected_page_start = None; |
| 232 | + // The size of page 0 is output_size if there are no other pages, or the start of page 1 |
| 233 | + // otherwise. |
| 234 | + let mut page0_size = output_size; |
| 235 | + |
| 236 | + for (page_id, page_start, page_size) in |
| 237 | + pages.iter().map(|(page_id, page)| (*page_id, page.start, page.size)).sorted() |
| 238 | + { |
| 239 | + assert_eq!( |
| 240 | + page_id, expected_page_id, |
| 241 | + "Expected page id {expected_page_id}, found {page_id}." |
| 242 | + ); |
| 243 | + if page_id == 1 { |
| 244 | + assert!(page_start > 0, "Page start must be greater than 0."); |
| 245 | + assert!( |
| 246 | + page_start <= output_size, |
| 247 | + "Page start must be less than or equal to output size. Found {page_start}, \ |
| 248 | + output size is {output_size}." |
| 249 | + ); |
| 250 | + page0_size = page_start; |
| 251 | + } else { |
| 252 | + assert_eq!(page_start, expected_page_start.unwrap()); |
| 253 | + } |
| 254 | + |
| 255 | + assert!(page_size > 0, "Page size must be greater than 0."); |
| 256 | + assert!( |
| 257 | + page_size <= output_size, |
| 258 | + "Page size must be less than or equal to output size. Found {page_size}, output \ |
| 259 | + size is {output_size}." |
| 260 | + ); |
| 261 | + expected_page_start = Some(page_start + page_size); |
| 262 | + expected_page_id += 1; |
| 263 | + } |
| 264 | + |
| 265 | + if !pages.is_empty() { |
| 266 | + assert_eq!( |
| 267 | + expected_page_start.unwrap(), |
| 268 | + output_size, |
| 269 | + "Pages must cover the entire program output. Expected size of \ |
| 270 | + {expected_page_start:?}, found {output_size}." |
| 271 | + ); |
| 272 | + } |
| 273 | + |
| 274 | + [vec![page0_size], pages.values().map(|page| page.size).collect::<Vec<usize>>()].concat() |
| 275 | + } |
| 276 | +} |
| 277 | + |
159 | 278 | fn multi_block0_output(full_output: bool) -> Vec<Felt> { |
160 | 279 | let partial_res = [ |
161 | 280 | vec![ |
@@ -287,7 +406,7 @@ fn multi_block1_output(full_output: bool, modifier: FailureModifier) -> Vec<Felt |
287 | 406 | addr: ContractAddress(CONTRACT_ADDR0.try_into().unwrap()), |
288 | 407 | prev_nonce: Nonce(Felt::ONE), |
289 | 408 | new_nonce: Nonce(Felt::TWO), |
290 | | - prev_class_hash: ClassHash(CLASS_HASH0_0), |
| 409 | + prev_class_hash: ClassHash(CLASS_HASH0_1), |
291 | 410 | new_class_hash: ClassHash(CLASS_HASH0_1), |
292 | 411 | storage_changes: vec![ |
293 | 412 | FullContractStorageUpdate { |
@@ -353,8 +472,6 @@ fn multi_block1_output(full_output: bool, modifier: FailureModifier) -> Vec<Felt |
353 | 472 | } else { |
354 | 473 | vec![] |
355 | 474 | }, |
356 | | - vec![COMPILED_CLASS_HASH0_1, CLASS_HASH1_0], |
357 | | - if full_output { vec![COMPILED_CLASS_HASH1_0] } else { vec![] }, |
358 | 475 | vec![COMPILED_CLASS_HASH0_2], |
359 | 476 | ] |
360 | 477 | .concat(); |
@@ -510,3 +627,112 @@ fn bootloader_output(full_output: bool, modifier: FailureModifier) -> Vec<Felt> |
510 | 627 | ] |
511 | 628 | .concat() |
512 | 629 | } |
| 630 | + |
| 631 | +#[rstest] |
| 632 | +#[case(false, false, FailureModifier::None, None)] |
| 633 | +#[case(true, false, FailureModifier::None, None)] |
| 634 | +#[case(false, true, FailureModifier::None, None)] |
| 635 | +#[case(true, true, FailureModifier::None, None)] |
| 636 | +#[case( |
| 637 | + true, |
| 638 | + false, |
| 639 | + FailureModifier::BlockHash, |
| 640 | + Some(format!("{MULTI_BLOCK0_HASH} != {}", MULTI_BLOCK0_HASH + 10)) |
| 641 | +)] |
| 642 | +#[case( |
| 643 | + true, |
| 644 | + false, |
| 645 | + FailureModifier::BlockNumber, |
| 646 | + Some(format!("{NUMBER_OF_BLOCKS_IN_MULTI_BLOCK} != {}", NUMBER_OF_BLOCKS_IN_MULTI_BLOCK + 10)) |
| 647 | +)] |
| 648 | +#[case(true, false, FailureModifier::ProgramHash, Some("0 != 10".to_string()))] |
| 649 | +#[case( |
| 650 | + true, |
| 651 | + false, |
| 652 | + FailureModifier::OsConfigHash, |
| 653 | + Some(format!("{} != {}", *OS_CONFIG_HASH, *OS_CONFIG_HASH + 10)) |
| 654 | +)] |
| 655 | +#[case( |
| 656 | + true, |
| 657 | + false, |
| 658 | + FailureModifier::StorageValue, |
| 659 | + Some(format!("{STORAGE_VALUE0_1} != {}", STORAGE_VALUE0_1 + 10)) |
| 660 | +)] |
| 661 | +#[case( |
| 662 | + true, |
| 663 | + false, |
| 664 | + FailureModifier::CompiledClassHash, |
| 665 | + Some(format!("{COMPILED_CLASS_HASH0_1} != {}", COMPILED_CLASS_HASH0_1 + 10)) |
| 666 | +)] |
| 667 | +fn test_aggregator( |
| 668 | + #[case] full_output: bool, |
| 669 | + #[case] use_kzg_da: bool, |
| 670 | + #[case] modifier: FailureModifier, |
| 671 | + #[case] error_message: Option<String>, |
| 672 | +) { |
| 673 | + let temp_file = NamedTempFile::new().unwrap(); |
| 674 | + let temp_file_path = temp_file.path(); |
| 675 | + |
| 676 | + let bootloader_output_data = bootloader_output(true, modifier); |
| 677 | + let aggregator_input = AggregatorInput { |
| 678 | + bootloader_output: Some(bootloader_output_data.clone()), |
| 679 | + full_output, |
| 680 | + da: if use_kzg_da { |
| 681 | + DataAvailability::Blob(temp_file_path.to_path_buf()) |
| 682 | + } else { |
| 683 | + DataAvailability::CallData |
| 684 | + }, |
| 685 | + debug_mode: false, |
| 686 | + fee_token_address: Felt::ZERO, |
| 687 | + chain_id: Felt::ZERO, |
| 688 | + public_keys: None, |
| 689 | + }; |
| 690 | + |
| 691 | + // Create the aggregator hint processor. |
| 692 | + let mut aggregator_hint_processor = |
| 693 | + AggregatorHintProcessor::new(&AGGREGATOR_PROGRAM, aggregator_input); |
| 694 | + |
| 695 | + let result = |
| 696 | + run_program(LayoutName::all_cairo, &AGGREGATOR_PROGRAM, &mut aggregator_hint_processor); |
| 697 | + |
| 698 | + let RunnerReturnObject { raw_output, cairo_pie, mut cairo_runner } = match result { |
| 699 | + Err(error) => { |
| 700 | + assert!(error.to_string().contains(error_message.unwrap().as_str())); |
| 701 | + return; |
| 702 | + } |
| 703 | + Ok(runner_output) => { |
| 704 | + assert!(error_message.is_none()); |
| 705 | + runner_output |
| 706 | + } |
| 707 | + }; |
| 708 | + |
| 709 | + validate_builtins(&mut cairo_runner); |
| 710 | + |
| 711 | + let combined_output = combined_output(full_output, use_kzg_da); |
| 712 | + assert_eq!( |
| 713 | + raw_output.iter().collect::<Vec<&Felt>>(), |
| 714 | + bootloader_output_data.iter().chain(combined_output.iter()).collect::<Vec<_>>() |
| 715 | + ); |
| 716 | + |
| 717 | + let BuiltinAdditionalData::Output(output_builtin_data) = |
| 718 | + cairo_pie.additional_data.0.get(&BuiltinName::output).unwrap() |
| 719 | + else { |
| 720 | + panic!("Output builtin data should be present in the CairoPie."); |
| 721 | + }; |
| 722 | + let fact_topology = |
| 723 | + FactTopology::from_output_additional_data(raw_output.len(), output_builtin_data); |
| 724 | + |
| 725 | + if use_kzg_da { |
| 726 | + assert_eq!(fact_topology, FactTopology::trivial(raw_output.len())); |
| 727 | + } else { |
| 728 | + let da_len = combined_output_da(full_output).len(); |
| 729 | + let len_without_da = raw_output.len() - da_len; |
| 730 | + assert_eq!( |
| 731 | + fact_topology, |
| 732 | + FactTopology { |
| 733 | + tree_structure: vec![2, 1, 0, 2], |
| 734 | + page_sizes: vec![len_without_da, da_len] |
| 735 | + } |
| 736 | + ); |
| 737 | + } |
| 738 | +} |
0 commit comments