diff --git a/contracts/.gitignore b/contracts/.gitignore index a3c6f49..62fc447 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -1,2 +1,3 @@ /target /abi +/generated diff --git a/contracts/Makefile b/contracts/Makefile index b01637a..85b241a 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -9,6 +9,7 @@ generate_artifacts: scarb build mkdir -p ${artifacts} + jq .abi ${scarb_build}basic${sierra} > ${artifacts}basic.abi.json jq .abi ${scarb_build}simple_events${sierra} > ${artifacts}events.abi.json jq .abi ${scarb_build}simple_get_set${sierra} > ${artifacts}simple_get_set.abi.json jq .abi ${scarb_build}simple_types${sierra} > ${artifacts}simple_types.abi.json @@ -19,6 +20,10 @@ generate_artifacts: jq .abi ${scarb_build}structs${sierra} > ${artifacts}structs.abi.json jq .abi ${scarb_build}byte_array${sierra} > ${artifacts}byte_array.abi.json +generate_rust: + scarb build + mkdir -p generated + cargo run --all-features -- --artifacts-path target/dev --output-dir generated --rust setup: setup_simple_get_set diff --git a/contracts/src/basic.cairo b/contracts/src/basic.cairo index 9a5ff4d..e1a9337 100644 --- a/contracts/src/basic.cairo +++ b/contracts/src/basic.cairo @@ -2,19 +2,19 @@ mod basic { #[storage] struct Storage { - v1: felt252, + v1: NonZero, v2: u256, v3: felt252, } #[external(v0)] - fn set_storage(ref self: ContractState, v1: felt252, v2: u256) { + fn set_storage(ref self: ContractState, v1: NonZero, v2: u256) { self.v1.write(v1); self.v2.write(v2); } #[external(v0)] - fn read_storage_tuple(self: @ContractState) -> (felt252, u256) { + fn read_storage_tuple(self: @ContractState) -> (NonZero, u256) { (self.v1.read(), self.v2.read()) } } diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index 7be9516..7df1135 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -10,3 +10,4 @@ mod abicov { } mod simple_get_set; +mod basic; diff --git a/crates/cairo-serde/README.md b/crates/cairo-serde/README.md index 8ab58b7..dc25646 100644 --- a/crates/cairo-serde/README.md +++ b/crates/cairo-serde/README.md @@ -8,7 +8,14 @@ By implementing this trait, the Rust type becomes (de)serializable from / into a The types considered built-in by Cairo Serde are the following: ```rust -pub const CAIRO_BASIC_STRUCTS: [&str; 4] = ["Span", "ClassHash", "ContractAddress", "EthAddress"]; +pub const CAIRO_BASIC_STRUCTS: [&str; 6] = [ + "Span", + "ClassHash", + "ContractAddress", + "EthAddress", + "NonZero", + "U256", +]; pub const CAIRO_BASIC_ENUMS: [&str; 3] = ["Option", "Result", "bool"]; ``` @@ -29,6 +36,8 @@ Cairo Serde provides serialization support for the following types: - `ClassHash` -> Custom type in this crate `ClassHash`. - `Array/Span` -> `Vec`. - `Tuple` -> native tuples + the unit `()` type. +- `NonZero` -> Custom type in this crate `NonZero`. +- `u256` -> Custom type in this crate `U256`. ## `CairoSerde` trait diff --git a/crates/cairo-serde/src/error.rs b/crates/cairo-serde/src/error.rs index b2e5fa9..62f689c 100644 --- a/crates/cairo-serde/src/error.rs +++ b/crates/cairo-serde/src/error.rs @@ -19,6 +19,8 @@ pub enum Error { Provider(#[from] ProviderError), #[error("Bytes31 out of range.")] Bytes31OutOfRange, + #[error("NonZero that is zero")] + ZeroedNonZero, } impl CairoSerde for Error { diff --git a/crates/cairo-serde/src/lib.rs b/crates/cairo-serde/src/lib.rs index e167b02..ffb3d99 100644 --- a/crates/cairo-serde/src/lib.rs +++ b/crates/cairo-serde/src/lib.rs @@ -13,14 +13,23 @@ pub mod call; pub mod types; pub use types::array_legacy::*; pub use types::byte_array::*; +pub use types::non_zero::*; pub use types::starknet::*; +pub use types::u256::*; pub use types::*; use ::starknet::core::types::FieldElement; /// Basic cairo structs that are already implemented inside /// this crate and hence skipped during ABI generation. -pub const CAIRO_BASIC_STRUCTS: [&str; 4] = ["Span", "ClassHash", "ContractAddress", "EthAddress"]; +pub const CAIRO_BASIC_STRUCTS: [&str; 6] = [ + "Span", + "ClassHash", + "ContractAddress", + "EthAddress", + "NonZero", + "U256", +]; /// Same as `CAIRO_BASIC_STRUCTS`, but for enums. pub const CAIRO_BASIC_ENUMS: [&str; 3] = ["Option", "Result", "bool"]; diff --git a/crates/cairo-serde/src/types/integers.rs b/crates/cairo-serde/src/types/integers.rs index 8e7f840..dc2decb 100644 --- a/crates/cairo-serde/src/types/integers.rs +++ b/crates/cairo-serde/src/types/integers.rs @@ -64,6 +64,7 @@ implement_trait_for_signed!(i16); implement_trait_for_signed!(i32); implement_trait_for_signed!(i64); implement_trait_for_signed!(i128); +implement_trait_for_signed!(isize); #[cfg(test)] mod tests { diff --git a/crates/cairo-serde/src/types/mod.rs b/crates/cairo-serde/src/types/mod.rs index 0341b37..6310b8a 100644 --- a/crates/cairo-serde/src/types/mod.rs +++ b/crates/cairo-serde/src/types/mod.rs @@ -4,10 +4,12 @@ pub mod boolean; pub mod byte_array; pub mod felt; pub mod integers; +pub mod non_zero; pub mod option; pub mod result; pub mod starknet; pub mod tuple; +pub mod u256; #[cfg(test)] mod tests { diff --git a/crates/cairo-serde/src/types/non_zero.rs b/crates/cairo-serde/src/types/non_zero.rs new file mode 100644 index 0000000..c3ee4f6 --- /dev/null +++ b/crates/cairo-serde/src/types/non_zero.rs @@ -0,0 +1,143 @@ +//! CairoSerde implementation for NonZero. +//! +//! NonZero serializes with zero ( hehe :) ) overhead as the inner value +//! +//! https://github.com/starkware-libs/cairo/blob/main/corelib/src/zeroable.cairo#L38 +use crate::{CairoSerde, ContractAddress, Result, U256}; +use starknet::core::types::FieldElement; + +#[derive(Debug, PartialEq, PartialOrd, Clone)] +pub struct NonZero(T); + +impl NonZero { + pub fn new(value: T) -> Option { + if value.is_zero() { + None + } else { + Some(NonZero(value)) + } + } + + pub fn inner(&self) -> &T { + &self.0 + } + + pub fn inner_mut(&mut self) -> &mut T { + &mut self.0 + } + + pub fn into_inner(self) -> T { + self.0 + } +} + +impl CairoSerde for NonZero +where + T: CairoSerde, + T: Zeroable, + RT: Zeroable, +{ + type RustType = NonZero; + + const SERIALIZED_SIZE: Option = T::SERIALIZED_SIZE; + const DYNAMIC: bool = T::DYNAMIC; + + #[inline] + fn cairo_serialized_size(rust: &Self::RustType) -> usize { + T::cairo_serialized_size(&rust.0) + } + + fn cairo_serialize(rust: &Self::RustType) -> Vec { + T::cairo_serialize(&rust.0) + } + + fn cairo_deserialize(felts: &[FieldElement], offset: usize) -> Result { + NonZero::new(T::cairo_deserialize(felts, offset)?).ok_or(crate::Error::ZeroedNonZero) + } +} + +pub trait Zeroable { + fn is_zero(&self) -> bool; +} + +macro_rules! implement_nonzeroable_for_integer { + ($type:ty) => { + impl Zeroable for $type { + fn is_zero(&self) -> bool { + *self == 0 as $type + } + } + }; +} + +implement_nonzeroable_for_integer!(u8); +implement_nonzeroable_for_integer!(u16); +implement_nonzeroable_for_integer!(u32); +implement_nonzeroable_for_integer!(u64); +implement_nonzeroable_for_integer!(u128); +implement_nonzeroable_for_integer!(usize); +implement_nonzeroable_for_integer!(i8); +implement_nonzeroable_for_integer!(i16); +implement_nonzeroable_for_integer!(i32); +implement_nonzeroable_for_integer!(i64); +implement_nonzeroable_for_integer!(i128); +implement_nonzeroable_for_integer!(isize); + +impl Zeroable for U256 { + fn is_zero(&self) -> bool { + self.low.is_zero() && self.high.is_zero() + } +} + +impl Zeroable for FieldElement { + fn is_zero(&self) -> bool { + *self == FieldElement::ZERO + } +} + +impl Zeroable for ContractAddress { + fn is_zero(&self) -> bool { + self.0 == FieldElement::ZERO + } +} + +#[cfg(test)] +mod tests { + use crate::Error; + + use super::*; + + #[test] + fn test_non_zero_cairo_serialize() { + let non_zero = NonZero(1_u32); + let felts = NonZero::::cairo_serialize(&non_zero); + assert_eq!(felts.len(), 1); + assert_eq!(felts[0], FieldElement::from(1_u32)); + } + + #[test] + fn test_non_zero_cairo_deserialize() { + let felts = vec![FieldElement::from(1_u32)]; + let non_zero = NonZero::::cairo_deserialize(&felts, 0).unwrap(); + assert_eq!(non_zero, NonZero(1_u32)) + } + + #[test] + fn test_non_zero_cairo_deserialize_zero() { + let felts = vec![FieldElement::ZERO, FieldElement::ZERO]; + let non_zero = NonZero::::cairo_deserialize(&felts, 0); + match non_zero { + Err(Error::ZeroedNonZero) => (), + _ => panic!("Expected ZeroedNonZero error"), + } + } + + #[test] + fn test_non_zero_const_size() { + assert_eq!(NonZero::::SERIALIZED_SIZE, Some(1)); + assert_eq!(NonZero::::SERIALIZED_SIZE, Some(2)); + + let is_dynamic = NonZero::::DYNAMIC; + assert!(!is_dynamic); + } +} diff --git a/crates/cairo-serde/src/types/starknet.rs b/crates/cairo-serde/src/types/starknet.rs index 6735ad7..6303a6b 100644 --- a/crates/cairo-serde/src/types/starknet.rs +++ b/crates/cairo-serde/src/types/starknet.rs @@ -77,7 +77,7 @@ impl CairoSerde for ClassHash { } /// EthAddress. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct EthAddress(pub FieldElement); impl From for EthAddress { diff --git a/crates/cairo-serde/src/types/u256.rs b/crates/cairo-serde/src/types/u256.rs new file mode 100644 index 0000000..f84956d --- /dev/null +++ b/crates/cairo-serde/src/types/u256.rs @@ -0,0 +1,238 @@ +use crate::CairoSerde; +use starknet::core::types::{FieldElement, ValueOutOfRangeError}; +use std::cmp::Ordering; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct U256 { + pub low: u128, + pub high: u128, +} + +impl PartialOrd for U256 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(match self.high.cmp(&other.high) { + Ordering::Equal => self.low.cmp(&other.low), + ordering => ordering, + }) + } +} + +impl CairoSerde for U256 { + type RustType = Self; + + const SERIALIZED_SIZE: Option = Some(2); + const DYNAMIC: bool = false; + + #[inline] + fn cairo_serialized_size(this: &U256) -> usize { + u128::cairo_serialized_size(&this.low) + u128::cairo_serialized_size(&this.high) + } + fn cairo_serialize(this: &U256) -> Vec { + [ + u128::cairo_serialize(&this.low), + u128::cairo_serialize(&this.high), + ] + .concat() + } + fn cairo_deserialize(felts: &[FieldElement], offset: usize) -> Result { + let low = u128::cairo_deserialize(felts, offset)?; + let high = u128::cairo_deserialize(felts, offset + u128::cairo_serialized_size(&low))?; + Ok(U256 { low, high }) + } +} +/// FieldElement to U256 conversion as if the tuple was a cairo serialized U256 +impl TryFrom<(FieldElement, FieldElement)> for U256 { + type Error = ValueOutOfRangeError; + fn try_from((a, b): (FieldElement, FieldElement)) -> Result { + let U256 { + low: a_low, + high: a_high, + } = U256::from_bytes_be(&a.to_bytes_be()); + let U256 { + low: b_low, + high: b_high, + } = U256::from_bytes_be(&b.to_bytes_be()); + if b_high != 0 || a_high != 0 { + return Err(ValueOutOfRangeError); + } + Ok(U256 { + low: a_low, + high: b_low, + }) + } +} + +impl U256 { + pub fn to_bytes_be(&self) -> [u8; 32] { + let mut bytes = [0; 32]; + bytes[0..16].copy_from_slice(&self.high.to_be_bytes()); + bytes[16..32].copy_from_slice(&self.low.to_be_bytes()); + bytes + } + pub fn to_bytes_le(&self) -> [u8; 32] { + let mut bytes = [0; 32]; + bytes[0..16].copy_from_slice(&self.low.to_le_bytes()); + bytes[16..32].copy_from_slice(&self.high.to_le_bytes()); + bytes + } + pub fn from_bytes_be(bytes: &[u8; 32]) -> Self { + let high = u128::from_be_bytes(bytes[0..16].try_into().unwrap()); + let low = u128::from_be_bytes(bytes[16..32].try_into().unwrap()); + U256 { low, high } + } + pub fn from_bytes_le(bytes: &[u8; 32]) -> Self { + let low = u128::from_le_bytes(bytes[0..16].try_into().unwrap()); + let high = u128::from_le_bytes(bytes[16..32].try_into().unwrap()); + U256 { low, high } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use starknet::core::types::FieldElement; + + #[test] + fn test_serialize_u256() { + let low = 9_u128; + let high = 8_u128; + let felts = U256::cairo_serialize(&U256 { low, high }); + assert_eq!(felts.len(), 2); + assert_eq!(felts[0], FieldElement::from(9_u128)); + assert_eq!(felts[1], FieldElement::from(8_u128)); + } + + #[test] + fn test_serialize_u256_max() { + let low = u128::MAX; + let high = u128::MAX; + let felts = U256::cairo_serialize(&U256 { low, high }); + assert_eq!(felts.len(), 2); + assert_eq!(felts[0], FieldElement::from(u128::MAX)); + assert_eq!(felts[1], FieldElement::from(u128::MAX)); + } + + #[test] + fn test_serialize_u256_min() { + let low = u128::MIN; + let high = u128::MIN; + let felts = U256::cairo_serialize(&U256 { low, high }); + assert_eq!(felts.len(), 2); + assert_eq!(felts[0], FieldElement::from(u128::MIN)); + assert_eq!(felts[1], FieldElement::from(u128::MIN)); + } + + #[test] + fn test_deserialize_u256() { + let felts = vec![FieldElement::from(9_u128), FieldElement::from(8_u128)]; + let num_u256 = U256::cairo_deserialize(&felts, 0).unwrap(); + assert_eq!(num_u256.low, 9_u128); + assert_eq!(num_u256.high, 8_u128); + } + + #[test] + fn test_serialized_size_u256() { + let u256 = U256 { + low: 9_u128, + high: 8_u128, + }; + assert_eq!(U256::cairo_serialized_size(&u256), 2); + } + + #[test] + fn test_to_bytes_be() { + let u256 = U256 { + low: 9_u128, + high: 8_u128, + }; + let bytes = u256.to_bytes_be(); + let expected_bytes: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, // high + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, // low + ]; + assert_eq!(bytes, expected_bytes); + } + + #[test] + fn test_to_bytes_le() { + let u256 = U256 { + low: 9_u128, + high: 8_u128, + }; + let bytes = u256.to_bytes_le(); + let expected_bytes: [u8; 32] = [ + 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // low + 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // high + ]; + assert_eq!(bytes, expected_bytes); + } + + #[test] + fn test_from_bytes_be() { + let bytes: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, // high + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, // low + ]; + let u256 = U256::from_bytes_be(&bytes); + assert_eq!(u256.low, 9_u128); + assert_eq!(u256.high, 8_u128); + } + + #[test] + fn test_from_bytes_le() { + let bytes: [u8; 32] = [ + 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // low + 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // high + ]; + let u256 = U256::from_bytes_le(&bytes); + assert_eq!(u256.low, 9_u128); + assert_eq!(u256.high, 8_u128); + } + + #[test] + fn test_from_field_element() { + let felts = (FieldElement::from(9_u128), FieldElement::from(8_u128)); + let u256 = U256::try_from(felts).unwrap(); + assert_eq!(u256.low, 9_u128); + assert_eq!(u256.high, 8_u128); + } + + #[test] + fn test_ordering_1() { + let u256_1 = U256 { + low: 9_u128, + high: 8_u128, + }; + let u256_2 = U256 { + low: 0_u128, + high: 9_u128, + }; + assert!(u256_1 < u256_2); + } + + #[test] + fn test_ordering_2() { + let u256_1 = U256 { + low: 9_u128, + high: 8_u128, + }; + let u256_2 = U256 { + low: 9_u128, + high: 8_u128, + }; + assert!(u256_1 == u256_2); + } + + #[test] + fn test_ordering_3() { + let u256_1 = U256 { + low: 8_u128, + high: 9_u128, + }; + let u256_2 = U256 { + low: 9_u128, + high: 9_u128, + }; + assert!(u256_1 < u256_2); + } +} diff --git a/crates/parser/src/tokens/constants.rs b/crates/parser/src/tokens/constants.rs index 6e26580..c6e67da 100644 --- a/crates/parser/src/tokens/constants.rs +++ b/crates/parser/src/tokens/constants.rs @@ -22,9 +22,14 @@ pub const CAIRO_CORE_BASIC: [&str; 17] = [ // to match array pattern. pub const CAIRO_CORE_SPAN_ARRAY: [&str; 2] = ["core::array::Span", "core::array::Array"]; -pub const CAIRO_GENERIC_BUILTINS: [&str; 2] = ["core::option::Option", "core::result::Result"]; +pub const CAIRO_GENERIC_BUILTINS: [&str; 3] = [ + "core::option::Option", + "core::result::Result", + "core::zeroable::NonZero", +]; -pub const CAIRO_COMPOSITE_BUILTINS: [&str; 2] = [ +pub const CAIRO_COMPOSITE_BUILTINS: [&str; 3] = [ "core::byte_array::ByteArray", "core::starknet::eth_address::EthAddress", + "core::integer::u256", ]; diff --git a/crates/rs-macro/src/spanned.rs b/crates/rs-macro/src/spanned.rs index 6037ef7..bd2fc70 100644 --- a/crates/rs-macro/src/spanned.rs +++ b/crates/rs-macro/src/spanned.rs @@ -29,6 +29,7 @@ impl Parse for Spanned { /// A struct that captures `Span` information for inner parsable data. #[cfg_attr(test, derive(Clone, Debug))] +#[allow(dead_code)] pub struct Spanned(Span, T); impl Spanned { diff --git a/crates/rs/src/expand/types.rs b/crates/rs/src/expand/types.rs index 969a194..b637283 100644 --- a/crates/rs/src/expand/types.rs +++ b/crates/rs/src/expand/types.rs @@ -125,6 +125,8 @@ fn basic_types_to_rust(type_name: &str) -> String { "felt" => "starknet::core::types::FieldElement".to_string(), "bytes31" => format!("{ccsp}::Bytes31"), "ByteArray" => format!("{ccsp}::ByteArray"), + "NonZero" => format!("{ccsp}::NonZero"), + "U256" => format!("{ccsp}::U256"), _ => type_name.to_string(), } } @@ -135,6 +137,8 @@ fn builtin_composite_to_rust(type_name: &str) -> (String, bool) { match type_name { "EthAddress" => (format!("{ccsp}::EthAddress"), true), "ByteArray" => (format!("{ccsp}::ByteArray"), true), + "NonZero" => (format!("{ccsp}::NonZero"), true), + "U256" => (format!("{ccsp}::U256"), true), _ => (type_name.to_string(), false), } } diff --git a/examples/simple_get_set.rs b/examples/simple_get_set.rs index 0ef8f8f..cdabb4f 100644 --- a/examples/simple_get_set.rs +++ b/examples/simple_get_set.rs @@ -1,3 +1,4 @@ +use cainome::cairo_serde::U256; use cainome::rs::abigen; use starknet::{ accounts::{Account, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount},