use std::fs;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use apollo_infra_utils::compile_time_cargo_manifest_dir;
use apollo_infra_utils::path::project_path;
use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass;
use digest::Digest;
use sha2::Sha256;
use starknet_api::contract_class::compiled_class_hash::{HashVersion, HashableCompiledClass};
use starknet_api::core::CompiledClassHash;
use starknet_api::felt;
use crate::cairo_compile::{
cairo1_compile,
lock_path_for,
verify_cairo1_package,
with_file_lock,
CompilationArtifacts,
LibfuncArg,
};
use crate::cairo_versions::{CairoVersion, RunnableCairo1};
use crate::contracts::FeatureContract;
static CACHE_DIR: LazyLock<PathBuf> =
LazyLock::new(|| project_path().unwrap().join("target/blockifier_test_artifacts"));
pub fn cached_compiled_path(contract: &FeatureContract) -> PathBuf {
CACHE_DIR.join(format!("cairo1/compiled/{}.casm.json", contract.get_non_erc20_base_name()))
}
pub fn cached_sierra_path(contract: &FeatureContract) -> PathBuf {
CACHE_DIR.join(format!("cairo1/sierra/{}.sierra.json", contract.get_non_erc20_base_name()))
}
fn cache_key_path(contract: &FeatureContract) -> PathBuf {
CACHE_DIR.join(format!("{}.hash", contract.get_base_name()))
}
fn cached_compiled_class_hash_path(
contract: &FeatureContract,
hash_version: &HashVersion,
) -> PathBuf {
let suffix = match hash_version {
HashVersion::V1 => "v1",
HashVersion::V2 => "v2",
};
CACHE_DIR.join(format!("cairo1/compiled_class_hashes/{}.{suffix}", contract.get_base_name()))
}
fn compute_and_write_compiled_class_hashes(contract: &FeatureContract, casm_json: &[u8]) {
let casm: CasmContractClass = serde_json::from_slice(casm_json)
.unwrap_or_else(|e| panic!("Failed to deserialize CASM for {contract:?}: {e}"));
for version in [HashVersion::V1, HashVersion::V2] {
let hash_hex = format!("0x{:x}", casm.hash(&version).0);
write_artifact(&cached_compiled_class_hash_path(contract, &version), hash_hex.as_bytes());
}
}
pub fn read_cached_compiled_class_hash(
contract: &FeatureContract,
hash_version: &HashVersion,
) -> CompiledClassHash {
let path = cached_compiled_class_hash_path(contract, hash_version);
let hex = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to read cached class hash from {path:?}: {e}"));
CompiledClassHash(felt!(hex.trim()))
}
fn compute_cache_key(
source_path: &Path,
compiler_version: &str,
crate_root: &Path,
libfunc_arg: &LibfuncArg,
) -> String {
let source_content = fs::read_to_string(source_path)
.unwrap_or_else(|e| panic!("Cannot read {source_path:?}: {e}"));
let mut hasher = Sha256::new();
hasher.update(source_content.as_bytes());
hasher.update(b"\x00");
hasher.update(compiler_version.as_bytes());
hasher.update(b"\x00");
match libfunc_arg {
LibfuncArg::ListFile(file) => {
let abs = crate_root.join(file);
let content = fs::read_to_string(&abs)
.unwrap_or_else(|e| panic!("Cannot read libfunc file {abs:?}: {e}"));
hasher.update(content.as_bytes());
}
LibfuncArg::ListName(name) => {
hasher.update(b"list-name:");
hasher.update(name.as_bytes());
}
}
format!("{:x}", hasher.finalize())
}
fn is_cache_fresh(contract: &FeatureContract, expected_hash: &str) -> bool {
cached_compiled_class_hash_path(contract, &HashVersion::V1).exists()
&& cached_compiled_class_hash_path(contract, &HashVersion::V2).exists()
&& fs::read_to_string(cache_key_path(contract))
.is_ok_and(|stored_hash| stored_hash.trim() == expected_hash)
}
fn write_artifact(path: &Path, content: &[u8]) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.unwrap_or_else(|e| panic!("Failed to create directory {parent:?}: {e}"));
}
fs::write(path, content).unwrap_or_else(|e| panic!("Failed to write {path:?}: {e}"));
}
fn lock_file_path(contract: &FeatureContract) -> PathBuf {
lock_path_for(&CACHE_DIR.join(contract.get_base_name()))
}
pub fn ensure_cairo1_compiled(contract: &FeatureContract) {
assert!(
!matches!(contract, FeatureContract::ERC20(_)),
"ERC20 uses committed artifacts, not the compilation cache."
);
assert!(
!contract.cairo_version().is_cairo0(),
"Cairo 0 contracts use committed artifacts, not the compilation cache."
);
let casm_path = cached_compiled_path(contract);
let sierra_path = cached_sierra_path(contract);
let version = contract.fixed_version();
let crate_root = PathBuf::from(compile_time_cargo_manifest_dir!());
let source_path = crate_root.join(contract.get_source_path());
let libfunc_arg = contract.libfunc_arg();
let abs_libfunc_arg = match &libfunc_arg {
LibfuncArg::ListFile(file) => {
LibfuncArg::ListFile(crate_root.join(file).to_string_lossy().to_string())
}
LibfuncArg::ListName(name) => LibfuncArg::ListName(name.clone()),
};
let cache_key = compute_cache_key(&source_path, &version, &crate_root, &libfunc_arg);
let is_fresh = || is_cache_fresh(contract, &cache_key);
with_file_lock(&lock_file_path(contract), is_fresh, || {
let start = std::time::Instant::now();
eprintln!(
"[compile_cache] Cache miss for {contract:?} (compiler v{version}), compiling from \
source..."
);
verify_cairo1_package(&version);
let CompilationArtifacts::Cairo1 { casm, sierra } =
cairo1_compile(source_path.to_string_lossy().to_string(), version, abs_libfunc_arg)
else {
unreachable!("cairo1_compile always returns Cairo1 variant");
};
write_artifact(&casm_path, &casm);
write_artifact(&sierra_path, &sierra);
compute_and_write_compiled_class_hashes(contract, &casm);
write_artifact(&cache_key_path(contract), cache_key.as_bytes());
eprintln!(
"[compile_cache] Compiled and cached {contract:?} in {:.1}s",
start.elapsed().as_secs_f64()
);
});
}
pub fn ensure_erc20_compiled_class_hashes() {
let contract = FeatureContract::ERC20(CairoVersion::Cairo1(RunnableCairo1::Casm));
let crate_root = PathBuf::from(compile_time_cargo_manifest_dir!());
let casm_abs = crate_root.join(contract.get_compiled_path());
let casm_content = fs::read_to_string(&casm_abs)
.unwrap_or_else(|e| panic!("Cannot read ERC20 CASM at {casm_abs:?}: {e}"));
let mut hasher = Sha256::new();
hasher.update(casm_content.as_bytes());
let content_hash = format!("{:x}", hasher.finalize());
with_file_lock(
&lock_file_path(&contract),
|| is_cache_fresh(&contract, &content_hash),
|| {
compute_and_write_compiled_class_hashes(&contract, casm_content.as_bytes());
write_artifact(&cache_key_path(&contract), content_hash.as_bytes());
},
);
}