Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class Hash #42

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f9f5f68
added conversion of StarkFelt to web3::types::U256 + unit test
marioiordanov Mar 29, 2023
793df44
Added a struct for starkfelt that serializes to decimal string
marioiordanov Mar 29, 2023
23fd213
added starknet_keccak function
marioiordanov Mar 30, 2023
5e4715b
made serde serde json use arbitrary precision
marioiordanov Mar 30, 2023
f010b31
json formatter for starknet
marioiordanov Mar 30, 2023
f802d31
methods for removing entries from a json obj, when used with serde js…
marioiordanov Mar 30, 2023
90ebeb7
unit tests for json obj modification and convertion
marioiordanov Mar 30, 2023
9362cb8
method for computing contract class hash
marioiordanov Mar 30, 2023
d4281c1
format code
marioiordanov Apr 4, 2023
37a357b
clippy + fmt
marioiordanov Apr 4, 2023
1084ab0
Merge branch 'main' into class_hash
marioiordanov Apr 4, 2023
6425512
clippy after merge
marioiordanov Apr 4, 2023
8bec3e1
add Deserialize trait to StarkFeltAsDecimal
marioiordanov Apr 5, 2023
211f356
Revert "add Deserialize trait to StarkFeltAsDecimal"
marioiordanov Apr 5, 2023
f2d615c
changed cargo dependency format
marioiordanov May 2, 2023
7352917
removed needless println! and unsafe block
marioiordanov May 3, 2023
f8f8cc6
used informative message
marioiordanov May 3, 2023
1f10947
replaced json with JSON
marioiordanov May 3, 2023
46c78f6
remove unused variable when returning from function
marioiordanov May 3, 2023
8edafa0
removed test prefix
marioiordanov May 3, 2023
439eee8
renamed variable in test
marioiordanov May 3, 2023
417faa9
added comment for starknet formatter
marioiordanov May 3, 2023
d65538f
comment test for scientific notation
marioiordanov May 3, 2023
8699891
removed test from serde_utils_test to utils_test
marioiordanov May 3, 2023
df98097
removed unused method
marioiordanov May 3, 2023
8fea9b6
cargo fmt
marioiordanov May 3, 2023
2901303
replace expect with Error
marioiordanov May 8, 2023
ecf4907
cargo clippy
marioiordanov May 8, 2023
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ testing = []
hex = { version = "0.4.3" }
indexmap = { version = "1.9.2", features = ["serde"] }
once_cell = { version = "1.16.0" }
sha3 = "0.10.6"
serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = { version = "1.0.81" }
serde_json = { version = "1.0.81", features = ["preserve_order", "arbitrary_precision"] }
starknet-crypto = { version = "0.2.0" }
thiserror = { version = "1.0.31" }
web3 = { version = "0.18.0" }
Expand Down
4,875 changes: 4,875 additions & 0 deletions resources/contract_compiled.json

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::fmt::Debug;
use derive_more::Display;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::{json, Serializer, Value};
use starknet_crypto::FieldElement;

use crate::hash::{pedersen_hash_array, StarkFelt, StarkHash};
Expand Down Expand Up @@ -73,6 +74,149 @@ pub fn calculate_contract_address(
ContractAddress::try_from(StarkFelt::from(address))
}

fn compute_class_hash_from_json(contract_class: &Value) -> String {
let mut abi_json = json!({
"abi": contract_class.get("abi").unwrap_or(&Value::Null),
"program": contract_class.get("program").unwrap_or(&Value::Null)
});

let program_json = abi_json.get_mut("program").expect("msg");
let debug_info_json = program_json.get_mut("debug_info");
if debug_info_json.is_some() {
program_json
.as_object_mut()
.expect("Not a json object")
.insert("debug_info".to_owned(), serde_json::Value::Null);
}

let mut new_object = serde_json::Map::<String, Value>::new();
let res = crate::utils::traverse_and_exclude_recursively(
&abi_json,
&mut new_object,
&|key, value| {
return (key == "attributes" || key == "accessible_scopes")
&& value.is_array()
&& value.as_array().expect("Not a json array").is_empty();
},
);

let mut writer = Vec::with_capacity(128);
let mut serializer =
Serializer::with_formatter(&mut writer, crate::serde_utils::StarknetFormatter);
res.serialize(&mut serializer).expect("Unable to serialize with custom formatter");
let str_json = unsafe { String::from_utf8_unchecked(writer) };
println!("{}", str_json);

let keccak_result = crate::hash::sn_keccak(str_json.as_bytes());
keccak_result
}

