diff --git a/Cargo.lock b/Cargo.lock index d9d3ce34..d7cefa3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1687,7 +1687,7 @@ dependencies = [ [[package]] name = "starknet-core" -version = "0.7.0" +version = "0.7.2" dependencies = [ "base64 0.21.0", "criterion", @@ -1755,7 +1755,7 @@ dependencies = [ [[package]] name = "starknet-ff" -version = "0.3.4" +version = "0.3.5" dependencies = [ "ark-ff", "bigdecimal", diff --git a/Cargo.toml b/Cargo.toml index e898cfac..16c657fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,9 @@ members = [ all-features = true [dependencies] -starknet-ff = { version = "0.3.4", path = "./starknet-ff", default-features = false } +starknet-ff = { version = "0.3.5", path = "./starknet-ff", default-features = false } starknet-crypto = { version = "0.6.1", path = "./starknet-crypto" } -starknet-core = { version = "0.7.0", path = "./starknet-core", default-features = false } +starknet-core = { version = "0.7.2", path = "./starknet-core", default-features = false } starknet-providers = { version = "0.7.0", path = "./starknet-providers" } starknet-contract = { version = "0.6.0", path = "./starknet-contract" } starknet-signers = { version = "0.5.0", path = "./starknet-signers" } diff --git a/examples/starknet-wasm/Cargo.toml b/examples/starknet-wasm/Cargo.toml index 48c05114..4b8300f1 100644 --- a/examples/starknet-wasm/Cargo.toml +++ b/examples/starknet-wasm/Cargo.toml @@ -19,7 +19,7 @@ crate-type = ["cdylib", "rlib"] default = ["console_error_panic_hook"] [dependencies] -starknet-ff = { version = "0.3.4", path = "../../starknet-ff" } +starknet-ff = { version = "0.3.5", path = "../../starknet-ff" } starknet-crypto = { version = "0.6.1", path = "../../starknet-crypto" } console_error_panic_hook = { version = "0.1.7", optional = true } wasm-bindgen = "0.2.84" diff --git a/starknet-accounts/Cargo.toml b/starknet-accounts/Cargo.toml index 95cd090e..f9805f56 100644 --- a/starknet-accounts/Cargo.toml +++ b/starknet-accounts/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["ethereum", "starknet", "web3"] exclude = ["test-data/**"] [dependencies] -starknet-core = { version = "0.7.0", path = "../starknet-core" } +starknet-core = { version = "0.7.2", path = "../starknet-core" } starknet-providers = { version = "0.7.0", path = "../starknet-providers" } starknet-signers = { version = "0.5.0", path = "../starknet-signers" } async-trait = "0.1.68" diff --git a/starknet-contract/Cargo.toml b/starknet-contract/Cargo.toml index 835d55be..8958d0c9 100644 --- a/starknet-contract/Cargo.toml +++ b/starknet-contract/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["ethereum", "starknet", "web3"] exclude = ["test-data/**"] [dependencies] -starknet-core = { version = "0.7.0", path = "../starknet-core" } +starknet-core = { version = "0.7.2", path = "../starknet-core" } starknet-providers = { version = "0.7.0", path = "../starknet-providers" } starknet-accounts = { version = "0.6.0", path = "../starknet-accounts" } serde = { version = "1.0.160", features = ["derive"] } diff --git a/starknet-core/Cargo.toml b/starknet-core/Cargo.toml index 8a8eeb1b..64c4d5b9 100644 --- a/starknet-core/Cargo.toml +++ b/starknet-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "starknet-core" -version = "0.7.0" +version = "0.7.2" authors = ["Jonathan LEI "] license = "MIT OR Apache-2.0" edition = "2021" @@ -18,7 +18,7 @@ all-features = true [dependencies] starknet-crypto = { version = "0.6.1", path = "../starknet-crypto", default-features = false, features = ["alloc"] } -starknet-ff = { version = "0.3.4", path = "../starknet-ff", default-features = false, features = ["serde"] } +starknet-ff = { version = "0.3.5", path = "../starknet-ff", default-features = false, features = ["serde"] } base64 = { version = "0.21.0", default-features = false, features = ["alloc"] } flate2 = { version = "1.0.25", optional = true } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/starknet-core/src/serde/byte_array.rs b/starknet-core/src/serde/byte_array.rs index 79d9411e..d5ddd470 100644 --- a/starknet-core/src/serde/byte_array.rs +++ b/starknet-core/src/serde/byte_array.rs @@ -1,8 +1,10 @@ pub mod base64 { - use alloc::{format, string::String, vec::Vec}; + use alloc::{fmt::Formatter, format, vec::Vec}; use base64::{engine::general_purpose::STANDARD, Engine}; - use serde::{Deserialize, Deserializer, Serializer}; + use serde::{de::Visitor, Deserializer, Serializer}; + + struct Base64Visitor; pub fn serialize(value: T, serializer: S) -> Result where @@ -16,12 +18,23 @@ pub mod base64 { where D: Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - match STANDARD.decode(value) { - Ok(value) => Ok(value), - Err(err) => Err(serde::de::Error::custom(format!( - "invalid base64 string: {err}" - ))), + deserializer.deserialize_any(Base64Visitor) + } + + impl<'de> Visitor<'de> for Base64Visitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + STANDARD + .decode(v) + .map_err(|err| serde::de::Error::custom(format!("invalid base64 string: {err}"))) } } } diff --git a/starknet-core/src/serde/num_hex.rs b/starknet-core/src/serde/num_hex.rs index 9aaf9076..985b9053 100644 --- a/starknet-core/src/serde/num_hex.rs +++ b/starknet-core/src/serde/num_hex.rs @@ -1,7 +1,9 @@ pub mod u64 { - use alloc::{format, string::String}; + use alloc::{fmt::Formatter, format}; - use serde::{Deserialize, Deserializer, Serializer}; + use serde::{de::Visitor, Deserializer, Serializer}; + + struct NumHexVisitor; pub fn serialize(value: &u64, serializer: S) -> Result where @@ -14,12 +16,22 @@ pub mod u64 { where D: Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - match u64::from_str_radix(value.trim_start_matches("0x"), 16) { - Ok(value) => Ok(value), - Err(err) => Err(serde::de::Error::custom(format!( - "invalid u64 hex string: {err}" - ))), + deserializer.deserialize_any(NumHexVisitor) + } + + impl<'de> Visitor<'de> for NumHexVisitor { + type Value = u64; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + u64::from_str_radix(v.trim_start_matches("0x"), 16) + .map_err(|err| serde::de::Error::custom(format!("invalid u64 hex string: {err}"))) } } } diff --git a/starknet-core/src/serde/unsigned_field_element.rs b/starknet-core/src/serde/unsigned_field_element.rs index a130366c..1a4d8a53 100644 --- a/starknet-core/src/serde/unsigned_field_element.rs +++ b/starknet-core/src/serde/unsigned_field_element.rs @@ -1,6 +1,9 @@ -use alloc::{format, string::String}; +use alloc::{fmt::Formatter, format}; -use serde::{de::Error as DeError, Deserialize, Deserializer, Serializer}; +use serde::{ + de::{Error as DeError, Visitor}, + Deserializer, Serializer, +}; use serde_with::{DeserializeAs, SerializeAs}; use crate::types::FieldElement; @@ -11,6 +14,10 @@ pub struct UfeHexOption; pub struct UfePendingBlockHash; +struct UfeHexVisitor; +struct UfeHexOptionVisitor; +struct UfePendingBlockHashVisitor; + impl SerializeAs for UfeHex { fn serialize_as(value: &FieldElement, serializer: S) -> Result where @@ -25,11 +32,23 @@ impl<'de> DeserializeAs<'de, FieldElement> for UfeHex { where D: Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - match FieldElement::from_hex_be(&value) { - Ok(value) => Ok(value), - Err(err) => Err(DeError::custom(format!("invalid hex string: {err}"))), - } + deserializer.deserialize_any(UfeHexVisitor) + } +} + +impl<'de> Visitor<'de> for UfeHexVisitor { + type Value = FieldElement; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: DeError, + { + FieldElement::from_hex_be(v) + .map_err(|err| DeError::custom(format!("invalid hex string: {err}"))) } } @@ -50,10 +69,24 @@ impl<'de> DeserializeAs<'de, Option> for UfeHexOption { where D: Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - match value.as_str() { + deserializer.deserialize_any(UfeHexOptionVisitor) + } +} + +impl<'de> Visitor<'de> for UfeHexOptionVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: DeError, + { + match v { "" => Ok(None), - _ => match FieldElement::from_hex_be(&value) { + _ => match FieldElement::from_hex_be(v) { Ok(value) => Ok(Some(value)), Err(err) => Err(DeError::custom(format!("invalid hex string: {err}"))), }, @@ -79,11 +112,25 @@ impl<'de> DeserializeAs<'de, Option> for UfePendingBlockHash { where D: Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - if value.is_empty() || value == "pending" || value == "None" { + deserializer.deserialize_any(UfePendingBlockHashVisitor) + } +} + +impl<'de> Visitor<'de> for UfePendingBlockHashVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: DeError, + { + if v.is_empty() || v == "pending" || v == "None" { Ok(None) } else { - match FieldElement::from_hex_be(&value) { + match FieldElement::from_hex_be(v) { Ok(value) => Ok(Some(value)), Err(err) => Err(DeError::custom(format!("invalid hex string: {err}"))), } @@ -95,6 +142,7 @@ impl<'de> DeserializeAs<'de, Option> for UfePendingBlockHash { mod tests { use super::*; + use serde::Deserialize; use serde_with::serde_as; #[serde_as] diff --git a/starknet-core/src/types/eth_address.rs b/starknet-core/src/types/eth_address.rs index 53d38ad4..21d483cd 100644 --- a/starknet-core/src/types/eth_address.rs +++ b/starknet-core/src/types/eth_address.rs @@ -1,7 +1,7 @@ -use alloc::{format, string::String}; +use alloc::{fmt::Formatter, format}; use core::str::FromStr; -use serde::{Deserialize, Serialize}; +use serde::{de::Visitor, Deserialize, Serialize}; use starknet_ff::FieldElement; // 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF @@ -17,6 +17,8 @@ pub struct EthAddress { inner: [u8; 20], } +struct EthAddressVisitor; + mod errors { use core::fmt::{Display, Formatter, Result}; @@ -84,9 +86,22 @@ impl<'de> Deserialize<'de> for EthAddress { where D: serde::Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - value - .parse() + deserializer.deserialize_any(EthAddressVisitor) + } +} + +impl<'de> Visitor<'de> for EthAddressVisitor { + type Value = EthAddress; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse() .map_err(|err| serde::de::Error::custom(format!("{}", err))) } } diff --git a/starknet-core/src/types/execution_result.rs b/starknet-core/src/types/execution_result.rs index 3479d554..43e8f652 100644 --- a/starknet-core/src/types/execution_result.rs +++ b/starknet-core/src/types/execution_result.rs @@ -68,13 +68,12 @@ impl<'de> Deserialize<'de> for ExecutionResult { match (raw.execution_status, raw.revert_reason) { (TransactionExecutionStatus::Succeeded, None) => Ok(Self::Succeeded), - (TransactionExecutionStatus::Reverted, Some(reason)) => Ok(Self::Reverted { reason }), + (TransactionExecutionStatus::Reverted, reason) => Ok(Self::Reverted { + reason: reason.unwrap_or_default(), + }), (TransactionExecutionStatus::Succeeded, Some(_)) => Err(serde::de::Error::custom( "field `revert_reason` must not exist when `execution_status` is `SUCCEEDED`", )), - (TransactionExecutionStatus::Reverted, None) => Err(serde::de::Error::custom( - "field `revert_reason` missing when `execution_status` is `REVERTED`", - )), } } } diff --git a/starknet-core/src/types/hash_256.rs b/starknet-core/src/types/hash_256.rs new file mode 100644 index 00000000..a75c9896 --- /dev/null +++ b/starknet-core/src/types/hash_256.rs @@ -0,0 +1,177 @@ +use alloc::{ + fmt::{Debug, Display, Formatter, Result as FmtResult}, + format, + str::FromStr, +}; + +use serde::{de::Visitor, Deserialize, Serialize}; +use starknet_ff::FieldElement; + +const HASH_256_BYTE_COUNT: usize = 32; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Hash256 { + inner: [u8; HASH_256_BYTE_COUNT], +} + +struct Hash256Visitor; + +mod errors { + use core::fmt::{Display, Formatter, Result}; + + #[derive(Debug)] + pub enum FromHexError { + UnexpectedLength, + InvalidHexString, + } + + #[derive(Debug)] + pub struct ToFieldElementError; + + #[cfg(feature = "std")] + impl std::error::Error for FromHexError {} + + #[cfg(feature = "std")] + impl std::error::Error for ToFieldElementError {} + + impl Display for FromHexError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + Self::UnexpectedLength => { + write!(f, "unexpected length for 256-bit hash") + } + Self::InvalidHexString => { + write!(f, "invalid hex string") + } + } + } + } + + impl Display for ToFieldElementError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "hash value out of range for FieldElement") + } + } +} +pub use errors::{FromHexError, ToFieldElementError}; + +impl Hash256 { + pub fn from_bytes(bytes: [u8; HASH_256_BYTE_COUNT]) -> Self { + Self { inner: bytes } + } + + pub fn from_hex(hex: &str) -> Result { + hex.parse() + } + + pub fn from_felt(felt: &FieldElement) -> Self { + felt.into() + } + + pub fn as_bytes(&self) -> &[u8; HASH_256_BYTE_COUNT] { + &self.inner + } +} + +impl Serialize for Hash256 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("0x{}", hex::encode(self.inner))) + } +} + +impl<'de> Deserialize<'de> for Hash256 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(Hash256Visitor) + } +} + +impl<'de> Visitor<'de> for Hash256Visitor { + type Value = Hash256; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse() + .map_err(|err| serde::de::Error::custom(format!("{}", err))) + } +} + +impl FromStr for Hash256 { + type Err = FromHexError; + + fn from_str(s: &str) -> Result { + let value = s.trim_start_matches("0x"); + + let hex_chars_len = value.len(); + let expected_hex_length = HASH_256_BYTE_COUNT * 2; + + let parsed_bytes: [u8; HASH_256_BYTE_COUNT] = if hex_chars_len == expected_hex_length { + let mut buffer = [0u8; HASH_256_BYTE_COUNT]; + hex::decode_to_slice(value, &mut buffer).map_err(|_| FromHexError::InvalidHexString)?; + buffer + } else if hex_chars_len < expected_hex_length { + let mut padded_hex = str::repeat("0", expected_hex_length - hex_chars_len); + padded_hex.push_str(value); + + let mut buffer = [0u8; HASH_256_BYTE_COUNT]; + hex::decode_to_slice(&padded_hex, &mut buffer) + .map_err(|_| FromHexError::InvalidHexString)?; + buffer + } else { + return Err(FromHexError::UnexpectedLength); + }; + + Ok(Self::from_bytes(parsed_bytes)) + } +} + +impl Debug for Hash256 { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "0x{}", hex::encode(self.inner)) + } +} + +impl Display for Hash256 { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "0x{}", hex::encode(self.inner)) + } +} + +impl From for Hash256 { + fn from(value: FieldElement) -> Self { + (&value).into() + } +} + +impl From<&FieldElement> for Hash256 { + fn from(value: &FieldElement) -> Self { + Self::from_bytes(value.to_bytes_be()) + } +} + +impl TryFrom for FieldElement { + type Error = ToFieldElementError; + + fn try_from(value: Hash256) -> Result { + (&value).try_into() + } +} + +impl TryFrom<&Hash256> for FieldElement { + type Error = ToFieldElementError; + + fn try_from(value: &Hash256) -> Result { + FieldElement::from_bytes_be(&value.inner).map_err(|_| ToFieldElementError) + } +} diff --git a/starknet-core/src/types/mod.rs b/starknet-core/src/types/mod.rs index dcb698db..18ff37aa 100644 --- a/starknet-core/src/types/mod.rs +++ b/starknet-core/src/types/mod.rs @@ -39,9 +39,15 @@ pub use codegen::{ pub mod eth_address; pub use eth_address::EthAddress; +pub mod hash_256; +pub use hash_256::Hash256; + mod execution_result; pub use execution_result::ExecutionResult; +mod msg; +pub use msg::MsgToL2; + // TODO: move generated request code to `starknet-providers` pub mod requests; @@ -262,6 +268,36 @@ pub enum ExecuteInvocation { Reverted(RevertedInvocation), } +mod errors { + use core::fmt::{Display, Formatter, Result}; + + #[derive(Debug)] + pub enum ParseMsgToL2Error { + EmptyCalldata, + FromAddressOutOfRange, + } + + #[cfg(feature = "std")] + impl std::error::Error for ParseMsgToL2Error {} + + impl Display for ParseMsgToL2Error { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + Self::EmptyCalldata => { + write!( + f, + "calldata must contain at least 1 element for from_address" + ) + } + Self::FromAddressOutOfRange => { + write!(f, "from_address is larger than 20 bytes") + } + } + } + } +} +pub use errors::ParseMsgToL2Error; + impl MaybePendingBlockWithTxHashes { pub fn transactions(&self) -> &[FieldElement] { match self { @@ -392,6 +428,24 @@ impl PendingTransactionReceipt { } } +impl L1HandlerTransaction { + pub fn parse_msg_to_l2(&self) -> Result { + if self.calldata.is_empty() { + return Err(ParseMsgToL2Error::EmptyCalldata); + } + + Ok(MsgToL2 { + from_address: self.calldata[0] + .try_into() + .map_err(|_| ParseMsgToL2Error::FromAddressOutOfRange)?, + to_address: self.contract_address, + selector: self.entry_point_selector, + payload: self.calldata[1..].to_vec(), + nonce: self.nonce, + }) + } +} + impl AsRef for BlockId { fn as_ref(&self) -> &BlockId { self @@ -434,6 +488,14 @@ impl AsRef for BroadcastedDeployAccountTran } } +impl TryFrom<&L1HandlerTransaction> for MsgToL2 { + type Error = ParseMsgToL2Error; + + fn try_from(value: &L1HandlerTransaction) -> Result { + value.parse_msg_to_l2() + } +} + impl TryFrom for StarknetError { type Error = (); @@ -492,4 +554,42 @@ mod tests { let parsed_object = serde_json::from_str::(raw).unwrap(); assert!(parsed_object.is_query); } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_parse_msg_to_l2() { + let l1_handler_tx = L1HandlerTransaction { + transaction_hash: FieldElement::from_hex_be( + "0x374286ae28f201e61ffbc5b022cc9701208640b405ea34ea9799f97d5d2d23c", + ) + .unwrap(), + version: 0, + nonce: 775628, + contract_address: FieldElement::from_hex_be( + "0x73314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82", + ) + .unwrap(), + entry_point_selector: FieldElement::from_hex_be( + "0x2d757788a8d8d6f21d1cd40bce38a8222d70654214e96ff95d8086e684fbee5", + ) + .unwrap(), + calldata: vec![ + FieldElement::from_hex_be("0xc3511006c04ef1d78af4c8e0e74ec18a6e64ff9e").unwrap(), + FieldElement::from_hex_be( + "0x689ead7d814e51ed93644bc145f0754839b8dcb340027ce0c30953f38f55d7", + ) + .unwrap(), + FieldElement::from_hex_be("0x2c68af0bb140000").unwrap(), + FieldElement::from_hex_be("0x0").unwrap(), + ], + }; + + let msg_to_l2 = l1_handler_tx.parse_msg_to_l2().unwrap(); + + let expected_hash = + Hash256::from_hex("c51a543ef9563ad2545342b390b67edfcddf9886aa36846cf70382362fc5fab3") + .unwrap(); + + assert_eq!(msg_to_l2.hash(), expected_hash); + } } diff --git a/starknet-core/src/types/msg.rs b/starknet-core/src/types/msg.rs new file mode 100644 index 00000000..aa621fbe --- /dev/null +++ b/starknet-core/src/types/msg.rs @@ -0,0 +1,152 @@ +use alloc::vec::Vec; + +use sha3::{Digest, Keccak256}; +use starknet_ff::FieldElement; + +use super::{EthAddress, Hash256, MsgToL1}; + +#[derive(Debug, Clone)] +pub struct MsgToL2 { + pub from_address: EthAddress, + pub to_address: FieldElement, + pub selector: FieldElement, + pub payload: Vec, + pub nonce: u64, +} + +impl MsgToL2 { + /// Calculates the message hash based on the algorithm documented here: + /// + /// https://docs.starknet.io/documentation/architecture_and_concepts/L1-L2_Communication/messaging-mechanism/ + pub fn hash(&self) -> Hash256 { + let mut hasher = Keccak256::new(); + + // FromAddress + hasher.update([0u8; 12]); + hasher.update(self.from_address.as_bytes()); + + // ToAddress + hasher.update(self.to_address.to_bytes_be()); + + // Nonce + hasher.update([0u8; 24]); + hasher.update(self.nonce.to_be_bytes()); + + // Selector + hasher.update(self.selector.to_bytes_be()); + + // Payload.length + hasher.update([0u8; 24]); + hasher.update((self.payload.len() as u64).to_be_bytes()); + + // Payload + for item in self.payload.iter() { + hasher.update(item.to_bytes_be()); + } + + let hash = hasher.finalize(); + + // Because we know hash is always 32 bytes + Hash256::from_bytes(unsafe { *(hash[..].as_ptr() as *const [u8; 32]) }) + } +} + +impl MsgToL1 { + /// Calculates the message hash based on the algorithm documented here: + /// + /// https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/messaging-mechanism/#structure_and_hashing_l2-l1 + pub fn hash(&self) -> Hash256 { + let mut hasher = Keccak256::new(); + + // FromAddress + hasher.update(self.from_address.to_bytes_be()); + + // ToAddress + hasher.update(self.to_address.to_bytes_be()); + + // Payload.length + hasher.update([0u8; 24]); + hasher.update((self.payload.len() as u64).to_be_bytes()); + + // Payload + for item in self.payload.iter() { + hasher.update(item.to_bytes_be()); + } + + let hash = hasher.finalize(); + + // Because we know hash is always 32 bytes + Hash256::from_bytes(unsafe { *(hash[..].as_ptr() as *const [u8; 32]) }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_msg_to_l2_hash() { + // Goerli-1 tx: 0374286ae28f201e61ffbc5b022cc9701208640b405ea34ea9799f97d5d2d23c + + let msg = MsgToL2 { + from_address: EthAddress::from_hex("0xc3511006C04EF1d78af4C8E0e74Ec18A6E64Ff9e") + .unwrap(), + to_address: FieldElement::from_hex_be( + "0x73314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82", + ) + .unwrap(), + selector: FieldElement::from_hex_be( + "0x2d757788a8d8d6f21d1cd40bce38a8222d70654214e96ff95d8086e684fbee5", + ) + .unwrap(), + payload: vec![ + FieldElement::from_hex_be( + "0x689ead7d814e51ed93644bc145f0754839b8dcb340027ce0c30953f38f55d7", + ) + .unwrap(), + FieldElement::from_hex_be("0x2c68af0bb140000").unwrap(), + FieldElement::from_hex_be("0x0").unwrap(), + ], + nonce: 775628, + }; + + let expected_hash = + Hash256::from_hex("c51a543ef9563ad2545342b390b67edfcddf9886aa36846cf70382362fc5fab3") + .unwrap(); + + assert_eq!(msg.hash(), expected_hash); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_msg_to_l1_hash() { + // Goerli-1 tx (L1): 59e37da138dcf7ab9ca4fc7fde15d3f113a06781c4181dfcf0d74753023060b1 Log #40. + // Goerli-1 tx (L2): 4e0bbc07ff29e5df13dfbcb7e4746fdde52c3649a6a69bd86b15397769722fd + + let msg = MsgToL1 { + from_address: FieldElement::from_hex_be( + "0x0164cba33fb7152531f6b4cfc3fff26b4d7b26b4900e0881042edd607b428a92", + ) + .unwrap(), + to_address: FieldElement::from_hex_be( + "0x000000000000000000000000b6dbfaa86bb683152e4fc2401260f9ca249519c0", + ) + .unwrap(), + payload: vec![ + FieldElement::from_hex_be("0x0").unwrap(), + FieldElement::from_hex_be("0x0").unwrap(), + FieldElement::from_hex_be("0x0182b8").unwrap(), + FieldElement::from_hex_be("0x0").unwrap(), + FieldElement::from_hex_be("0x0384").unwrap(), + FieldElement::from_hex_be("0x0").unwrap(), + ], + }; + + let expected_hash = + Hash256::from_hex("0x326a04493fc8f24ac6c6ae7bdba23243ce03ec3aae53f0ed3a0d686eb8cac930") + .unwrap(); + + assert_eq!(msg.hash(), expected_hash); + } +} diff --git a/starknet-core/src/types/serde_impls.rs b/starknet-core/src/types/serde_impls.rs index ac959764..39ee3e49 100644 --- a/starknet-core/src/types/serde_impls.rs +++ b/starknet-core/src/types/serde_impls.rs @@ -1,12 +1,14 @@ -use alloc::{format, string::String}; +use alloc::{fmt::Formatter, format}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use serde_with::{DeserializeAs, SerializeAs}; use super::{SyncStatus, SyncStatusType}; pub(crate) struct NumAsHex; +struct NumAsHexVisitor; + impl SerializeAs for NumAsHex { fn serialize_as(value: &u64, serializer: S) -> Result where @@ -21,14 +23,48 @@ impl<'de> DeserializeAs<'de, u64> for NumAsHex { where D: Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - match u64::from_str_radix(&value[2..], 16) { + deserializer.deserialize_any(NumAsHexVisitor) + } +} + +impl<'de> Visitor<'de> for NumAsHexVisitor { + type Value = u64; + + fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { + write!(formatter, "string or number") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match u64::from_str_radix(v.trim_start_matches("0x"), 16) { Ok(value) => Ok(value), Err(err) => Err(serde::de::Error::custom(format!( "invalid hex string: {err}" ))), } } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + match v.try_into() { + Ok(value) => self.visit_u64(value), + Err(_) => Err(serde::de::Error::custom(format!( + "value cannot be negative: {}", + v + ))), + } + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(v) + } } #[derive(Deserialize)] @@ -231,7 +267,13 @@ mod enum_ser_impls { #[cfg(test)] mod tests { - use super::super::{BlockId, BlockTag, FieldElement}; + use serde::Deserialize; + use serde_with::serde_as; + + use super::{ + super::{BlockId, BlockTag, FieldElement}, + NumAsHex, + }; #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] @@ -251,4 +293,16 @@ mod tests { assert_eq!(serde_json::from_str::(json).unwrap(), block_id); } } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn test_num_as_hex_deser() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Deserialize)] + struct Value(#[serde_as(as = "NumAsHex")] u64); + + for (num, json) in [(Value(100), "\"0x64\""), (Value(100), "100")].into_iter() { + assert_eq!(serde_json::from_str::(json).unwrap(), num); + } + } } diff --git a/starknet-crypto-codegen/Cargo.toml b/starknet-crypto-codegen/Cargo.toml index 87034a67..b4fa8c15 100644 --- a/starknet-crypto-codegen/Cargo.toml +++ b/starknet-crypto-codegen/Cargo.toml @@ -17,5 +17,5 @@ proc-macro = true [dependencies] starknet-curve = { version = "0.4.0", path = "../starknet-curve" } -starknet-ff = { version = "0.3.4", path = "../starknet-ff", default-features = false } +starknet-ff = { version = "0.3.5", path = "../starknet-ff", default-features = false } syn = { version = "2.0.15", default-features = false } diff --git a/starknet-crypto/Cargo.toml b/starknet-crypto/Cargo.toml index 32ef67d4..e0f18e18 100644 --- a/starknet-crypto/Cargo.toml +++ b/starknet-crypto/Cargo.toml @@ -16,7 +16,7 @@ exclude = ["test-data/**"] [dependencies] starknet-crypto-codegen = { version = "0.3.2", path = "../starknet-crypto-codegen" } starknet-curve = { version = "0.4.0", path = "../starknet-curve" } -starknet-ff = { version = "0.3.4", path = "../starknet-ff", default-features = false } +starknet-ff = { version = "0.3.5", path = "../starknet-ff", default-features = false } crypto-bigint = { version = "0.5.1", default-features = false, features = ["generic-array", "zeroize"] } hmac = { version = "0.12.1", default-features = false } num-bigint = { version = "0.4.3", default-features = false } diff --git a/starknet-curve/Cargo.toml b/starknet-curve/Cargo.toml index 9a9f3976..3dd29224 100644 --- a/starknet-curve/Cargo.toml +++ b/starknet-curve/Cargo.toml @@ -13,4 +13,4 @@ Stark curve keywords = ["ethereum", "starknet", "web3", "no_std"] [dependencies] -starknet-ff = { version = "0.3.4", path = "../starknet-ff", default-features = false } +starknet-ff = { version = "0.3.5", path = "../starknet-ff", default-features = false } diff --git a/starknet-ff/Cargo.toml b/starknet-ff/Cargo.toml index 81434578..87c6217b 100644 --- a/starknet-ff/Cargo.toml +++ b/starknet-ff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "starknet-ff" -version = "0.3.4" +version = "0.3.5" authors = ["Jonathan LEI "] license = "MIT OR Apache-2.0" edition = "2021" diff --git a/starknet-ff/src/lib.rs b/starknet-ff/src/lib.rs index 4a0ff4f4..d3032cf3 100644 --- a/starknet-ff/src/lib.rs +++ b/starknet-ff/src/lib.rs @@ -552,10 +552,18 @@ impl fmt::UpperHex for FieldElement { #[cfg(feature = "serde")] mod serde_field_element { + #[cfg(feature = "std")] + use core::fmt::{Formatter, Result as FmtResult}; + use super::*; #[cfg(not(feature = "std"))] - use alloc::string::{String, ToString}; - use serde::{Deserialize, Serialize}; + use alloc::{ + fmt::{Formatter, Result as FmtResult}, + string::ToString, + }; + use serde::{de::Visitor, Deserialize, Serialize}; + + struct FieldElementVisitor; impl Serialize for FieldElement { fn serialize(&self, serializer: S) -> Result @@ -571,8 +579,22 @@ mod serde_field_element { where D: serde::Deserializer<'de>, { - let value = String::deserialize(deserializer)?; - Self::from_str(&value).map_err(serde::de::Error::custom) + deserializer.deserialize_any(FieldElementVisitor) + } + } + + impl<'de> Visitor<'de> for FieldElementVisitor { + type Value = FieldElement; + + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + FieldElement::from_str(v).map_err(serde::de::Error::custom) } } } diff --git a/starknet-macros/Cargo.toml b/starknet-macros/Cargo.toml index eee8f90f..27634105 100644 --- a/starknet-macros/Cargo.toml +++ b/starknet-macros/Cargo.toml @@ -16,7 +16,7 @@ keywords = ["ethereum", "starknet", "web3"] proc-macro = true [dependencies] -starknet-core = { version = "0.7.0", path = "../starknet-core" } +starknet-core = { version = "0.7.2", path = "../starknet-core" } syn = "2.0.15" [features] diff --git a/starknet-providers/Cargo.toml b/starknet-providers/Cargo.toml index 49f40fe8..7b1587fc 100644 --- a/starknet-providers/Cargo.toml +++ b/starknet-providers/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["ethereum", "starknet", "web3"] exclude = ["test-data/**"] [dependencies] -starknet-core = { version = "0.7.0", path = "../starknet-core" } +starknet-core = { version = "0.7.2", path = "../starknet-core" } async-trait = "0.1.68" auto_impl = "1.0.1" ethereum-types = "0.14.1" diff --git a/starknet-providers/tests/jsonrpc.rs b/starknet-providers/tests/jsonrpc.rs index 17b0a552..34c5a5f3 100644 --- a/starknet-providers/tests/jsonrpc.rs +++ b/starknet-providers/tests/jsonrpc.rs @@ -828,7 +828,6 @@ async fn jsonrpc_trace_invoke() { } #[tokio::test] -#[ignore = "disabled until pathfinder PR #1457 is released"] async fn jsonrpc_trace_invoke_reverted() { let rpc_client = create_jsonrpc_client(); diff --git a/starknet-signers/Cargo.toml b/starknet-signers/Cargo.toml index 362dd706..3de88b58 100644 --- a/starknet-signers/Cargo.toml +++ b/starknet-signers/Cargo.toml @@ -13,7 +13,7 @@ Starknet signer implementations keywords = ["ethereum", "starknet", "web3"] [dependencies] -starknet-core = { version = "0.7.0", path = "../starknet-core" } +starknet-core = { version = "0.7.2", path = "../starknet-core" } starknet-crypto = { version = "0.6.1", path = "../starknet-crypto" } async-trait = "0.1.68" auto_impl = "1.0.1"