Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Fix sierra class hash calculation #886

Merged
merged 7 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ once_cell = "1.17.1"
starknet = { workspace = true }
base64 = { version = "0.21.0", default-features = false, features = ["alloc"] }
flate2 = "1.0.25"
serde_json_pythonic = { git = "https://github.com/xJonathanLEI/serde_json_pythonic", tag = "v0.1.2"}

[dev-dependencies]
assert_matches = "1.5.0"
Expand Down
18 changes: 18 additions & 0 deletions src/core/contract_address/casm_contract_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,22 @@ mod tests {
expected_result
);
}

#[test]
fn test_declare_tx_class_hash() {
let file = File::open("starknet_programs/cairo2/declare_tx_from_testnet.casm").unwrap();
let reader = BufReader::new(file);

let contract_class: CasmContractClass = serde_json::from_reader(reader).unwrap();

// this is the compiled_class_hash from: https://alpha4.starknet.io/feeder_gateway/get_transaction?transactionHash=0x01b852f1fe2b13db21a44f8884bc4b7760dc277bb3820b970dba929860275617
let expected_result = felt_str!(
"2011836827876139258613930428521012424481847645471980617287552173098289225455"
);

assert_eq!(
compute_casm_class_hash(&contract_class).unwrap(),
expected_result
)
}
}
138 changes: 77 additions & 61 deletions src/core/contract_address/sierra_contract_address.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use crate::core::errors::contract_address_errors::ContractAddressError;
use crate::services::api::contract_classes::deprecated_contract_class::{
ContractEntryPoint, EntryPointType,
};
use crate::{core::errors::contract_address_errors::ContractAddressError, EntryPointType};
use cairo_lang_starknet::{
contract::starknet_keccak, contract_class::ContractClass as SierraContractClass,
contract::starknet_keccak,
contract_class::{ContractClass as SierraContractClass, ContractEntryPoint},
};
use cairo_vm::felt::Felt252;
use starknet_crypto::{poseidon_hash_many, FieldElement};
use starknet_crypto::{poseidon_hash_many, FieldElement, PoseidonHasher};

const CONTRACT_CLASS_VERSION: &[u8] = b"CONTRACT_CLASS_V0.1.0";

Expand All @@ -19,30 +17,33 @@ fn get_contract_entry_points_hashed(
entry_point_type: &EntryPointType,
) -> Result<FieldElement, ContractAddressError> {
let contract_entry_points = get_contract_entry_points(contract_class, entry_point_type)?;

// for each entry_point, we need to store 2 FieldElements: selector and offset.
let mut entry_points_flatted = Vec::with_capacity(contract_entry_points.len() * 2);
let mut hasher = PoseidonHasher::new();

for entry_point in contract_entry_points {
entry_points_flatted.push(
FieldElement::from_bytes_be(&entry_point.selector().to_be_bytes()).map_err(|_err| {
ContractAddressError::Cast("Felt252".to_string(), "FieldElement".to_string())
})?,
);
entry_points_flatted.push(FieldElement::from(entry_point.offset()));
let selector =
FieldElement::from_dec_str(&entry_point.selector.to_str_radix(10)).map_err(|_err| {
ContractAddressError::Cast("String".to_string(), "FieldElement".to_string())
})?;
let function_idx = FieldElement::from(entry_point.function_idx);

hasher.update(selector);
hasher.update(function_idx);
}

Ok(poseidon_hash_many(&entry_points_flatted))
Ok(hasher.finalize())
}