fn entry_points_hash_by_type_from_json(
contract_class: &Value,
entry_point_type: &str,
) -> StarkFelt {
let felts = contract_class
.get("entry_points_by_type")
.unwrap_or(&serde_json::Value::Null)
.get(entry_point_type)
.unwrap_or(&serde_json::Value::Null)
.as_array()
.unwrap_or(&Vec::<serde_json::Value>::new())
.iter()
.flat_map(|entry| {
let selector = get_starkfelt_from_json_unsafe(entry, "selector");
let offset = get_starkfelt_from_json_unsafe(entry, "offset");

vec![selector, offset]
})
.collect::<Vec<StarkFelt>>();

pedersen_hash_array(&felts)
}

fn get_starkfelt_from_json_unsafe(json: &Value, key: &str) -> StarkFelt {
StarkFelt::try_from(json.get(key).expect("Key not found").as_str().expect("Not a json string"))
.expect("Not a valid hash")
}

pub fn compute_contract_class_hash_v0(contract_class: &serde_json::Value) -> ClassHash {
// api version
let api_version = StarkFelt::try_from(format!("0x{}", hex::encode([0u8])).as_str())
.expect("Not a valid hash");

// external entry points hash
let external_entry_points_hash =
entry_points_hash_by_type_from_json(contract_class, "EXTERNAL");

// l1 handler entry points hash
let l1_entry_points_hash = entry_points_hash_by_type_from_json(contract_class, "L1_HANDLER");

// constructor handler entry points hash
let constructor_entry_points_hash =
entry_points_hash_by_type_from_json(contract_class, "CONSTRUCTOR");

// builtins hash
let builtins_encoded = contract_class
.get("program")
.unwrap_or(&serde_json::Value::Null)
.get("builtins")
.unwrap_or(&serde_json::Value::Null)
.as_array()
.unwrap_or(&Vec::<serde_json::Value>::new())
.iter()
.map(|str| {
let hex_str = str
.as_str()
.expect("Not a json string")
.as_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<String>>()
.join("");
format!("0x{}", hex_str)
})
.collect::<Vec<String>>();

let builtins_encoded_as_felts = builtins_encoded
.iter()
.map(|s| {
return StarkFelt::try_from(s.as_str()).expect("Not a valid hash");
})
.collect::<Vec<StarkFelt>>();

let builtins_hash = pedersen_hash_array(&builtins_encoded_as_felts);

// hinted class hash
let hinted_class_hash = compute_class_hash_from_json(contract_class);

// program data hash
let program_data_felts = contract_class
.get("program")
.unwrap_or(&Value::Null)
.get("data")
.unwrap_or(&Value::Null)
.as_array()
.unwrap_or(&Vec::<Value>::new())
.iter()
.map(|str| {
return StarkFelt::try_from(str.as_str().expect("Not a json string"))
.expect("Not a valid hash");
})
.collect::<Vec<StarkFelt>>();

let program_data_hash = pedersen_hash_array(&program_data_felts);

ClassHash(pedersen_hash_array(&vec![
api_version,
external_entry_points_hash,
l1_entry_points_hash,
constructor_entry_points_hash,
builtins_hash,
StarkFelt::try_from(hinted_class_hash.as_str()).expect("Not a valid hash"),
program_data_hash,
]))
}

/// The hash of a ContractClass.
#[derive(
Debug,
Expand Down
14 changes: 14 additions & 0 deletions src/core_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,17 @@ fn test_calculate_contract_address() {

assert_eq!(actual_address, expected_address);
}

