Skip to content

Commit

Permalink
Feature: compute program hash chain (#1647)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
odesenfans authored Apr 5, 2024
1 parent 4777afd commit c692b75
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions vm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
204 changes: 204 additions & 0 deletions vm/src/program_hash.rs
Original file line number Diff line number Diff line change
@@ -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<FieldElement, HashChainError>
where
I: Iterator<Item = &'a FieldElement> + 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<FieldElement, ProgramHashError> {
// 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<FieldElement, ProgramHashError> {
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<FieldElement, ProgramHashError> {
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<FieldElement, ProgramHashError> {
let program_main = program.main;
let program_main = FieldElement::from(program_main);

// Convert builtin names to field elements
let builtin_list: Result<Vec<FieldElement>, _> = 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<Vec<_>, _> = 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<FieldElement> = 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);
}
}

0 comments on commit c692b75

Please sign in to comment.