Skip to content
Draft
46 changes: 45 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
extend-exclude = [
# exclude submodule
"contracts/l1-l2-messaging/solidity",
"crates/**/test_data"
"crates/**/test_data",
"crates/starknet-devnet-types/precompiled/inputs"
]
ignore-vcs = true

Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This can be minified

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How does removing debug data affect behavior on error and the available stack trace?

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions crates/starknet-devnet-core/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,24 @@ impl CustomStateReader for StarknetState {
}
}

fn map_serde_hash_to_casm_hash(serde_hash: &str) -> Option<Felt> {
match serde_hash {
"32b9d9bb859c02ba9e82dbdab077d2834b15b9729c340060471c7f0371b63e8b" => {
Felt::from_hex("0x4fee83e07dd76f7977c32368a749fee076ca63fc53ccac796120dab24c87209").ok()
}
"66acd524a8274a36718f71cdab07dded32e4c2d655e74646e36c745ad7927711" => {
Felt::from_hex("0x4b6c84e0947405bbf16296d90f6be9731cc5927fcaa13707f7a3fd89bd2852d").ok()
}
"fa71f97808a5cd6f6ab674c8f03a4a79af667f1f92683d93637b4278ad15278a" => {
Felt::from_hex("0x49df345d52ac50b88821baa4490c820cf285f09a89e5e366236681a1af8c324").ok()
}
"489acacf8c8b6c932ab243f5ef7063402b49d9d727252833fd8ba457e715475c" => {
Felt::from_hex("0x266f53b3f6cc2367c334b75ea86aff748ca27aa321019778af81be69d549159").ok()
}
_ => None,
}
}