#[test]
fn test_contract_class_hash_generation() {
let data_str = std::fs::read_to_string("./resources/contract_compiled.json").unwrap();
println!("{data_str}");
let data: serde_json::Value = serde_json::from_str(&data_str).unwrap();
let expected_class_hash = ClassHash(
StarkHash::try_from("0x399998c787e0a063c3ac1d2abac084dcbe09954e3b156d53a8c43a02aa27d35")
.unwrap(),
);

let resulted_class_hash = crate::core::compute_contract_class_hash_v0(&data);
assert_eq!(resulted_class_hash, expected_class_hash);
}
8 changes: 4 additions & 4 deletions src/deprecated_contract_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ impl TryFrom<String> for EntryPointOffset {

pub fn number_or_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<usize, D::Error> {
let usize_value = match Value::deserialize(deserializer)? {
Value::Number(number) => {
number.as_u64().ok_or(DeserializationError::custom("Cannot cast number to usize."))?
as usize
}
Value::Number(number) => number
.as_u64()
.ok_or_else(|| DeserializationError::custom("Cannot cast number to usize."))?
as usize,
Value::String(s) => hex_string_try_into_usize(&s).map_err(DeserializationError::custom)?,
_ => return Err(DeserializationError::custom("Cannot cast value into usize.")),
};
Expand Down
43 changes: 43 additions & 0 deletions src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use std::fmt::{Debug, Display};
use std::io::Error;

use serde::{Deserialize, Serialize};
use sha3::Digest;
use starknet_crypto::{pedersen_hash as starknet_crypto_pedersen_hash, FieldElement};
use web3::types::U256;

use crate::serde_utils::{
bytes_from_hex_str, hex_str_from_bytes, BytesAsHex, NonPrefixedBytesAsHex, PrefixedBytesAsHex,
Expand Down Expand Up @@ -43,12 +45,41 @@ pub fn pedersen_hash_array(felts: &[StarkFelt]) -> StarkHash {
pedersen_hash(&current_hash, &data_len)
}

/// Starknet Keccak Hash
pub fn sn_keccak(data: &[u8]) -> String {
let keccak256 = sha3::Keccak256::digest(data);
let number = U256::from_big_endian(keccak256.as_slice());
let mask = U256::pow(U256::from(2), U256::from(250)) - U256::from(1);
let masked_number = number & mask;
let mut res_bytes: [u8; 32] = [0; 32];
masked_number.to_big_endian(&mut res_bytes);
format!("0x{}", hex::encode(res_bytes).trim_start_matches('0'))
}

// TODO: Move to a different crate.
/// The StarkNet [field element](https://docs.starknet.io/documentation/architecture_and_concepts/Hashing/hash-functions/#domain_and_range).
#[derive(Copy, Clone, Eq, PartialEq, Default, Hash, Deserialize, Serialize, PartialOrd, Ord)]
#[serde(try_from = "PrefixedBytesAsHex<32_usize>", into = "PrefixedBytesAsHex<32_usize>")]
pub struct StarkFelt([u8; 32]);

#[derive(Clone, Copy, Eq, PartialEq, Default)]
pub struct StarkFeltAsDecimal(U256);

impl Serialize for StarkFeltAsDecimal {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0.to_string())
}
}

impl Display for StarkFeltAsDecimal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.to_string())
}
}

impl StarkFelt {
/// Returns a new [`StarkFelt`].
pub fn new(bytes: [u8; 32]) -> Result<StarkFelt, StarknetApiError> {
Expand Down Expand Up @@ -200,6 +231,18 @@ impl TryFrom<StarkFelt> for usize {
}
}

impl From<StarkFelt> for U256 {
fn from(felt: StarkFelt) -> Self {
web3::types::U256::from_big_endian(&felt.0)
}
}

impl From<StarkFelt> for StarkFeltAsDecimal {
fn from(felt: StarkFelt) -> Self {
StarkFeltAsDecimal(U256::from(felt))
}
}

impl Debug for StarkFelt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.str_format(f)
Expand Down
12 changes: 11 additions & 1 deletion src/hash_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::hash::{pedersen_hash, pedersen_hash_array, StarkFelt};
use crate::hash::{pedersen_hash, pedersen_hash_array, StarkFelt, StarkFeltAsDecimal};
use crate::stark_felt;

#[test]
Expand Down Expand Up @@ -67,3 +67,13 @@ fn hash_serde() {
assert_eq!(bytes, d.0);
}
}

#[test]
fn stark_felt_from_hex_to_decimal() {
let felt = stark_felt!("0x264d6571d5f186bab2a9d5d8d30aa38bf5502bc4354870edbfe194c6f655c9b");
let felt_decimal = StarkFeltAsDecimal::from(felt);
assert_eq!(
felt_decimal.to_string(),
"1082789725971120866445625682125121757273101071727151005357998861560691645595"
);
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod hash;
pub mod serde_utils;
pub mod state;
pub mod transaction;
pub mod utils;

use std::num::ParseIntError;

Expand Down
28 changes: 28 additions & 0 deletions src/serde_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,31 @@ pub fn hex_str_from_bytes<const N: usize, const PREFIXED: bool>(bytes: [u8; N])
hex_str = if hex_str.is_empty() { "0" } else { hex_str };
if PREFIXED { format!("0x{hex_str}") } else { hex_str.to_string() }
}

use std::io;

use serde_json::ser::Formatter;
pub struct StarknetFormatter;

impl Formatter for StarknetFormatter {
fn begin_object_value<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
writer.write_all(b": ")
}

fn begin_object_key<W>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
where
W: ?Sized + io::Write,
{
if first { Ok(()) } else { writer.write_all(b", ") }
}

fn begin_array_value<W>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
where
W: ?Sized + io::Write,
{
if first { Ok(()) } else { writer.write_all(b", ") }
}
}
Loading