From 51502c182dec1d3d475fa091869bb03b9a498868 Mon Sep 17 00:00:00 2001 From: Krisztian Kovacs Date: Mon, 5 Sep 2022 16:09:43 +0200 Subject: [PATCH] fix(class_hash): fix up named tuple types for references It turns out that the `cairo_type` fields under `identifiers` are not the only place we can have Cairo type definitions... References have a `value` that's a Cairo expression, which can also contain type definitions via `cast()`: https://cairo-lang.org/docs/reference/syntax.html#expressions This change applies the replace hack "fix" to these properties too, so that named tuples for pre-0.10 classes (without `compiler_version`) are hashed properly. --- crates/pathfinder/src/state/class_hash.rs | 101 +++++++++++++++++----- py/src/compute_class_hash.py | 6 +- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/crates/pathfinder/src/state/class_hash.rs b/crates/pathfinder/src/state/class_hash.rs index 9163f9aff3..d24c807e42 100644 --- a/crates/pathfinder/src/state/class_hash.rs +++ b/crates/pathfinder/src/state/class_hash.rs @@ -117,39 +117,75 @@ fn compute_class_hash0(mut contract_definition: json::ContractDefinition<'_>) -> Ok(()) })?; - /// Insert's a space ": " -> " : " for every tuple value in `cairo_type`. This is required by StarkNet 0.10 for - /// some obscure backwards compatibility reason for older contracts. - fn insert_space(object: &mut serde_json::Map) { + fn add_extra_space_to_cairo_named_tuples(value: &mut serde_json::Value) { + match value { + serde_json::Value::Array(v) => walk_array(v), + serde_json::Value::Object(m) => walk_map(m), + _ => {} + } + } + + fn walk_array(array: &mut [serde_json::Value]) { + for v in array.iter_mut() { + add_extra_space_to_cairo_named_tuples(v); + } + } + + fn walk_map(object: &mut serde_json::Map) { for (k, v) in object.iter_mut() { - if k == "cairo_type" { - // Check that the value is a tuple. - if let Some(tuple_inner) = v - .as_str() - .and_then(|v| v.strip_prefix('(').and_then(|v| v.strip_suffix(')'))) + match v { + serde_json::Value::String(s) => { + let new_value = add_extra_space_to_named_tuple_type_definition(k, s); + if new_value.as_ref() != s { + *v = serde_json::Value::String(new_value.into()); + } + } + _ => add_extra_space_to_cairo_named_tuples(v), + } + } + } + + fn add_extra_space_to_named_tuple_type_definition<'a>( + key: &str, + value: &'a str, + ) -> std::borrow::Cow<'a, str> { + use std::borrow::Cow::*; + match key { + "cairo_type" => { + if let Some(tuple_inner) = value.strip_prefix('(').and_then(|v| v.strip_suffix(')')) { - let new_val = format!( - "({})", - // FIXME: Replace this crude hack with something more intelligent. - // It is required because if we receive an already correct ` : `, we will still - // "repair" it to ` : ` which we then fix at the end. - tuple_inner.replace(": ", " : ").replace(" :", " :") - ); - *v = serde_json::Value::String(new_val); + let new_val = format!("({})", add_extra_space_before_colon(tuple_inner)); + Owned(new_val) + } else { + Borrowed(value) } - } else if let Some(inner_object) = v.as_object_mut() { - // `cairo_type` occurs at multiple levels in the object, so we must explore deeper. - insert_space(inner_object); } + "value" => { + if let Some(cast_inner) = value + .strip_prefix("[cast(") + .and_then(|v| v.strip_suffix(")]")) + { + let new_val = format!("[cast({})]", add_extra_space_before_colon(cast_inner)); + Owned(new_val) + } else { + Borrowed(value) + } + } + _ => Borrowed(value), } } + fn add_extra_space_before_colon(v: &str) -> String { + // This is required because if we receive an already correct ` : `, we will still + // "repair" it to ` : ` which we then fix at the end. + v.replace(": ", " : ").replace(" :", " :") + } + // Handle a backwards compatibility hack which is required if compiler_version is not present. // See `insert_space` for more details. if contract_definition.program.compiler_version.is_none() { - let identifiers = contract_definition.program.identifiers.as_object_mut(); - if let Some(identifiers) = identifiers { - insert_space(identifiers); - } + add_extra_space_to_cairo_named_tuples(&mut contract_definition.program.identifiers); + add_extra_space_to_cairo_named_tuples(&mut contract_definition.program.reference_manager); } let truncated_keccak = { @@ -524,6 +560,25 @@ mod json { assert_eq!(hash.0, expected); } + + #[tokio::test] + async fn cairo_0_10_part_3() { + let expected = crate::starkhash!( + "066af14b94491ba4e2aea1117acf0a3155c53d92fdfd9c1f1dcac90dc2d30157" + ); + + // Contract who's class contains `compiler_version` property as well as `cairo_type` with tuple values. + // These tuple values require a space to be injected in order to achieve the correct hash. + let resp = reqwest::get("https://alpha4.starknet.io/feeder_gateway/get_full_contract?blockNumber=latest&contractAddress=0x0424e799d610433168a31aab44c0d3e38b45d97387b45de80089f56c184fa315") + .await + .unwrap(); + + let payload = resp.text().await.expect("response wasn't a string"); + + let hash = super::super::compute_class_hash(payload.as_bytes()).unwrap(); + + assert_eq!(hash.0, expected); + } } #[cfg(test)] diff --git a/py/src/compute_class_hash.py b/py/src/compute_class_hash.py index 0225480dc6..84e16d28ac 100644 --- a/py/src/compute_class_hash.py +++ b/py/src/compute_class_hash.py @@ -1,8 +1,8 @@ # reads stdin for a contract_definition json blob, writes a class hash to stdout # example: python py/src/compute_class_hash.py < class_definition.json -from starkware.starknet.business_logic.state_objects import ContractDefinitionFact -from starkware.starknet.services.api.contract_definition import ContractDefinition +from starkware.starknet.business_logic.state.objects import ContractClassFact +from starkware.starknet.services.api.contract_class import ContractClass from starkware.cairo.lang.vm.crypto import pedersen_hash import sys @@ -20,7 +20,7 @@ def main(): sys.stdin.reconfigure(encoding="utf-8") contents = sys.stdin.read() - cdf = ContractDefinitionFact(ContractDefinition.loads(contents)) + cdf = ContractClassFact(ContractClass.loads(contents)) print(cdf._hash(pedersen_hash).hex()) sys.exit(0)