impl CustomState for StarknetState {
/// writes directly to the most underlying state, skipping cache
fn predeclare_contract_class(
Expand All @@ -367,8 +385,13 @@ impl CustomState for StarknetState {
let class_hash = starknet_api::core::ClassHash(class_hash);

if let ContractClass::Cairo1(cairo_lang_contract_class) = &contract_class {
let casm_hash =
compile_sierra_contract(cairo_lang_contract_class)?.compiled_class_hash();
let casm_hash = match serde_json::to_value(cairo_lang_contract_class)
.and_then(|v| starknet_types::canonical_serde_hash(&v))
.map(|h| map_serde_hash_to_casm_hash(&h))
{
Ok(Some(hash)) => hash,
_ => compile_sierra_contract(cairo_lang_contract_class)?.compiled_class_hash(),
};

self.state.state.set_compiled_class_hash(
class_hash,
Expand Down
10 changes: 10 additions & 0 deletions crates/starknet-devnet-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ license-file.workspace = true

[features]
testing = []
fastpath = []

[lints]
workspace = true

[build-dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
blake3 = "1.5"
usc = { workspace = true }
serde_json_canonicalizer = "0.3.1"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be in the root Cargo.toml? Applies to L26 and L61.


[dependencies]
blake3 = "1.5"
base64 = { workspace = true }
blockifier = { workspace = true }
cairo-lang-starknet-classes = { workspace = true }
Expand Down Expand Up @@ -49,6 +58,7 @@ cairo-lang-sierra-generator = { workspace = true }
cairo-lang-sierra-to-casm = { workspace = true }
cairo-lang-syntax = { workspace = true }
cairo-lang-utils = { workspace = true }
serde_json_canonicalizer = "0.3.1"

[dev-dependencies]

Expand Down
83 changes: 83 additions & 0 deletions crates/starknet-devnet-types/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// build.rs
use std::path::PathBuf;
use std::{env, fs};

use serde_json::Value;
use serde_json_canonicalizer::to_vec as canonical_to_vec;

fn compile_at_build_time(input: &Value) -> serde_json::Value {
usc::compile_contract(input.clone())
.unwrap_or_else(|e| panic!("usc::compile_contract failed in build.rs: {e}"))
}

#[allow(clippy::expect_used)]
fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("Failed to get manifest directory"));
let input_dir = manifest_dir.join("precompiled").join("inputs");

// let fastpath_enabled = env::var("CARGO_FEATURE_FASTPATH").is_ok();

let out_dir = PathBuf::from(env::var("OUT_DIR").expect("Failed to get output directory"));
let gen_path = out_dir.join("usc_fastpath.rs");

// if !fastpath_enabled {
// // feature disabled → emit a stub
// fs::write(&gen_path, "pub fn lookup(_: &str) -> Option<&'static [u8]> { None }\n")
// .unwrap();
// return;
// }

let mut json_files = vec![];
if input_dir.exists() {
for e in fs::read_dir(&input_dir).expect("Failed to read precompiled inputs directory") {
let p = e.expect("Failed to read directory entry").path();
if p.extension().and_then(|s| s.to_str()) == Some("json") {
json_files.push(p);
}
}
json_files.sort();
}

if json_files.is_empty() {
fs::write(&gen_path, "pub fn lookup(_: &str) -> Option<&'static [u8]> { None }\n")
.expect("Failed to write empty lookup function");
println!("cargo:rerun-if-changed=precompiled/inputs");
println!("cargo:rerun-if-changed=build.rs");
return;
}

let mut code = String::from("// @generated — DO NOT EDIT\n");

// Emit OUTPUT_* and DIGEST_* constants
for (idx, path) in json_files.iter().enumerate() {
let raw = fs::read(path).expect("Failed to read JSON file");
let input_val: Value = serde_json::from_slice(&raw).expect("Failed to parse JSON file");

let canon_bytes = canonical_to_vec(&input_val).expect("Failed to canonicalize JSON");

// Use blake3 for hashing instead of sha256 and convert to hex without using hex crate
let hash = blake3::hash(&canon_bytes).to_string();

let casm_json = compile_at_build_time(&input_val);
let casm_bytes =
serde_json::to_vec(&casm_json).expect("Failed to serialize compiled contract to JSON");

code.push_str(&format!("pub static OUTPUT_{idx}: &[u8] = &{:?};\n", casm_bytes));
code.push_str(&format!("pub const DIGEST_{idx}: &str = \"{hash}\";\n"));
}

// Emit lookup()
code.push_str(
"\npub fn lookup(hash_hex: &str) -> Option<&'static [u8]> {\n match hash_hex {\n",
);
for idx in 0..json_files.len() {
code.push_str(&format!(" DIGEST_{idx} => Some(OUTPUT_{idx}),\n"));
}
code.push_str(" _ => None,\n }\n}\n");

fs::write(&gen_path, code).expect("Failed to write generated code to output file");

println!("cargo:rerun-if-changed=precompiled/inputs");
println!("cargo:rerun-if-changed=build.rs");
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/starknet-devnet-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ mod utils;

// Re export libraries
pub use rpc::{contract_address, contract_class, emitted_event, felt, messaging};
pub use utils::{compile_sierra_contract, compile_sierra_contract_json};
pub use utils::{canonical_serde_hash, compile_sierra_contract, compile_sierra_contract_json};
pub use {num_bigint, starknet_api};
24 changes: 18 additions & 6 deletions crates/starknet-devnet-types/src/rpc/contract_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ use starknet_types_core::felt::Felt;
use crate::error::{ConversionError, DevnetResult, Error, JsonError};
use crate::serde_helpers::rpc_sierra_contract_class_to_sierra_contract_class::deserialize_to_sierra_contract_class;
use crate::traits::HashProducer;
use crate::utils::compile_sierra_contract;
use crate::utils::{canonical_serde_hash, compile_sierra_contract};

pub mod deprecated;
pub use deprecated::Cairo0ContractClass;

mod usc_fastpath {
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/usc_fastpath.rs"));
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "testing", derive(Eq, PartialEq))]
#[allow(clippy::large_enum_variant)]
Expand Down Expand Up @@ -309,11 +314,18 @@ fn jsonified_sierra_to_runnable_casm(
jsonified_sierra: serde_json::Value,
sierra_version: &str,
) -> Result<RunnableCompiledClass, Error> {
let casm_json = usc::compile_contract(jsonified_sierra)
.map_err(|err| Error::SierraCompilationError { reason: err.to_string() })?;

let casm = serde_json::from_value::<CasmContractClass>(casm_json)
.map_err(|err| Error::JsonError(JsonError::Custom { msg: err.to_string() }))?;
let hash = canonical_serde_hash(&jsonified_sierra);

let casm = match hash.map(|h| usc_fastpath::lookup(&h)) {
Ok(Some(bytes)) => serde_json::from_slice::<CasmContractClass>(bytes)
.map_err(|err| Error::JsonError(JsonError::SerdeJsonError(err))),
_ => {
let casm_json_value = usc::compile_contract(jsonified_sierra)
.map_err(|err| Error::SierraCompilationError { reason: err.to_string() })?;
serde_json::from_value::<CasmContractClass>(casm_json_value)
.map_err(|err| Error::JsonError(JsonError::Custom { msg: err.to_string() }))
}
}?;

let versioned_casm = (casm, SierraVersion::from_str(sierra_version)?);
let compiled = versioned_casm.try_into().map_err(|e: ProgramError| {
Expand Down
25 changes: 24 additions & 1 deletion crates/starknet-devnet-types/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ use serde_json::ser::Formatter;
use serde_json::{Map, Value};

use crate::error::{DevnetResult, Error, JsonError};
#[cfg(not(clippy))]
mod usc_fastpath {
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/usc_fastpath.rs"));
}

/// The preserve_order feature enabled in the serde_json crate
/// removing a key from the object changes the order of the keys
Expand Down Expand Up @@ -87,6 +92,16 @@ impl Formatter for StarknetFormatter {
}
}

pub fn canonical_serde_hash(v: &serde_json::Value) -> Result<String, serde_json::Error> {
let bytes = match serde_json_canonicalizer::to_vec(v) {
Ok(bytes) => bytes,
Err(_) => serde_json::to_vec(v)?,
};

let hash = blake3::hash(&bytes).to_string();
Ok(hash)
}

pub fn compile_sierra_contract(sierra_contract: &ContractClass) -> DevnetResult<CasmContractClass> {
let sierra_contract_json = serde_json::to_value(sierra_contract)
.map_err(|err| Error::JsonError(JsonError::SerdeJsonError(err)))?;
Expand All @@ -95,8 +110,16 @@ pub fn compile_sierra_contract(sierra_contract: &ContractClass) -> DevnetResult<
}

pub fn compile_sierra_contract_json(
sierra_contract_json: serde_json::Value,
sierra_contract_json: Value,
) -> DevnetResult<CasmContractClass> {
#[cfg(not(clippy))]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why was this line necessary?

if let Ok(Some(bytes)) =
canonical_serde_hash(&sierra_contract_json).map(|h| usc_fastpath::lookup(&h))
{
return serde_json::from_slice::<CasmContractClass>(bytes)
.map_err(|err| Error::JsonError(JsonError::SerdeJsonError(err)));
}

let casm_json = usc::compile_contract(sierra_contract_json)
.map_err(|err| Error::SierraCompilationError { reason: err.to_string() })?;

Expand Down