Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

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

42 changes: 42 additions & 0 deletions crates/cairo-lang-starknet-classes/src/contract_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use starknet_types_core::felt::Felt as Felt252;
use starknet_types_core::hash::{Poseidon, StarkHash};
use thiserror::Error;

use crate::abi::Contract;
Expand All @@ -13,6 +14,7 @@ use crate::compiler_version::{VersionId, current_compiler_version_id, current_si
use crate::felt252_serde::{
Felt252SerdeError, sierra_from_felt252s, sierra_to_felt252s, version_id_from_felt252s,
};
use crate::keccak::starknet_keccak;

#[cfg(test)]
#[path = "contract_class_test.rs"]
Expand Down Expand Up @@ -79,6 +81,42 @@ impl ContractClass {
Ok(ExtractedSierraProgram { program, sierra_version, compiler_version })
}

/// Computes the Starknet class hash of this (Sierra) contract class.
///
/// Mirrors the algorithm used downstream (the sequencer / `starknet_api`): a Poseidon hash
/// over the contract-class version marker, the three entry-point groups, the ABI hash, and the
/// Sierra program hash.
///
/// NOTE: this duplicates security-sensitive logic that normally lives outside the compiler. In
/// particular, the ABI is hashed via its canonical [`Contract::json`] string representation;
/// the exact serialization affects the resulting hash, so this implementation must be validated
/// against known class-hash reference vectors before being relied upon.
pub fn class_hash(&self) -> Felt252 {
let entry_points_hash = |entry_points: &[ContractEntryPoint]| {
let mut elements = vec![];
for entry_point in entry_points {
elements.push(Felt252::from(&entry_point.selector));
elements.push(Felt252::from(entry_point.function_idx));
}
Poseidon::hash_array(&elements)
};
let abi_hash = self
.abi
.as_ref()
.map_or(Felt252::ZERO, |abi| Felt252::from(&starknet_keccak(abi.json().as_bytes())));
let sierra_program_hash = Poseidon::hash_array(
&self.sierra_program.iter().map(|felt| Felt252::from(&felt.value)).collect::<Vec<_>>(),
);
Poseidon::hash_array(&[
Felt252::from_bytes_be_slice(CONTRACT_CLASS_VERSION_MARKER),
entry_points_hash(&self.entry_points_by_type.external),
entry_points_hash(&self.entry_points_by_type.l1_handler),
entry_points_hash(&self.entry_points_by_type.constructor),
abi_hash,
sierra_program_hash,
])
}

/// Sanity checks the contract class.
/// Currently only checks that if ABI exists, its counts match the entry points counts.
pub fn sanity_check(&self) {
Expand Down Expand Up @@ -135,6 +173,10 @@ impl ExtractedSierraProgram {

const DEFAULT_CONTRACT_CLASS_VERSION: &str = "0.1.0";

/// The version marker hashed into the Sierra contract class hash (see
/// [`ContractClass::class_hash`]).
const CONTRACT_CLASS_VERSION_MARKER: &[u8] = b"CONTRACT_CLASS_V0.1.0";

#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContractEntryPoints {
#[serde(rename = "EXTERNAL")]
Expand Down
21 changes: 21 additions & 0 deletions crates/cairo-lang-starknet-classes/src/contract_class_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::io::BufReader;
use indoc::indoc;
use num_bigint::BigUint;
use pretty_assertions::assert_eq;
use starknet_types_core::felt::Felt as Felt252;
use test_case::test_case;

use crate::contract_class::{
Expand Down Expand Up @@ -53,6 +54,26 @@ fn test_serialization() {
assert_eq!(contract, serde_json::from_str(&serialized).unwrap())
}

#[test]
fn test_class_hash_is_deterministic_and_sensitive() {
let contract_path = get_example_file_path("hello_starknet__hello_starknet.contract_class.json");
let contract: ContractClass =
serde_json::from_reader(BufReader::new(std::fs::File::open(contract_path).unwrap()))
.unwrap();

let class_hash = contract.class_hash();
// Deterministic.
assert_eq!(class_hash, contract.class_hash());
// Non-trivial.
assert_ne!(class_hash, Felt252::ZERO);

// Sensitive to the entry points: flipping an external entry point's function index changes the
// hash.
let mut modified = contract.clone();
modified.entry_points_by_type.external[0].function_idx += 1;
assert_ne!(modified.class_hash(), class_hash);
}

// Tests the serialization and deserialization of a contract.
#[test_case("test_contract__test_contract")]
#[test_case("hello_starknet__hello_starknet")]
Expand Down
1 change: 1 addition & 0 deletions crates/cairo-lang-starknet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const_format.workspace = true
indent.workspace = true
indoc.workspace = true
itertools = { workspace = true, default-features = true }
num-bigint = { workspace = true, default-features = true }
rayon.workspace = true
salsa.workspace = true
serde = { workspace = true, default-features = true }
Expand Down
42 changes: 42 additions & 0 deletions crates/cairo-lang-starknet/src/compile.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::{Context, Result};
use cairo_lang_compiler::db::RootDatabase;
use cairo_lang_compiler::diagnostics::DiagnosticsReporter;
use cairo_lang_compiler::project::setup_project;
use cairo_lang_compiler::{CompilerConfig, ensure_diagnostics};
use cairo_lang_defs::ids::TopLevelLanguageElementId;
Expand All @@ -10,6 +12,7 @@ use cairo_lang_filesystem::ids::{CrateId, CrateInput};
use cairo_lang_lowering::ids::ConcreteFunctionWithBodyId;
use cairo_lang_lowering::optimizations::config::Optimizations;
use cairo_lang_lowering::utils::InliningStrategy;
use cairo_lang_semantic::items::constant::ConstValueId;
use cairo_lang_sierra::debug_info::Annotations;
use cairo_lang_sierra_generator::canonical_id_replacer::CanonicalReplacer;
use cairo_lang_sierra_generator::db::SierraGenGroup;
Expand All @@ -22,8 +25,10 @@ use cairo_lang_starknet_classes::contract_class::{
};
use cairo_lang_utils::CloneableDatabase;
use itertools::{Itertools, chain};
use num_bigint::BigInt;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use salsa::Database;
use starknet_types_core::felt::Felt as Felt252;

use crate::abi::AbiBuilder;
use crate::aliased::Aliased;
Expand Down Expand Up @@ -57,10 +62,47 @@ pub fn compile_path(
let main_crate_inputs = setup_project(&mut db, path)?;
compiler_config.diagnostics_reporter =
compiler_config.diagnostics_reporter.with_crates(&main_crate_inputs);

// The generated `class_hash()` accessor injects the contract's own class hash at the Sierra
// generation stage via the reserved `__externally_provided_const__` extern. Since the hash is
// derived from the compiled program, we compute it in two passes: first with the extern stubbed
// to zero (to obtain the class hash), then with that hash injected. Embedding the hash changes
// the bytecode, so the final class's true hash differs from the injected value (a deliberate,
// documented approximation).
Comment on lines +69 to +71

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Well that's no good for us

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ignore this - it is not correct - as long as you set the data in the provider - by asking for a calculation it shouldn't matter at all. this is just an example in any case.

//
// `main_crate_ids` are re-derived after each provider change, as the crate ids are bound to the
// database and cannot be held across a mutation.
set_class_hash_provider(&mut db, Felt252::ZERO);
let stub_main_crate_ids = CrateInput::into_crate_ids(&db, main_crate_inputs.clone());
let stub_class = compile_contract_in_prepared_db(
&db,
contract_path,
stub_main_crate_ids,
CompilerConfig {
// Scope to the main crate (like the final pass) and stay silent: the real diagnostics
// are reported by the second pass.
diagnostics_reporter: DiagnosticsReporter::ignoring()
.with_crates(&main_crate_inputs)
.allow_warnings(),
..Default::default()
},
)?;
set_class_hash_provider(&mut db, stub_class.class_hash());

let main_crate_ids = CrateInput::into_crate_ids(&db, main_crate_inputs);
compile_contract_in_prepared_db(&db, contract_path, main_crate_ids, compiler_config)
}

/// Installs an [`cairo_lang_sierra_generator::db::ExternalConstProvider`] that resolves every call
/// to the reserved `__externally_provided_const__` extern function (used by the generated
/// `class_hash()` accessor) to `value`, as a constant of the call's declared return type.
fn set_class_hash_provider(db: &mut RootDatabase, value: Felt252) {
let value = BigInt::from(value.to_biguint());
db.set_external_const_provider(Some(Arc::new(move |db, _full_path, ty| {
Ok(ConstValueId::from_int(db, ty, &value))
})));
}

/// Runs Starknet contract compiler on the specified contract.
/// If no contract was specified, verify that there is only one.
/// Otherwise, returns an error.
Expand Down
29 changes: 29 additions & 0 deletions crates/cairo-lang-starknet/src/compile_test.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use cairo_lang_compiler::CompilerConfig;
use cairo_lang_lowering::utils::InliningStrategy;
use cairo_lang_starknet_classes::allowed_libfuncs::ListSelector;
use cairo_lang_test_utils::compare_contents_or_fix_with_path;
use test_case::test_case;

use crate::compile::compile_path;
use crate::test_utils::{get_example_file_path, get_test_contract};

/// Tests that the Sierra compiled from a contract in the contracts crate is the same as in
Expand Down Expand Up @@ -47,3 +50,29 @@ fn test_compile_path_from_contracts_crate(example_contract_path: &str) {
extracted.program.to_string(),
);
}

/// Tests the two-pass class-hash injection: the contract's external `get_class_hash` calls the
/// generated `__class_hash__::class_hash()`, making the reserved `__externally_provided_const__`
/// extern reachable. Compilation succeeds only because `compile_path` installs a provider for it
/// (computing the contract's own class hash); otherwise Sierra generation would fail. The result
/// is also deterministic across runs.
#[test]
fn class_hash_injection_compiles() {
let path = get_example_file_path("class_hash_test_contract.cairo");
let compile = || {
compile_path(
&path,
None,
CompilerConfig { replace_ids: true, ..Default::default() },
InliningStrategy::default(),
)
.unwrap()
};

let contract = compile();
assert_eq!(contract.entry_points_by_type.external.len(), 1);
contract.sanity_check();

// The injected class hash is deterministic, so recompilation yields an identical program.
assert_eq!(contract.sierra_program, compile().sierra_program);
}
2 changes: 2 additions & 0 deletions crates/cairo-lang-starknet/src/plugin/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use const_format::formatcp;
pub const EXTERNAL_MODULE: &str = "__external";
pub const L1_HANDLER_MODULE: &str = "__l1_handler";
pub const CONSTRUCTOR_MODULE: &str = "__constructor";
/// The hidden inner module holding the contract's own class hash accessor (`class_hash()`).
pub const CLASS_HASH_MODULE: &str = "__class_hash__";
pub const WRAPPER_PREFIX: &str = "__wrapper__";
pub const STORAGE_STRUCT_NAME: &str = "Storage";
pub const EVENT_TYPE_NAME: &str = "Event";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ use starknet::storage::Map as LegacyMap;
#[cfg(target: 'test')]
pub const TEST_CLASS_HASH: starknet::ClassHash = 0x512a6d7124a897370409ddf4a36e94192a8e84b6eb12aa805962db55776618.try_into().unwrap();

#[doc(hidden)]
pub mod __class_hash__ {
extern fn __externally_provided_const__() -> starknet::ClassHash nopanic;
pub fn class_hash() -> starknet::ClassHash {
__externally_provided_const__()
}
#[feature("forward-impl")]
pub impl ForwardingClassHashImpl<T> of starknet::ForwardingClassHash<T> {
fn class_hash(self: @T) -> starknet::ClassHash {
class_hash()
}
}
}

#[doc(hidden)]
#[implicit_precedence(core::pedersen::Pedersen, core::RangeCheck, core::integer::Bitwise, core::ec::EcOp, core::poseidon::Poseidon, core::SegmentArena, core::circuit::RangeCheck96, core::circuit::AddMod, core::circuit::MulMod, core::gas::GasBuiltin, System)]
fn __wrapper__get_something(mut data: core::array::Span::<felt252>) -> core::array::Span::<felt252> {
Expand Down Expand Up @@ -568,6 +582,20 @@ use starknet::storage::Map as LegacyMap;
#[cfg(target: 'test')]
pub const TEST_CLASS_HASH: starknet::ClassHash = 0x19e877991b74c1b921fadcea48b83b3d8a5a0e86737a0ad2db0c95d8768eaea.try_into().unwrap();

#[doc(hidden)]
pub mod __class_hash__ {
extern fn __externally_provided_const__() -> starknet::ClassHash nopanic;
pub fn class_hash() -> starknet::ClassHash {
__externally_provided_const__()
}
#[feature("forward-impl")]
pub impl ForwardingClassHashImpl<T> of starknet::ForwardingClassHash<T> {
fn class_hash(self: @T) -> starknet::ClassHash {
class_hash()
}
}
}


#[doc(hidden)]
pub mod __external {
Expand Down
Loading
Loading