diff --git a/crates/apollo_starknet_os_program/build/compile_program.rs b/crates/apollo_starknet_os_program/build/compile_program.rs index 22ae476918d..4e59fbfe191 100644 --- a/crates/apollo_starknet_os_program/build/compile_program.rs +++ b/crates/apollo_starknet_os_program/build/compile_program.rs @@ -52,6 +52,14 @@ pub async fn compile_test_contracts(out_dir: PathBuf) { "builtin_offset_increase_test", None, )); + // The proof-fact fold module is not yet reachable from the OS program, so it is + // compiled standalone for its Cairo-vs-Rust tests. + task_set.spawn(compile_and_output_program( + out_dir.clone(), + "starkware/starknet/core/os/proof_fact_fold.cairo", + "proof_fact_fold", + None, + )); task_set.join_all().await; } diff --git a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/proof_fact_fold.cairo b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/proof_fact_fold.cairo new file mode 100644 index 00000000000..007fe02e0e5 --- /dev/null +++ b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/proof_fact_fold.cairo @@ -0,0 +1,245 @@ +// Folds a block's privacy-transaction proof facts into a single Blake2s digest - the OS +// mirror of the recursive proof tree the proving side builds over the corresponding leaf +// proofs (proving side: `stwo_run_and_prove_recursive_tree`). The tree's root proof is +// checked by the Cairo circuit verifier, whose public output is a digest of the folded +// facts; the fold here must therefore reproduce the proving side's digest chain bit for +// bit, so that the two can be compared. +// +// The digest chain: +// * A fold-tree entry is 16 u32 words: a circuit hash (8 words) followed by an output +// digest (8 words). All digests are Blake2s-256, and every 32-byte digest is read as +// eight little-endian u32 words. +// * Leaf (one privacy transaction): the output digest is +// blake2s(encode_felt252s_to_u32s(proof_facts[2:])). The preimage drops +// proof_facts[0] (the proof version) and proof_facts[1] (the proof variant marker) and +// keeps [program_hash, ...virtual OS output] - exactly the preimage the leaf simple +// bootloader hashes to its own output. The entry's circuit hash is the leaf +// cairo-verifier circuit's. +// * Internal node: adjacent entries are paired left to right; a parent's output digest +// is blake2s over the 32 raw u32 words of its two children's entries (no felt +// encoding at fold levels), and its circuit hash is the multiverifier circuit's. A +// trailing unpaired entry is carried to the next layer unchanged, keeping its own +// circuit hash. +// * A block with a single entry self-folds: the root preimage is that entry in both +// child slots (the multiverifier verifies the same proof twice). The root is therefore +// always a multiverifier node. +// * The digest the circuit verifier outputs for a proof whose facts fold to some entry +// is blake2s over that entry's 16 words (circuit hash, then output digest). +// +// The fold arity is pinned to 2; a change on the proving side is a protocol change that +// must be mirrored here. + +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.cairo_blake2s.blake2s import blake_with_opcode, encode_felt252s_to_u32s +from starkware.cairo.common.math import assert_not_zero +from starkware.cairo.common.memcpy import memcpy + +// Number of u32 words in a Blake2s-256 digest. +const BLAKE2S_DIGEST_N_WORDS = 8; +// A fold-tree entry: a circuit hash (8 words) followed by an output digest (8 words). +const FOLD_ENTRY_N_WORDS = 2 * BLAKE2S_DIGEST_N_WORDS; + +// The circuit hashes identifying the leaf cairo-verifier circuit and the multiverifier +// circuit: blake2s(log_blowup_factor || component_log_sizes || preprocessed_root), as +// eight little-endian u32 words. These are circuit identities from the proving side's +// circuit registry and are not derivable from the proof facts; a change to either is a +// protocol version change. +// +// NOTE: these are the `canonical_small` test-registry values (proving side: +// crates/stwo_run_and_prove_recursive_tree/test_data/circuit_registry.json), pinned by +// tests against the same values in the Rust mirror (starknet_os::proof_fact_fold). They +// must be replaced with the production registry's values before production use. +const LEAF_VERIFIER_CIRCUIT_HASH_0 = 0xd2d85a42; +const LEAF_VERIFIER_CIRCUIT_HASH_1 = 0x79697b22; +const LEAF_VERIFIER_CIRCUIT_HASH_2 = 0x3a41a061; +const LEAF_VERIFIER_CIRCUIT_HASH_3 = 0x011cb393; +const LEAF_VERIFIER_CIRCUIT_HASH_4 = 0x7a040ec9; +const LEAF_VERIFIER_CIRCUIT_HASH_5 = 0x4508f4ca; +const LEAF_VERIFIER_CIRCUIT_HASH_6 = 0x42239409; +const LEAF_VERIFIER_CIRCUIT_HASH_7 = 0x60f3baea; + +const MULTIVERIFIER_CIRCUIT_HASH_0 = 0xa5989715; +const MULTIVERIFIER_CIRCUIT_HASH_1 = 0x2377c07a; +const MULTIVERIFIER_CIRCUIT_HASH_2 = 0xc6d1e844; +const MULTIVERIFIER_CIRCUIT_HASH_3 = 0x54f0a04d; +const MULTIVERIFIER_CIRCUIT_HASH_4 = 0x8be65a7d; +const MULTIVERIFIER_CIRCUIT_HASH_5 = 0xfd73c261; +const MULTIVERIFIER_CIRCUIT_HASH_6 = 0x9078e728; +const MULTIVERIFIER_CIRCUIT_HASH_7 = 0x973f680f; + +// Folds the proof facts of a block's privacy transactions into the block's root entry: +// [multiverifier circuit hash (8 words), root output digest (8 words)]. +// +// `proof_facts_sizes[i]` is the length of the i'th transaction's proof facts, and the +// facts themselves are laid out consecutively in `concatenated_proof_facts`. +// Preconditions: `n_transactions` is at least 1 (a block with no contributing +// transactions has no fold and must be omitted by the caller); every size is at least 3 +// (the two version markers plus the program hash), as enforced for non-empty proof facts +// by `check_proof_facts`; transactions with empty proof facts must not be included. +func fold_block_proof_facts{range_check_ptr}( + n_transactions: felt, proof_facts_sizes: felt*, concatenated_proof_facts: felt* +) -> (root_entry: felt*) { + alloc_locals; + assert_not_zero(n_transactions); + let (local leaf_entries: felt*) = alloc(); + build_leaf_entries( + n_transactions=n_transactions, + proof_facts_sizes=proof_facts_sizes, + concatenated_proof_facts=concatenated_proof_facts, + leaf_entries=leaf_entries, + ); + return fold_entries_to_root(n_entries=n_transactions, entries=leaf_entries); +} + +// Computes the digest the Cairo circuit verifier outputs for the proof whose facts fold +// to `entry`: blake2s over the entry's 16 words (proving side: +// `get_verification_output`). Returns the digest as 8 u32 words. +func compute_fold_digest{range_check_ptr}(entry: felt*) -> (fold_digest: felt*) { + alloc_locals; + let (local fold_digest: felt*) = alloc(); + blake_with_opcode(len=FOLD_ENTRY_N_WORDS, data=entry, out=fold_digest); + return (fold_digest=fold_digest); +} + +// Computes one transaction's leaf output digest: +// blake2s(encode_felt252s_to_u32s(proof_facts[2:])), as 8 u32 words. This is the digest +// the leaf simple bootloader writes to its output, which the leaf cairo-verifier circuit +// emits verbatim as its public output. +// Precondition: `proof_facts_size` is at least 3. +func compute_leaf_output_digest{range_check_ptr}(proof_facts_size: felt, proof_facts: felt*) -> ( + output_digest: felt* +) { + alloc_locals; + let (local encoded_words: felt*) = alloc(); + let encoded_words_len = encode_felt252s_to_u32s( + packed_values_len=proof_facts_size - 2, + packed_values=proof_facts + 2, + unpacked_u32s=encoded_words, + ); + let (local output_digest: felt*) = alloc(); + blake_with_opcode(len=encoded_words_len, data=encoded_words, out=output_digest); + return (output_digest=output_digest); +} + +// Returns the leaf cairo-verifier circuit's hash as 8 u32 words. +func get_leaf_verifier_circuit_hash() -> (circuit_hash: felt*) { + let (circuit_hash: felt*) = alloc(); + assert circuit_hash[0] = LEAF_VERIFIER_CIRCUIT_HASH_0; + assert circuit_hash[1] = LEAF_VERIFIER_CIRCUIT_HASH_1; + assert circuit_hash[2] = LEAF_VERIFIER_CIRCUIT_HASH_2; + assert circuit_hash[3] = LEAF_VERIFIER_CIRCUIT_HASH_3; + assert circuit_hash[4] = LEAF_VERIFIER_CIRCUIT_HASH_4; + assert circuit_hash[5] = LEAF_VERIFIER_CIRCUIT_HASH_5; + assert circuit_hash[6] = LEAF_VERIFIER_CIRCUIT_HASH_6; + assert circuit_hash[7] = LEAF_VERIFIER_CIRCUIT_HASH_7; + return (circuit_hash=circuit_hash); +} + +// Returns the multiverifier circuit's hash as 8 u32 words. +func get_multiverifier_circuit_hash() -> (circuit_hash: felt*) { + let (circuit_hash: felt*) = alloc(); + assert circuit_hash[0] = MULTIVERIFIER_CIRCUIT_HASH_0; + assert circuit_hash[1] = MULTIVERIFIER_CIRCUIT_HASH_1; + assert circuit_hash[2] = MULTIVERIFIER_CIRCUIT_HASH_2; + assert circuit_hash[3] = MULTIVERIFIER_CIRCUIT_HASH_3; + assert circuit_hash[4] = MULTIVERIFIER_CIRCUIT_HASH_4; + assert circuit_hash[5] = MULTIVERIFIER_CIRCUIT_HASH_5; + assert circuit_hash[6] = MULTIVERIFIER_CIRCUIT_HASH_6; + assert circuit_hash[7] = MULTIVERIFIER_CIRCUIT_HASH_7; + return (circuit_hash=circuit_hash); +} + +// Builds the layer-0 fold entries, one per transaction, consecutively at `leaf_entries`. +func build_leaf_entries{range_check_ptr}( + n_transactions: felt, + proof_facts_sizes: felt*, + concatenated_proof_facts: felt*, + leaf_entries: felt*, +) { + alloc_locals; + if (n_transactions == 0) { + return (); + } + let (leaf_verifier_circuit_hash) = get_leaf_verifier_circuit_hash(); + memcpy(dst=leaf_entries, src=leaf_verifier_circuit_hash, len=BLAKE2S_DIGEST_N_WORDS); + let (output_digest) = compute_leaf_output_digest( + proof_facts_size=proof_facts_sizes[0], proof_facts=concatenated_proof_facts + ); + memcpy( + dst=leaf_entries + BLAKE2S_DIGEST_N_WORDS, src=output_digest, len=BLAKE2S_DIGEST_N_WORDS + ); + return build_leaf_entries( + n_transactions=n_transactions - 1, + proof_facts_sizes=proof_facts_sizes + 1, + concatenated_proof_facts=concatenated_proof_facts + proof_facts_sizes[0], + leaf_entries=leaf_entries + FOLD_ENTRY_N_WORDS, + ); +} + +// Folds `n_entries` consecutive entries at `entries` up to the tree's single root entry. +// Precondition: `n_entries` is at least 1. A single layer-0 entry self-folds; with more, +// layers of adjacent pairs are folded until one entry remains (no self-fold at the end). +func fold_entries_to_root{range_check_ptr}(n_entries: felt, entries: felt*) -> (root_entry: felt*) { + alloc_locals; + if (n_entries == 1) { + // Self-fold: the root preimage is the single entry in both child slots. + let (local self_fold_preimage: felt*) = alloc(); + memcpy(dst=self_fold_preimage, src=entries, len=FOLD_ENTRY_N_WORDS); + memcpy(dst=self_fold_preimage + FOLD_ENTRY_N_WORDS, src=entries, len=FOLD_ENTRY_N_WORDS); + let (local root_entry: felt*) = alloc(); + fold_pair(children=self_fold_preimage, parent_entry=root_entry); + return (root_entry=root_entry); + } + return fold_layers_to_root(n_entries=n_entries, entries=entries); +} + +// Folds layers of adjacent pairs until a single entry remains and returns it. +// Precondition: `n_entries` is at least 2 (a lone layer-0 entry self-folds instead - see +// `fold_entries_to_root`). +func fold_layers_to_root{range_check_ptr}(n_entries: felt, entries: felt*) -> (root_entry: felt*) { + alloc_locals; + let (local next_layer_entries: felt*) = alloc(); + let (n_next_layer_entries) = fold_layer( + n_entries=n_entries, entries=entries, next_layer_entries=next_layer_entries + ); + if (n_next_layer_entries == 1) { + return (root_entry=next_layer_entries); + } + return fold_layers_to_root(n_entries=n_next_layer_entries, entries=next_layer_entries); +} + +// Folds one layer: pairs adjacent entries left to right into parent entries written +// consecutively at `next_layer_entries`, carrying a trailing unpaired entry unchanged. +// Returns the number of entries written to the next layer. +func fold_layer{range_check_ptr}(n_entries: felt, entries: felt*, next_layer_entries: felt*) -> ( + n_next_layer_entries: felt +) { + if (n_entries == 0) { + return (n_next_layer_entries=0); + } + if (n_entries == 1) { + // Carry the unpaired entry unchanged, keeping its own circuit hash. + memcpy(dst=next_layer_entries, src=entries, len=FOLD_ENTRY_N_WORDS); + return (n_next_layer_entries=1); + } + fold_pair(children=entries, parent_entry=next_layer_entries); + let (n_rest_entries) = fold_layer( + n_entries=n_entries - 2, + entries=entries + 2 * FOLD_ENTRY_N_WORDS, + next_layer_entries=next_layer_entries + FOLD_ENTRY_N_WORDS, + ); + return (n_next_layer_entries=n_rest_entries + 1); +} + +// Folds the two consecutive entries at `children` into `parent_entry`: the parent's +// output digest is blake2s over the children's 32 raw u32 words - the two consecutive +// entries are exactly that preimage - and its circuit hash is the multiverifier's. +func fold_pair{range_check_ptr}(children: felt*, parent_entry: felt*) { + alloc_locals; + let (multiverifier_circuit_hash) = get_multiverifier_circuit_hash(); + memcpy(dst=parent_entry, src=multiverifier_circuit_hash, len=BLAKE2S_DIGEST_N_WORDS); + blake_with_opcode( + len=2 * FOLD_ENTRY_N_WORDS, data=children, out=parent_entry + BLAKE2S_DIGEST_N_WORDS + ); + return (); +} diff --git a/crates/apollo_starknet_os_program/src/test_programs.rs b/crates/apollo_starknet_os_program/src/test_programs.rs index a583dcce0ce..84561b89c8f 100644 --- a/crates/apollo_starknet_os_program/src/test_programs.rs +++ b/crates/apollo_starknet_os_program/src/test_programs.rs @@ -2,3 +2,5 @@ pub const ALIASES_TEST_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/aliases_test_bytes")); pub const BUILTIN_OFFSET_INCREASE_TEST_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/builtin_offset_increase_test_bytes")); +pub const PROOF_FACT_FOLD_BYTES: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/proof_fact_fold_bytes")); diff --git a/crates/starknet_os/src/proof_fact_fold_test.rs b/crates/starknet_os/src/proof_fact_fold_test.rs index 0622f83e5e9..6079944f5b7 100644 --- a/crates/starknet_os/src/proof_fact_fold_test.rs +++ b/crates/starknet_os/src/proof_fact_fold_test.rs @@ -1,11 +1,133 @@ +use std::collections::HashMap; + +use apollo_starknet_os_program::test_programs::PROOF_FACT_FOLD_BYTES; +use cairo_vm::types::builtin_name::BuiltinName; +use cairo_vm::types::layout_name::LayoutName; +use cairo_vm::types::relocatable::MaybeRelocatable; +use rstest::rstest; use starknet_types_core::felt::Felt; use super::{ + compute_fold_digest, compute_leaf_output_digest, fold_block_proof_facts, Blake2sDigestWords, + FoldEntry, + BLAKE2S_DIGEST_N_WORDS, + LEAF_VERIFIER_CIRCUIT_HASH, MULTIVERIFIER_CIRCUIT_HASH, }; +use crate::test_utils::cairo_runner::{ + initialize_and_run_cairo_0_entry_point, + EndpointArg, + EntryPointRunnerConfig, + ImplicitArg, + PointerArg, +}; + +/// A fold-tree entry's word count: a circuit hash followed by an output digest. +const FOLD_ENTRY_N_WORDS: usize = 2 * BLAKE2S_DIGEST_N_WORDS; + +fn entrypoint_runner_config() -> EntryPointRunnerConfig { + EntryPointRunnerConfig { + layout: LayoutName::all_cairo, + trace_enabled: false, + verify_secure: false, + proof_mode: false, + add_main_prefix_to_entrypoint: true, + validate_builtins_offset: true, + } +} + +fn felt_array_arg(felts: &[Felt]) -> EndpointArg { + EndpointArg::Pointer(PointerArg::Array( + felts.iter().map(|felt| MaybeRelocatable::Int(*felt)).collect(), + )) +} + +/// Runs a function of `proof_fact_fold.cairo` that returns a single pointer to +/// `n_returned_words` u32 words, and returns those words. +fn run_cairo_function_returning_words( + function_name: &str, + explicit_args: &[EndpointArg], + implicit_args: &[ImplicitArg], + n_returned_words: usize, +) -> Vec { + let expected_return_values = vec![EndpointArg::Pointer(PointerArg::Array(vec![ + MaybeRelocatable::from(Felt::ZERO); + n_returned_words + ]))]; + let (_, explicit_return_values, _) = initialize_and_run_cairo_0_entry_point( + &entrypoint_runner_config(), + PROOF_FACT_FOLD_BYTES, + function_name, + explicit_args, + implicit_args, + &expected_return_values, + HashMap::new(), + None, + ) + .unwrap_or_else(|error| panic!("Failed to run Cairo function {function_name}: {error:?}")); + let [EndpointArg::Pointer(PointerArg::Array(returned_words))] = + explicit_return_values.as_slice() + else { + panic!("Expected {function_name} to return a single words-array pointer."); + }; + returned_words + .iter() + .map(|returned_word| { + let MaybeRelocatable::Int(word_felt) = returned_word else { + panic!("Expected a felt digest word, got {returned_word:?}."); + }; + u32::try_from(word_felt.to_biguint()).expect("A digest word must fit in a u32.") + }) + .collect() +} + +/// Runs the Cairo `fold_block_proof_facts` and returns the root entry. +fn run_cairo_fold_block_proof_facts(per_transaction_proof_facts: &[&[Felt]]) -> FoldEntry { + let proof_facts_sizes: Vec = per_transaction_proof_facts + .iter() + .map(|proof_facts| Felt::from(proof_facts.len())) + .collect(); + let concatenated_proof_facts: Vec = per_transaction_proof_facts + .iter() + .flat_map(|proof_facts| proof_facts.iter().copied()) + .collect(); + let root_entry_words = run_cairo_function_returning_words( + "fold_block_proof_facts", + &[ + EndpointArg::from(Felt::from(per_transaction_proof_facts.len())), + felt_array_arg(&proof_facts_sizes), + felt_array_arg(&concatenated_proof_facts), + ], + &[ImplicitArg::Builtin(BuiltinName::range_check)], + FOLD_ENTRY_N_WORDS, + ); + FoldEntry { + circuit_hash: root_entry_words[..BLAKE2S_DIGEST_N_WORDS].try_into().unwrap(), + output_digest: root_entry_words[BLAKE2S_DIGEST_N_WORDS..].try_into().unwrap(), + } +} + +/// The proof facts of a synthetic transaction, shaped like real ones (two version +/// markers, a program hash, the virtual OS output header and message hashes), with +/// values derived from `transaction_index` so different transactions have different +/// digests. +fn synthetic_proof_facts(transaction_index: u64) -> Vec { + vec![ + Felt::from_hex("0x50524f4f4631").unwrap(), // 'PROOF1' + Felt::from_hex("0x5649525455414c5f534e4f53").unwrap(), // 'VIRTUAL_SNOS' + // A program hash and block hash above 2^63, exercising the 8-word felt encoding. + Felt::from(2u64).pow(200u64) + Felt::from(transaction_index), + Felt::from_hex("0x5649525455414c5f534e4f5330").unwrap(), // 'VIRTUAL_SNOS0' + Felt::from(1000 + transaction_index), + Felt::from(2u64).pow(150u64) + Felt::from(transaction_index), + Felt::from(transaction_index * 17), + Felt::ONE, + Felt::from(2u64).pow(100u64) + Felt::from(transaction_index), + ] +} /// Golden from the proving side: the leaf simple bootloader's output-preimage digest /// (proving-dev crates/stwo_run_and_prove_recursive_tree/src/tests.rs, `H1` of the @@ -55,3 +177,68 @@ fn test_four_leaf_fold_matches_proving_side_golden() { assert_eq!(root_entry.output_digest, expected_root_output_digest); assert_eq!(root_entry.circuit_hash, MULTIVERIFIER_CIRCUIT_HASH); } + +#[rstest] +#[case::minimal_facts(vec![Felt::ZERO, Felt::ZERO, Felt::ONE])] +#[case::small_values(vec![Felt::ZERO, Felt::ZERO, Felt::from(42), Felt::from(1337)])] +#[case::boundary_below_2_63(vec![Felt::ZERO, Felt::ZERO, Felt::from((1u64 << 63) - 1)])] +#[case::boundary_at_2_63(vec![Felt::ZERO, Felt::ZERO, Felt::from(1u64 << 63)])] +#[case::realistic_facts(synthetic_proof_facts(0))] +fn test_cairo_leaf_output_digest_matches_rust(#[case] proof_facts: Vec) { + let cairo_digest_words = run_cairo_function_returning_words( + "compute_leaf_output_digest", + &[EndpointArg::from(Felt::from(proof_facts.len())), felt_array_arg(&proof_facts)], + &[ImplicitArg::Builtin(BuiltinName::range_check)], + BLAKE2S_DIGEST_N_WORDS, + ); + assert_eq!(cairo_digest_words, compute_leaf_output_digest(&proof_facts)); +} + +/// Covers the self-fold (one transaction), a full layer (two, four), the carry rule at +/// one layer (three) and at two layers (five), and a deeper tree (seven). +#[rstest] +#[case::single_transaction_self_fold(1)] +#[case::two_transactions(2)] +#[case::three_transactions_carry(3)] +#[case::four_transactions(4)] +#[case::five_transactions_double_carry(5)] +#[case::seven_transactions(7)] +fn test_cairo_fold_block_proof_facts_matches_rust(#[case] n_transactions: u64) { + let per_transaction_proof_facts: Vec> = + (0..n_transactions).map(synthetic_proof_facts).collect(); + let proof_facts_references: Vec<&[Felt]> = + per_transaction_proof_facts.iter().map(Vec::as_slice).collect(); + let cairo_root_entry = run_cairo_fold_block_proof_facts(&proof_facts_references); + assert_eq!(cairo_root_entry, fold_block_proof_facts(&proof_facts_references)); +} + +#[test] +fn test_cairo_fold_digest_matches_rust() { + let proof_facts = synthetic_proof_facts(0); + let root_entry = fold_block_proof_facts(&[&proof_facts, &proof_facts]); + let root_entry_words: Vec = root_entry + .circuit_hash + .iter() + .chain(root_entry.output_digest.iter()) + .map(|word| Felt::from(*word)) + .collect(); + let cairo_fold_digest_words = run_cairo_function_returning_words( + "compute_fold_digest", + &[felt_array_arg(&root_entry_words)], + &[ImplicitArg::Builtin(BuiltinName::range_check)], + BLAKE2S_DIGEST_N_WORDS, + ); + assert_eq!(cairo_fold_digest_words, compute_fold_digest(&root_entry)); +} + +#[rstest] +#[case::leaf_verifier("get_leaf_verifier_circuit_hash", LEAF_VERIFIER_CIRCUIT_HASH)] +#[case::multiverifier("get_multiverifier_circuit_hash", MULTIVERIFIER_CIRCUIT_HASH)] +fn test_cairo_circuit_hash_constants_match_rust( + #[case] getter_name: &str, + #[case] expected_circuit_hash: Blake2sDigestWords, +) { + let cairo_circuit_hash_words = + run_cairo_function_returning_words(getter_name, &[], &[], BLAKE2S_DIGEST_N_WORDS); + assert_eq!(cairo_circuit_hash_words, expected_circuit_hash); +}