use cairo_vm::types::builtin_name::BuiltinName;
use cairo_vm::types::relocatable::MaybeRelocatable;
use cairo_vm::vm::runners::cairo_pie::StrippedProgram;
use cairo_vm::Felt252;
use starknet_crypto::{pedersen_hash, poseidon_hash_many, Felt};
use starknet_types_core::hash::Blake2Felt252;
use std::iter::once;
use super::types::HashFunc;
type HashFunction = fn(&Felt, &Felt) -> Felt;
#[derive(thiserror_no_std::Error, Debug)]
pub enum HashChainError {
#[error("Data array must contain at least one element.")]
EmptyData,
}
#[derive(thiserror_no_std::Error, Debug)]
pub enum ProgramHashError {
#[error(transparent)]
HashChain(#[from] HashChainError),
#[error(
"Invalid program builtin: builtin name too long to be converted to field element: {0}"
)]
InvalidProgramBuiltin(&'static str),
#[error("Invalid program data: data contains relocatable(s)")]
InvalidProgramData,
#[error("Conversion from Felt252 to Felt failed")]
Felt252ToFeltConversionFailed,
}
fn compute_hash_chain<'a, I>(data: I, hash_func: HashFunction) -> Result<Felt, HashChainError>
where
I: Iterator<Item = &'a Felt> + DoubleEndedIterator,
{
match data.copied().rev().reduce(|x, y| hash_func(&y, &x)) {
Some(result) => Ok(result),
None => Err(HashChainError::EmptyData),
}
}
fn builtin_to_felt(builtin: &BuiltinName) -> Result<Felt, ProgramHashError> {
let builtin_name = builtin.to_str();
if builtin_name.len() > 31 {
return Err(ProgramHashError::InvalidProgramBuiltin(builtin.to_str()));
}
Ok(Felt::from_bytes_be_slice(builtin_name.as_bytes()))
}
fn felt252_to_felt(felt: &Felt252) -> Result<Felt, ProgramHashError> {
let bytes = felt.to_bytes_be();
if bytes.len() > 32 {
return Err(ProgramHashError::Felt252ToFeltConversionFailed);
}
Ok(Felt::from_bytes_be(&bytes))
}
fn maybe_relocatable_to_felt(
maybe_relocatable: &MaybeRelocatable,
) -> Result<Felt, ProgramHashError> {
let felt = maybe_relocatable
.get_int_ref()
.ok_or(ProgramHashError::InvalidProgramData)?;
felt252_to_felt(felt)
}
pub fn compute_program_hash_chain(
program: &StrippedProgram,
bootloader_version: usize,
program_hash_function: HashFunc,
) -> Result<Felt, ProgramHashError> {
let program_main = program.main;
let program_main = Felt::from(program_main);
let builtin_list: Result<Vec<Felt>, _> = program.builtins.iter().map(builtin_to_felt).collect();
let builtin_list = builtin_list?;
let program_header = vec![
Felt::from(bootloader_version),
program_main,
Felt::from(program.builtins.len()),
];
let program_data: Result<Vec<_>, _> =
program.data.iter().map(maybe_relocatable_to_felt).collect();
let program_data = program_data?;
let data_without_len: Vec<Felt> = [&program_header, &builtin_list, &program_data]
.iter()
.flat_map(|&v| v.iter().copied())
.collect();
let hash = match program_hash_function {
HashFunc::Pedersen => {
let data_chain_len_felt = Felt::from(data_without_len.len());
compute_hash_chain(
once(&data_chain_len_felt).chain(data_without_len.iter()),
pedersen_hash,
)?
}
HashFunc::Poseidon => poseidon_hash_many(&data_without_len),
HashFunc::Blake => {
Blake2Felt252::encode_felt252_data_and_calc_blake_hash(&data_without_len)
}
};
Ok(hash)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use cairo_vm::math_utils::signed_felt;
use cairo_vm::types::layout_name::LayoutName;
use cairo_vm::types::program::Program;
use cairo_vm::Felt252;
use rstest::rstest;
use starknet_crypto::pedersen_hash;
use crate::types::RunMode;
use crate::{cairo_run_program, ProgramInput};
use super::*;
#[test]
fn test_compute_hash_chain() {
let data: Vec<Felt> = vec![Felt::from(1u64), Felt::from(2u64), Felt::from(3u64)];
let expected_hash = pedersen_hash(
&Felt::from(1u64),
&pedersen_hash(&Felt::from(2u64), &Felt::from(3u64)),
);
let computed_hash = compute_hash_chain(data.iter(), pedersen_hash)
.expect("Hash computation failed unexpectedly");
assert_eq!(computed_hash, expected_hash);
}
#[rstest]
#[case::fibonacci(
"resources/compiled_programs/test_programs/fibonacci_compiled.json",
"0x4f028ea2f2568d9b71509641f65cd3879482a371ab2a18d20ca7528d960dd45"
)]
#[case::builtin_usage(
"resources/compiled_programs/test_programs/builtin_usage_compiled.json",
"0x4b91dbb7dc2f33ff104023b86e8dac0b94382695bcdcca4a352e6a51ef522c7"
)]
fn test_compute_program_hash_chain(
#[case] program_path: PathBuf,
#[case] expected_program_hash: String,
) {
let program =
Program::from_file(program_path.as_path(), Some("main"))
.expect("Could not load program. Did you compile the sample programs? Run `make test` in the root directory.");
let stripped_program = program.get_stripped_program().unwrap();
let bootloader_version = 0;
let program_hash =
compute_program_hash_chain(&stripped_program, bootloader_version, HashFunc::Pedersen)
.expect("Failed to compute program hash.");
let program_hash_hex = format!("{program_hash:#x}");
assert_eq!(program_hash_hex, expected_program_hash);
}
#[rstest]
#[case::pedersen(HashFunc::Pedersen)]
#[case::poseidon(HashFunc::Poseidon)]
#[case::blake(HashFunc::Blake)]
fn test_bootloader_program_hash_matches_rust_impl(#[case] hash_func: HashFunc) {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let simple_bootloader_path = manifest_dir
.join("resources/compiled_programs/bootloaders/simple_bootloader_compiled.json");
let fibonacci_path = manifest_dir.join("examples/fibonacci.json");
let simple_bootloader_program =
Program::from_file(simple_bootloader_path.as_path(), Some("main"))
.expect("Could not load simple bootloader program.");
let fibonacci_program = Program::from_file(fibonacci_path.as_path(), Some("main"))
.expect("Could not load fibonacci program.");
let stripped_program = fibonacci_program
.get_stripped_program()
.expect("Could not get stripped program.");
let program_hash_function_str = match hash_func {
HashFunc::Pedersen => "pedersen",
HashFunc::Poseidon => "poseidon",
HashFunc::Blake => "blake",
};
let program_input_contents = format!(
r#"{{
"tasks": [
{{
"path": "{}",
"program_hash_function": "{}",
"type": "RunProgramTask"
}}
],
"single_page": true
}}"#,
fibonacci_path.display(),
program_hash_function_str,
);
let cairo_run_config = RunMode::Proof {
layout: LayoutName::starknet_with_keccak,
dynamic_layout_params: None,
disable_trace_padding: false,
relocate_mem: true,
}
.create_config();
let mut runner = cairo_run_program(
&simple_bootloader_program,
Some(ProgramInput::Json(program_input_contents)),
cairo_run_config,
None,
)
.expect("Bootloader run failed.");
let expected_hash = compute_program_hash_chain(&stripped_program, 0, hash_func)
.expect("Failed to compute program hash in Rust.");
let mut output_buffer = String::new();
runner
.vm
.write_output(&mut output_buffer)
.expect("Failed to write VM output.");
let expected_hash_output_format =
signed_felt(Felt252::from_bytes_be(&expected_hash.to_bytes_be())).to_string();
let output_lines: Vec<&str> = output_buffer.lines().collect();
assert!(
output_lines.len() > 2,
"Expected at least 3 output lines (n_tasks, n_task_words, program_hash). Got:\n{output_buffer}"
);
assert_eq!(
output_lines[2],
expected_hash_output_format,
"Bootloader output program hash should match compute_program_hash_chain for {hash_func:?}",
);
}
}