pub fn compute_sierra_class_hash(
contract_class: &SierraContractClass,
) -> Result<Felt252, ContractAddressError> {
let api_version =
FieldElement::from_bytes_be(&Felt252::from_bytes_be(CONTRACT_CLASS_VERSION).to_be_bytes())
.map_err(|_err| {
ContractAddressError::Cast("Felt252".to_string(), "FieldElement".to_string())
})?;
let mut hasher = PoseidonHasher::new();

// hash the API version
let api_version = FieldElement::from_byte_slice_be(CONTRACT_CLASS_VERSION).map_err(|_err| {
ContractAddressError::Cast("&[u8]".to_string(), "FieldElement".to_string())
})?;

hasher.update(api_version);

// Entrypoints by type, hashed.
let external_functions =
Expand All @@ -51,44 +52,42 @@ pub fn compute_sierra_class_hash(
let constructors =
get_contract_entry_points_hashed(contract_class, &EntryPointType::Constructor)?;

// hash the entrypoint hashes
hasher.update(external_functions);
hasher.update(l1_handlers);
hasher.update(constructors);

// Hash abi_hash.
let abi = contract_class
.abi
.clone()
.ok_or(ContractAddressError::MissingAbi)?
.json();

let abi_hash =
FieldElement::from_bytes_be(&Felt252::from(starknet_keccak(abi.as_bytes())).to_be_bytes())
.map_err(|_err| {
ContractAddressError::Cast("Felt252".to_string(), "FieldElement".to_string())
})?;
let abi = serde_json_pythonic::to_string_pythonic(
&contract_class
.abi
.clone()
.ok_or(ContractAddressError::MissingAbi)?
.items,
)
.unwrap();

let abi_hash = FieldElement::from_byte_slice_be(&starknet_keccak(abi.as_bytes()).to_bytes_be())
.map_err(|_err| {
ContractAddressError::Cast("&[u8]".to_string(), "FieldElement".to_string())
})?;

hasher.update(abi_hash);

let mut sierra_program_vector = Vec::with_capacity(contract_class.sierra_program.len());
for number in &contract_class.sierra_program {
sierra_program_vector.push(
FieldElement::from_bytes_be(&Felt252::from(number.value.clone()).to_be_bytes())
.map_err(|_err| {
ContractAddressError::Cast("Felt252".to_string(), "FieldElement".to_string())
})?,
);
let fe = FieldElement::from_dec_str(&number.value.to_str_radix(10)).map_err(|_err| {
ContractAddressError::Cast("String".to_string(), "FieldElement".to_string())
})?;
sierra_program_vector.push(fe);
}

// Hash Sierra program.
let sierra_program_ptr = poseidon_hash_many(&sierra_program_vector);

let flatted_contract_class = vec![
api_version,
external_functions,
l1_handlers,
constructors,
abi_hash,
sierra_program_ptr,
];

Ok(Felt252::from_bytes_be(
&poseidon_hash_many(&flatted_contract_class).to_bytes_be(),
))
hasher.update(sierra_program_ptr);
let hash = hasher.finalize();
Ok(Felt252::from_bytes_be(&hash.to_bytes_be()))
}

fn get_contract_entry_points(
Expand All @@ -103,22 +102,39 @@ fn get_contract_entry_points(
EntryPointType::L1Handler => contract_class.entry_points_by_type.l1_handler.clone(),
};

let program_len = program_length;
for entry_point in &entry_points {
if entry_point.function_idx > program_len {
if entry_point.function_idx > program_length {
return Err(ContractAddressError::InvalidOffset(
entry_point.function_idx,
));
}
}

Ok(entry_points
.iter()
.map(|entry_point| {
ContractEntryPoint::new(
entry_point.selector.clone().into(),
entry_point.function_idx,
)
})
.collect())
Ok(entry_points)
}

#[cfg(test)]
mod tests {
use crate::core::contract_address::compute_sierra_class_hash;
use cairo_lang_starknet::contract_class::ContractClass as SierraContractClass;
use cairo_vm::felt::felt_str;
use std::{fs::File, io::BufReader};

#[test]
fn test_declare_tx_from_testnet() {
let file = File::open("starknet_programs/cairo2/declare_tx_from_testnet.sierra").unwrap();
let reader = BufReader::new(file);

let sierra_contract_class: SierraContractClass = serde_json::from_reader(reader).unwrap();

// this is the class_hash from: https://alpha4.starknet.io/feeder_gateway/get_transaction?transactionHash=0x01b852f1fe2b13db21a44f8884bc4b7760dc277bb3820b970dba929860275617
let expected_result = felt_str!(
"487202222862199115032202787294865701687663153957776561394399544814644144883"
);

assert_eq!(
compute_sierra_class_hash(&sierra_contract_class).unwrap(),
expected_result
)
}
}
57 changes: 57 additions & 0 deletions starknet_programs/cairo2/events.cairo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the contract used in the test, but its name was only changed in one of the two places. @juanbono

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use core::debug::PrintTrait;
use core::traits::Into;
use core::result::ResultTrait;
use starknet::syscalls::{deploy_syscall, get_block_hash_syscall};
use traits::TryInto;
use option::OptionTrait;
use starknet::SyscallResultTrait;
use starknet::class_hash::Felt252TryIntoClassHash;
use array::ArrayTrait;
use array::SpanTrait;

#[starknet::interface]
trait IContractWithEvent<T> {
fn emit_event(ref self: T, incremental: bool);
}

#[starknet::contract]
mod ContractWithEvent {
use traits::Into;
use starknet::info::get_contract_address;
#[storage]
struct Storage {
value: u128,
}

#[derive(Copy, Drop, PartialEq, starknet::Event)]
struct IncrementalEvent {
value: u128,
}

#[derive(Copy, Drop, PartialEq, starknet::Event)]
struct StaticEvent {}

#[event]
#[derive(Copy, Drop, PartialEq, starknet::Event)]
enum Event {
IncrementalEvent: IncrementalEvent,
StaticEvent: StaticEvent,
}

#[constructor]
fn constructor(ref self: ContractState) {
self.value.write(0);
}

#[external(v0)]
fn emit_event(ref self: ContractState, incremental: bool) {
if incremental {
self.emit(Event::IncrementalEvent(IncrementalEvent { value: self.value.read() }));
self.value.write(self.value.read() + 1);
} else {
self.emit(Event::StaticEvent(StaticEvent {}));
}
}
}

use ContractWithEvent::{Event, IncrementalEvent, StaticEvent};