cairo_program_runner_lib/hints/
program_hash.rs1use 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 #[error("Conversion from Felt252 to Felt failed")]
35 Felt252ToFeltConversionFailed,
36}
37fn 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
56fn builtin_to_felt(builtin: &BuiltinName) -> Result<Felt, ProgramHashError> {
61 let builtin_name = builtin.to_str();
63
64 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
72fn felt252_to_felt(felt: &Felt252) -> Result<Felt, ProgramHashError> {
76 let bytes = felt.to_bytes_be();
77 if bytes.len() > 32 {
79 return Err(ProgramHashError::Felt252ToFeltConversionFailed);
80 }
81 Ok(Felt::from_bytes_be(&bytes))
82}
83
84fn 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
96pub 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 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 #[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 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 #[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 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}