From c692b7594a74ee3245cf7affc7c8cfc296b0398c Mon Sep 17 00:00:00 2001 From: Olivier Desenfans Date: Fri, 5 Apr 2024 23:58:30 +0200 Subject: [PATCH] Feature: compute program hash chain (#1647) * Feature: compute program hash chain Problem: computing the hash of a program is useful to verify its integrity. This hash is also used for different purposes, like when loading a program with the bootloader. Solution: add a `program_hash` module and the `compute_program_hash_chain` function that computes the hash of a stripped program, making it usable with `Program` and `CairoPie` objects. * compile module in no-std --- CHANGELOG.md | 2 + vm/src/lib.rs | 1 + vm/src/program_hash.rs | 204 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 vm/src/program_hash.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b4aa330e8..58e0254b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ * Adds the flag `append_return_values` to both the CLI and `Cairo1RunConfig` struct. * Enabling flag will add the output builtin and the necessary instructions to append the return values to the output builtin's memory segment. +* feat: Compute program hash chain [#1647](https://github.com/lambdaclass/cairo-vm/pull/1647) + * feat: Add cairo1-run output pretty-printing for felts, arrays/spans and dicts [#1630](https://github.com/lambdaclass/cairo-vm/pull/1630) * feat: output builtin features for bootloader support [#1580](https://github.com/lambdaclass/cairo-vm/pull/1580) diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 36509273b3..bbd334ae81 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -57,6 +57,7 @@ pub mod air_public_input; pub mod cairo_run; pub mod hint_processor; pub mod math_utils; +pub mod program_hash; pub mod serde; pub mod types; pub mod utils; diff --git a/vm/src/program_hash.rs b/vm/src/program_hash.rs new file mode 100644 index 0000000000..3ff1fa798c --- /dev/null +++ b/vm/src/program_hash.rs @@ -0,0 +1,204 @@ +use starknet_crypto::{pedersen_hash, FieldElement}; + +use crate::Felt252; + +use crate::serde::deserialize_program::BuiltinName; +use crate::stdlib::vec::Vec; +use crate::types::relocatable::MaybeRelocatable; +use crate::vm::runners::cairo_pie::StrippedProgram; + +type HashFunction = fn(&FieldElement, &FieldElement) -> FieldElement; + +#[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, + + /// Conversion from Felt252 to FieldElement failed. This is unlikely to happen + /// unless the implementation of Felt252 changes and this code is not updated properly. + #[error("Conversion from Felt252 to FieldElement failed")] + Felt252ToFieldElementConversionFailed, +} + +/// Computes a hash chain over the data, in the following order: +/// h(data[0], h(data[1], h(..., h(data[n-2], data[n-1])))). +/// +/// Reimplements this Python function: +/// def compute_hash_chain(data, hash_func=pedersen_hash): +/// assert len(data) >= 1, f"len(data) for hash chain computation must be >= 1; got: {len(data)}." +/// return functools.reduce(lambda x, y: hash_func(y, x), data[::-1]) +fn compute_hash_chain<'a, I>( + data: I, + hash_func: HashFunction, +) -> Result +where + I: Iterator + DoubleEndedIterator, +{ + match data.copied().rev().reduce(|x, y| hash_func(&y, &x)) { + Some(result) => Ok(result), + None => Err(HashChainError::EmptyData), + } +} + +/// Creates an instance of `FieldElement` from a builtin name. +/// +/// Converts the builtin name to bytes then attempts to create a field element from +/// these bytes. This function will fail if the builtin name is over 31 characters. +fn builtin_to_field_element(builtin: &BuiltinName) -> Result { + // The Python implementation uses the builtin name without suffix + let builtin_name = builtin + .name() + .strip_suffix("_builtin") + .unwrap_or(builtin.name()); + + FieldElement::from_byte_slice_be(builtin_name.as_bytes()) + .map_err(|_| ProgramHashError::InvalidProgramBuiltin(builtin.name())) +} + +/// The `value: FieldElement` is `pub(crate)` and there is no accessor. +/// This function converts a `Felt252` to a `FieldElement` using a safe, albeit inefficient, +/// method. +fn felt_to_field_element(felt: &Felt252) -> Result { + let bytes = felt.to_bytes_be(); + FieldElement::from_bytes_be(&bytes) + .map_err(|_e| ProgramHashError::Felt252ToFieldElementConversionFailed) +} + +/// Converts a `MaybeRelocatable` into a `FieldElement` value. +/// +/// Returns `InvalidProgramData` if `maybe_relocatable` is not an integer +fn maybe_relocatable_to_field_element( + maybe_relocatable: &MaybeRelocatable, +) -> Result { + let felt = maybe_relocatable + .get_int_ref() + .ok_or(ProgramHashError::InvalidProgramData)?; + felt_to_field_element(felt) +} + +/// Computes the Pedersen hash of a program. +/// +/// Reimplements this Python function: +/// def compute_program_hash_chain(program: ProgramBase, bootloader_version=0): +/// builtin_list = [from_bytes(builtin.encode("ascii")) for builtin in program.builtins] +/// # The program header below is missing the data length, which is later added to the data_chain. +/// program_header = [bootloader_version, program.main, len(program.builtins)] + builtin_list +/// data_chain = program_header + program.data +/// +/// return compute_hash_chain([len(data_chain)] + data_chain) +pub fn compute_program_hash_chain( + program: &StrippedProgram, + bootloader_version: usize, +) -> Result { + let program_main = program.main; + let program_main = FieldElement::from(program_main); + + // Convert builtin names to field elements + let builtin_list: Result, _> = program + .builtins + .iter() + .map(builtin_to_field_element) + .collect(); + let builtin_list = builtin_list?; + + let program_header = vec![ + FieldElement::from(bootloader_version), + program_main, + FieldElement::from(program.builtins.len()), + ]; + + let program_data: Result, _> = program + .data + .iter() + .map(maybe_relocatable_to_field_element) + .collect(); + let program_data = program_data?; + + let data_chain_len = program_header.len() + builtin_list.len() + program_data.len(); + let data_chain_len_vec = vec![FieldElement::from(data_chain_len)]; + + // Prepare a chain of iterators to feed to the hash function + let data_chain = [ + &data_chain_len_vec, + &program_header, + &builtin_list, + &program_data, + ]; + + let hash = compute_hash_chain(data_chain.iter().flat_map(|&v| v.iter()), pedersen_hash)?; + Ok(hash) +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "std")] + use {crate::types::program::Program, rstest::rstest, std::path::PathBuf}; + + use starknet_crypto::pedersen_hash; + + use super::*; + + #[test] + fn test_compute_hash_chain() { + let data: Vec = vec![ + FieldElement::from(1u64), + FieldElement::from(2u64), + FieldElement::from(3u64), + ]; + let expected_hash = pedersen_hash( + &FieldElement::from(1u64), + &pedersen_hash(&FieldElement::from(2u64), &FieldElement::from(3u64)), + ); + let computed_hash = compute_hash_chain(data.iter(), pedersen_hash) + .expect("Hash computation failed unexpectedly"); + + assert_eq!(computed_hash, expected_hash); + } + + #[cfg(feature = "std")] + #[rstest] + // Expected hashes generated with `cairo-hash-program` + #[case::fibonacci( + "../cairo_programs/fibonacci.json", + "0x43b17e9592f33142246af4c06cd2b574b460dd1f718d76b51341175a62b220f" + )] + #[case::field_arithmetic( + "../cairo_programs/field_arithmetic.json", + "0x1031772ca86e618b058101af9c9a3277bac90712b750bcea1cc69d6c7cad8a7" + )] + #[case::keccak_copy_inputs( + "../cairo_programs/keccak_copy_inputs.json", + "0x49484fdc8e7a85061f9f21b7e21fe276d8a88c8e96681101a2518809e686c6c" + )] + 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) + .expect("Failed to compute program hash."); + + let program_hash_hex = format!("{:#x}", program_hash); + + assert_eq!(program_hash_hex, expected_program_hash); + } +}