Skip to main content

cairo_program_runner_lib/hints/
program_hash.rs

1use cairo_vm::types::builtin_name::BuiltinName;
2use cairo_vm::types::relocatable::MaybeRelocatable;
3use cairo_vm::vm::runners::cairo_pie::StrippedProgram;
4use cairo_vm::Felt252;
5use starknet_crypto::{pedersen_hash, poseidon_hash_many, Felt};
6use starknet_types_core::hash::Blake2Felt252;
7use std::iter::once;
8
9use super::types::HashFunc;
10
11type HashFunction = fn(&Felt, &Felt) -> Felt;
12
13#[derive(thiserror_no_std::Error, Debug)]
14pub enum HashChainError {
15    #[error("Data array must contain at least one element.")]
16    EmptyData,
17}
18
19#[derive(thiserror_no_std::Error, Debug)]
20pub enum ProgramHashError {
21    #[error(transparent)]
22    HashChain(#[from] HashChainError),
23
24    #[error(
25        "Invalid program builtin: builtin name too long to be converted to field element: {0}"
26    )]
27    InvalidProgramBuiltin(&'static str),
28
29    #[error("Invalid program data: data contains relocatable(s)")]
30    InvalidProgramData,
31
32    /// Conversion from Felt252 to Felt failed. This is unlikely to happen
33    /// unless the implementation of Felt252 changes and this code is not updated properly.
34    #[error("Conversion from Felt252 to Felt failed")]
35    Felt252ToFeltConversionFailed,
36}
37/*
38 Computes a hash chain over the data, in the following order:
39     h(data[0], h(data[1], h(..., h(data[n-2], data[n-1])))).
40
41 Reimplements this Python function:
42 def compute_hash_chain(data, hash_func=pedersen_hash):
43     assert len(data) >= 1, f"len(data) for hash chain computation must be >= 1; got: {len(data)}."
44     return functools.reduce(lambda x, y: hash_func(y, x), data[::-1])
45*/
46fn compute_hash_chain<'a, I>(data: I, hash_func: HashFunction) -> Result<Felt, HashChainError>
47where
48    I: Iterator<Item = &'a Felt> + DoubleEndedIterator,
49{
50    match data.copied().rev().reduce(|x, y| hash_func(&y, &x)) {
51        Some(result) => Ok(result),
52        None => Err(HashChainError::EmptyData),
53    }
54}
55
56/// Creates an instance of `Felt` from a builtin name.
57///
58/// Converts the builtin name to bytes then attempts to create a field element from
59/// these bytes. This function will fail if the builtin name is over 31 characters.
60fn builtin_to_felt(builtin: &BuiltinName) -> Result<Felt, ProgramHashError> {
61    // The Python implementation uses the builtin name without suffix
62    let builtin_name = builtin.to_str();
63
64    // TODO(idanh): not sure if this check is correct, documentation of Felt::from_bytes_be_slice
65    // works in chunks of 32 bytes and not 31...
66    if builtin_name.len() > 31 {
67        return Err(ProgramHashError::InvalidProgramBuiltin(builtin.to_str()));
68    }
69    Ok(Felt::from_bytes_be_slice(builtin_name.as_bytes()))
70}
71
72/// The `value: Felt` is `pub(crate)` and there is no accessor.
73/// This function converts a `Felt252` to a `Felt` using a safe, albeit inefficient,
74/// method.
75fn felt252_to_felt(felt: &Felt252) -> Result<Felt, ProgramHashError> {
76    let bytes = felt.to_bytes_be();
77    // Check if bytes length is over 31
78    if bytes.len() > 32 {
79        return Err(ProgramHashError::Felt252ToFeltConversionFailed);
80    }
81    Ok(Felt::from_bytes_be(&bytes))
82}
83
84/// Converts a `MaybeRelocatable` into a `Felt` value.
85///
86/// Returns `InvalidProgramData` if `maybe_relocatable` is not an integer
87fn maybe_relocatable_to_felt(
88    maybe_relocatable: &MaybeRelocatable,
89) -> Result<Felt, ProgramHashError> {
90    let felt = maybe_relocatable
91        .get_int_ref()
92        .ok_or(ProgramHashError::InvalidProgramData)?;
93    felt252_to_felt(felt)
94}
95
96/// Computes the program hash chain using the selected hash function.
97/// This implementation takes into account the program header in addition to the program data,
98/// which aligns with how the bootloader computes the program hash of its subtasks.
99pub fn compute_program_hash_chain(
100    program: &StrippedProgram,
101    bootloader_version: usize,
102    program_hash_function: HashFunc,
103) -> Result<Felt, ProgramHashError> {
104    let program_main = program.main;
105    let program_main = Felt::from(program_main);
106
107    // Convert builtin names to field elements
108    let builtin_list: Result<Vec<Felt>, _> = program.builtins.iter().map(builtin_to_felt).collect();
109    let builtin_list = builtin_list?;
110
111    let program_header = vec![
112        Felt::from(bootloader_version),
113        program_main,
114        Felt::from(program.builtins.len()),
115    ];
116
117    let program_data: Result<Vec<_>, _> =
118        program.data.iter().map(maybe_relocatable_to_felt).collect();
119    let program_data = program_data?;
120
121    let data_without_len: Vec<Felt> = [&program_header, &builtin_list, &program_data]
122        .iter()
123        .flat_map(|&v| v.iter().copied())
124        .collect();
125
126    let hash = match program_hash_function {
127        HashFunc::Pedersen => {
128            let data_chain_len_felt = Felt::from(data_without_len.len());
129            compute_hash_chain(
130                once(&data_chain_len_felt).chain(data_without_len.iter()),
131                pedersen_hash,
132            )?
133        }
134        HashFunc::Poseidon => poseidon_hash_many(&data_without_len),
135        HashFunc::Blake => {
136            Blake2Felt252::encode_felt252_data_and_calc_blake_hash(&data_without_len)
137        }
138    };
139    Ok(hash)
140}
141
142#[cfg(test)]
143mod tests {
144    use std::path::PathBuf;
145
146    use cairo_vm::math_utils::signed_felt;
147    use cairo_vm::types::layout_name::LayoutName;
148    use cairo_vm::types::program::Program;
149    use cairo_vm::Felt252;
150    use rstest::rstest;
151    use starknet_crypto::pedersen_hash;
152
153    use crate::types::RunMode;
154    use crate::{cairo_run_program, ProgramInput};
155
156    use super::*;
157
158    #[test]
159    fn test_compute_hash_chain() {
160        let data: Vec<Felt> = vec![Felt::from(1u64), Felt::from(2u64), Felt::from(3u64)];
161        let expected_hash = pedersen_hash(
162            &Felt::from(1u64),
163            &pedersen_hash(&Felt::from(2u64), &Felt::from(3u64)),
164        );
165        let computed_hash = compute_hash_chain(data.iter(), pedersen_hash)
166            .expect("Hash computation failed unexpectedly");
167
168        assert_eq!(computed_hash, expected_hash);
169    }
170
171    #[rstest]
172    // Expected hashes generated with `cairo-hash-program`
173    #[case::fibonacci(
174        "resources/compiled_programs/test_programs/fibonacci_compiled.json",
175        "0x4f028ea2f2568d9b71509641f65cd3879482a371ab2a18d20ca7528d960dd45"
176    )]
177    #[case::builtin_usage(
178        "resources/compiled_programs/test_programs/builtin_usage_compiled.json",
179        "0x4b91dbb7dc2f33ff104023b86e8dac0b94382695bcdcca4a352e6a51ef522c7"
180    )]
181    // TODO(idanh): try to compute the hashchain seperately as well to see that this function works
182    // correctly. if neccessary, change the expected hashes.
183    fn test_compute_program_hash_chain(
184        #[case] program_path: PathBuf,
185        #[case] expected_program_hash: String,
186    ) {
187        let program =
188            Program::from_file(program_path.as_path(), Some("main"))
189                .expect("Could not load program. Did you compile the sample programs? Run `make test` in the root directory.");
190        let stripped_program = program.get_stripped_program().unwrap();
191        let bootloader_version = 0;
192
193        let program_hash =
194            compute_program_hash_chain(&stripped_program, bootloader_version, HashFunc::Pedersen)
195                .expect("Failed to compute program hash.");
196
197        let program_hash_hex = format!("{program_hash:#x}");
198
199        assert_eq!(program_hash_hex, expected_program_hash);
200    }
201
202    /// Runs the simple bootloader with a fibonncci program task with each of the 3 hash func
203    /// variants, and asserts that the program hash at output buffer index 2 (third element) matches
204    /// the Rust `compute_program_hash_chain` result.
205    #[rstest]
206    #[case::pedersen(HashFunc::Pedersen)]
207    #[case::poseidon(HashFunc::Poseidon)]
208    #[case::blake(HashFunc::Blake)]
209    fn test_bootloader_program_hash_matches_rust_impl(#[case] hash_func: HashFunc) {
210        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
211        let simple_bootloader_path = manifest_dir
212            .join("resources/compiled_programs/bootloaders/simple_bootloader_compiled.json");
213        let fibonacci_path = manifest_dir.join("examples/fibonacci.json");
214
215        let simple_bootloader_program =
216            Program::from_file(simple_bootloader_path.as_path(), Some("main"))
217                .expect("Could not load simple bootloader program.");
218        let fibonacci_program = Program::from_file(fibonacci_path.as_path(), Some("main"))
219            .expect("Could not load fibonacci program.");
220        let stripped_program = fibonacci_program
221            .get_stripped_program()
222            .expect("Could not get stripped program.");
223
224        let program_hash_function_str = match hash_func {
225            HashFunc::Pedersen => "pedersen",
226            HashFunc::Poseidon => "poseidon",
227            HashFunc::Blake => "blake",
228        };
229        let program_input_contents = format!(
230            r#"{{
231                "tasks": [
232                    {{
233                        "path": "{}",
234                        "program_hash_function": "{}",
235                        "type": "RunProgramTask"
236                    }}
237                ],
238                "single_page": true
239            }}"#,
240            fibonacci_path.display(),
241            program_hash_function_str,
242        );
243
244        let cairo_run_config = RunMode::Proof {
245            layout: LayoutName::starknet_with_keccak,
246            dynamic_layout_params: None,
247            disable_trace_padding: false,
248            relocate_mem: true,
249        }
250        .create_config();
251
252        let mut runner = cairo_run_program(
253            &simple_bootloader_program,
254            Some(ProgramInput::Json(program_input_contents)),
255            cairo_run_config,
256            None,
257        )
258        .expect("Bootloader run failed.");
259
260        let expected_hash = compute_program_hash_chain(&stripped_program, 0, hash_func)
261            .expect("Failed to compute program hash in Rust.");
262        let mut output_buffer = String::new();
263        runner
264            .vm
265            .write_output(&mut output_buffer)
266            .expect("Failed to write VM output.");
267
268        // write_output renders integers as signed felt decimals.
269        let expected_hash_output_format =
270            signed_felt(Felt252::from_bytes_be(&expected_hash.to_bytes_be())).to_string();
271        let output_lines: Vec<&str> = output_buffer.lines().collect();
272        assert!(
273            output_lines.len() > 2,
274            "Expected at least 3 output lines (n_tasks, n_task_words, program_hash). Got:\n{output_buffer}"
275        );
276        assert_eq!(
277            output_lines[2],
278            expected_hash_output_format,
279            "Bootloader output program hash should match compute_program_hash_chain for {hash_func:?}",
280        );
281    }
282}