Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: compute program hash chain #1647

Merged
merged 2 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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)]

Check warning on line 12 in vm/src/program_hash.rs

View check run for this annotation

Codecov / codecov/patch

vm/src/program_hash.rs#L12

Added line #L12 was not covered by tests
pub enum HashChainError {
#[error("Data array must contain at least one element.")]
EmptyData,
}

#[derive(thiserror_no_std::Error, Debug)]

Check warning on line 18 in vm/src/program_hash.rs

View check run for this annotation

Codecov / codecov/patch

vm/src/program_hash.rs#L18

Added line #L18 was not covered by tests
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),

Check warning on line 53 in vm/src/program_hash.rs

View check run for this annotation

Codecov / codecov/patch

vm/src/program_hash.rs#L53

Added line #L53 was not covered by tests
}
}

/// 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);
}
}
Loading